Skip to content

[HV5] nightly harvest scoring + library_metrics (the flywheel's own eval) (#1327)#1369

Open
100yenadmin wants to merge 1 commit into
mainfrom
hv5/flywheel-ops
Open

[HV5] nightly harvest scoring + library_metrics (the flywheel's own eval) (#1327)#1369
100yenadmin wants to merge 1 commit into
mainfrom
hv5/flywheel-ops

Conversation

@100yenadmin

Copy link
Copy Markdown
Member

What (HV5 epic #1327, slices 2+3 of the flywheel-ops sprint)

Builds on slice 1 (#1342, closeout auto-nomination → qa/nominations.jsonl, merged). This PR ships
the two remaining core slices; the weekly-curation stub (item 3 in the dispatch) was deprioritized
in favor of full test depth on 2+3 — flagged below, not silently dropped.

Slice 2 — nightly batch scoring (qa/nightly_harvest.py)

Reads the accumulated qa/nominations.jsonl, finds nominations whose artifact_id has no scored
artifacts row yet (same "unscored" definition promote.py uses: row missing OR overall is None),
and scores each via HV1's plain callable entrypoint qa/artifact_score.score_artifact_panel — the
SAME seam promote.py's score_if_unscored calls, so there is exactly one place a live LLM panel
call happens anywhere in the harvest loop.

  • Deferred from the duo hot path, per the epic: nominate.py (slice 1) only nominates; this batch
    is the separate, off-line scoring step.
  • Bounded: --max-per-run (default 20) caps one invocation; the remainder waits for the next
    nightly tick.
  • Resumable + idempotent: the artifacts table itself is the "already scored" source of truth
    (mirrors promote.py); nothing is double-scored or double-billed across runs.
  • Non-fatal isolation: one artifact's load failure (bad source_path, id mismatch) or scoring
    failure (score.sh sentinel/exception) is caught, logged to its own resumable progress log
    (qa/nightly_harvest_log.jsonl, separate from library/.promoted.jsonl), and retried next run —
    never aborts the batch.
  • --dry-run previews the capped work list; scores nothing, writes nothing.

Slice 3 — library_metrics table (the flywheel's own eval)

Additive library_metrics table in qa/scores.db, added via qa/scores_db.py (the sole schema/
writer surface — add_library_metrics / fetch_library_metrics / render_library_metrics_markdown,
mirroring the artifacts table's own pattern). One row per SNAPSHOT (time-series, not a replace-by-
key like runs/artifacts): size_total, size_by_class_json, size_by_tier_json,
reuse_count_sum (Σ reuse_count), promotion_pass_rate (+ promoted_total/rejected_total), and
pct_library_sourced (left None — HV4 wiring doesn't exist yet in this repo state; an honest
"not measured" rather than a guess).

qa/library_metrics.py's snapshot_library() is the sole writer: a pure, read-only scan of
library/**/*.json + library/.promoted.jsonl, then one add_library_metrics call. Never a second
writer of library/promote.py stays its sole writer; this only reads it.

Deferred (flagged, not silently dropped)

Item 3 (weekly-curation stub listing stable-tier entries eligible for canonical review) was not
built in this PR — prioritized full offline test depth on slices 2+3 instead. Follow-up scoped
separately if wanted; canonical promotion stays human-only regardless.

Offline-safety

Every test in qa/test_nightly_harvest.py monkeypatches artifact_score.score_artifact_panel to a
fabricated stub (mirrors test_promote_pipeline.py's test_score_if_unscored_is_isolated_from_promotion_path
discipline) — no test invokes score.sh or a live claude -p. qa/test_library_metrics.py is pure
filesystem + sqlite over fabricated tmp_path trees. Nightly runs (a live LLM panel) are the
orchestrator's job, never CI/pytest.

Invariants held

  • Additive: library_metrics is a new, separate table — runs and artifacts schemas
    untouched (confirmed via detect_changes: only scores_db.py's schema-ensure chain is touched,
    same blast radius HV1's artifacts table addition already established).
  • scores.db writes only through qa/scores_db.py: add_library_metrics is the sole writer;
    nightly_harvest.py never hand-rolls SQL (it calls artifact_score.record_artifact_score
    internally via score_artifact_panel, the existing writer).
  • promote.py stays sole writer of library/: both new modules are read-only over it.
  • Nightly scoring off the duo latency path: confirmed by construction — nightly_harvest.py is
    never invoked from run_duo.sh/nominate.py's hot-path hook.
  • No new chk(): these are harvest-ops readers/writers, not behavioral-gate checks — nothing
    registered in BEHAVIORAL_GATE_TAXONOMY.json / gate_corpus/manifest.json.

Tests

  • qa/test_nightly_harvest.py — 21 offline tests: scores-only-unscored, idempotency, resumability
    across capped runs, bounded --max-per-run (incl. CLI rejection of a negative cap), non-fatal
    load/score-failure isolation + retry-next-run, dry-run (zero scorer calls), malformed-line
    tolerance, never touches library/.
  • qa/test_library_metrics.py — 28 offline tests: scores_db.py schema/round-trip/render (incl.
    cross-table isolation from runs/artifacts), scan_library (size/tier/reuse counting, malformed-
    entry handling), scan_promotion_log (pass-rate math, gated-vs-non-gated verdicts), snapshot_library
    end-to-end against a known fabricated library/ state, CLI dry-run/render.
  • 70 new tests total (single-process, -p no:xdist). Full regression: 212 passed, 1 pre-existing
    skip across test_nominate.py / test_promote_pipeline.py / test_scores_db.py /
    test_artifact_evals.py / test_scores_db_comparability.py / test_closeout.py.
  • qa/fast_gate.sh green — 257 passed, $0 deterministic.
  • python3 tools/library/library_lint.py — clean.

Flags

  • pct_library_sourced stays unset until HV4 (questgen._derive_hooks library candidate source)
    wires per-run attribution — today's expected state, not a bug (documented in the column comment
    and the rendered ledger's blockquote).
  • qa/library_metrics_ledger.md is intentionally NOT committed here (mirrors qa/nominations.jsonl's
    own precedent in slice 1 — a runtime-generated artifact, not founding-PR content); it materializes
    on the first real snapshot_library() run.
  • Weekly-curation stub (dispatch item 3) deferred — see "Deferred" above.

Do NOT merge — orchestrator review.

… eval) (#1327)

Slice 2: qa/nightly_harvest.py scores UNSCORED qa/nominations.jsonl entries via
HV1's score_artifact_panel seam, off the duo hot path (bounded --max-per-run,
resumable, idempotent, non-fatal per-artifact isolation).

Slice 3: additive library_metrics table in qa/scores.db (qa/scores_db.py) +
qa/library_metrics.py's snapshot_library() reads library/ + .promoted.jsonl
to record size-by-class/tier, Σreuse_count, and promotion pass-rate — the
"less AI dependence" trend the epic names.

70 new offline tests (fabricated fixtures, no live LLM); fast_gate.sh green
(257); library-lint clean; scores_db.py additions are additive-only.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Priority Level: P1

  1. What is wrong: qa/nightly_harvest.main() returns 0 for invalid --max-per-run instead of exiting with an error.
    Why it matters: the CLI validation path is effectively silent, and the new test test_negative_max_per_run_rejected_at_cli expects a SystemExit on bad input. As written, negative caps won’t fail the command in the way the test and CLI contract imply.
    Location: qa/nightly_harvest.py in main(), around the if args.max_per_run < 0: check.
    Recommended fix: change the branch to p.error(...) or raise SystemExit(...) so invalid input exits non-zero and matches the documented rejection behavior.
    Required before merge: yes

Walkthrough

This PR adds two independent QA scripts: library_metrics.py, which scans the library/ directory and promotion log to compute health metrics and persist them via a new library_metrics table in scores_db.py; and nightly_harvest.py, a resumable batch scorer that scores unscored nominations via artifact_score.score_artifact_panel. Both ship offline pytest suites.

Changes

Library Metrics Snapshot

Layer / File(s) Summary
library_metrics schema, CRUD, rendering
qa/scores_db.py
Adds LIBRARY_METRICS_COLUMNS, additive schema migration, add_library_metrics/fetch_library_metrics, render_library_metrics_markdown, and corresponding --add/--list/--render-library-metrics CLI flags.
Scan and snapshot computation
qa/library_metrics.py
scan_library and scan_promotion_log compute size/reuse/promotion-pass-rate metrics from disk; snapshot_library combines them into a payload and writes it via scores_db.add_library_metrics (or returns it unwritten in dry-run).
CLI entrypoint
qa/library_metrics.py
Argparse-based main() wires path/SHA/notes/dry-run/render options to snapshot_library.
Test suite
qa/test_library_metrics.py
Pytest coverage for schema/CRUD round-trips, fabricated-library scan correctness, promotion pass-rate edge cases, end-to-end snapshot invariants, and CLI dry-run/render behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Nightly Harvest Batch Scorer

Layer / File(s) Summary
Nomination reading and scoring helpers
qa/nightly_harvest.py
read_nominations tolerantly parses the queue; _scored_ids queries already-scored artifacts; _append_log records per-attempt verdicts; _load_nomination_artifact validates and loads referenced artifact JSON.
Batch orchestration
qa/nightly_harvest.py
harvest_batch dedups/caps nominations, supports a --dry-run preview, and scores each nomination with per-artifact exception isolation (load-failed/score-failed continue the batch).
CLI entrypoint
qa/nightly_harvest.py
Argparse main() validates --max-per-run >= 0, runs the batch, and prints the JSON report.
Test suite
qa/test_nightly_harvest.py
Offline pytest coverage of scoring selection, idempotency, dedup, capping/resumption, failure isolation and retry, dry-run, progress logging, and tolerant parsing.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as library_metrics.main
  participant Snapshot as snapshot_library
  participant Scan as scan_library/scan_promotion_log
  participant DB as scores_db
  CLI->>Snapshot: invoke with args
  Snapshot->>Scan: scan library dir and promoted.jsonl
  Scan-->>Snapshot: size/reuse/pass_rate metrics
  Snapshot->>DB: add_library_metrics(payload)
  DB-->>Snapshot: row_id
  Snapshot-->>CLI: payload with row_id
  CLI->>DB: render_library_metrics_markdown (optional)
Loading
sequenceDiagram
  participant CLI as nightly_harvest.main
  participant Batch as harvest_batch
  participant DB as scores_db
  participant Loader as _load_nomination_artifact
  participant Scorer as artifact_score.score_artifact_panel
  participant Log as progress log
  CLI->>Batch: run with args
  Batch->>DB: fetch_artifacts (scored ids)
  Batch->>Batch: dedup + cap nominations
  loop each nomination
    Batch->>Loader: load artifact JSON
    Loader-->>Batch: artifact or load-failed
    Batch->>Scorer: score_artifact_panel(artifact)
    Scorer-->>Batch: score result or exception
    Batch->>Log: append verdict (scored/load-failed/score-failed)
  end
  Batch-->>CLI: batch report JSON
Loading

Possibly related issues

Possibly related PRs

  • electricsheephq/WorldOS#1331: nightly_harvest.py reads/writes against the per-artifact scores.db artifacts table introduced there.
  • electricsheephq/WorldOS#1338: scan_promotion_log() depends directly on the library/.promoted.jsonl schema/lifecycle introduced by the HV3 promote pipeline.

AI Agent Review Notes (adversarial pass, confidence-scored)

  1. Race/TOCTOU on _scored_ids snapshot vs. write in harvest_batch — Confidence 55%. _scored_ids is computed once at batch start via fetch_artifacts. If two harvest_batch invocations run concurrently (e.g., overlapping cron runs), both could read the same "unscored" set before either writes, causing duplicate scoring of the same artifact_id. Root cause: no row-level locking/claim mechanism, only a pre-read snapshot. Impact: wasted scorer calls, potential duplicate artifacts rows if add-style insert isn't idempotent per artifact_id. Fix: add a unique constraint on artifact_id in the artifacts table (if not present) or claim-before-score semantics (mark "in-progress" before invoking the scorer).

  2. _load_nomination_artifact mismatch handling may mask corrupted state silently — Confidence 40%. On artifact_id mismatch, this raises/returns an error that gets logged as load-failed, but there's no distinct verdict differentiating "file missing" vs "id mismatch" vs "malformed JSON" in the summarized log schema (all funnel through generic error text). Impact: operators debugging a spike in load-failed can't distinguish systemic data corruption from a benign missing path without inspecting raw log entries. Fix: consider a distinct verdict/error-code taxonomy.

  3. scan_library/scan_promotion_log tolerate malformed JSON by skipping — silent data loss risk — Confidence 35%. Malformed library entries are counted toward size totals but excluded from reuse/tier aggregation (per summary), and malformed promotion-log lines are skipped entirely. This is a deliberate design per docstring, but it means a corrupted .promoted.jsonl line silently degrades the pass-rate denominator without alerting anyone. Impact: metrics could quietly become inaccurate over time with no surfaced warning/counter for "skipped malformed" entries. Fix: track and expose a malformed_count metric in the payload for observability.

  4. add_library_metrics never replaces — unbounded table growth — Confidence 20% (likely intentional). Each snapshot call always inserts a new row (confirmed by tests asserting append-only semantics). No apparent retention/pruning logic. Impact: over long-running nightly cron use, scores.db could grow indefinitely. Low severity given typical QA-tooling scale, but worth confirming there's a retention plan elsewhere.

  5. main() in both scripts always exits 0 (nightly_harvest) — confirmed by summary — Confidence 60%. "main()... always exits 0" per the raw summary. This means CI/cron consumers cannot detect batch-level failures (e.g., DB connection errors that got caught generically) via exit code alone; they'd need to parse the JSON report. Impact: silent failures in automation pipelines that only check exit codes. Fix: return non-zero when score_failed_count or load_failed_count exceeds a threshold, or when the batch report indicates systemic failure (e.g., 100% failures).

Overall: no critical correctness bugs identified from the summaries alone (i.e., nothing that would definitely crash or corrupt data), but the concurrency/idempotency assumptions in harvest_batch (#1) and the swallowed-failure exit code (#5) deserve explicit verification against the actual diff before merge — these are exactly the categories of bugs that summaries can hide.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Confidence 95%: The title names the two real changes—nightly harvest scoring and library_metrics—and is specific, concise, and on-topic.
Description check ✅ Passed Confidence 88%: It thoroughly summarizes scope, invariants, flags, and tests, but omits several template sections like Linked Issue, CLA, Review State, and Evidence.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hv5/flywheel-ops

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@qa/nightly_harvest.py`:
- Around line 195-219: The report’s already_scored value is being derived from
the post-dedup candidates list in the nightly harvest flow, which mixes
pre-scored skips with intra-batch duplicate removal. Update the logic around the
noms/candidates processing in nightly_harvest so already_scored counts only
artifact_id values filtered by already_scored, and track the seen-based de-dup
separately for the report. Keep the candidates and report fields in sync so the
summary reflects true pre-scored items without double-counting duplicate
nominations.

In `@qa/scores_db.py`:
- Around line 538-597: Add range validation inside add_library_metrics for the
percent-style fields promotion_pass_rate and pct_library_sourced so invalid
values cannot be persisted. Before building the INSERT, check any provided
values for these keys are numeric and within 0.0 to 1.0 inclusive, and raise a
ValueError with a clear message if not. Keep the validation in
add_library_metrics so both CLI paths that feed it are covered, and leave the
existing unknown-field handling and JSON coercion unchanged.

In `@qa/test_nightly_harvest.py`:
- Around line 199-208: The duplicate-artifact test in
test_duplicate_artifact_id_in_queue_scored_once only checks report["scored"], so
it misses the already_scored reporting bug. Update this test to also assert
report["already_scored"] is 0 after _run(env), using the existing
harvest_batch/report output and the same duplicate nomination setup, so the
metric regression is caught when duplicates are deduped rather than pre-scored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 94be5377-138c-45b7-8951-6ce0252cf0b9

📥 Commits

Reviewing files that changed from the base of the PR and between 0ca870b and 435c73c.

📒 Files selected for processing (5)
  • qa/library_metrics.py
  • qa/nightly_harvest.py
  • qa/scores_db.py
  • qa/test_library_metrics.py
  • qa/test_nightly_harvest.py
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: test
  • GitHub Check: viewer-tests
  • GitHub Check: viewer-tests
  • GitHub Check: test
⚠️ CI failures not shown inline (2)

GitHub Actions: LLM Quality Gate (advisory) / quality-gate: [HV5] nightly harvest scoring + library_metrics (the flywheel's own eval) (#1327)

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1mecho "[selfcheck] bash -n on the duo runner + scorer"�[0m
 �[36;1mbash -n qa/run_duo.sh�[0m
 �[36;1mbash -n qa/score.sh�[0m
 �[36;1mtest -x qa/run_duo.sh�[0m
 �[36;1mtest -x qa/score.sh�[0m
 �[36;1m�[0m
 �[36;1mecho "[selfcheck] regression detector imports + answers --help (pure reader, no DB write)"�[0m
 �[36;1muv run --directory servers/engine python "${GITHUB_WORKSPACE}/qa/detect_regression.py" --help >/dev/null�[0m
 �[36;1m�[0m
 �[36;1mecho "[selfcheck] scorer guard path emits a hashed artifact into a TEMP dir (gateway-free)"�[0m
 �[36;1mtmp="$(mktemp -d)"�[0m
 �[36;1mprintf '# transcript\n' > "$tmp/t.md"�[0m
 �[36;1mprintf '{}\n'           > "$tmp/state.json"�[0m
 �[36;1mWORLDOS_SCORE_GUARD_ONLY=1 bash qa/score.sh \�[0m
 �[36;1m  "$tmp/t.md" "$tmp/state.json" qa/rubric.md qa/score_schema.json "$tmp/out.json" 0.01�[0m
 �[36;1mtest -s "$tmp/out.json"�[0m
 �[36;1mrm -rf "$tmp"�[0m
 �[36;1m�[0m
 �[36;1mecho "[selfcheck] jq present (scorecard tool)"�[0m
 �[36;1mcommand -v jq >/dev/null || { echo "::error title=LLM Quality Gate::jq missing on runner"; exit 1; }�[0m

GitHub Actions: LLM Quality Gate (advisory) / 0_quality-gate.txt: [HV5] nightly harvest scoring + library_metrics (the flywheel's own eval) (#1327)

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1mecho "[selfcheck] bash -n on the duo runner + scorer"�[0m
 �[36;1mbash -n qa/run_duo.sh�[0m
 �[36;1mbash -n qa/score.sh�[0m
 �[36;1mtest -x qa/run_duo.sh�[0m
 �[36;1mtest -x qa/score.sh�[0m
 �[36;1m�[0m
 �[36;1mecho "[selfcheck] regression detector imports + answers --help (pure reader, no DB write)"�[0m
 �[36;1muv run --directory servers/engine python "${GITHUB_WORKSPACE}/qa/detect_regression.py" --help >/dev/null�[0m
 �[36;1m�[0m
 �[36;1mecho "[selfcheck] scorer guard path emits a hashed artifact into a TEMP dir (gateway-free)"�[0m
 �[36;1mtmp="$(mktemp -d)"�[0m
 �[36;1mprintf '# transcript\n' > "$tmp/t.md"�[0m
 �[36;1mprintf '{}\n'           > "$tmp/state.json"�[0m
 �[36;1mWORLDOS_SCORE_GUARD_ONLY=1 bash qa/score.sh \�[0m
 �[36;1m  "$tmp/t.md" "$tmp/state.json" qa/rubric.md qa/score_schema.json "$tmp/out.json" 0.01�[0m
 �[36;1mtest -s "$tmp/out.json"�[0m
 �[36;1mrm -rf "$tmp"�[0m
 �[36;1m�[0m
 �[36;1mecho "[selfcheck] jq present (scorecard tool)"�[0m
 �[36;1mcommand -v jq >/dev/null || { echo "::error title=LLM Quality Gate::jq missing on runner"; exit 1; }�[0m
🧰 Additional context used
🪛 ast-grep (0.44.1)
qa/library_metrics.py

[info] 240-240: use jsonify instead of json.dumps for JSON output
Context: json.dumps(payload, indent=2, ensure_ascii=False)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

qa/test_nightly_harvest.py

[info] 58-58: use jsonify instead of json.dumps for JSON output
Context: json.dumps(obj, indent=2, sort_keys=True)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 63-63: use jsonify instead of json.dumps for JSON output
Context: json.dumps(r)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 214-214: use jsonify instead of json.dumps for JSON output
Context: json.dumps(_nom(f"quest:bg:q{i}", p))
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 228-228: use jsonify instead of json.dumps for JSON output
Context: json.dumps(_nom(f"quest:bg:q{i}", p))
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 346-346: use jsonify instead of json.dumps for JSON output
Context: json.dumps(_nom(f"quest:bg:q{i}", p))
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 388-388: use jsonify instead of json.dumps for JSON output
Context: json.dumps(_nom("quest:bg:q1", p))
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

qa/scores_db.py

[info] 567-567: use jsonify instead of json.dumps for JSON output
Context: json.dumps(_jv, ensure_ascii=False, sort_keys=True)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1455-1455: use jsonify instead of json.dumps for JSON output
Context: json.dumps(fetch_library_metrics(db), indent=2, ensure_ascii=False)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

qa/nightly_harvest.py

[info] 141-141: use jsonify instead of json.dumps for JSON output
Context: json.dumps(rec, sort_keys=True)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 286-286: use jsonify instead of json.dumps for JSON output
Context: json.dumps(report, indent=2, ensure_ascii=False)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

qa/test_library_metrics.py

[info] 137-137: use jsonify instead of json.dumps for JSON output
Context: json.dumps(entry)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 211-211: use jsonify instead of json.dumps for JSON output
Context: json.dumps(l)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 260-260: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"artifact_id": "a1", "verdict": "promoted"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🪛 OpenGrep (1.23.0)
qa/scores_db.py

[ERROR] 354-356: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.

(coderabbit.sql-injection.python-fstring-execute)


[ERROR] 578-578: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.

(coderabbit.sql-injection.python-fstring-execute)

🪛 Ruff (0.15.20)
qa/library_metrics.py

[warning] 94-94: Unnecessary dict comprehension for iterable; use dict.fromkeys instead

Replace with dict.fromkeys(iterable))

(C420)


[warning] 153-153: for loop variable line overwritten by assignment target

(PLW2901)

qa/test_nightly_harvest.py

[warning] 42-42: Missing type annotation for **payload_extra

(ANN003)


[warning] 90-90: Missing return type annotation for private function _fake

(ANN202)


[warning] 90-90: Unused function argument: budget

(ARG001)


[warning] 107-107: Missing return type annotation for private function _fake

Add return type annotation: NoReturn

(ANN202)


[warning] 107-107: Missing type annotation for *a

(ANN002)


[warning] 107-107: Unused function argument: a

(ARG001)


[warning] 107-107: Missing type annotation for **k

(ANN003)


[warning] 107-107: Unused function argument: k

(ARG001)


[warning] 108-108: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 112-112: Missing return type annotation for private function _run

(ANN202)


[warning] 112-112: Missing type annotation for **kw

(ANN003)


[warning] 241-241: Unused function argument: capsys

(ARG001)


[warning] 285-285: Unused function argument: failing_panel

(ARG001)


[warning] 305-305: Missing return type annotation for private function _flaky

(ANN202)


[warning] 305-305: Missing type annotation for **kw

(ANN003)


[warning] 308-308: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 343-343: Unused function argument: fake_panel

(ARG001)


[warning] 358-358: Unused function argument: fake_panel

(ARG001)


[error] 364-364: Ambiguous variable name: l

(E741)


[warning] 371-371: Unused function argument: failing_panel

(ARG001)


[error] 377-377: Ambiguous variable name: l

(E741)


[warning] 386-386: Unused function argument: fake_panel

(ARG001)


[warning] 402-402: Unused function argument: fake_panel

(ARG001)

qa/scores_db.py

[warning] 544-544: Dynamically typed expressions (typing.Any) are disallowed in **fields

(ANN401)


[warning] 558-560: Avoid specifying long messages outside the exception class

(TRY003)


[error] 578-578: Possible SQL injection vector through string-based query construction

(S608)

qa/nightly_harvest.py

[warning] 106-106: for loop variable line overwritten by assignment target

(PLW2901)


[warning] 155-155: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 159-162: Avoid specifying long messages outside the exception class

(TRY003)

qa/test_library_metrics.py

[warning] 65-65: pytest.raises(ValueError) is too broad, set the match parameter or use a more specific exception

(PT011)


[warning] 127-127: Missing type annotation for **extra

(ANN003)


[error] 212-212: Ambiguous variable name: l

(E741)


[warning] 372-372: Unused function argument: capsys

(ARG001)

🔇 Additional comments (11)
qa/nightly_harvest.py (4)

94-115: LGTM!


121-163: LGTM!


226-254: LGTM!


260-292: LGTM!

qa/test_nightly_harvest.py (1)

42-410: LGTM!

qa/scores_db.py (3)

254-293: LGTM!

Also applies to: 318-358


886-948: LGTM!


1389-1397: LGTM!

Also applies to: 1443-1469, 1496-1498

qa/library_metrics.py (2)

83-132: LGTM!

Also applies to: 138-170, 176-215, 221-247


70-77: 🗄️ Data Integrity & Integration

Drop the tier-drift warning. _CLASS_SUBDIRS and PROCESSED_LOG_NAME match tools/library/promote.py; _VALID_TIERS is only a local metrics bucket list, not a promotion-side contract. Confidence 94%.

			> Likely an incorrect or invalid review comment.
qa/test_library_metrics.py (1)

1-393: LGTM!

Comment thread qa/nightly_harvest.py
Comment on lines +195 to +219
# De-dup by artifact_id within this batch's candidate list (a nominations.jsonl may carry the
# same artifact_id more than once across separate nominate.py runs) — score it at most once.
seen: set[str] = set()
candidates: list[dict] = []
for nom in noms:
aid = nom["artifact_id"]
if aid in already_scored or aid in seen:
continue
seen.add(aid)
candidates.append(nom)

work = candidates[:max_per_run]
remaining = len(candidates) - len(work)

report: dict[str, Any] = {
"nominations_total": len(noms),
"already_scored": len(noms) - len(candidates) if noms else 0,
"candidates_unscored": len(candidates),
"scored": 0,
"load_failed": 0,
"score_failed": 0,
"remaining_for_next_run": max(remaining, 0),
"dry_run": dry_run,
"details": [],
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file list ==\n'
git ls-files qa/nightly_harvest.py
git ls-files | rg '(^|/)test|(^|/)tests|nightly_harvest|harvest'

printf '\n== outline qa/nightly_harvest.py ==\n'
ast-grep outline qa/nightly_harvest.py --view expanded || true

printf '\n== relevant lines qa/nightly_harvest.py ==\n'
wc -l qa/nightly_harvest.py
sed -n '160,240p' qa/nightly_harvest.py | cat -n

printf '\n== search tests ==\n'
rg -n "duplicate_artifact_id_in_queue_scored_once|already_scored|candidates_unscored|remaining_for_next_run|nominations_total" -S .

Repository: electricsheephq/WorldOS

Length of output: 18459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== qa/test_nightly_harvest.py around relevant tests ==\n'
sed -n '120,245p' qa/test_nightly_harvest.py | cat -n

printf '\n== tiny behavioral probe ==\n'
python3 - <<'PY'
noms = [{"artifact_id": "a"}, {"artifact_id": "a"}, {"artifact_id": "b"}]
already_scored = set()
seen = set()
candidates = []
for nom in noms:
    aid = nom["artifact_id"]
    if aid in already_scored or aid in seen:
        continue
    seen.add(aid)
    candidates.append(nom)
print({
    "nominations_total": len(noms),
    "candidates": len(candidates),
    "already_scored_metric": len(noms) - len(candidates),
    "duplicate_count": len(noms) - len(set(n["artifact_id"] for n in noms)),
})
PY

Repository: electricsheephq/WorldOS

Length of output: 6161


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "\balready_scored\b" -S qa servers viewer . | sed -n '1,200p'

Repository: electricsheephq/WorldOS

Length of output: 1185


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "\balready_scored\b" -S qa servers viewer . | sed -n '1,220p'

Repository: electricsheephq/WorldOS

Length of output: 1185


Fix already_scored accounting at qa/nightly_harvest.py:211 (confidence 95%) already_scored is computed as len(noms) - len(candidates), but candidates also drops intra-batch duplicates via seen. That makes the report overstate “already scored” whenever the same unscored artifact_id appears twice; the batch still scores it once, yet the summary claims one was pre-scored. Count the already_scored filter separately, or split duplicate-dedup from pre-scored skips into distinct metrics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/nightly_harvest.py` around lines 195 - 219, The report’s already_scored
value is being derived from the post-dedup candidates list in the nightly
harvest flow, which mixes pre-scored skips with intra-batch duplicate removal.
Update the logic around the noms/candidates processing in nightly_harvest so
already_scored counts only artifact_id values filtered by already_scored, and
track the seen-based de-dup separately for the report. Keep the candidates and
report fields in sync so the summary reflects true pre-scored items without
double-counting duplicate nominations.

Comment thread qa/scores_db.py
Comment on lines +538 to +597
# ---------------------------------------------------------------------------
# The one append helper every future LIBRARY_METRICS snapshot should call (HV5 slice 2, #1327)
# ---------------------------------------------------------------------------
def add_library_metrics(
*,
db_path: Path | str = DB_PATH,
**fields: Any,
) -> int:
"""Append ONE library-health snapshot row to the additive `library_metrics` table.

Mirrors :func:`add_run` / :func:`add_artifact`'s validation discipline, but writes the SEPARATE
`library_metrics` table and NEVER touches `runs` or `artifacts`. Pass any subset of
:data:`LIBRARY_METRICS_COLUMNS` as keyword args. Unknown keys raise (a typo is caught, not
silently dropped). ``size_by_class_json`` / ``size_by_tier_json`` may be passed as dicts and are
JSON-encoded automatically. ``ts`` defaults to now (UTC, ISO8601) if omitted. Unlike ``runs``/
``artifacts``, there is no caller-supplied id — every call INSERTS a new row (a snapshot never
replaces a prior one; the row id is autoincrement). Returns the new row's integer id.
"""
unknown = set(fields) - set(LIBRARY_METRICS_COLUMNS)
if unknown:
raise ValueError(
f"unknown field(s) {sorted(unknown)}; valid: {sorted(LIBRARY_METRICS_COLUMNS)}"
)

if fields.get("ts") is None:
fields["ts"] = datetime.now(timezone.utc).isoformat(timespec="seconds")

for _jcol in ("size_by_class_json", "size_by_tier_json"):
_jv = fields.get(_jcol)
if _jv is not None and not isinstance(_jv, str):
fields[_jcol] = json.dumps(_jv, ensure_ascii=False, sort_keys=True)

cols = [c for c in LIBRARY_METRICS_COLUMNS if c in fields]
vals = [fields[c] for c in cols]
placeholders = ", ".join("?" for _ in cols)
quoted = ", ".join(f'"{c}"' for c in cols)

own = isinstance(db_path, (str, os.PathLike))
conn = connect(db_path) if own else db_path # type: ignore[arg-type]
try:
cur = conn.execute(f"INSERT INTO library_metrics ({quoted}) VALUES ({placeholders})", vals)
conn.commit()
return int(cur.lastrowid)
finally:
if own:
conn.close()


def fetch_library_metrics(db_path: Path | str = DB_PATH) -> list[dict]:
"""Return all `library_metrics` snapshot rows as dicts, newest-first (ts desc, then id desc)."""
conn = connect(db_path)
try:
rows = conn.execute(
"SELECT * FROM library_metrics ORDER BY ts DESC, id DESC"
).fetchall()
return [dict(r) for r in rows]
finally:
conn.close()


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

No range guard on the two percent-shaped columns (confidence: 75%).

add_library_metrics() validates unknown keys but never validates that promotion_pass_rate / pct_library_sourced fall inside 0.0-1.0. Both CLI entrypoints that ultimately funnel into this function (scores_db.py's --libmetric-promotion-pass-rate/--libmetric-pct-library-sourced at Line 1446-1451, and library_metrics.py's --pct-library-sourced at Line 228-229) only do a bare float() cast — a fat-fingered 50 instead of 0.5 silently persists as a nonsensical 5000% in the ledger and downstream trend charts (render_library_metrics_markdown at Line 940 would print "5000%"), with no error raised anywhere in the write path. Root cause is here, not at either CLI layer, so fixing here covers both call sites.

🛡️ Proposed fix
     for _jcol in ("size_by_class_json", "size_by_tier_json"):
         _jv = fields.get(_jcol)
         if _jv is not None and not isinstance(_jv, str):
             fields[_jcol] = json.dumps(_jv, ensure_ascii=False, sort_keys=True)
 
+    for _pcol in ("promotion_pass_rate", "pct_library_sourced"):
+        _pv = fields.get(_pcol)
+        if _pv is not None and not (0.0 <= float(_pv) <= 1.0):
+            raise ValueError(f"{_pcol} must be in [0.0, 1.0], got {_pv!r}")
+
     cols = [c for c in LIBRARY_METRICS_COLUMNS if c in fields]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# ---------------------------------------------------------------------------
# The one append helper every future LIBRARY_METRICS snapshot should call (HV5 slice 2, #1327)
# ---------------------------------------------------------------------------
def add_library_metrics(
*,
db_path: Path | str = DB_PATH,
**fields: Any,
) -> int:
"""Append ONE library-health snapshot row to the additive `library_metrics` table.
Mirrors :func:`add_run` / :func:`add_artifact`'s validation discipline, but writes the SEPARATE
`library_metrics` table and NEVER touches `runs` or `artifacts`. Pass any subset of
:data:`LIBRARY_METRICS_COLUMNS` as keyword args. Unknown keys raise (a typo is caught, not
silently dropped). ``size_by_class_json`` / ``size_by_tier_json`` may be passed as dicts and are
JSON-encoded automatically. ``ts`` defaults to now (UTC, ISO8601) if omitted. Unlike ``runs``/
``artifacts``, there is no caller-supplied idevery call INSERTS a new row (a snapshot never
replaces a prior one; the row id is autoincrement). Returns the new row's integer id.
"""
unknown = set(fields) - set(LIBRARY_METRICS_COLUMNS)
if unknown:
raise ValueError(
f"unknown field(s) {sorted(unknown)}; valid: {sorted(LIBRARY_METRICS_COLUMNS)}"
)
if fields.get("ts") is None:
fields["ts"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
for _jcol in ("size_by_class_json", "size_by_tier_json"):
_jv = fields.get(_jcol)
if _jv is not None and not isinstance(_jv, str):
fields[_jcol] = json.dumps(_jv, ensure_ascii=False, sort_keys=True)
cols = [c for c in LIBRARY_METRICS_COLUMNS if c in fields]
vals = [fields[c] for c in cols]
placeholders = ", ".join("?" for _ in cols)
quoted = ", ".join(f'"{c}"' for c in cols)
own = isinstance(db_path, (str, os.PathLike))
conn = connect(db_path) if own else db_path # type: ignore[arg-type]
try:
cur = conn.execute(f"INSERT INTO library_metrics ({quoted}) VALUES ({placeholders})", vals)
conn.commit()
return int(cur.lastrowid)
finally:
if own:
conn.close()
def fetch_library_metrics(db_path: Path | str = DB_PATH) -> list[dict]:
"""Return all `library_metrics` snapshot rows as dicts, newest-first (ts desc, then id desc)."""
conn = connect(db_path)
try:
rows = conn.execute(
"SELECT * FROM library_metrics ORDER BY ts DESC, id DESC"
).fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
# ---------------------------------------------------------------------------
# The one append helper every future LIBRARY_METRICS snapshot should call (HV5 slice 2, `#1327`)
# ---------------------------------------------------------------------------
def add_library_metrics(
*,
db_path: Path | str = DB_PATH,
**fields: Any,
) -> int:
"""Append ONE library-health snapshot row to the additive `library_metrics` table.
Mirrors :func:`add_run` / :func:`add_artifact`'s validation discipline, but writes the SEPARATE
`library_metrics` table and NEVER touches `runs` or `artifacts`. Pass any subset of
:data:`LIBRARY_METRICS_COLUMNS` as keyword args. Unknown keys raise (a typo is caught, not
silently dropped). ``size_by_class_json`` / ``size_by_tier_json`` may be passed as dicts and are
JSON-encoded automatically. ``ts`` defaults to now (UTC, ISO8601) if omitted. Unlike ``runs``/
``artifacts``, there is no caller-supplied idevery call INSERTS a new row (a snapshot never
replaces a prior one; the row id is autoincrement). Returns the new row's integer id.
"""
unknown = set(fields) - set(LIBRARY_METRICS_COLUMNS)
if unknown:
raise ValueError(
f"unknown field(s) {sorted(unknown)}; valid: {sorted(LIBRARY_METRICS_COLUMNS)}"
)
if fields.get("ts") is None:
fields["ts"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
for _jcol in ("size_by_class_json", "size_by_tier_json"):
_jv = fields.get(_jcol)
if _jv is not None and not isinstance(_jv, str):
fields[_jcol] = json.dumps(_jv, ensure_ascii=False, sort_keys=True)
for _pcol in ("promotion_pass_rate", "pct_library_sourced"):
_pv = fields.get(_pcol)
if _pv is not None and not (0.0 <= float(_pv) <= 1.0):
raise ValueError(f"{_pcol} must be in [0.0, 1.0], got {_pv!r}")
cols = [c for c in LIBRARY_METRICS_COLUMNS if c in fields]
vals = [fields[c] for c in cols]
placeholders = ", ".join("?" for _ in cols)
quoted = ", ".join(f'"{c}"' for c in cols)
own = isinstance(db_path, (str, os.PathLike))
conn = connect(db_path) if own else db_path # type: ignore[arg-type]
try:
cur = conn.execute(f"INSERT INTO library_metrics ({quoted}) VALUES ({placeholders})", vals)
conn.commit()
return int(cur.lastrowid)
finally:
if own:
conn.close()
def fetch_library_metrics(db_path: Path | str = DB_PATH) -> list[dict]:
"""Return all `library_metrics` snapshot rows as dicts, newest-first (ts desc, then id desc)."""
conn = connect(db_path)
try:
rows = conn.execute(
"SELECT * FROM library_metrics ORDER BY ts DESC, id DESC"
).fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 567-567: use jsonify instead of json.dumps for JSON output
Context: json.dumps(_jv, ensure_ascii=False, sort_keys=True)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🪛 OpenGrep (1.23.0)

[ERROR] 578-578: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.

(coderabbit.sql-injection.python-fstring-execute)

🪛 Ruff (0.15.20)

[warning] 544-544: Dynamically typed expressions (typing.Any) are disallowed in **fields

(ANN401)


[warning] 558-560: Avoid specifying long messages outside the exception class

(TRY003)


[error] 578-578: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/scores_db.py` around lines 538 - 597, Add range validation inside
add_library_metrics for the percent-style fields promotion_pass_rate and
pct_library_sourced so invalid values cannot be persisted. Before building the
INSERT, check any provided values for these keys are numeric and within 0.0 to
1.0 inclusive, and raise a ValueError with a clear message if not. Keep the
validation in add_library_metrics so both CLI paths that feed it are covered,
and leave the existing unknown-field handling and JSON coercion unchanged.

Comment on lines +199 to +208
def test_duplicate_artifact_id_in_queue_scored_once(env, fake_panel):
"""The same artifact_id nominated twice (two separate nominate.py runs appending to the same
queue) must be scored — and billed — at most once per batch."""
p = _artifact_json(env["artifacts_out"], "quest:bg:q1")
_write_noms(env["noms"], [_nom("quest:bg:q1", p), _nom("quest:bg:q1", p)])

report = _run(env)

assert report["scored"] == 1
assert fake_panel == ["quest:bg:q1"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Coverage gap that hides the already_scored metric bug (confidence ~80%).

This is the one test that exercises duplicate artifact_ids, but it only asserts scored == 1. Add an assertion on already_scored (expected 0 — nothing was pre-scored, just deduped) and it will fail against the current harvest_batch arithmetic, catching the reporting defect flagged in nightly_harvest.py Line 211.

💚 Suggested assertion
     assert report["scored"] == 1
+    assert report["already_scored"] == 0  # a de-duped nomination is NOT "already scored"
     assert fake_panel == ["quest:bg:q1"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_duplicate_artifact_id_in_queue_scored_once(env, fake_panel):
"""The same artifact_id nominated twice (two separate nominate.py runs appending to the same
queue) must be scoredand billedat most once per batch."""
p = _artifact_json(env["artifacts_out"], "quest:bg:q1")
_write_noms(env["noms"], [_nom("quest:bg:q1", p), _nom("quest:bg:q1", p)])
report = _run(env)
assert report["scored"] == 1
assert fake_panel == ["quest:bg:q1"]
def test_duplicate_artifact_id_in_queue_scored_once(env, fake_panel):
"""The same artifact_id nominated twice (two separate nominate.py runs appending to the same
queue) must be scoredand billedat most once per batch."""
p = _artifact_json(env["artifacts_out"], "quest:bg:q1")
_write_noms(env["noms"], [_nom("quest:bg:q1", p), _nom("quest:bg:q1", p)])
report = _run(env)
assert report["scored"] == 1
assert report["already_scored"] == 0 # a de-duped nomination is NOT "already scored"
assert fake_panel == ["quest:bg:q1"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/test_nightly_harvest.py` around lines 199 - 208, The duplicate-artifact
test in test_duplicate_artifact_id_in_queue_scored_once only checks
report["scored"], so it misses the already_scored reporting bug. Update this
test to also assert report["already_scored"] is 0 after _run(env), using the
existing harvest_batch/report output and the same duplicate nomination setup, so
the metric regression is caught when duplicates are deduped rather than
pre-scored.

@evaos-code-review-bot

evaos-code-review-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

evaOS review status: completed

PR: #1369 - [HV5] nightly harvest scoring + library_metrics (the flywheel's own eval) (#1327)
Head: 435c73c3c10c9800f0ae08d27b469418194f498a
Updated: 2026-07-07T03:56:43.872Z

evaOS review completed for this PR head.

Automation note: agents should wait for this comment to reach completed, stale_head, closed_or_merged_before_review, skipped, or failed before treating evaOS review as settled for this head. provider_deferred means evaOS still intends to retry.

PR URL: #1369

Review URL: #1369 (review)

@evaos-code-review-bot evaos-code-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Walkthrough

PR: #1369 - [HV5] nightly harvest scoring + library_metrics (the flywheel's own eval) (#1327)
Head: 435c73c3c10c9800f0ae08d27b469418194f498a into main. Review event: COMMENT.
Provider: GLM/Z.ai through ZCode (zcode-glm, zcode, model GLM-5.2).

Estimated review effort: 5/5 (~60 min)

Changed Files

File Status Churn Purpose Risk
qa/library_metrics.py added +251/-0 Changed file Elevated: large change
qa/nightly_harvest.py added +292/-0 Changed file Moderate: validated P3 finding
qa/scores_db.py modified +215/-1 Changed file Elevated: large change
qa/test_library_metrics.py added +393/-0 Test coverage Elevated: large change
qa/test_nightly_harvest.py added +409/-0 Test coverage Elevated: large change

Review Signal

Validated inline findings: 2 (P0: 0, P1: 0, P2: 0, P3: 2).
Dropped findings before posting: 0. High-severity findings: 0.

Risk Taxonomy

  • Runtime correctness: 2

Validation and Proof

1 required validation/proof recommendation(s) selected from changed files.

  • required: Unity editor or Play Mode smoke - WorldOS repo profile implies Unity runtime risk. Proof: Unity editor smoke; Play Mode log; scene/prefab screenshot or recording.
    Proof status: missing - 1 required validation/proof recommendation(s) missing from PR metadata.
    Profile validation hints: Prefer correctness, persistence, CI, release, and regression findings over style-only feedback.
    Profile proof expectations: Look for Unity editor, play-mode, fixture, or focused smoke evidence when runtime behavior changes.

Related Context

Related issues/PRs: #1327, #1342.
Suggested labels: tests.
Suggested reviewers: none from current metadata.

Review Settings Preview

  • Profile: assertive
  • Enabled sections: Review summary (inline_review); Walkthrough (inline_review); Changed-files table (walkthrough); Effort estimate (walkthrough); Related issues/PRs (walkthrough); Suggested labels (suggestion_only); Review status comment (sticky_status)
  • Path instructions: Assets/** - Prioritize scene, prefab, save-state, asset-reference, and gameplay regressions.
  • Path instructions: ProjectSettings/** - Treat build, platform, input, graphics, and release behavior changes as high risk.
  • Label suggestions: unity, gameplay, regression-hardening
  • Reviewer suggestions: none
  • Suggestion behavior: suggestions only; labels and reviewers are not auto-applied.
  • Roadmap-only settings: auto-apply labels; auto-request reviewers; required status checks

Pre-merge checklist

  • Inline comments target current RIGHT-side diff lines.
  • No secret-like content survived into posted inline comments.
  • REQUEST_CHANGES is only used when eligible P0/P1 findings survive validation.
  • Required behavior proof is present or not applicable.
  • Labels and reviewers are suggestions only; the bot did not auto-apply them.

Comment thread qa/nightly_harvest.py
Pure w.r.t. everything except: (a) new `artifacts` rows via score_artifact_panel for each
artifact actually scored, and (b) an appended line per attempt in ``log_path`` — neither happens
under ``dry_run`` (a pure preview of the capped work list, writing nothing, scoring nothing).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: dry-run initializes/creates the scores.db file despite "writes nothing" contract

harvest_batch() computes already_scored = _scored_ids(db_path) unconditionally, before the if dry_run: return guard. _scored_ids -> scores_db.fetch_artifacts -> connect() -> _ensure_schema(), which runs CREATE TABLE IF NOT EXISTS for runs/artifacts/library_metrics and commits. So a --dry-run invocation that targets a non-existent db_path will CREATE and schema-initialize that db file on disk. The module docstring and harvest_batch docstring both promise dry-run "writes nothing (no scoring call, no db row, no log line)". The existing test (test_dry_run_reports_would_score_and_writes_nothing) only asserts len(fetch_artifacts(...)) == 0 (zero rows) and not env["log"].exists(), so it does not catch the db-file creation; it would pass even though the contract is partially violated. Move the _scored_ids(db_path) call to after the dry-run early return (dry-run does not need the already-scored set to report would_score, only candidates_unscored/remaining_for_next_run, which are derivable from noms alone), or document that dry-run may initialize an empty db file.

Category: Runtime correctness

Why this matters: A nightly operator running --dry-run as a pure preview against a fresh path (e.g. a temp/scratch db, or to validate the work list without side effects) will silently get a new scores.db created on disk. For an unattended cron that uses dry-run as a guard, this is an unexpected side effect that contradicts the stated "writes nothing" invariant and could surprise operators who rely on dry-run being truly side-effect-free.

Comment thread qa/nightly_harvest.py

report: dict[str, Any] = {
"nominations_total": len(noms),
"already_scored": len(noms) - len(candidates) if noms else 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: already_scored report counter conflates already-scored with intra-batch duplicates

already_scored is computed as len(noms) - len(candidates) when noms is non-empty. But candidates excludes BOTH already-scored ids AND intra-batch duplicates (the seen set at line 197-204 dedups). So if nominations.jsonl contains the same unscored artifact_id twice, already_scored is inflated by the duplicate count, misreporting how many nominations were truly already scored. For example, two unscored nominations for the same id yields already_scored=1 even though neither was previously scored. This only affects the diagnostic report (not scoring/billing — the dedup is correct and the duplicate test test_duplicate_artifact_id_in_queue_scored_once does not assert on already_scored), so impact is limited to trend/dashboards reading the report.

Category: Runtime correctness

Why this matters: The nightly report is the operator's view into queue health (how backed up the scoring lane is). An inflated already_scored makes it look like more artifacts have been scored than actually have, which can mask a stuck/behind scoring lane in trend monitoring.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant