Skip to content

External results importer: Stage 1 freeze import, authorized overlays, evaluation - #2

Open
cotenthusiast wants to merge 47 commits into
choicebenchfrom
feat/external-results-importer
Open

cotenthusiast wants to merge 47 commits into
choicebenchfrom
feat/external-results-importer

Conversation

@cotenthusiast

Copy link
Copy Markdown
Owner

Summary

Adds an external-results importer to ChoiceBench, scoped to the actual research objective (import the immutable Stage 1 data freeze, authorize and apply repair/offline-transformation overlays, evaluate) rather than the original, larger ~19-task plan. Built additively on top of the existing manifest v2 system — no v2 code path is touched.

  • Manifest v3 (additive): make_manifest_v3/validate_manifest_v3/initial_run_state_v3/validate_run_state_v3, alongside the untouched v2 schema.
  • Result artifacts v2: publish/validate manifest-bound result CSVs.
  • Row validation & evidence status: join declared source rows to an expected dataset by question identity; independently compute evidence status (complete/qualified/partial/malformed/recoverable/failed) from real row content rather than trusting a declared label.
  • Typed authorization + overlay derivation: inference_repair (executable) vs offline_transformation (non-executable) authorization bundles; pure derivation of replaced-row lineage/content from an authorized overlay.
  • Content-addressed evidence storage + atomic transactions: checksum-bound evidence blobs; real renameat2(RENAME_NOREPLACE)-backed no-clobber publication.
  • Import engine: build_import_plan/execute_import/verify_import_run for the base (non-overlay) path, and execute_overlay_import/_merge_overlay_realization to merge an authorized overlay with a verified base run's retained rows into a new, separate, immutable run — the base run is never mutated.
  • Read + evaluate: v3-only read_manifest_result_set/build_evaluation_report_for_result_set, built on the same full-graph verifier as everything else.
  • Stage 1 profile (profiles/stage1_paper_freeze.py — the only module allowed to know paper-specific facts): verified queue/status/matrix translation, an ARC/MMLU expected-dataset trust chain with real-freeze-verified revalidation and dedup, full ImportSpec assembly across all 102 real cells with per-condition evidence-status reconciliation, and a per-cell AuthorizationSpec builder.

Explicitly out of scope per the reduced objective: CLI, docs, experiment methods/configs, repair execution (running new inference), the final 2×2 analysis, job submission, native-run v3 migration.

Validation

Ran the complete workflow against the real, read-only Stage 1 freeze (a 3.9GB, 102-cell research data snapshot) via a one-off validation script (not committed):

  • Assembled all 102 real cells into one ImportSpec.
  • Full dry run, then a real import into an isolated temporary CHOICEBENCH_HOME.
  • Confirmed idempotent re-import and divergent-overwrite refusal.
  • Verified representative complete/qualified/malformed/variable-option-ARC cases.
  • Applied one synthetic inference-repair overlay and one offline-transformation-mechanism overlay into new immutable derived runs.
  • Read and evaluated the base run and both derived runs.
  • Confirmed the freeze remains byte-identical (checksum ledger unchanged; zero files under the freeze root newer than a pre-run marker).

This surfaced and fixed 4 real bugs (none previously caught by synthetic-fixture tests alone): overlay row/lineage order not following the expected dataset's own question order; no way to declare a source containment root independent from the run workspace; an uncached, O(all-installed-packages) call in provenance identity hashing that made a 102-condition/100k-row run take 30+ minutes instead of ~4 (also cut the full test suite from ~8–9 min to ~64s); and an identity check that required full row coverage for "qualified" realizations regardless of scope disposition, missed because "qualified but excluded from the paper's matrix" was never exercised before real data.

Two independent bounded reviews (spec-compliance, adversarial/code-quality) of everything built this session both found the same additional bug — overlay lineage_components built from set iteration, making the merged realization digest nondeterministic across processes — fixed with a deterministic regression test verified to fail against the pre-fix code.

Finding for the record (not a bug): ChoiceBench's independently-computed evidence status for the 102 real cells (complete=49/qualified=8/malformed=45) disagrees substantially with the freeze's own classification (57/5/28/4/6/2) — confirmed via finding codes to be entirely MISSING_PREDICTION, not a mapping bug. Also, none of the 6 real offline-transformation-authority cells currently have an evaluable base realization (each has 112–229/1000 rows with an unrelated parse failure), so the offline-transformation overlay demo used a different, evaluable real cell to prove the mechanism.

Test plan

  • 1333 full tests passing (pytest -q)
  • Full workflow validated end-to-end against the real Stage 1 freeze (dry run, real import, idempotent re-import, divergent-overwrite refusal, repair + offline-transformation overlays, read/evaluate, freeze byte-identity)
  • Two independent bounded reviews (spec-compliance: Approved; adversarial/code-quality: Ready to merge), sole Critical finding fixed and verified

https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj

Eighth-cycle review found six residual identity gaps:

