Skip to content

M2X-041: make the Phase 1B gate worth running — seal, replayable fixtures, derived citations - #35

Open
yashpancholi09 wants to merge 3 commits into
mainfrom
feature/m2x-041-gate-preconditions
Open

M2X-041: make the Phase 1B gate worth running — seal, replayable fixtures, derived citations#35
yashpancholi09 wants to merge 3 commits into
mainfrom
feature/m2x-041-gate-preconditions

Conversation

@yashpancholi09

Copy link
Copy Markdown
Collaborator

Makes the Phase 1B gate worth running. Three fixes, one per commit.

Run before M2X-040 rather than after it. This ticket says "ONLY if M2X-040 fails" — which, followed literally, means burning the sealed held-out set to unlock the ticket that repairs the thing the run would have failed on. Supervisor's decision on 2026-08-13: fix first, gate later. Recorded in docs/design/day4-gate-recovery.md, docs/gates.md and on the Odoo tickets.

1. The seal did not exist (4a1524a)

git ls-files eval/labels/heldout/ returned .gitkeep and nothing else, against a CLAUDE.md rule that says the seal is physical.

Two artefacts, because they answer different questions. The ciphertext makes the set recoverable. The SHA-256 manifest makes it checkable — and it is the one that proves anything, since gpg salts every invocation and a .gpg diff therefore cannot distinguish a re-seal from a content change. Digests leak nothing, so verify runs without the passphrase: a reviewer can confirm the set is unedited while remaining unable to read it.

gpg rather than age, per the supervisor — age is not installed here. The *.age whitelist stays.

Recorded, not glossed: the seal was applied after the cases were written and after dev iteration had run, so it makes edits visible from this commit forward and says nothing about the window before it.

2. The number was not reproducible by anyone (99874f0)

v3 scored 0.4279 on one checkout and 0.3086 on another — same prompt, matcher, threshold and 15/15 case set, differing only in which sampled outputs each git-ignored data/cache/ held.

--fixtures replay scores committed outcomes and contacts no provider. Verified end to end on the real dev set: record and replay both print 0.3882, with identical per-field TP/FP/FN and the same schema failure named.

refusal what it prevents
missing fixture aborts a case leaving the denominator invisibly
transcript-digest mismatch aborts scoring an old answer against edited eval/tiron/ words
provider failures never recorded a 429 becoming a permanent gate number

Schema failures are recorded — 100% schema validity is a gate leg, and a fixture set of only successes would report it green by construction.

Honest caveat: replay removes the generation provider, not the embedding one. Embeddings are a single forward pass with no sampling and reproduce given the same model; generation samples. Freezing the sampled half is sufficient.

3. Citation drift (6ed6794)

The extractor paired a segment id with the previous line's timestamps (seg-0033 cited as 580.3-581.4 when it runs 581.44-586.445), burning the case's whole retry budget. v3's explicit citation rule did not move it.

Evidence.t_start/t_end are now derived from segment_id; any value the model supplies is discarded. segment_id still resolves, and that is the check that catches invention. Gives up sub-span citation deliberately — all 84 cited items in the dev labels name a whole turn.

v7 was written to test the prompt half, measured, and lost. Like-for-like over the fourteen cases both prompts answered: v3 0.3882, v7 0.3704. Its 15/15 schema-validity is not a demonstrated fix either — v3 also scored 15/15 at d486372, so the single failure here is sampling noise. v3 stays pinned; v7 stays on disk with a changelog row saying it failed.

Nothing is lost by not pinning it, because the fix is in the schema. Instructor renders Field(description=...) into the request, so "Do not emit this field" reached the model and overrode v3's own instruction — every citation on the v3 run was {"segment_id": ...} and nothing else.

Injections re-measured under the new contract: still 3/3.

Review notes

  • Four existing tests were changed, not added — they asserted the contract this replaces. Two rejections are now unreachable rather than caught, which is the stronger property.
  • Not in this branch: M2X-041's fresh 5-case held-out retry. Those labels must be written after the fixes settle or they target a moving object; the sealing tooling they need is what this delivers.
  • One manual step remains — the ciphertext needs the Evaluator's passphrase, deliberately unavailable to the tooling that generated the manifest: uv run python scripts/seal_heldout.py seal.

566 tests green.

…gests

CLAUDE.md requires a gate number the supervisor reproduces on a fresh clone, and
requires the held-out seal to be physical. Neither held: eval/labels/heldout/ had
only .gitkeep in git, so a fresh clone had no set to run and nothing showed the ten
cases were unedited between the freeze and the gate.

