M2X-041: make the Phase 1B gate worth running — seal, replayable fixtures, derived citations - #35
Open
yashpancholi09 wants to merge 3 commits into
Open
M2X-041: make the Phase 1B gate worth running — seal, replayable fixtures, derived citations#35yashpancholi09 wants to merge 3 commits into
yashpancholi09 wants to merge 3 commits into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdand on the Odoo tickets.1. The seal did not exist (
4a1524a)git ls-files eval/labels/heldout/returned.gitkeepand nothing else, against aCLAUDE.mdrule 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
.gpgdiff therefore cannot distinguish a re-seal from a content change. Digests leak nothing, soverifyruns 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
*.agewhitelist 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 replayscores 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.eval/tiron/wordsSchema 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-0033cited as580.3-581.4when it runs581.44-586.445), burning the case's whole retry budget. v3's explicit citation rule did not move it.Evidence.t_start/t_endare now derived fromsegment_id; any value the model supplies is discarded.segment_idstill 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
uv run python scripts/seal_heldout.py seal.566 tests green.