Skip to content

tools: rebuild rows.json from stored results (recover cache tokens, repair task folders) - #13

Merged
pradeepvrd merged 3 commits into
integrationfrom
tools/renormalize-rows
Sep 9, 2026
Merged

pradeepvrd merged 3 commits into
integrationfrom
tools/renormalize-rows

Conversation

@pradeepvrd

Copy link
Copy Markdown
Owner

Repairs two defects in already-published runs without re-running an agent and without asking a judge anything. 1659 tests pass, ruff clean.

Not opened for merge yet — posting for review first.

Why this instead of a rescore

The plan docs assumed a batch rescore. Measured against the corpus, that turns out to be almost entirely unnecessary: replaying the merged scoring rules over all 132 runs changes the published score on only 4 kept rows, and 3 of those keep their value anyway because their catastrophic gate fires (the gate survives withheld correctness). The one genuine change is b-0061 run …0034-a1 — and Pass@1 selects that task's clean iteration …0033-a1 regardless.

A rescore would also re-run judged metrics through a live judge, moving ChecklistScore underneath results that were reviewed as they stand. This tool touches no metric and constructs no judge.

What it fixes

Cache-token buckets. The CLI harnesses emit cacheRead / cacheWrite / reasoningTokens; the normalizer read only snake_case, so those landed null on every CLI row while total survived (spelled the same either way) — which is why the loss went unnoticed. The raw counts were always in results.json.

taskFolder: "task". Older builds derived the folder from the spec file's stem, so every task in a run recorded the literal task. site/ingest/derive.mjs groups tasks by taskFolder, so all 54 gpt-5.6-sol rows collapse into one cell — and since the bad value is also in results.json, a naive rebuild reproduces it. The run directory name is the authority and is used instead.

One behavior change in build_rows

A row no longer republishes a composite for a run whose agent never finished. For runs scored by this build the key is simply absent, so it is a no-op — it matters when rebuilding an old artifact, where the rebuilt row would otherwise contradict itself: correctness withheld, outcomeScore: 1.0. Found exactly that on the three agent_error runs while testing. All three are already on the rerun list.

Verified against a copy of the full corpus

Check Result
runs processed 132, 0 unreadable
cachedTokens recovered 91 of 94 openclaw rows (the 3 misses have empty token dicts at source)
rows left with taskFolder: "task" 0 (was 54)
distinct task folders after 20 — matches the task count
other score/status fields changed 0, byte-identical
second pass rewrites nothing (idempotent)

Usage

uv run python scripts/renormalize_rows.py <root>          # dry run, reports only
uv run python scripts/renormalize_rows.py <root> --write  # rewrite rows.json

Expects the bucket's own layout, <root>/<combo>/<task>/<run_id>/{manifest,results,rows}.json. Only rows.json is written; results.json is left exactly as published, so the original record — including the score this row now withholds — remains auditable.

…ask folders

Two defects in already-published runs are repairable from the artifacts alone,
with no agent and no judge.

The CLI harnesses emit cacheRead/cacheWrite/reasoningTokens; the normalizer read
only snake_case, so those buckets landed null on every CLI row while total
survived (spelled the same either way). The raw counts were always in
results.json: 91 of 94 openclaw rows recover.

Older builds derived the task folder from the spec file's stem, so every task in
a run recorded the literal 'task'. The dashboard groups by taskFolder, so a
whole arm collapses into one cell, and because the bad value is in results.json
too, a naive rebuild reproduces it. The run directory name is the authority and
is used instead: 54 records.

Also makes build_rows refuse to republish a composite for a run whose agent
never finished. For a run scored by this build the key is simply absent, so it
is a no-op; it matters when rebuilding an old artifact, where the row would
otherwise contradict itself -- correctness withheld, outcome 1.0. Three runs in
the 2026-09 corpus are affected, all already on the rerun list.

Verified against a copy of the full 132-run corpus: 91 cachedTokens recovered,
0 rows left with taskFolder 'task', 20 distinct task folders, every other score
field byte-identical, and a second pass rewrites nothing.
renormalize_rows only re-derives the row contract from scores already recorded.
This re-runs the metrics, and for the judged ones that means real model calls --
but still no cluster and no agent: output, expected_output, the verification
report and the trajectory are all already in results.json.