scripts/seal_heldout.py writes two artefacts because they answer different
questions. The ciphertext makes the set recoverable. The SHA-256 manifest makes it
checkable -- and it is the one that proves anything, since gpg salts every
invocation and a *.gpg diff therefore cannot distinguish a re-seal from a content
change. Digests leak nothing, so verify runs without the passphrase: a reviewer can
confirm the set is unedited while remaining unable to read it.

gpg symmetric rather than age, per the supervisor's decision -- age is not installed
on this machine and gpg ships with Git for Windows. The *.age whitelist stays.

Recorded, not glossed: the seal was applied after the cases were written and after
dev iteration had already run here, so it makes edits visible from this commit
forward and says nothing about the window before it. Sealing at freeze time would
have been strictly stronger.

The ciphertext itself is not in this commit -- it needs the Evaluator's passphrase,
which is deliberately not available to the tooling that generated the manifest.
… number

PR #34 raised this for the supervisor and it is now decided: freeze the extraction
outcomes rather than seeding the sampler or reporting an interval.

The defect: the same prompt, commit, matcher and fifteen cases scored 0.3086 on one
checkout and 0.4279 on another. temperature is already 0.0, no seed is sent, and
data/cache/ is git-ignored -- so the only difference was which sampled outputs each
clone happened to hold. docs/gates.md asks for a number the supervisor reproduces on
a fresh clone; nobody could reproduce that one.

--fixtures replay scores committed outcomes and contacts no provider, so a fresh
clone with no API key lands on the same number. It reproduces scoring, not sampling:
a fixture freezes one draw, and re-recording is an explicit act that appears as a
diff. That is precisely what the cache could not offer.

Three refusals carry the trust, each closing a way the number could lie. A missing
fixture aborts rather than shrinking the denominator. A fixture whose transcript
digest no longer matches aborts rather than scoring an old answer against edited
words -- labels store bounds and re-derive segments, so an eval/tiron/ edit would
otherwise move the text under a fixture silently. Provider failures are never
recorded, because a 429 is a fact about a network and freezing one would make a bad
afternoon permanent.

Schema failures are recorded, unlike provider failures: 100% schema validity is a
gate leg, and a fixture set holding only successes would report that leg green by
construction.

Scoring is factored into one helper used by both paths, so live and replay cannot
drift apart -- the one thing replay must never do.
…for them

The largest single contributor to schema-validity failures. The extractor paired a
segment id with the *previous* line's timestamps -- seg-0033 cited as 580.3-581.4
when it runs 581.44-586.445 -- which failed evidence validation, consumed that
case's whole retry budget, and removed it from the micro-F1 denominator. v3's
explicit citation rule did not move it, which pointed at the model rather than the
wording.

So the field the model kept getting wrong is no longer a field the model fills.
Evidence.t_start and t_end are derived from segment_id and any supplied value is
discarded. This is M2X-044's principle applied to extraction: a timestamp the model
cannot type is one it cannot invent. segment_id still resolves against the
transcript, and that is the check which catches invention -- a model that fabricates
a decision fabricates a segment id to go with it.

Gives up sub-span citation, deliberately. Segments are single speaker turns and all
84 cited items in the dev labels name a whole turn. Removes TIME_TOLERANCE_S, which
existed only because 83 of those 84 copied the rendered one-decimal timestamp rather
than the segment's exact float bounds.

Four tests changed rather than added: they asserted the contract this replaces. Two
rejections are now unreachable rather than caught, which is the stronger property.

v7 is v3 with the matching prompt rule, added so the claim could be measured rather
than assumed. It did not win: 0.3704 against v3's 0.3882 over the fourteen cases
both answered. Its 15/15 schema-validity is not a demonstrated fix either -- v3 also
scored 15/15 at d486372, so v3's single failure here is sampling noise, and one
sample per prompt cannot tell a fix from a lucky draw. v3 stays pinned; v7 stays on
disk with a changelog row saying it failed.

Nothing is lost by not pinning it, because the fix is in the schema. Instructor
renders Field(description=...) into the request, so "Do not emit this field" reached
the model through the schema and overrode v3's own instruction to supply
timestamps -- every citation on the v3 run was {"segment_id": ...} and nothing else.
The schema is a prompt, and where the two disagreed the schema won.

Injections re-measured under the new contract: still 3/3.
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.

2 participants