- importer-core code identity was never bound into
  importer_implementation_identity; only adapter/validator were, so an
  identity.py change was invisible to realization identity.
- declared sources and lineage components could be unreachable: nothing
  required every source to be referenced by a lineage component's
  source_digests, or every lineage component to own a result-origin row.
- non-derived realizations (native_execution/external_import) could carry
  lineage components whose operation_type didn't match the declared
  derivation_origin, smuggling repair/transformation-labeled lineage past
  the authorization/overlay gates that only trigger off the top-level
  derivation_origin.
- source provenance (source_run_id/source_repository/source_commit) could
  be null with no explicit reason, unlike every other nullable provenance
  field in this module.
- runtime callable identity hashed only source/defaults, so a
  behavior-affecting module-level global the callable reads could change
  without changing the callable's identity.
- ExpectedDataset derivation revision/fingerprint were accepted unvalidated,
  letting audit paths, timestamps, or machine-local values enter stable
  dataset identity.

Adds one failing regression test per confirmed gap, then the narrowest
production fix: bind importer-core via _runtime_callable_record; require
every declared source/lineage component to be reachable from a
result-origin row; reject non-matching operation_type on non-derived
realizations; require unknown_reasons for null source provenance fields
(reusing the existing _validate_unknown_reason_contract); bind resolved
non-callable globals into the runtime callable digest; validate
revision/fingerprint with the existing _validate_stable_values guard.