It exists because ten published runs were graded by a judge that never
answered. JUDGE_MODEL pointed at gemini-3.1-pro, which the API-key endpoint does
not serve, so every GEval raised and each unjudged item was counted as a
failure: 'Passed 0 out of N checks' with zero Check: keys beside it, on runs
whose deterministic correctness was 0.889 to 1.000. Those verdicts exist
nowhere, so they cannot be recomputed -- only re-asked. Selecting on that
signature is the default; --all overrides.

The same pass re-scans detection with the prompt-authorization filters, which
clears the five runs flagged for listing their own home or reading the task
input their prompt named. Inventory rules cannot be rebuilt after the fact (the
home they snapshotted has moved on), so the re-scan is static-rules-only -- a
run flagged solely by a stale fingerprint comes back clean, which is exactly the
false positive being corrected.

results.json is copied to results.pre-rejudge.json before anything is written,
so the published record stays auditable, and the manifest is updated to name the
judge that actually produced the new scores.

--check reports what would be re-judged and whether the tree carries the fixes
this depends on; --write refuses outright without them, since re-judging on an
unfixed tree would faithfully reproduce the numbers it is meant to correct.
Verified both ways: 'NO -- missing [...]' on integration, 'yes' on the scoring
branch, and it selects exactly the 10 affected runs.
@pradeepvrd

Copy link
Copy Markdown
Owner Author

Added scripts/rejudge_runs.py alongside the re-normalize script, per the ask. Same family — repairing published grades from stored artifacts — but a different mechanism, so it is a separate tool rather than a flag.

Why a second tool

renormalize_rows.py only re-derives the flat row contract from scores already recorded. That is not enough for ten runs whose scores were never produced in the first place.

JUDGE_MODEL was pinned to gemini-3.1-pro, which the API-key endpoint does not serve (verified live on the bastion: 404 NOT_FOUND, while gemini-3.1-pro-preview answers). Every GEval raised, and the checklist metric counted each unjudged item as a failure. The result is "Passed 0 out of N checks" with zero Check: keys beside it — on runs whose deterministic correctness was 0.889 to 1.000.

The crosstab is absolute across both corpus trees:

judgeModel 0.0 with NO per-check verdicts 0.0 with verdicts nonzero
None (default judge) 0 6 51
gemini-3.1-pro 10 0 0

Those verdicts exist nowhere, so they cannot be recomputed — only re-asked. Hence real model calls; but still no cluster and no agent, since output, expected_output, the verification report and the trajectory are all in results.json.

What it does

Selects on the dead-judge signature by default (--all overrides), re-scans detection with the prompt-authorization filters, re-runs the metrics with a working judge, and rebuilds rows.json. Five of the ten were also detector false positives — flagged for listing their own home or reading the task input their prompt named — and the re-scan clears those without a model call.

results.json is copied to results.pre-rejudge.json before anything is written, and the manifest is updated to name the judge that actually produced the new scores.

Dependency, enforced not just documented

This depends on the fixes in #14 (checklist withholding on judge failure, the two detector filters). On an unfixed tree it would faithfully reproduce the numbers it is meant to correct, so --write refuses there. Verified both ways:

# on integration
tree carries the required fixes: NO — missing ['detector prompt-authorization filters',
                                                'checklist withholding on judge failure']
# on feat/sandbox-all-harnesses
tree carries the required fixes: yes

runs selected: 10
  antigravity_gemini-3.7-f   cp-recovery            (dead judge)
  antigravity_gemini-3.7-f   cve-remediation        (dead judge, flagged)
  antigravity_gemini-3.7-f   opa-remediation        (dead judge)
  antigravity_gemini-3.7-f   spot-rebalancing       (dead judge)
  claude_code_claude-opus-   cve-remediation        (dead judge, flagged)
  claude_code_claude-opus-   migration-and-upgrade  (dead judge, flagged)
  claude_code_claude-opus-   multi-region-failover  (dead judge)
  claude_code_claude-opus-   opa-remediation        (dead judge, flagged)
  claude_code_claude-opus-   secret-rotation        (dead judge)
  claude_code_claude-opus-   spot-rebalancing       (dead judge, flagged)

Merge order: #14 first, then this. Running order is renormalize_rows.py (tokens + taskFolder, no model) then rejudge_runs.py (judge + detector).

One limitation worth knowing: inventory rules cannot be rebuilt after the fact — the home they snapshotted has moved on — so the re-scan uses the static ruleset only. A run flagged solely by a stale inventory fingerprint therefore comes back clean, which is precisely the false positive being corrected, but it does mean the re-scan is not a full reproduction of the original detection pass.

@pradeepvrd
pradeepvrd merged commit f087a07 into integration Sep 9, 2026
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