test(tools): gate the logprob gap check on the engine preamble; add the native-MTP witness - #1357
Open
monotophic wants to merge 13 commits into
Open
monotophic wants to merge 13 commits into
monotophic wants to merge 13 commits into
Conversation
The evidence tooling reads the engine's own stdout. Two of its lines are typed records -- the "== GLM C engine" banner and the "loaded in ..." line that follows it -- and every consumer that wants a field out of them was about to reimplement the same grammar. This adds one module that owns the exact text, the field ranges each number may take, and the errors a malformed line produces, so the consumers agree by construction. It also owns the canonical form of an ablation manifest: the engine reads a manifest a line at a time, drops the terminator and one carriage return before it, and digests the record followed by a single newline. canonical_manifest_bytes() is that rule written once for the Python side, so a checker and the engine cannot drift into hashing different bytes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
check_ablate_evidence.py re-checks one ablation manifest and its JSONL evidence artifact against an independently supplied config.json, without the engine and without any test binary: the config and manifest field bounds, every record's key set and field types, the header's binding to the config and to the manifest digest, and the truncation, replay and mismatch cases at each record boundary. Three cross-record invariants are enforced because the producer guarantees them for every row it can emit -- nll is never negative, corr agrees with the argmax-equals-gold comparison, and the argmax logit is never below the target's. Top-k order is deliberately not enforced: it is unsorted on the wire by design. The manifest is reduced through the shared canonical form before it is parsed or hashed, so a manifest saved with CRLF endings or without a final newline -- both of which the engine accepts and normalises -- is checked rather than refused, and binds the same digest the engine bound. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
eval_glm.py scored answers from whatever the engine printed and wrote its results only after the whole run finished. Two consequences: a foreign line on that stream was absorbed silently, and an interrupted run left nothing to read. Result rows are now appended per request, so a run that dies mid-task leaves a valid partial file carrying an INCOMPLETE marker and still prints the accuracy table over the rows that landed. Every line of the scoring stream is classified against the engine's documented SCORE-mode output and an unrecognised line is refused by name instead of ignored, and the numeric grammar accepts both the six-decimal form the engine ships today and the older %.17g form. Where the engine build cannot supply the evidence record the harness would bind to, the run is marked UNBOUND by name rather than reported as bound. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pack_python.py reaches a file two ways: by following imports out from coli, and by spotting scripts that are launched as subprocesses. Only the first kind had its own imports followed, so a module that only a subprocess-launched script imports was left out of the release archive while --check still reported the archive complete. On dev this happens twice: tools/iq3_pack.py, which tools/convert_fp8_to_int4.py imports inside quant_e8(), and tools/engine_evidence.py, which this branch's eval_glm.py imports. Both are now reached. The test suite grows a real-tree assertion as well as fixture cases, because every existing case builds a disposable tree and so could not have caught either omission. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The gap checker read a transcript without first establishing that the transcript came from an engine it understands, and it reported problems without saying where in the byte stream they were. It now parses the engine's preamble through the shared helper and fails loudly when that preamble is unparsable, rather than silently treating an unrecognised header as no header. The protocol grammar is exact, the numeric tail and the top-k cap are enforced, and every reported problem cites the byte offset of the frame it concerns. The numeric grammar accepts both the fixed six-decimal form the engine ships today and an earlier %.17g form; a token can satisfy both spellings exactly, so form classification stays informational -- a tally in the summary line -- and never a pass/fail rule. The transcript's capture shape is classified and enforced explicitly instead of being inferred. The docstring records that the gap this check hunts is not producible by today's mux loop: it is regression coverage for that invariant, and the preamble check does not itself bind a binary or container digest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
check_native_mtp_witness.py captures and validates a witness that the native multi-token-prediction path really accepted the tokens it claims, binding the capture to the binary, the container and the request bytes by SHA-256 so a witness cannot be replayed against different inputs. It reuses the gap checker's frame grammar rather than restating it, so the two cannot drift. Its exit statuses are distinct on purpose: 0 is a live-capture pass, 1 is every ordinary malformed, incomplete or inconsistent witness (including an offline replay, which is always incomplete on provenance even when clean), and 3 is the separate case where the engine build cannot produce this witness at all. That last one is proof about the build, not a malformed witness, so it prints its own stderr prefix and does not share an exit status with real failures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On Windows, os.kill(pid, SIGTERM) is TerminateProcess with the exit code set to the signal number: no Python handler runs and the whole unittest process dies with status 15, which is how the Windows UCRT64 job reported `make: *** [test-python] Error 15` with no test summary. The behaviour the test asserts (a real SIGTERM converted into engine-child cleanup) is a POSIX mechanism; the SIGINT/exception half of the pair covers child cleanup on every platform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 0a8bc2a)
On Windows, os.kill(pid, SIGTERM) is TerminateProcess with the exit code set to the signal number: no Python handler runs and the whole unittest process dies with status 15, which is how the Windows UCRT64 job reported `make: *** [test-python] Error 15` with no test summary. The behaviour the test asserts (a real SIGTERM converted into engine-child cleanup) is a POSIX mechanism; the SIGINT/exception half of the pair covers child cleanup on every platform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… its newline The checker prints its PASS line with print(), so a Windows child hands the test CRLF where POSIX hands LF, and the byte-exact assertion failed on the Windows UCRT64 job (the only failure in a 914-test run). Nothing consumes that line byte-for-byte; the pin is on the content, so the test now folds CRLF to LF before comparing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit b8d3c67)
… its newline The checker prints its PASS line with print(), so a Windows child hands the test CRLF where POSIX hands LF, and the byte-exact assertion failed on the Windows UCRT64 job (the only failure in a 914-test run). Nothing consumes that line byte-for-byte; the pin is on the content, so the test now folds CRLF to LF before comparing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… refuses on Windows The witness proves a snapshot payload did not change while it was hashed by comparing fstat of the open descriptor with lstat of the path on device, inode, type, size and times. That agreement is POSIX-defined. On the Windows UCRT64 job the two disagreed on 9 of 45 fixture files (file-dependent; the exact field is not established), which surfaced as a spurious "snapshot payload changed while hashing". The witness is an instrument for engine runs on the Linux fleet, so it now refuses on Windows with an error that names the requirement instead of reporting a change that did not happen. The fixture suite skips on Windows for the same reason; two new tests, which run on every platform, pin the refusal and the working case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolves the conflict JustVugg#1364 created, so this branch does not need a rebase. JustVugg#1364 landed the packer commit from this PR (cherry-picked, authorship intact) plus a second edge it left open: a data file opened next to a reached module. tools/pack_python.py therefore takes dev's version wholesale, which is a strict superset of this branch's -- the only lines unique here were the ones that commit replaced. tests/test_pack_python.py takes dev's version and keeps this branch's engine_evidence assertion as an ADDITIONAL real-tree case rather than a replacement. On dev that assertion could not run, because engine_evidence.py is introduced here; on this branch it pins the edge this branch adds, next to the one that pins the historical bug. 12 tests, all green.
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.
Authored by Claude Opus in Claude Code, analysis in partnership with @monotophic
Two command-line checks that read an engine transcript and say whether it is
internally consistent.
tests/check_data_logprob_gaps.pyalready existed and had two weaknesses. Itread a transcript without first establishing that the transcript came from an
engine it understands, so an unrecognised header was treated as no header; and
it reported problems without saying where in the byte stream they were, which
makes a disagreement on a long capture tedious to localise. It now parses the
engine's preamble through a shared helper and fails loudly when that preamble
is unparsable, the protocol grammar is exact, the numeric tail and the top-k
cap are enforced, and every reported problem cites the byte offset of the frame
it concerns. The numeric grammar accepts both the fixed six-decimal form the
engine ships today and an earlier
%.17gform (plusnan/inf/-inf); atoken can satisfy both spellings exactly, so form classification stays
informational — a tally in the summary line — rather than a pass/fail rule.
tests/check_native_mtp_witness.pyis new. It captures and validates a witnessthat the native multi-token-prediction path really accepted the tokens it
claims, binding the capture to the binary, the container and the request bytes
by SHA-256 so a witness cannot be replayed against different inputs. It reuses
the gap check's frame grammar by importing it rather than restating it, so the
two cannot drift apart. Its exit statuses are distinct on purpose:
0is alive-capture pass;
1is every ordinary malformed, incomplete or inconsistentwitness, including an offline replay, which is always incomplete on provenance
even when clean; and
3is the separate case where the engine build cannotproduce this witness at all — that is a fact about the build, not a malformed
witness, so it prints its own stderr prefix and does not share an exit status
with real failures.
That last case is the common one today: the per-emission record this proof
depends on is not printed by the engine as it stands, so on a current build the
tool reports
UNSUPPORTEDand exits 3 rather than inventing a verdict. Thedocstring says so.
Context. This PR is one of four independent contributions derived from a
single locally-verified working tree (offline evidence tooling, the engine's
ablation scoring mode, these two transcript checks, and a raw wire-format
adapter). This one carries the two transcript checks and their suites. It uses
one file from the evidence-tooling PR (#1355, #1355),
tools/engine_evidence.py, which both tools import at module load for thepreamble grammar — measured: with that PR absent both modules fail to import, and
with it present the two suites here run 105 cases green. The others are proposed
separately, each with its own evidence.
Behavioral contract
offending line, never a silent pass.
evidence of anything by itself.
unsupported, with its own exit status, and never as a pass or an ordinary
failure.
built from literals inside the suite.
Capstone matrix — one decisive artifact per claim.
test_unparsed_banner_is_a_named_failure_quoting_the_line,test_unparsed_loaded_is_a_named_failure_quoting_the_line, andtest_full_capture_with_unparsed_preamble_fails_the_whole_check— the last one drives a complete, otherwise-valid capture and requires the whole check to failtest_preamble_returning_none_is_defensively_named_not_silently_passed— the case where the parser returns nothing at all is asserted to be a named failure, which is the shape a "no news is good news" gate would passtest_fixed6_form_is_accepted_and_reported,test_c17g_form_is_accepted_and_reported, andtest_a_token_matching_both_grammars_is_ambiguous_and_still_accepted; the fixtures use the engine's own printf formatstest_done_domains_and_fixed_grammar_bite_independently— each rule is shown to fail on its own input rather than being masked by a neighbourtest_engine_witness_unsupported_when_mtpemit_absentandtest_engine_witness_unsupported_is_distinguishable_at_the_cli— the second asserts the exit status and the stderr prefix, not just the internal statustest_every_artifact_binding_bites_independently, plustest_capture_owned_paths_cannot_be_replaced_before_binding: a capture whose file is swapped between hashing and binding is refused, and each binding is shown to fail on its owncheck/parse_frames; there is no second copy of the grammar to keep in stepFuller matrix and origin accounting
python3 -m unittest tests.test_check_data_logprob_gaps tests.test_check_native_mtp_witnessondevplus these four files and that PR'sRan 105 tests … OKA limitation we would rather state than have you find: the gap this check hunts
is not producible by the engine's current multiplexing loop, so on today's builds
it is regression coverage for an invariant rather than a live detector, and the
preamble check binds no binary or container digest by itself. Both facts are in
the tool's own docstring.
Origin accounting. The witness tool is new content. The gap check's changes are
re-expressed from work done against this engine earlier and brought onto current
dev; the byte offsets and the explicit classification of the capture's shapewere added during review, as was the separate exit status for the unsupported
case. No file outside the four listed is touched.
Durable vs current state: the grammars, the gate and the exit-status
contract are durable. The case count (105) is current state, measured 2026-09-04
against base
e1efc68on macOS (Apple silicon) with Python 3.12.