HOMELAB-2682: fix(sbx-gits): the ledger says which class ran and keeps each run's results (PR A of the evidence fixes) #853
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!853
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-2682-evidence-fixes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two of the three evidence gaps gits-hypervisor found while checking a real merge. (The third — seat transcripts in
/tmp/pi-subagents-1000— touchesstatefulset.yaml, which this branch's pinnedT7[diff]set forbids, so it is PR B, on its own branch.)The hard constraint first
The ledger started a fresh chain at 2026-11-05 13:08Z and already had lines on it. A field that
checkrequires would re-break every head already recorded. So both new fields are OPTIONAL:chain_textfromorigin/liveand from this branch and executing both (harness §24 case 3);prev), so adding, removing or retyping either one breaks the chain;checkverifies them only when present, and the message names the leg that failed.Live ledger state read (read-only, nothing written):
kubectl exec -n sbx-gits sbx-gits-0 -c sandbox -- wc -l /home/ld/work/.emu-proof/ledger.jsonl→ 19 lines, first post-boundary linef068805b…at 13:15:10Z (result:FAIL, rc:5, tests_run:0), lastb3382259…at 16:21:28Z (OK,tests_run:1). All 18/19 lines carry the same key set — none hasclassorresults_sha256, which is exactly the shape the optional design has to keep validating. Verified:checkon a copy of that live file reports nohash no cuadra/prev no encadena(only the gate differences of a suite sha that this PR moves, and the captures not copied along with the file).(1) The line did not say which class ran
task:app:connectedDebugAndroidTestnames the task, not the class, and the class does not have to come from the repo: anandroid.testInstrumentationRunnerArguments.classin a sharedgradle.properties($GRADLE_USER_HOME, i.e./home/ld/work/.gradle/gradle.propertiesin the pod, outside the tree) filters the instrumentation whiletree_cleanstaystrue. A green line attesting a whole suite while one filtered class ran.gits-sbx-emu-smoke.sh --class <fqcn[#metodo]>— an explicit ARGUMENT (one mechanism; no env var: an env is inherited, is not in theargvand would be the same "from outside" hole). Validated before anything touches the emulator (non-empty, qualified name, no spaces/wildcards), imposed by the run as-Pandroid.testInstrumentationRunnerArguments.class=<clase>, and printed, so the capture says it. With--quick,--classwins (declared, not guessed). Without--classthe behaviour is exactly the old one: no filter is added and the Gradle invocation is unchanged.--class, the run names any effectivegradle.properties(project or shared) that filters by class, with the file and the value. It never changes therc.run --class <fqcn>— validates it before spending the emulator, passes it to the suite as--class, records it as theclassfield, and declares it in thePROOF-RUNheader.check— if the line hasclass, the capture's header must declare the same class. The class is bound to the captured bytes, not just to the line.(2) Result files were not kept per run
A later smoke overwrote the person-route suite's XML, so what an earlier line attested no longer existed. Now the XMLs of the run (
mtime >= start) are copied (never moved — the suite keeps its own layout) to<ledger>.runs/<head>.<output_sha256>.results/, preserving their relative path, and the line carriesresults_sha256, a manifest ofsha256 + relative pathper file, sorted.build/outputs/androidTest-results/in the next run no longer changes what the line attests (case 5);results: el manifiesto de … no es el que declara la linea) (case 6);NO existe la copia de resultados) (case 6b).Files
core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.shclass/results_sha256inchain_text+ the line,--class, the per-run copy and manifest, the two newchecklegs (only when present),--helpcore/manifests/apps/sbx-gits/tools/gits-sbx-emu-smoke.sh--class(argument, validated, imposed as filter, printed), the shared-gradle.propertiesAVISO, the class in the run headercore/manifests/apps/sbx-gits/README.mdledger.runs/; the optional fields and whydocs/gits-sbx/run-proof.md--class, results-per-run, the newchecklegs and the new failure messagesdocs/gits-sbx/t7-emu-smoke.md--classsection: the finding, one mechanism, the AVISO, backwards compatibilityscripts/gits-sbx-epic-test.shorigin/live'schain_text, a hand-built OLD line still validating, per-run retention, three red mutations,--classvalidation, the REALphase_connected_testrun against a fake gradle (-P…class=present / absent) and the shared-properties detector;SBX_GITS_DIFF_SETre-measured for this branch's three filesGuards / verification
bash -nclean on both tools and the harness./tmppaths, never concurrent). Pre-existing reds unchanged: the knownEMPTY --dircase and the twomanifest[3]/[3b]image-pin checks (both unrelated to this branch — they are red onorigin/livetoo).T7[diff]/17[R159] (d): the pinned set is re-measured againstorigin/live=b5c8498cand is exactlyM README.md,M tools/gits-sbx-emu-proof.sh,M tools/gits-sbx-emu-smoke.sh. No manifest, Service, NetworkPolicy,kustomization.yaml,statefulset.yaml, no new file undertools/.Honest notes for the reviewer
script_sha256, so lines written by the previous suite stop matching the current one — that is pre-existing semantics (checkcompares against the suite the manifest delivers) and the reason an official run is repeated on the final head. This branch does not weaken that; it just triggers it.classandresults_sha256are not required bycheckeven though they exist: requiring them is the one thing that would have re-broken the chain that was just restarted. If you want them required, that is a deliberate schema break with a fresh ledger, and it is not this PR.--class-less run: it can only warn when a sharedgradle.propertiesfilters. Making it impossible needs the filter to be unsettable from outside the repo, which the lane does not control.Not merged: the landing lock's rules apply (verdicts on the exact head, green CI).
Two of the three evidence gaps the sbx-gits hypervisor found while checking a real merge. Both are OPTIONAL fields, on purpose: the ledger started a fresh chain at 13:08Z today, so a required field in `check` would re-break every head already recorded. A line without the new fields hashes over the same 18-field text, byte for byte (proved by executing the origin/live `chain_text` and this one side by side); a line with them carries them INSIDE the hashed text. (1) The line did not say which test class ran. `task:app:connectedDebugAndroidTest` names the TASK, not the class, and the class does not have to come from the repo: `android.testInstrumentationRunnerArguments.class` in a SHARED gradle.properties ($GRADLE_USER_HOME, outside the tree) filters the instrumentation while tree_clean stays true — a green line that attests a whole suite while one filtered class ran. Now: - `gits-sbx-emu-smoke.sh --class <fqcn[#metodo]>`: an explicit ARGUMENT (not an env var), validated before anything touches the emulator, imposed as the instrumentation filter by the run itself, and PRINTED so the capture says it. With no `--class` the behaviour is exactly the old one (no filter added), plus an AVISO naming any effective gradle.properties that is filtering by class. - `run --class <fqcn>` validates it, passes it to the suite as `--class`, records it as the optional `class` field and declares it in the PROOF-RUN header. `check` verifies the header declares the same class — only when the line has it. (2) Result files were not kept per run: a later smoke overwrote the person-route suite's XML, so what an earlier line attested was gone. Now the XMLs of the run are COPIED (never moved: the suite keeps its own layout) to <ledger>.runs/<head>.<output_sha256>.results/ preserving their relative path, and the line carries the optional `results_sha256` manifest (relative path + sha256 per file). Overwriting the suite's own directory in the next run no longer changes what the line attests; touching the copies does, and `check` catches it. Docs (run-proof.md, t7-emu-smoke.md, README §10.4) and §24 of the epic test (executed cases, including red mutations for the tampered copy, the retouched class and a capture that declares another class). The third gap (seat transcripts in /tmp/pi-subagents-1000) touches statefulset.yaml, which this branch's pinned T7[diff] set forbids, so it goes in its own PR; SBX_GITS_DIFF_SET is re-measured for THIS branch's three files.Head moved twice since the PR was opened:
9149e28b→38825d1d(merge oforigin/live, which had advanced tocc408793) →18bc3251.HOMELAB-2479,sandboxmemory 12Gi/13Gi and CPU 12) onlivewhile this branch was open, sogit diff origin/livebriefly showedstatefulset.yamlas modified by this branch — i.e. the PR would have reverted it. There is no rebase and no force-push here (R159):origin/livewas merged in, clean.18bc3251fixes a real defect in my own previous commit: the two rewrittenT7[diff]pass messages had backticks inside a double-quoted shell string, so bash ranclass,checkand--class <fqcn[#metodo]>as commands (harmless no-ops there, but exactly the class of bug a guard must not have, and it swallowed the text). Quoted names are spelled without backticks.Harness, solo, on
38825d1d(same tree as18bc3251for the assertions): 622 checks, 5 failed, 0 pending —T7[diff]and17[R159] (d)pass, with their red mutations: the set is exactlyM README.md,M tools/gits-sbx-emu-proof.sh,M tools/gits-sbx-emu-smoke.sh, and a fourth touched file is red.§24 (the new evidence section) passes in full, including the two that matter most:
origin/live(bothchain_textimplementations executed side by side), and a hand-built OLD-format line passes chain integrity and is rejected only for the artefact it lacks. No head already in the ledger re-validates differently.classon an existing line →hash no cuadra; a well-chained line whose capture declares another class →NO declara la clase … que dice la linea; a byte added to the per-run copy →results: …; the copy deleted →NO existe la copia de resultados. Plus: overwriting the suite's own XML directory after the run still givesPROOF: OK.The live ledger was read read-only and not touched: 19 lines, first post-boundary line
f068805b…at 13:15:10Z, all with the same 18-key set and none carryingclass/results_sha256.Note for the lander: PR #854 (transcripts,
statefulset.yaml+ the guard fix inp1_spine/p1_list) touches the same harness file. Whichever of the two lands second must merge live and re-measureSBX_GITS_DIFF_SETfor the combined set.Verdict of record — ld-reviewer — head
18bc325160: APPROVEmodel: deepseek-flash
session: 01a1ad3e-6516-757a-b84d-276d99f7d229
Blockers
None.
Concerns
--classaccepts$andsafe_valsilently rewrites it. Both validators allow[A-Za-z_$](org.Foo$Inner, a legal nested Java/Kotlin class, passes), but the ledger/PROOF-RUN/arg path goes throughsafe_val, which maps$to_. Measured:run --class 'org.Foo$Inner'passesorg.Foo_Innerto the suite and records"class":"org.Foo_Inner". It cannot manufacture a false green for an undeclared class (the attested class is exactly what gradle was told to run, and a name that matches nothing leavestests_run=0, which the gate refuses), but the validation and the sanitizer disagree about the same string. Fix is one line: either reject$in the validators or keep$in the class sanitizer.gradle_taskechoes the full gradle argv (== CONNECTED-TEST: ./gradlew app:connectedDevDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=<clase> ==) andphase_connected_testprintsfiltro de clase: <clase> (declarado con --class), both inside the hashed capture.checktoday only requires the header'sclass=to equal the line's; it does not look for the-Pline. The binding is still sound (script_sha256pins the suite to the manifest smoke, which is the file that imposes the filter), so this is not required for the merge, but that grep is the assertion that catches "the smoke took--classand dropped it" directly.--classthe gate can still pass on a run whose class was filtered from a sharedgradle.properties. The smoke's AVISO is inside the hashed capture, so it is legible, but nothing fails andcheckdoes not mention it. This follows from the hard constraint (requiring the field would red every existing line, so it cannot become amissingfield), and I would not change it here. Cheap follow-up: havecheckprint a NOTICE (not a failure) when the passing line carries noclass, so the gap is visible in the gate's output at the moment it matters.ledger.runs/grows one.logand one.results/per unique run, with no pruning: 22 entries / 872 KiB in the pod now (~40 KiB per run) against 401 GiB free, so not urgent — but XML copies can be far larger than the captured stdout, and the volume has a 40 GiB cap.rundies after the emulator has already spent its 20 minutes and no line is written. Fail-closed and deliberate; worth knowing that the alternative (write the line withoutresults_sha256, warn loudly) is now free because the field is optional.script:leg until it is re-run. That is inherent to touching the suite (every earlier smoke change did the same) and the PR body discloses it; chain integrity itself is preserved, which is the part that had to be.What I verified, from primary sources
Head
18bc325160db6c7db0fe065c532bb7161fc2e9d0(read from the API,mergeable=true, 11/11 CI checks green).git diff --name-status origin/live= exactly 6 files:apps/sbx-gits/README.md,tools/gits-sbx-emu-proof.sh,tools/gits-sbx-emu-smoke.sh,docs/gits-sbx/run-proof.md,docs/gits-sbx/t7-emu-smoke.md,scripts/gits-sbx-epic-test.sh. Nostatefulset.yaml, nokustomization.yaml, no file added undertools/; the derived set under thesbx-gits/prefix is exactly the threeMlinesSBX_GITS_DIFF_SETnow pins.1. The optional fields, against the real ledger. I did not take the byte-identity on trust: I extracted
chain_textfromorigin/liveand from the head, executed both with the same 18 arguments andcmp'd them — identical in three cases (323 / 201 / 204 bytes), and the new fields appear only afterprev, only when non-empty. Then, end to end on/tmpcopies: a 19-key line rebuilt with the OLD composition passescheckunder the OLD tool and under the NEW tool (samePROOF: OKline, rc 0) — socheckdoes not require either field. A mixed ledger (old-format line 1, new-format line 2 chained on it) passes for both heads; editing a field of line 1 giveshash no cuadra; retypingclasson a line giveshash no cuadra; deleting theclassfield from a line and leaving its hash alone also giveshash no cuadra— the field is inside the hashed text, not beside it.The live ledger reads 21 lines now (it grew after the PR body was written;
ledger.runs/has 22 entries), all 21 carrying the same 18-key set and none withclassorresults_sha256. A copy of it plus itsledger.runs/run through both tools gives byte-identical output: zerohash no cuadra, zeroprev no encadena, zerole faltan campos; the only gate lines are thescript:leg of the head this PR moves. I wrote nothing to the pod —wc -l,du,lsand reads only.2. The class argument is real, not cosmetic (the question asked explicitly).
run --classvalidates the class before the emulator is spent, passes it as an explicit--class <fqcn>argument to the suite, the smoke re-validates it and imposes-Pandroid.testInstrumentationRunnerArguments.class=<clase>on the gradle command line (a command-line-Pbeats$GRADLE_USER_HOME/gradle.properties), andgradle_taskechoes the whole gradle argv into the captured stdout — so the filter the run applied is in the hashed capture, not only in the header. Driven independently: a mock suite received--class org.reynoza.ghostty.TerminalViewTestfromrun --classand thePROOF-RUNheader carriedclass=; the harness's §24(10) drives the realphase_connected_testagainst a stub gradle and shows the-Parriving when the class is declared and no-Pwhen it is not. Can the smoke lie about the class it passed to gradle? Only by ignoring its own argument — and then its sha256 is no longer the manifest's suite and the gate refuses it. The residual gap is C2, not a hole: theclassin the line is anchored to the header, and the header is anchored to the sha-identical smoke that imposes the filter.3. Per-run results. Copies land in
ledger.runs/<head>.<output_sha256>.results/keeping the relative path,results_sha256is a sorted (relative path, sha256) manifest. Overwriting the suite's ownbuild/outputs/androidTest-results/after the run leavescheckat rc 0 — the person-route overwrite that started this no longer erases anything attested. One byte appended to a copy →results: el manifiesto de … no es el que declara la linea; copy deleted →results: NO existe la copia de resultados de la corrida; a line withoutresults_sha256skips the leg entirely.4. The harness additions are not vacuous, and #851's reds are not re-measured here. I extracted §24 and ran it standalone against the head tree: 26 PASS, 0 FAIL, and the four red mutations are executed rather than described ((6) tampered copy, (6b) deleted copy, (7) retyped
class, (8) header declaring another class). §24(3) re-runs bothchain_textimplementations side by side and §24(4) hand-builds an old-format line with a valid chain. The diff to the harness touches onlySBX_GITS_DIFF_SET, the twoT7[diff]pass messages, two§23doc assertions (run-proof.md's "no new field" sentence, which this PR legitimately rewrites) and the new§24;manifest[2]and the §10T7totals are untouched and no hunk mentions them, so the two reds #851 left onliveare correctly left alone.5. Docs (
run-proof.md,t7-emu-smoke.md, README §10.4) state what the code does, including the--class-beats---quickbehaviour and the "optional, because requiring them would red every recorded head" rationale. I found no claim in them that the implementation does not keep.Nothing here warrants holding the change: the constraint that mattered — the 21-line ledger in
sbx-gits-0keeps validating, and a line's new fields are inside the hash when present — is proven, not asserted, and the class survives the transition from cosmetic label to imposed filter.Verdict of record — ld-reviewer — head
18bc325160: APPROVEmodel: deepseek-flash
session: 01a1ad4b-7b12-757a-b84d-277093a6f23e
Blockers
None.
Concerns
classis evidence of the filter the run imposed, not proof of what executed. The chain is sound:run --class Xvalidates X, puts it on the suite's argv, writesclass=Xinto thePROOF-RUNheader and into the hashed line, andscript_sha256pins the suite to the manifest smoke — which turns--classinto-Pandroid.testInstrumentationRunnerArguments.class=Xand echoes the whole gradle argv into the hashed capture. What is not checked anywhere: the copied XMLs'<testcase classname="…">values against the declared class (grep -c classnameongits-sbx-emu-proof.sh= 0). So a green line means "this run told gradle to run X and gradle produced N>0 results", not "the cases that ran are X's". The realistic escapes are a customtestInstrumentationRunner, a projectbuild.gradlethat mergestestInstrumentationRunnerArgumentsin a way that wins over the property, and anandroidTestshard; a wrong class that matches nothing leavestests_run=0, which the gate refuses — so the failure mode is blind, not false-verifiable. Cheap upgrade that turns declaration into verification: when a line carries bothclassandresults_sha256, require every<testcase classname=…>in the per-run copies to match the declared class (orclass#method). Not required for this merge; the ticket asked for the declaration and the binding, and both are there.--classthe line omits the field, socheckaccepts it exactly as before; the shared-gradle.propertiesAVISO is a legible line in the captured stdout butchecknever reads stdout at all. Declaring the capability is a real improvement; calling it "the gate now sees the swapped class" is not what the bytes do. There is a closure that does not violate the hard invariant and does not touch a single existing line: the AVISO string (un gradle.properties COMPARTIDO filtra por clase y la corrida NO lo declara) is new in this PR (0 occurrences inorigin/live, 1 at head), socheckcan refuse a line that carries noclassand whose hashed capture contains that marker. Old lines' captures predate the string and cannot match. Worth a follow-up ticket rather than a change here.results_manifest()walksfind . -type f, so: one byte appended to a copy →results: el manifiesto … no es el que declara la linea; a regular file added → same rejection; a copy deleted →results: NO existe la copia …; butln -s /etc/hostname …/link.xml→checkstill rc 0, and an added empty directory → rc 0. That does not change what the line attests (the pinned bytes and paths stay pinned), butrun-proof.mdsays "un XML editado, borrado o añadido en la copia es un rechazo", and an added symlink is invisible to it. One-token fix:find . -mindepth 1 \( -type f -o -type l \).classis fatal outright. Measured on a 3-line ledger for one head: tampering with line 1's.results/copies →checkanswersPROOF: OK … linea=2(rc 0), because the candidate loop takes the first line that passes; editingclasson line 2 → rc 1 withhash no cuadra, even though lines 1 and 3 are intact, because a chain failure isreasonsand returns before the gate. Both are by design (the gate is an existence proof: "some good run on this head"; the chain is fail-closed as a whole), and neither is introduced here — but "tocar las copias sí lo caza" should be read as per-line, not per-gate.-Pbeating a$GRADLE_USER_HOME/gradle.propertiesclass filter is Gradle's documented precedence, andgradle_task's echo proves the-Preached the command line — but no case runs the conflict (a sharedgradle.propertieswith…arguments.class=org.otra.Escondidaplus--class), because that needs real gradle. §24(10c/10d) tests only the AVISO detector. So the sentence "the run imposes the filter itself" rests on the echo plus Gradle's precedence, not on a green case. Fine to leave; do not cite it as measured.ledger.runs/grows one.logand one.results/tree per unique run and nothing prunes it; no retention note inrun-proof.mdor the README, while the volume has a cap. Also: the.results/copies are XML trees, which are typically larger than the captured stdout that was already there.\class\. The backtick fix at18bc3251left a literal backslash pair in bothT7[diff]pass messages (—\class\ (la clase que corrio…)). Bash prints it verbatim, so the pass line reads\class\. No command substitution escapes any more (verified), so this is only a wart.safe_valmaps$→_while both validators accept$, soorg.Foo$Inneris recorded and forwarded asorg.Foo_Inner; andsafe_valalso truncates at 200 chars, so a >200-char class would be recorded truncated in header and line (they agree with each other) while the suite receives the full name — i.e. the recorded class would be a prefix of what ran. Unreachable in practice (FQCN limits), but it is the same "validator and sanitizer disagree" shape as C1.What I verified, from primary sources (adversarial seat 2)
Head
18bc325160db6c7db0fe065c532bb7161fc2e9d0read from the API.git diff --name-status origin/live= the 6 files the PR body names;git diff origin/live -- core/manifests/apps/sbx-gits= exactlyM README.md,M tools/gits-sbx-emu-proof.sh,M tools/gits-sbx-emu-smoke.sh— nostatefulset.yaml, nokustomization.yaml, nothing added undertools/. The branch is behindliveby#850/#855, butgit diff HEAD...origin/live -- core/manifests/apps/sbx-gitsis empty, so the diff set the harness pins is unaffected by the merge.(2) Optional field, chain composition — recomputed, not trusted.
chain_textextracted fromorigin/liveand from the head, executed with the same 18 arguments: byte-identical (cmp, not a visual check); withclass/results_sha256supplied, both appear afterprevand only when non-empty. Then end to end on/tmp: a 3-line ledger where line 2 carriesclassand lines 1/3 do not →checkrc 0. Retypingclasson line 2 →hash no cuadra(recalculated hash differs, line 1 and 3 unchanged); deleting theclassfield and leaving the hash →hash no cuadra; addingclassto a line that never had it →hash no cuadra. The field is inside the hashed text, not beside it, in both directions.(3)
results_sha256— what is hashed, and can it be moved after the fact. It is a manifest of the copies directory: for every regular file, one linesha256(bytes) relative_path, sorted by path, itself sha256'd. I recomputed it from the directory alone and got the line's value finger-for-finger (a44ff18c…), so it pins set + paths + contents, not modes, mtimes or ownership. Regular files cannot be added, removed or edited undetected (measured, all three); symlinks and empty directories can (S2-3). No shared directory: copies land atledger.runs/<head>.<output_sha256>.results/, i.e. run-bound — the case that started this (a later person-route smoke overwriting the suite's ownbuild/outputs/androidTest-results/) no longer erases what an older line attested (measured: overwrite the suite dir →checkrc 0).(5) Live pod, read-only.
/home/ld/work/.emu-proof/ledger.jsonl= 21 lines, zero carryingclass, zero carryingresults_sha256; 5 distinctscript_sha256values in it (16 ×ea38a86e, 2 ×34f21227, ande74156e3/71a77a0b/39e03a35once each). The pod's tools are byte-identical toorigin/live(proof.shb9d098e8…,smoke.sh71a77a0b…), so this measured main, not a stale pod:check 833b5f35…(the newest head whose recorded suite is the pod's) → rc 0;check 26cd6d87…→ rc 1 on thescript:leg only (that head was recorded with a different smoke — pre-existing behaviour, untouched by this PR). Then the decisive one: the PR's tool against a copy of the real 21-line ledger + itsledger.runs/→ rc 1 with exactly one reason, thescript:leg for line 19, and zerohash no cuadra/prev no encadena/le faltan campos/results:lines. The hard invariant holds on real data, not just on fixtures. Nothing was written to the pod (kubectl cpout,wc/ls/duonly).(6) Harness non-vacuity — proven by mutating the product three ways. §24 extracted and run standalone against the head tree: 25 PASS, 0 FAIL, 0 SKIP. Then, each mutation applied to a copy of the tree: (a) move the
classinsertion beforeprev→ §24(3) red ("el texto hasheado … CAMBIA respecto a origin/live"); (b)runrecordsclassbut drops it from the suite's argv → §24 red (con-class:la-suite-NO-recibio---class-en-su-argv); (c) the smoke accepts--classbut drops the gradle-P→ §24(10) red (smoke:con-la-clase-declarada-gradle-no-recibe-el-filtro-P). So the three claims that matter are measured, not described.bash -nclean on both tools;shellcheck -S warningsilent.#851's reds are untouched.
manifest[2]still asserts6Gi/10Giwhile the sandbox container now requests12Giand limits13Gi(from#851) — red for a reason outside this PR, and its assertion (line ~964) is not in any hunk. The §10T7totals assertion (~5227) and the README totals rows it clamps (lines 188-189) are outside every hunk too (the README hunks are 710-760; both rows still match byte-for-byte). The epic-test@@hunks are only at 4933, 5168, 8876, 12399, 12413. No schema boundary was added anywhere in the repo: no consumer parses the ledger line by field count (only the tool itself, the docs, and greps for the path).