Focused gate (tests/test_condition_grid.py + tests/importing/test_identity.py):
159 passed. Full suite: 1195 passed, only the pre-existing Python 3.14
google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
An adversarial re-review of ee2108b found that the resolved-globals fix
(finding #5) still had a gap: it skipped every callable global on the
theory that a helper's behavior is "already covered by the callable's own
source digest." That's false whenever the helper lives in a different
file than the target: implementation_identity() (and therefore
_runtime_callable_record) only hashes the target's own defining file, so
a cross-module helper's source was never hashed by anything. Verified
with a live repro: an adapter calling a helper imported from another
module produced a byte-identical identity record across two genuinely
different helper implementations.

Fix: function/class globals are now bound via a shallow
implementation_identity() call (their own defining-file digest), not
expanded further, so mutually-referencing helpers can't cause unbounded
or cyclic recursion. Also fail closed (raise ImportIdentityError) instead
of silently dropping a resolved global that can't be represented or is an
uninspectable stateful callable, matching how this module already treats
unsupported callable defaults elsewhere. Removes a redundant
assigned_lineage_ids recomputation the same review flagged as a trivial
duplication (Minor).

Adds tests/importing/_cross_module_helpers.py (two helper functions in a
separate module) plus a regression test proving the callable identity now
changes when the referenced cross-module helper changes, and a test
proving an uninspectable resolved global is rejected rather than ignored.

Focused gate (tests/test_condition_grid.py + tests/importing/test_identity.py):
161 passed. Full suite: 1197 passed, only the pre-existing Python 3.14
google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
The final adversarial re-review of the cross-module-helper fix (1c6625c)
found an Important performance liability: implementation_identity() walks
and hashes a non-choicebench package's entire file tree, and the resolved-
globals binding called it uncached for every function/class-valued global
a callable references. A live repro showed a bare `from pandas import
isna` costing ~0.3s per identity computation, repeated with zero
memoization on every call.

Fix: memoize implementation_identity's validated output per referenced
object via functools.lru_cache. Functions/classes hash by identity, and
their defining files don't change within a process, so caching by object
identity is safe. Verified the exact reproducer now costs ~0.3s on the
first call and ~9 microseconds on the second.

No behavior change: same identity records, same rejection paths, just
memoized. Focused gate (tests/test_condition_grid.py +
tests/importing/test_identity.py): 161 passed in ~5m20s (down from
~9m24s). Full suite: 1197 passed in ~6m54s (down from ~10m44s), only the
pre-existing Python 3.14 google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
Reduced-scope Task 6: adds PROTOCOL_V3_VERSION/MANIFEST_V3_SCHEMA_VERSION/
RUN_STATE_V3_SCHEMA_VERSION and make_manifest_v3/validate_manifest_v3/
initial_run_state_v3/validate_run_state_v3/load_run_state_v3, dispatching
strictly on exact schema_version. Every existing v2 function/constant is
untouched.

Deliberately narrower than the original plan: no ManifestView cross-version
abstraction and no normalize_manifest() legacy-v2 synthesis. Every manifest
this module builds is a fresh v3 import (a base Stage 1 import or a repair
overlay import); nothing here ever needs to read an existing v2 run through
a unified interface. That unification would only matter for migrating
ChoiceBench's own native runner to v3, which is out of scope for finishing
the importer.

v3 payload holds only semantic_conditions/realizations tables (keyed by
condition_id/realization_id, built by the Task 5 identity module) plus
protocol/canonicalization versions; audit is a separate top-level field,
excluded from the experiment digest. validate_manifest_v3 recomputes every
condition/realization ID and digest from its own identity payload rather
than trusting the stored value, and requires each realization's declared
condition_digest to match its owning semantic condition.

Focused: 13 new tests passed. Full suite: 1210 passed (1197 baseline + 13),
only the pre-existing Python 3.14 google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
Reduced-scope Task 7: adds PreparedResultArtifact, prepare_manifest_result,
publish_manifest_result to src/choicebench/io/writers.py, and extends the
existing validate_result_artifact with optional manifest/realization kwargs
that dispatch to a new v2 path. Calling it with no kwargs (the only way any
existing caller does) hits the exact unchanged v1 branch.

prepare_manifest_result computes result identity strictly after the
manifest/realization are already fixed (result_artifact_id/digest are never
part of the manifest), validates that every result row matches its declared
result_origin row_assignment (question_id, prediction_origin,
prediction_lineage_id) with no missing/extra/mismatched rows, orders rows
by the declared assignment order regardless of input order, and requires
(never fabricates) the mandatory per-row identity columns (condition,
realization, experiment, dataset/model/method/prompt, split) already
injected upstream by row normalization.

publish_manifest_result writes only to an absent result/sidecar pair;
an existing pair is a byte-for-byte no-op or a hard refusal on any
divergence, never a silent overwrite.

Focused: 14 new tests in tests/importing/test_result_artifact.py, plus the
existing tests/io/test_writers.py, tests/io/test_readers.py,
tests/importing/test_manifest_v2_compat.py, and
tests/test_publication_identity.py all still pass unchanged (71 total).
Full suite: 1224 passed (1210 baseline + 14), only the pre-existing Python
3.14 google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
…e status

Collapsed Tasks 8+9 into one module (they share the same file in the
original plan; Task 9 only extends Task 8's types). Reduced scope: exact
option-count/method-specific-extension richness is dropped in favor of
what the repair-import workflow concretely needs.

validate_source_rows joins by exact string question_id via a caller-
supplied semantic-field mapping (needed since the plan's own interface
didn't specify how raw CSV columns map to semantic fields; this module
needs that explicitly to be self-contained). No row is dropped, padded,
deduplicated, reordered, or repaired: every input row is kept (with
findings) except when its question_id is genuinely ambiguous (duplicated
within the source). Missing/unexpected/duplicate question IDs, question-
text and correct_option mismatches against the expected-dataset snapshot,
and missing/invalid parsed predictions (checked against the question's
real choices_json-derived option-letter range, not an invented
choice_a/choice_b column shape) all accumulate as ValidationFinding
records with a findings_digest.

normalize_realization_rows recomputes evidence_status from exact coverage
(present vs expected question IDs) and defects (findings tied to a
present question_id), and raises ImportValidationError on any mismatch
against the condition's declared evidence_status -- never silently
trusting the caller's declaration. Only complete/qualified + scope
included realizations produce evaluable_rows; partial/malformed/
recoverable/failed/excluded/held realizations produce none, matching the
plan's evidence-only rule. recoverable is computed (not just copied from
a declared status): coverage gaps must be an exact subset of the
condition's declared recoverable_question_ids with no other defects.

prepare/write/validate_realization_validation_artifact publish one
self-digested JSON sidecar per realization at
artifacts/imports/validation/<realization_id>.json: byte-identical
existing content is a no-op, any divergence is refused, and the
realization_id/digest binding is checked on read.

Focused: 18 new tests, all passing in 0.15s (no identity.py-style file
hashing in this module). Full suite: 1242 passed (1224 baseline + 18),
only the pre-existing Python 3.14 google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
Task 10, reduced scope. This is the core of the repair-queueing objective:
authorization.py types a repair/offline-transformation grant as either
inference_repair (executable=true, a real model may run) or
offline_transformation (executable=false, non-executing semantic
rematching only), refusing any type/executable mismatch. Every grant is
checked against the caller's own known condition digests and expected
dataset selections -- an authorization cannot grant a question outside
its condition's selection, cannot have an empty reason, and cannot
self-assign an authorization_id (it must equal the bundle's own
recomputed digest, so tampering with the source bytes used to build it
is caught by ID mismatch, not a separate checksum field the schema
doesn't carry). authorization_for_condition slices one condition's grant
out of a bundle while keeping the whole bundle's ID/digest, so a slice
can never borrow another condition's grants or diverge from its source
bundle.

overlays.py's derive_overlay is a pure function over an already-verified
VerifiedBaseRealization (never a bare path or unverified dict): it checks
the overlay's base/authorization binding, requires exact evidence-digest
agreement across the overlay declaration, the authorization's granted
evidence, and the base's own evidence sources, and refuses replacing an
unauthorized or out-of-selection question. Reuses OverlaySpec's existing
result_origin field (ResultOriginSpec) for per-question origin
assignment rather than inventing new parameters: inference_repair
requires an explicit, valid repair origin for every replaced question
(no bare "mixed", no missing/extra assignments); offline_transformation
forbids declaring a new origin and retains the base response's own
underlying prediction_origin. Builds each replaced row's lineage
component via the existing Task 5 identity.py helpers, so the narrower
"declared_external" identity/source-ownership checks already built into
make_lineage_component apply here unchanged.

Scope boundary (documented in the module docstring): derive_overlay
covers only the transformation's own replaced rows and their lineage; it
does not merge them with the base's retained rows or construct the final
realization -- that full-graph assembly belongs to the import engine
(Unit G), which alone has the base run directory and destination for the
new run.

Focused: 21 new tests (11 authorization, 10 overlays), all passing in
under 0.2s. Full suite: 1263 passed (1242 baseline + 21), only the
pre-existing Python 3.14 google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
Task 11, reduced scope. evidence.py: open_verified_source reads a declared
regular file exactly once and hashes those same bytes forward (never
re-reads or re-derives the digest separately), refuses symlinks and paths
outside an optional containment root, and requires the read bytes match
the declared expected_sha256. write_evidence_blob stores referenced
row-level bytes at a run-local sha256-addressed path; identical bytes
already staged are reused, any divergent existing blob or sidecar is
refused. write_evidence_index/validate_evidence_index publish and verify
one self-digested index over all evidence records, transitively
validating every referenced blob.

transaction.py: fsync_tree flushes every file and directory under a
staged tree before publication. atomic_publish_directory_no_replace uses
a real renameat2(RENAME_NOREPLACE) syscall wrapper (isolated so tests can
mock its absence) and fails closed if the primitive is unavailable --
never falls back to os.replace or an existence-check race.
ImportTransaction holds the run's manifest lock for its lifetime, stages
new runs under a same-filesystem sibling directory marked with an
owner file (so cleanup only ever touches its own staging directory, never
an unrelated leftover), and for an existing run skips staging entirely --
publish() calls the caller's validator against the already-published
final directory instead.

Focused: 19 new tests (11 evidence, 8 transaction), all passing in
under 4s together with tests/test_release_adversarial.py and
tests/infra/. Full suite: 1282 passed (1263 baseline + 19), only the
pre-existing Python 3.14 google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
Task 12, base (non-overlay) import path. build_import_plan resolves
sources/datasets/models/methods/prompts, opens and parses each condition's
declared CSV(s), validates rows against the expected dataset snapshot,
computes evidence status, builds one lineage component per evaluable row,
and assembles a v3 manifest -- performing every read/validation/identity
step without writing anything. execute_import(dry_run=True) returns the
same report a real import would, with wrote_artifacts=False and no
workspace directories created. A real import stages evidence blobs/index,
the manifest, run state, per-realization validation artifacts, and result
CSVs through ImportTransaction, publishing once atomically; an existing
run is verified in place (idempotent no-op) rather than restaged.
verify_import_run is the shared full-graph verifier: it validates the
manifest, run state, evidence index, every validation artifact against
its recorded checksum, and every result artifact, returning immutable
VerifiedBaseRealization values only after the whole graph passes -- the
foundation the not-yet-wired overlay path (see below) will build on.

Report schema is reduced from the plan: counts (conditions/evidence_status/
scope_disposition), a defect summary, and condition/realization digests --
enough to confirm exact status/queue reproduction and reject held/excluded
work, not the full historical checksum-report/column-disposition breakdown.

Two supporting fixes surfaced by wiring real code through Task 5's identity
layer:
- identity.py's canonicalize() gained bytes/bytearray support (hex-encoded).
  Real functions reference plain data constants (e.g. csv_adapter.py's UTF-8
  BOM marker) that a resolved-globals check now inspects; canonicalize
  previously had no representation for bytes at all. Narrow and additive:
  the only previous behavior for bytes was an unconditional TypeError, and
  the full suite (1289 tests, up from 1282) confirms no existing caller
  relied on that.
- The engine binds importer/lineage runtime-callable identity to small
  local marker functions rather than the real parse_csv_source/
  validate_source_rows: those reference many stdlib globals (hashlib.sha256,
  a C builtin; re; etc.) that are neither plain data nor inspectable
  user-level code, which Task 5's runtime-callable identity correctly
  refuses rather than silently ignoring. Binding to engine.py-local markers
  still captures "did this engine's own orchestration change" (identity
  hashes the whole defining file); the installed package version already
  carried in importer_implementation_identity is the primary signal for
  "did the installed release change." Documented in engine.py.

Scope boundary (unchanged from the module docstring, now proven out by a
working base-import path): applying an authorized repair/offline-
transformation overlay on top of a published base run is the next
increment. Every piece it needs -- identity, validation, authorization,
overlay derivation, evidence storage, atomic transactions, and this
module's verify_import_run -- is built and tested; the merge-and-publish
orchestration itself is not yet wired into execute_import.

Focused: 7 new engine tests (plan validity, dry-run no-op, real import,
idempotence, verify, failure reporting, report serialization), all
passing together with every prior importing/* + schema test (223 total).
Full suite: 1289 passed (1282 baseline + 7), only the pre-existing Python
3.14 google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
Task 13, reduced scope. Adds read_manifest_result_set/
build_evaluation_report_for_result_set to io/readers.py; the existing
read_manifest_results() v2 wrapper is untouched, and cli/evaluate_run.py
(the pre-existing v1-only evaluator CLI) is not modified -- direct Python
API access is sufficient for the research workflow this session targets,
consistent with deferring the installed-CLI work (Task 17).

read_manifest_result_set never reads a manifest/state/result directly;
it goes through engine.py's verify_import_run (the shared full-graph
verifier), so an evaluated result set can never be built from an
unverified or tampered run. Auto-selection picks the single eligible
(result-bearing) realization per condition and refuses when a condition
has more than one, matching the plan's ambiguity-is-a-refusal rule;
explicit realization_ids selection validates every ID is both declared
and eligible before reading any CSV.

build_evaluation_report_for_result_set accounts for every realization in
the manifest, not just selected ones: each carries its evidence_status,
scope_disposition, prediction_origins, and a selected flag; only selected
realizations get computed accuracy metrics, so partial/malformed/held/
excluded realizations are visible in the report with empty metrics rather
than silently dropped.

Focused: 5 new tests, all passing together with the existing v2 reader/
evaluator tests (36 total) with zero changes to their behavior. Full
suite: 1294 passed (1289 baseline + 5), only the pre-existing Python 3.14
google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
Task 15, reduced scope. translate_stage1_paper_freeze() verifies checksums
for the canonical manifest, cell-status/expected matrices, and all four
queue files against checksums/checksums.sha256, parses the canonical
manifest into per-cell records, and explodes the queue files into
(cell_id, question_id) pairs -- reproducing the exact counts the research
objective needs to confirm.

Verified directly against the real, read-only Stage 1 freeze at
/home/cotenthusiast/Projects/model-generalization/paper_data_freeze during
development (never copied into the repo or committed): 138 approved / 687
held / 3 excluded pairs, all pairwise disjoint; the 100/84/4/12/2 paper-
matrix breakdown; and the 6-cell/18-pair offline-recoverable authority
(exact cell IDs and question IDs matching) all reproduce exactly.

One real correction this verification surfaced: an earlier assumption
(from this session's plan-based review, before real freeze access) that
rerun_queue.csv was a disjoint "776 forensic" authority tier was wrong.
Running the checksum-verified parser against the real file showed all 776
of its pairs are already a subset of the 828 classified (approved/held/
excluded) pairs -- and rerun_queue.csv carries no queue_disposition/
execution_authority/executable columns at all, unlike the three
classification files. Fixed to verify "rerun candidates are a subset of
classified" (a real, meaningful invariant) rather than a disjointness
check that would reject the real data.

Fail-closed invariants enforced: approved rows must declare
queue_disposition=approved/execution_authority=authoritative/
executable=true; held and excluded rows must declare executable=false;
approved/held/excluded must be pairwise disjoint; every rerun-queue
candidate pair must already be classified; all cells claiming
recoverable_question_ids must authorize the exact same question set.

Scope boundary (module docstring): this does not yet build ImportSpec/
AuthorizationSpec objects for the generic engine -- that needs the ARC/MMLU
ExpectedDataset trust chain (Task 16/Unit J, next) to validate authorization
grants against real selected question IDs, plus per-cell SourceArtifactSpec
construction across the freeze's distinct method CSV schemas.

Focused: 10 new tests against a small synthetic fixture (never real
historical data, per the plan's global constraint), all passing. Full
suite: 1304 passed (1294 baseline + 10), only the pre-existing Python 3.14
google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
Task 16, reduced scope. build_stage1_expected_datasets() verifies
checksums for each benchmark's raw/normalized/robustness_ids/
robustness_metadata files, then independently recomputes question_text/
correct_option/correct_answer_text/choices from each raw row and compares
them fieldwise against the archived normalized row at the same file
position. Raw and normalized files have identical row counts and order in
the real freeze, so a positional join is sufficient and avoids needing to
reproduce the archived normalizer's own question_id hash algorithm --
this is a ChoiceBench revalidation of internal freeze consistency,
recorded as a limitation on the returned ExpectedDataset, not a claim that
the archived normalizer or upstream publisher was authentic.

MMLU duplicate question_ids are handled with drop_duplicates(keep="first")
semantics, but only after requiring every duplicate occurrence to agree on
all parsed fields -- a disagreeing duplicate fails closed rather than
silently picking one. Selected question IDs (from robustness_ids.json,
preserving its own order, which differs from source file order) must all
exist in the deduplicated normalized set or the whole benchmark is
refused. Deduplicated rows are canonicalized into fresh CSV bytes and
handed to the existing Task 4 build_expected_dataset()/DatasetReferenceSpec
machinery, which independently re-validates and re-derives all identity
digests.

Verified directly against the real, read-only Stage 1 freeze (never
copied into the repo) during development: both ExpectedDataset objects
build successfully with exactly 1000 selected questions each, every one
of 1172 ARC and 14042 MMLU rows revalidates, and all 27 real MMLU
duplicate groups are confirmed field-identical before deduplication. This
surfaced one genuine, verified freeze quirk: 3 raw ARC questions have a
5th option that the archived normalizer silently drops (all 3 correct
answers fall within the first 4, so truncation never removes the correct
option) -- modeled explicitly with a fail-closed bound check rather than
either ignoring the option or crashing on an honest schema mismatch.

Scope boundary (unchanged from the module docstring, now fully proven
out): this and Unit I (queue/status/matrix accounting) are the two
verified building blocks a full ImportSpec/AuthorizationSpec assembly for
the actual repair-import workflow would combine; that combination --
plus per-cell SourceArtifactSpec construction across the freeze's method
CSV schemas -- is not done here.

Focused: 8 new tests (ARC variable/five-option handling, MMLU duplicate
dedup and disagreement rejection, checksum/revalidation/row-count/
missing-selected-ID failures) against a synthetic fixture verified to
match the real freeze's exact file formats; 18 total in this file, all
passing. Full suite: 1312 passed (1304 baseline + 8), only the
pre-existing Python 3.14 google.genai.types deprecation warning.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
…erride

- engine.py: execute_overlay_import/_merge_overlay_realization merge an
  authorized repair/offline-transformation overlay with a verified base
  run's retained rows into a new, separate, immutable run. Row/lineage
  assignments are reassembled in the expected dataset's question order
  (not retained-then-replaced concatenation order), matching
  make_realization's ordered-subset identity requirement. Divergent
  overwrite of an existing overlay run_id now raises (matching the base
  import path's verify_import_run(expected=...) contract) instead of
  silently treating a different experiment as an idempotent no-op.
- engine.py: ImportRequest gains an optional expected_datasets override so
  a profile needing dataset-specific revalidation/dedup (e.g. Stage 1's
  ARC/MMLU trust chain) can supply pre-built ExpectedDataset objects
  instead of the generic per-source rebuild, which cannot reproduce that
  logic (needed for MMLU's real duplicate question_ids).
- validation.py: extract compute_evidence_status as a reusable pure
  function from normalize_realization_rows (no behavior change), so a
  profile assembling declarations can discover the correct evidence_status
  to declare instead of guessing against the declaration-mismatch check.
- tests/importing/test_engine_overlay.py (new): merge, offline-
  transformation, dry-run, idempotence, divergent-overwrite refusal,
  unknown-base-realization, forged-source rejection.
- tests/importing/test_engine.py: expected_datasets override coverage.

1321 full tests passed.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
- profiles/stage1_paper_freeze.py: build_stage1_import_spec assembles all
  102 real cells into one ImportSpec (104 sources: 102 per-cell canonical
  CSVs + 2 documentation-only ARC/MMLU normalized-reference sources bound
  to ImportRequest.expected_datasets, never opened by build_import_plan).
  Each condition's declared evidence_status is reconciled via an actual
  probe (validate_source_rows + compute_evidence_status) rather than
  trusted blindly from the freeze's own STATUS_MAP-mapped status.
  build_stage1_cell_authorization builds one AuthorizationSpec scoped to
  exactly one cell (a multi-cell bundle can never satisfy
  derive_overlay's evidence-digest cross-check for any single cell's
  overlay -- verified by reading overlays.py).

  VERIFIED DIRECTLY against the real freeze (never committed): assembly
  runs cleanly end-to-end and reconciles to complete=49/malformed=45/
  qualified=8 (vs. the freeze's own 57/28/5/4/6/2 STATUS_MAP breakdown).
  Confirmed by inspecting finding codes that the entire divergence is
  MISSING_PREDICTION (zero CORRECT_OPTION_MISMATCH/INVALID_PREDICTION/
  UNEXPECTED_QUESTION_ID anywhere, i.e. no column-mapping bug): 20 of 28
  "malformed_requires_inference" cells have zero row-level defects (their
  defect, e.g. a phantom/NaN rendered option D, is parse_ok/valid-answer
  and invisible to generic validation) and reconcile to "complete"; 28 of
  57 "canonical_complete" cells have MISSING_PREDICTION rows the freeze's
  own classification tolerates (one MMLU cell has 152/1000 parse_missing
  rows, its own score_status="score_unscorable", yet is declared
  "canonical_complete") that ChoiceBench's fail-closed generic validator
  does not -- a genuine, intentional strictness difference, not a bug.
  All 102 conditions' build_import_semantic_identity calls succeed with
  102 unique condition_digests.

- tests/importing/test_stage1_import_spec.py (new): synthetic-fixture
  coverage for assembly shape, the recoverable-cell and malformed-cell
  reconciliations, excluded-cell scope_disposition, the per-cell
  checksum-ledger cross-check, and a full build_stage1_cell_authorization
  -> validate_authorization_bundle round trip.

  Also fixes a real bug this surfaced: build_stage1_import_spec's
  ImportConditionSpec left unknown_reasons empty despite declaring
  calibration_identity/preflight_identity as None, violating identity.py's
  unknown-reason contract.

520 focused / 520 targeted tests passed (tests/importing/).

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
…y gap)

Discovered and fixed while running Task 19's full-workflow validation of
the importer against the real, read-only Stage 1 freeze:

- engine.py: ImportRequest/OverlayImportRequest gain an optional
  source_containment_root, separate from workspace_root, for
  open_verified_source's path-escape check. Sources previously had to live
  under workspace_root itself; importing a real, independent, read-only
  data root into an isolated temporary CHOICEBENCH_HOME needs these to
  differ. Defaults to workspace_root, so all existing behavior/tests are
  unaffected. 2 new tests cover the rejection and the override.

- provenance.py: implementation_identity() called
  importlib.metadata.packages_distributions() -- an uncached full scan of
  every installed distribution's metadata -- once per lineage component
  (i.e. potentially once per row). At 102 conditions x up to 1000 rows,
  that's ~100,000 calls of something that cannot change within a
  process's lifetime, and it made a real import run take 30+ minutes
  instead of ~4. Fixed with a simple process-wide cache
  (_cached_packages_distributions). This also cut the full test suite
  from ~8-9 minutes to ~64 seconds, since many tests exercise
  make_realization/implementation_identity too.

- identity.py: _validate_realization_identity required "complete"/
  "qualified" realizations to have full result-origin coverage of the
  expected dataset regardless of scope_disposition, but
  normalize_realization_rows's own evaluable computation already treats
  scope_disposition != "included" as non-evaluable regardless of
  evidence_status. This combination (individually row-complete/qualified,
  but excluded_from_paper_matrix for an unrelated scientific reason) was
  never exercised before real data -- the real freeze has exactly 2 such
  cells (both "pride"/qwen-2.5-7b-instruct-turbo). Widened the check to
  match normalize_realization_rows's own definition. 2 new tests: one
  regression (full coverage still required when included), one for the
  newly-allowed case.

Full Task 19 validation (assemble all 102 real cells, dry run, real
import into an isolated CHOICEBENCH_HOME, idempotent re-import, verify
representative complete/qualified/malformed/variable-option-ARC cases,
apply one synthetic inference-repair overlay and one offline-
transformation-mechanism overlay into new immutable derived runs, read +
evaluate all three runs, confirm divergent-overwrite refusal, confirm the
freeze remains byte-identical) now passes end-to-end against the real
freeze.

Finding for the record (not a bug): of the 102 real cells, ChoiceBench's
independently-computed evidence_status (complete=49/qualified=8/
malformed=45) disagrees substantially with the freeze's own STATUS_MAP-
mapped classification (57/5/28/4/6/2) -- confirmed via finding codes to be
entirely MISSING_PREDICTION (no mapping bug). Also: none of the 6 real
offline-transformation-authority cells currently have an evaluable base
realization (each has 112-229/1000 rows with a parse failure unrelated to
their 3 authorized recoverable questions), so the offline-transformation
overlay demo above uses a different, evaluable real cell to prove the
mechanism, not one of the 6 official recoverable cells.

1332 full tests passed (up from 1321; +2 engine, +2 identity, no
regressions).

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
…cesses

Two independent bounded reviews (spec-compliance and adversarial code
quality) of commits 965dc77..79fffee both found the same Critical bug:
_merge_overlay_realization built retained_lineage_components by iterating
retained_lineage_ids, a set, so its order depended on Python's per-process
string hash randomization. That order flowed unchanged into
realization_identity["lineage_components"], which make_realization hashes
in list order -- so the merged realization/experiment digest was not
reproducible across processes whenever more than one base row was
retained (the normal case for a real repair overlay: replace a handful of
questions, retain the rest). A fresh-process idempotent re-import could
then spuriously compute a different experiment_digest and raise "belongs
to a different experiment" instead of the intended no-op.

Fix: build lineage_components in the same deterministic order already
used for row_assignments (expected_dataset question order), via a
lineage_id -> component lookup keyed off the already-ordered
row_assignments list, instead of iterating a set directly.
retained_lineage_ids itself is unchanged (still a set) since its only
remaining use, lineage_runtime_callables, is a dict keyed by lineage_id
where iteration order doesn't matter.

Added a deterministic (non-flaky) regression test: a 3-question overlay
retaining 2 rows and replacing 1, asserting lineage_components matches
row_assignments' order exactly. Verified the test fails against the
pre-fix code (order came out ['q2','q3','q1'], the old "retained then
replaced" order) and passes with the fix (['q1','q2','q3'], matching
expected_dataset's own order) -- confirming it actually catches the bug,
not just probabilistically via hash-seed comparison. Also extended
test_engine.py's _raw_spec/_spec fixtures with an optional question_ids
parameter (default unchanged) to support building larger synthetic
fixtures.

1333 full tests passed.

Claude-Session: https://claude.ai/code/session_017nAzkbNH4HiJeZnojCGhBj
Backport of ac949ed from analysis/final-paper-authoritative-run.

_merge_overlay_realization computed transformation_input_digest and
preownership_output_digest from derive_overlay's sorted(replacement_ids)
order, but identity.py recomputes them from the realization's own
dataset-ordered lineage_components. When >=2 replaced IDs' dataset order
differs from their sorted order, integrity_digest (which hashes list
order) diverged and identity validation raised a false-positive
'offline_transformation overlay input, output, or implementation digest
conflicts with its lineage components.' Compute the digests from the same
dataset-ordered, operation_type-filtered derived components the verifier
uses. Adds a real-lineage regression test (reverse dataset-order IDs).

(cherry picked from commit ac949ed)
…se realizations

Backport of 0f1c0a6 from analysis/final-paper-authoritative-run.

The overlay-merge path previously gated inference_repair/offline_transformation
overlays on the base realization being evaluable (a published, scoreable
result). This wrongly refused overlays against a malformed-but-structurally-
complete base -- e.g. a cell whose repair-target rows are fine but which has
unrelated, benign parse-missing rows elsewhere -- even though such a base has
a full, publishable canonical row set safe to patch.

Introduces a distinct overlayable gate: the base must have published a full
result CSV whose row-identity is exactly the expected complete question set
(verified via verify_import_run reading results for a new 'published_unscored'
run status, alongside the existing 'completed'). Structurally broken bases
(missing/duplicate/unexpected rows, or no published result) are still refused
fail-closed with the same behavior as before.

After a merge, the derived realization's evidence_status is recomputed from
the actual merged row content (never forced to the overlay's declared status),
while the overlay's declared/intended historical status is preserved
separately as declared_evidence_status. The base realization remains
immutable throughout.

(cherry picked from commit 0f1c0a6)
…r portion)

Backport of 2477625 from analysis/final-paper-authoritative-run, importer
portion only. verify_import_run's per-row dict conversion re-coerced a blank
(unparseable) predicted_option cell to a float NaN even after DataFrame-level
NA cleanup (pandas' per-row Series dtype inference during iterrows() collapses
a real None back to NaN -- confirmed empirically). The manifest writer's
canonicalize() rejects NaN outright, so a malformed-but-structurally-complete
base with a genuinely blank prediction in a retained (non-overlaid) row
previously blew up mid-overlay. Normalize per-value at the read boundary
instead. A 'completed' realization's rows never contain NaN (evaluable
requires a valid prediction in every row), so this is a no-op on the existing
path. Adds a real-engine regression test in test_engine_overlay.py.

The original commit's other half -- import_orchestration.py's
authorized_question_ids cross-check against a patch's declared question IDs
-- lives in experiments/final_paper_analysis/, which does not exist on this
branch (that's analysis-branch-specific orchestration code built on top of
this importer, not part of the importer itself). Only the importer-side fix
applies here; nothing in this branch currently calls the overlay-request
builders that half of the original commit hardened.

(cherry picked from commit 2477625)
…rter portion)

Backport of daed183 from analysis/final-paper-authoritative-run, importer
portion only. csv_adapter.py's _validate_structured_choices required every
choice object to carry an explicit string 'label' key; real fourth-cell data
only carries an integer 'source_index' (no label at all). Now accepts either
an explicit string label (existing behavior, unchanged) or a positional
integer index (0 -> 'A', 1 -> 'B', ...), letterized before the existing
duplicate-label check. Added regression tests: positional labels accepted,
out-of-range index rejected, duplicate positional index rejected.

The original commit's other half -- import_orchestration.py's
build_fourth_cell_source switching to mode='structured_json' against the
real choices_json/source_index shape -- lives in experiments/final_paper_analysis/,
which does not exist on this branch (analysis-branch-specific orchestration
built on top of this importer). Only the importer-side format-handling fix
applies here.

(cherry picked from commit daed183)
…kages

test_built_wheel_and_sdist_run_outside_repository's functional CLI venv was
created with --system-site-packages and installed the wheel with --no-deps,
assuming the runtime deps (pandas, etc.) would be inherited from context.
Under a genuinely clean venv/container --system-site-packages inherits an
empty or unrelated site-packages instead, so choicebench-run etc. crash on
missing imports. Reproduced with PYTHONNOUSERSITE=1: the nested venv had no
pandas. Fixed by making the venv fully isolated and installing the wheel
with its full dependency set instead of relying on inheritance. Verified
green under PYTHONNOUSERSITE=1 (full suite, 810 passed) to confirm the fix
holds without this machine's incidental ~/.local package leak.
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