HOMELAB-2682: fix(sbx-gits): the ledger says which class ran and keeps each run's results (PR A of the evidence fixes) #853

Merged
pi-landing merged 3 commits from plane/HOMELAB-2682-evidence-fixes into live 2026-11-05 20:45:19 +00:00
Owner

Two of the three evidence gaps gits-hypervisor found while checking a real merge. (The third — seat transcripts in /tmp/pi-subagents-1000 — touches statefulset.yaml, which this branch's pinned T7[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 check requires would re-break every head already recorded. So both new fields are OPTIONAL:

  • a line without them hashes the same 18-field canonical text byte for byte — proved here by extracting chain_text from origin/live and from this branch and executing both (harness §24 case 3);
  • a line with them carries them inside the hashed text (after prev), so adding, removing or retyping either one breaks the chain;
  • check verifies 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 line f068805b… at 13:15:10Z (result:FAIL, rc:5, tests_run:0), last b3382259… at 16:21:28Z (OK, tests_run:1). All 18/19 lines carry the same key set — none has class or results_sha256, which is exactly the shape the optional design has to keep validating. Verified: check on a copy of that live file reports no hash 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:connectedDebugAndroidTest names the task, not the class, and the class does not have to come from the repo: an android.testInstrumentationRunnerArguments.class in a shared gradle.properties ($GRADLE_USER_HOME, i.e. /home/ld/work/.gradle/gradle.properties in the pod, outside the tree) filters the instrumentation while tree_clean stays true. 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 the argv and 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, --class wins (declared, not guessed). Without --class the behaviour is exactly the old one: no filter is added and the Gradle invocation is unchanged.
  • A new AVISO when the class comes from outside anyway: with no --class, the run names any effective gradle.properties (project or shared) that filters by class, with the file and the value. It never changes the rc.
  • run --class <fqcn> — validates it before spending the emulator, passes it to the suite as --class, records it as the class field, and declares it in the PROOF-RUN header.
  • check — if the line has class, 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 carries results_sha256, a manifest of sha256 + relative path per file, sorted.

  • overwriting the suite's own build/outputs/androidTest-results/ in the next run no longer changes what the line attests (case 5);
  • touching a copied XML → RED (results: el manifiesto de … no es el que declara la linea) (case 6);
  • deleting the copy → RED (NO existe la copia de resultados) (case 6b).

Files

file what
core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh optional class / results_sha256 in chain_text + the line, --class, the per-run copy and manifest, the two new check legs (only when present), --help
core/manifests/apps/sbx-gits/tools/gits-sbx-emu-smoke.sh --class (argument, validated, imposed as filter, printed), the shared-gradle.properties AVISO, the class in the run header
core/manifests/apps/sbx-gits/README.md §10.4: the class is declared; results live under ledger.runs/; the optional fields and why
docs/gits-sbx/run-proof.md the two optional fields in the table, why they are optional, --class, results-per-run, the new check legs and the new failure messages
docs/gits-sbx/t7-emu-smoke.md --class section: the finding, one mechanism, the AVISO, backwards compatibility
scripts/gits-sbx-epic-test.sh §24 (new): executed cases — line without/with the fields, byte-for-byte compatibility against origin/live's chain_text, a hand-built OLD line still validating, per-run retention, three red mutations, --class validation, the REAL phase_connected_test run against a fake gradle (-P…class= present / absent) and the shared-properties detector; SBX_GITS_DIFF_SET re-measured for this branch's three files

Guards / verification

  • bash -n clean on both tools and the harness.
  • Harness solo (fixed /tmp paths, never concurrent). Pre-existing reds unchanged: the known EMPTY --dir case and the two manifest[3]/[3b] image-pin checks (both unrelated to this branch — they are red on origin/live too).
  • T7[diff] / 17[R159] (d): the pinned set is re-measured against origin/live = b5c8498c and is exactly M 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 under tools/.

Honest notes for the reviewer

  • Any change to the smoke moves its script_sha256, so lines written by the previous suite stop matching the current one — that is pre-existing semantics (check compares 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.
  • class and results_sha256 are not required by check even 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.
  • The tool cannot lie about the class of a --class-less run: it can only warn when a shared gradle.properties filters. 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 **gits-hypervisor** found while checking a real merge. (The third — seat transcripts in `/tmp/pi-subagents-1000` — touches `statefulset.yaml`, which this branch's pinned `T7[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 `check` *requires* would re-break every head already recorded. So both new fields are **OPTIONAL**: * a line **without** them hashes the same 18-field canonical text **byte for byte** — proved here by extracting `chain_text` from `origin/live` and from this branch and executing both (harness §24 case 3); * a line **with** them carries them **inside** the hashed text (after `prev`), so adding, removing or retyping either one breaks the chain; * `check` verifies 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 line `f068805b…` at 13:15:10Z (`result:FAIL, rc:5, tests_run:0`), last `b3382259…` at 16:21:28Z (`OK`, `tests_run:1`). All 18/19 lines carry the same key set — **none** has `class` or `results_sha256`, which is exactly the shape the optional design has to keep validating. Verified: `check` on a copy of that live file reports **no** `hash 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:connectedDebugAndroidTest` names the **task**, not the class, and the class does not have to come from the repo: an `android.testInstrumentationRunnerArguments.class` in a **shared** `gradle.properties` (`$GRADLE_USER_HOME`, i.e. `/home/ld/work/.gradle/gradle.properties` in the pod, outside the tree) filters the instrumentation while `tree_clean` stays `true`. 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 the `argv` and 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`, `--class` wins (declared, not guessed). **Without `--class` the behaviour is exactly the old one**: no filter is added and the Gradle invocation is unchanged. * **A new AVISO when the class comes from outside anyway**: with no `--class`, the run names any *effective* `gradle.properties` (project or shared) that filters by class, with the file and the value. It never changes the `rc`. * **`run --class <fqcn>`** — validates it before spending the emulator, passes it to the suite as `--class`, records it as the `class` field, and declares it in the `PROOF-RUN` header. * **`check`** — if the line has `class`, 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 carries `results_sha256`, a manifest of `sha256 + relative path` per file, sorted. * overwriting the suite's own `build/outputs/androidTest-results/` in the next run **no longer changes** what the line attests (case 5); * touching a copied XML → RED (`results: el manifiesto de … no es el que declara la linea`) (case 6); * deleting the copy → RED (`NO existe la copia de resultados`) (case 6b). ## Files | file | what | |---|---| | `core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh` | optional `class` / `results_sha256` in `chain_text` + the line, `--class`, the per-run copy and manifest, the two new `check` legs (only when present), `--help` | | `core/manifests/apps/sbx-gits/tools/gits-sbx-emu-smoke.sh` | `--class` (argument, validated, imposed as filter, printed), the shared-`gradle.properties` AVISO, the class in the run header | | `core/manifests/apps/sbx-gits/README.md` | §10.4: the class is declared; results live under `ledger.runs/`; the optional fields and why | | `docs/gits-sbx/run-proof.md` | the two optional fields in the table, why they are optional, `--class`, results-per-run, the new `check` legs and the new failure messages | | `docs/gits-sbx/t7-emu-smoke.md` | `--class` section: the finding, one mechanism, the AVISO, backwards compatibility | | `scripts/gits-sbx-epic-test.sh` | §24 (new): executed cases — line without/with the fields, byte-for-byte compatibility against `origin/live`'s `chain_text`, a hand-built OLD line still validating, per-run retention, three red mutations, `--class` validation, the REAL `phase_connected_test` run against a fake gradle (`-P…class=` present / absent) and the shared-properties detector; `SBX_GITS_DIFF_SET` re-measured for this branch's three files | ## Guards / verification * `bash -n` clean on both tools and the harness. * Harness **solo** (fixed `/tmp` paths, never concurrent). Pre-existing reds unchanged: the known `EMPTY --dir` case and the two `manifest[3]/[3b]` image-pin checks (both unrelated to this branch — they are red on `origin/live` too). * `T7[diff]` / `17[R159] (d)`: the pinned set is re-measured against `origin/live` = `b5c8498c` and is exactly `M 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 under `tools/`. ## Honest notes for the reviewer * Any change to the smoke moves its `script_sha256`, so lines written by the previous suite stop matching the current one — that is pre-existing semantics (`check` compares 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. * `class` and `results_sha256` are **not** required by `check` even 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. * The tool cannot lie about the class of a `--class`-less run: it can only *warn* when a shared `gradle.properties` filters. 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).
HOMELAB-2682: fix(sbx-gits): the ledger says which class ran and keeps each run's results
Some checks failed
CI Review / helm-validate (pull_request) Successful in 12s
CI Review / runner-manifest-test (pull_request) Successful in 11s
CI Review / receiver-test (pull_request) Successful in 14s
Alert Disposition Contract / alert-contract (pull_request) Successful in 22s
CI Review / alert-receiver-test (pull_request) Successful in 31s
Lint & Validate / shellcheck (pull_request) Successful in 21s
Lint & Validate / yaml-lint (pull_request) Successful in 42s
CI Review / record-sql-test (pull_request) Successful in 1m22s
CI Review / cockpit-test (pull_request) Successful in 2m56s
CI Review / dispatcher-test (pull_request) Successful in 4m33s
Lint & Validate / terraform-validate (pull_request) Successful in 5m8s
CI Review / cockpit-e2e (pull_request) Has been cancelled
9149e28b64
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.
Merge remote-tracking branch 'origin/live' into plane/HOMELAB-2682-evidence-fixes
Some checks failed
CI Review / runner-manifest-test (pull_request) Successful in 18s
CI Review / receiver-test (pull_request) Successful in 18s
CI Review / helm-validate (pull_request) Successful in 25s
CI Review / alert-receiver-test (pull_request) Successful in 33s
Lint & Validate / shellcheck (pull_request) Successful in 27s
Lint & Validate / yaml-lint (pull_request) Successful in 27s
CI Review / record-sql-test (pull_request) Successful in 1m32s
CI Review / cockpit-test (pull_request) Successful in 3m6s
CI Review / dispatcher-test (pull_request) Successful in 3m25s
Lint & Validate / terraform-validate (pull_request) Successful in 4m43s
CI Review / cockpit-e2e (pull_request) Has been cancelled
38825d1d0a
HOMELAB-2682: fix(sbx-gits): the T7[diff] pass message must not run command substitution
All checks were successful
CI Review / receiver-test (pull_request) Successful in 8s
CI Review / runner-manifest-test (pull_request) Successful in 16s
CI Review / helm-validate (pull_request) Successful in 18s
CI Review / alert-receiver-test (pull_request) Successful in 27s
Lint & Validate / shellcheck (pull_request) Successful in 30s
Lint & Validate / yaml-lint (pull_request) Successful in 40s
CI Review / record-sql-test (pull_request) Successful in 1m26s
CI Review / cockpit-test (pull_request) Successful in 3m20s
CI Review / dispatcher-test (pull_request) Successful in 3m59s
Lint & Validate / terraform-validate (pull_request) Successful in 11m5s
CI Review / cockpit-e2e (pull_request) Successful in 14m50s
18bc325160
Backticks inside a double-quoted shell string are command substitution: the two
pass messages I rewrote in the last commit executed the words between them
(harmless here — they were no-ops — but it swallowed the text and it is exactly the
class of bug a guard must not have). Quoted names are spelled without backticks.
Author
Owner

Head moved twice since the PR was opened: 9149e28b → 38825d1d (merge of origin/live, which had advanced to cc408793) → 18bc3251.

  • The merge was required, not cosmetic. Another branch landed #851 (HOMELAB-2479, sandbox memory 12Gi/13Gi and CPU 12) on live while this branch was open, so git diff origin/live briefly showed statefulset.yaml as modified by this branch — i.e. the PR would have reverted it. There is no rebase and no force-push here (R159): origin/live was merged in, clean.
  • 18bc3251 fixes a real defect in my own previous commit: the two rewritten T7[diff] pass messages had backticks inside a double-quoted shell string, so bash ran class, check and --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 as 18bc3251 for the assertions): 622 checks, 5 failed, 0 pending —

FAIL  probe uses an EMPTY pre-existing --dir and leaves it in place (rc=2)      ← known, pre-existing
FAIL  manifest[2] memoria requests 6Gi / limit 10Gi                             ← #851 drift on live
FAIL  manifest[3]  imagenes = sbx-fleet@sha256:7e0513ca…                        ← pre-existing
FAIL  manifest[3b] bump de imagen H5 (falta: …)                                 ← pre-existing
FAIL  T7 emulador dentro del pod (requests-memoria=18, limits-memoria=21, limits-cpu=24)  ← #851 drift

T7[diff] and 17[R159] (d) pass, with their red mutations: the set is exactly M 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:

  • the constraint: the 18-field hashed text of a line without the new fields is byte for byte the one from origin/live (both chain_text implementations 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.
  • red mutations, executed: a retyped class on 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 gives PROOF: 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 carrying class/results_sha256.

Note for the lander: PR #854 (transcripts, statefulset.yaml + the guard fix in p1_spine/p1_list) touches the same harness file. Whichever of the two lands second must merge live and re-measure SBX_GITS_DIFF_SET for the combined set.

Head moved twice since the PR was opened: `9149e28b` → `38825d1d` (merge of `origin/live`, which had advanced to `cc408793`) → `18bc3251`. * **The merge was required, not cosmetic.** Another branch landed **#851** (`HOMELAB-2479`, `sandbox` memory 12Gi/13Gi and CPU 12) on `live` while this branch was open, so `git diff origin/live` briefly showed `statefulset.yaml` as *modified by this branch* — i.e. the PR would have reverted it. There is no rebase and no force-push here (R159): `origin/live` was merged in, clean. * **`18bc3251`** fixes a real defect in my own previous commit: the two rewritten `T7[diff]` pass messages had backticks inside a **double-quoted** shell string, so bash ran `class`, `check` and `--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 as `18bc3251` for the assertions): **622 checks, 5 failed, 0 pending** — ``` FAIL probe uses an EMPTY pre-existing --dir and leaves it in place (rc=2) ← known, pre-existing FAIL manifest[2] memoria requests 6Gi / limit 10Gi ← #851 drift on live FAIL manifest[3] imagenes = sbx-fleet@sha256:7e0513ca… ← pre-existing FAIL manifest[3b] bump de imagen H5 (falta: …) ← pre-existing FAIL T7 emulador dentro del pod (requests-memoria=18, limits-memoria=21, limits-cpu=24) ← #851 drift ``` `T7[diff]` and `17[R159] (d)` **pass**, with their red mutations: the set is exactly `M 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: * **the constraint**: the 18-field hashed text of a line without the new fields is **byte for byte** the one from `origin/live` (both `chain_text` implementations 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. * **red mutations, executed**: a retyped `class` on 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 gives `PROOF: 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 carrying `class`/`results_sha256`. Note for the lander: PR #854 (transcripts, `statefulset.yaml` + the guard fix in `p1_spine`/`p1_list`) touches the same harness file. Whichever of the two lands second must merge live and re-measure `SBX_GITS_DIFF_SET` for the combined set.
Collaborator

Verdict of record — ld-reviewer — head 18bc325160: APPROVE
model: deepseek-flash
session: 01a1ad3e-6516-757a-b84d-276d99f7d229

Blockers

None.

Concerns

  • C1 (minor, fail-closed) — --class accepts $ and safe_val silently 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 through safe_val, which maps $ to _. Measured: run --class 'org.Foo$Inner' passes org.Foo_Inner to 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 leaves tests_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.
  • C2 (hardening) — the capture's own evidence of the imposed filter is not checked. gradle_task echoes the full gradle argv (== CONNECTED-TEST: ./gradlew app:connectedDevDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=<clase> ==) and phase_connected_test prints filtro de clase: <clase> (declarado con --class), both inside the hashed capture. check today only requires the header's class= to equal the line's; it does not look for the -P line. The binding is still sound (script_sha256 pins 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 --class and dropped it" directly.
  • C3 (by design, record it) — without --class the gate can still pass on a run whose class was filtered from a shared gradle.properties. The smoke's AVISO is inside the hashed capture, so it is legible, but nothing fails and check does not mention it. This follows from the hard constraint (requiring the field would red every existing line, so it cannot become a missing field), and I would not change it here. Cheap follow-up: have check print a NOTICE (not a failure) when the passing line carries no class, so the gap is visible in the gate's output at the moment it matters.
  • C4 (note) — retention. ledger.runs/ grows one .log and 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.
  • C5 (note) — a completed run is all-or-nothing. If the per-run copy fails (ENOSPC, permission), run dies 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 without results_sha256, warn loudly) is now free because the field is optional.
  • C6 (consequence, not a defect) — this PR changes the smoke, so its sha256 moves and every head already in the ledger stops satisfying the 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. No statefulset.yaml, no kustomization.yaml, no file added under tools/; the derived set under the sbx-gits/ prefix is exactly the three M lines SBX_GITS_DIFF_SET now pins.

1. The optional fields, against the real ledger. I did not take the byte-identity on trust: I extracted chain_text from origin/live and from the head, executed both with the same 18 arguments and cmp'd them — identical in three cases (323 / 201 / 204 bytes), and the new fields appear only after prev, only when non-empty. Then, end to end on /tmp copies: a 19-key line rebuilt with the OLD composition passes check under the OLD tool and under the NEW tool (same PROOF: OK line, rc 0) — so check does 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 gives hash no cuadra; retyping class on a line gives hash no cuadra; deleting the class field from a line and leaving its hash alone also gives hash 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 with class or results_sha256. A copy of it plus its ledger.runs/ run through both tools gives byte-identical output: zero hash no cuadra, zero prev no encadena, zero le faltan campos; the only gate lines are the script: leg of the head this PR moves. I wrote nothing to the pod — wc -l, du, ls and reads only.

2. The class argument is real, not cosmetic (the question asked explicitly). run --class validates 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 -P beats $GRADLE_USER_HOME/gradle.properties), and gradle_task echoes 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.TerminalViewTest from run --class and the PROOF-RUN header carried class=; the harness's §24(10) drives the real phase_connected_test against a stub gradle and shows the -P arriving when the class is declared and no -P when 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: the class in 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_sha256 is a sorted (relative path, sha256) manifest. Overwriting the suite's own build/outputs/androidTest-results/ after the run leaves check at 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 without results_sha256 skips 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 both chain_text implementations side by side and §24(4) hand-builds an old-format line with a valid chain. The diff to the harness touches only SBX_GITS_DIFF_SET, the two T7[diff] pass messages, two §23 doc assertions (run-proof.md's "no new field" sentence, which this PR legitimately rewrites) and the new §24; manifest[2] and the §10 T7 totals are untouched and no hunk mentions them, so the two reds #851 left on live are correctly left alone.

5. Docs (run-proof.md, t7-emu-smoke.md, README §10.4) state what the code does, including the --class-beats---quick behaviour 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-0 keeps 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 18bc325160db6c7db0fe065c532bb7161fc2e9d0: APPROVE** model: deepseek-flash session: 01a1ad3e-6516-757a-b84d-276d99f7d229 ## Blockers None. ## Concerns - **C1 (minor, fail-closed) — `--class` accepts `$` and `safe_val` silently 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 through `safe_val`, which maps `$` to `_`. Measured: `run --class 'org.Foo$Inner'` passes `org.Foo_Inner` to 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 leaves `tests_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. - **C2 (hardening) — the capture's own evidence of the imposed filter is not checked.** `gradle_task` echoes the full gradle argv (`== CONNECTED-TEST: ./gradlew app:connectedDevDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=<clase> ==`) and `phase_connected_test` prints `filtro de clase: <clase> (declarado con --class)`, both inside the hashed capture. `check` today only requires the header's `class=` to equal the line's; it does not look for the `-P` line. The binding is still sound (`script_sha256` pins 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 `--class` and dropped it" directly. - **C3 (by design, record it) — without `--class` the gate can still pass on a run whose class was filtered from a shared `gradle.properties`.** The smoke's AVISO is inside the hashed capture, so it is legible, but nothing fails and `check` does not mention it. This follows from the hard constraint (requiring the field would red every existing line, so it cannot become a `missing` field), and I would not change it here. Cheap follow-up: have `check` print a NOTICE (not a failure) when the passing line carries no `class`, so the gap is visible in the gate's output at the moment it matters. - **C4 (note) — retention.** `ledger.runs/` grows one `.log` and 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. - **C5 (note) — a completed run is all-or-nothing.** If the per-run copy fails (ENOSPC, permission), `run` dies 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 without `results_sha256`, warn loudly) is now free because the field is optional. - **C6 (consequence, not a defect)** — this PR changes the smoke, so its sha256 moves and every head already in the ledger stops satisfying the `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`. No `statefulset.yaml`, no `kustomization.yaml`, no file added under `tools/`; the derived set under the `sbx-gits/` prefix is exactly the three `M` lines `SBX_GITS_DIFF_SET` now pins. **1. The optional fields, against the real ledger.** I did not take the byte-identity on trust: I extracted `chain_text` from `origin/live` and from the head, executed both with the same 18 arguments and `cmp`'d them — identical in three cases (323 / 201 / 204 bytes), and the new fields appear only after `prev`, only when non-empty. Then, end to end on `/tmp` copies: a 19-key line rebuilt with the OLD composition passes `check` under the OLD tool **and** under the NEW tool (same `PROOF: OK` line, rc 0) — so `check` does 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 gives `hash no cuadra`; retyping `class` on a line gives `hash no cuadra`; deleting the `class` field from a line and leaving its hash alone also gives `hash 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** with `class` or `results_sha256`. A copy of it plus its `ledger.runs/` run through both tools gives **byte-identical output**: zero `hash no cuadra`, zero `prev no encadena`, zero `le faltan campos`; the only gate lines are the `script:` leg of the head this PR moves. I wrote nothing to the pod — `wc -l`, `du`, `ls` and reads only. **2. The class argument is real, not cosmetic** (the question asked explicitly). `run --class` validates 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 `-P` beats `$GRADLE_USER_HOME/gradle.properties`), and `gradle_task` echoes 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.TerminalViewTest` from `run --class` and the `PROOF-RUN` header carried `class=`; the harness's §24(10) drives the **real** `phase_connected_test` against a stub gradle and shows the `-P` arriving when the class is declared and **no** `-P` when 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: the `class` in 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_sha256` is a sorted (relative path, sha256) manifest. Overwriting the suite's own `build/outputs/androidTest-results/` after the run leaves `check` at 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 without `results_sha256` skips 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 both `chain_text` implementations side by side and §24(4) hand-builds an old-format line with a valid chain. The diff to the harness touches only `SBX_GITS_DIFF_SET`, the two `T7[diff]` pass messages, two `§23` doc assertions (`run-proof.md`'s "no new field" sentence, which this PR legitimately rewrites) and the new `§24`; `manifest[2]` and the §10 `T7` totals are untouched and no hunk mentions them, so the two reds #851 left on `live` are correctly left alone. **5. Docs** (`run-proof.md`, `t7-emu-smoke.md`, README §10.4) state what the code does, including the `--class`-beats-`--quick` behaviour 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-0` keeps 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.
Collaborator

Verdict of record — ld-reviewer — head 18bc325160: APPROVE
model: deepseek-flash
session: 01a1ad4b-7b12-757a-b84d-277093a6f23e

Blockers

None.

Concerns

  • S2-1 (scope of the claim — the code is honest, the sentence is not) — class is evidence of the filter the run imposed, not proof of what executed. The chain is sound: run --class X validates X, puts it on the suite's argv, writes class=X into the PROOF-RUN header and into the hashed line, and script_sha256 pins the suite to the manifest smoke — which turns --class into -Pandroid.testInstrumentationRunnerArguments.class=X and 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 classname on gits-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 custom testInstrumentationRunner, a project build.gradle that merges testInstrumentationRunnerArguments in a way that wins over the property, and an androidTest shard; a wrong class that matches nothing leaves tests_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 both class and results_sha256, require every <testcase classname=…> in the per-run copies to match the declared class (or class#method). Not required for this merge; the ticket asked for the declaration and the binding, and both are there.
  • S2-2 (hardening, and the one place the PR over-claims) — the gate still passes a run whose class was decided behind its back. Without --class the line omits the field, so check accepts it exactly as before; the shared-gradle.properties AVISO is a legible line in the captured stdout but check never 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 in origin/live, 1 at head), so check can refuse a line that carries no class and 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.
  • S2-3 (low, measured) — the results manifest is blind to symlinks and empty directories. results_manifest() walks find . -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 …; but ln -s /etc/hostname …/link.xml → check still rc 0, and an added empty directory → rc 0. That does not change what the line attests (the pinned bytes and paths stay pinned), but run-proof.md says "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 \).
  • S2-4 (gate semantics, worth recording because the docs read stronger than the code) — a tampered results copy is not fatal when a sibling intact line exists for the head, while a retyped class is fatal outright. Measured on a 3-line ledger for one head: tampering with line 1's .results/ copies → check answers PROOF: OK … linea=2 (rc 0), because the candidate loop takes the first line that passes; editing class on line 2 → rc 1 with hash no cuadra, even though lines 1 and 3 are intact, because a chain failure is reasons and 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.
  • S2-5 (test coverage) — the premise of the fix is documented, not measured. -P beating a $GRADLE_USER_HOME/gradle.properties class filter is Gradle's documented precedence, and gradle_task's echo proves the -P reached the command line — but no case runs the conflict (a shared gradle.properties with …arguments.class=org.otra.Escondida plus --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.
  • S2-6 (note) — retention. ledger.runs/ grows one .log and one .results/ tree per unique run and nothing prunes it; no retention note in run-proof.md or 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.
  • S2-7 (trivial, cosmetic) — \class\. The backtick fix at 18bc3251 left a literal backslash pair in both T7[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.
  • Confirming seat 1's C1 independently, plus one more instance of the same family. safe_val maps $→_ while both validators accept $, so org.Foo$Inner is recorded and forwarded as org.Foo_Inner; and safe_val also 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 18bc325160db6c7db0fe065c532bb7161fc2e9d0 read 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 = exactly M README.md, M tools/gits-sbx-emu-proof.sh, M tools/gits-sbx-emu-smoke.sh — no statefulset.yaml, no kustomization.yaml, nothing added under tools/. The branch is behind live by #850/#855, but git diff HEAD...origin/live -- core/manifests/apps/sbx-gits is empty, so the diff set the harness pins is unaffected by the merge.

(2) Optional field, chain composition — recomputed, not trusted. chain_text extracted from origin/live and from the head, executed with the same 18 arguments: byte-identical (cmp, not a visual check); with class/results_sha256 supplied, both appear after prev and only when non-empty. Then end to end on /tmp: a 3-line ledger where line 2 carries class and lines 1/3 do not → check rc 0. Retyping class on line 2 → hash no cuadra (recalculated hash differs, line 1 and 3 unchanged); deleting the class field and leaving the hash → hash no cuadra; adding class to 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 line sha256(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 at ledger.runs/<head>.<output_sha256>.results/, i.e. run-bound — the case that started this (a later person-route smoke overwriting the suite's own build/outputs/androidTest-results/) no longer erases what an older line attested (measured: overwrite the suite dir → check rc 0).

(5) Live pod, read-only. /home/ld/work/.emu-proof/ledger.jsonl = 21 lines, zero carrying class, zero carrying results_sha256; 5 distinct script_sha256 values in it (16 × ea38a86e, 2 × 34f21227, and e74156e3/71a77a0b/39e03a35 once each). The pod's tools are byte-identical to origin/live (proof.sh b9d098e8…, smoke.sh 71a77a0b…), 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 the script: 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 + its ledger.runs/ → rc 1 with exactly one reason, the script: leg for line 19, and zero hash 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 cp out, wc/ls/du only).

(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 class insertion before prev → §24(3) red ("el texto hasheado … CAMBIA respecto a origin/live"); (b) run records class but drops it from the suite's argv → §24 red (con-class:la-suite-NO-recibio---class-en-su-argv); (c) the smoke accepts --class but 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 -n clean on both tools; shellcheck -S warning silent.

#851's reds are untouched. manifest[2] still asserts 6Gi/10Gi while the sandbox container now requests 12Gi and limits 13Gi (from #851) — red for a reason outside this PR, and its assertion (line ~964) is not in any hunk. The §10 T7 totals 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).

**Verdict of record — ld-reviewer — head 18bc325160db6c7db0fe065c532bb7161fc2e9d0: APPROVE** model: deepseek-flash session: 01a1ad4b-7b12-757a-b84d-277093a6f23e ## Blockers None. ## Concerns - **S2-1 (scope of the claim — the code is honest, the sentence is not) — `class` is evidence of the filter the run *imposed*, not proof of what *executed*.** The chain is sound: `run --class X` validates X, puts it on the suite's argv, writes `class=X` into the `PROOF-RUN` header and into the hashed line, and `script_sha256` pins the suite to the manifest smoke — which turns `--class` into `-Pandroid.testInstrumentationRunnerArguments.class=X` and 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 classname` on `gits-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 custom `testInstrumentationRunner`, a project `build.gradle` that merges `testInstrumentationRunnerArguments` in a way that wins over the property, and an `androidTest` shard; a wrong class that matches nothing leaves `tests_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 **both** `class` and `results_sha256`, require every `<testcase classname=…>` in the per-run copies to match the declared class (or `class#method`). Not required for this merge; the ticket asked for the declaration and the binding, and both are there. - **S2-2 (hardening, and the one place the PR over-claims) — the gate still passes a run whose class was decided behind its back.** Without `--class` the line omits the field, so `check` accepts it exactly as before; the shared-`gradle.properties` AVISO is a *legible* line in the captured stdout but `check` never 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 in `origin/live`, 1 at head), so `check` can refuse a line that carries no `class` **and** 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. - **S2-3 (low, measured) — the results manifest is blind to symlinks and empty directories.** `results_manifest()` walks `find . -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 …`; but `ln -s /etc/hostname …/link.xml` → **`check` still rc 0**, and an added empty directory → rc 0. That does not change what the line attests (the pinned bytes and paths stay pinned), but `run-proof.md` says "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 \)`. - **S2-4 (gate semantics, worth recording because the docs read stronger than the code) — a tampered results copy is not fatal when a sibling intact line exists for the head, while a retyped `class` is fatal outright.** Measured on a 3-line ledger for one head: tampering with line 1's `.results/` copies → `check` answers `PROOF: OK … linea=2` (rc 0), because the candidate loop takes the first line that passes; editing `class` on line 2 → rc 1 with `hash no cuadra`, even though lines 1 and 3 are intact, because a chain failure is `reasons` and 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. - **S2-5 (test coverage) — the premise of the fix is documented, not measured.** `-P` beating a `$GRADLE_USER_HOME/gradle.properties` class filter is Gradle's documented precedence, and `gradle_task`'s echo proves the `-P` reached the command line — but no case runs the conflict (a shared `gradle.properties` with `…arguments.class=org.otra.Escondida` **plus** `--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. - **S2-6 (note) — retention.** `ledger.runs/` grows one `.log` and one `.results/` tree per unique run and nothing prunes it; no retention note in `run-proof.md` or 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. - **S2-7 (trivial, cosmetic) — `\class\`.** The backtick fix at `18bc3251` left a literal backslash pair in both `T7[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. - **Confirming seat 1's C1 independently, plus one more instance of the same family.** `safe_val` maps `$`→`_` while both validators accept `$`, so `org.Foo$Inner` is recorded and forwarded as `org.Foo_Inner`; and `safe_val` also 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 `18bc325160db6c7db0fe065c532bb7161fc2e9d0` read 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` = exactly `M README.md`, `M tools/gits-sbx-emu-proof.sh`, `M tools/gits-sbx-emu-smoke.sh` — no `statefulset.yaml`, no `kustomization.yaml`, nothing added under `tools/`. The branch is behind `live` by `#850`/`#855`, but `git diff HEAD...origin/live -- core/manifests/apps/sbx-gits` is **empty**, so the diff set the harness pins is unaffected by the merge. **(2) Optional field, chain composition — recomputed, not trusted.** `chain_text` extracted from `origin/live` and from the head, executed with the same 18 arguments: **byte-identical** (`cmp`, not a visual check); with `class`/`results_sha256` supplied, both appear **after `prev`** and only when non-empty. Then end to end on `/tmp`: a 3-line ledger where line 2 carries `class` and lines 1/3 do not → `check` **rc 0**. Retyping `class` on line 2 → `hash no cuadra` (recalculated hash differs, line 1 and 3 unchanged); **deleting** the `class` field and leaving the hash → `hash no cuadra`; **adding** `class` to 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 line `sha256(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 at `ledger.runs/<head>.<output_sha256>.results/`, i.e. run-bound — the case that started this (a later person-route smoke overwriting the suite's own `build/outputs/androidTest-results/`) no longer erases what an older line attested (measured: overwrite the suite dir → `check` rc 0). **(5) Live pod, read-only.** `/home/ld/work/.emu-proof/ledger.jsonl` = **21 lines, zero** carrying `class`, **zero** carrying `results_sha256`; 5 distinct `script_sha256` values in it (16 × `ea38a86e`, 2 × `34f21227`, and `e74156e3`/`71a77a0b`/`39e03a35` once each). The pod's tools are byte-identical to `origin/live` (`proof.sh` `b9d098e8…`, `smoke.sh` `71a77a0b…`), 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 the `script:` 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 + its `ledger.runs/`** → rc 1 with **exactly one reason**, the `script:` leg for line 19, and **zero** `hash 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 cp` out, `wc`/`ls`/`du` only). **(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 `class` insertion *before* `prev` → §24(3) red (*"el texto hasheado … CAMBIA respecto a origin/live"*); (b) `run` records `class` but drops it from the suite's argv → §24 red (`con-class:la-suite-NO-recibio---class-en-su-argv`); (c) the smoke accepts `--class` but 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 -n` clean on both tools; `shellcheck -S warning` silent. **#851's reds are untouched.** `manifest[2]` still asserts `6Gi`/`10Gi` while the sandbox container now requests `12Gi` and limits `13Gi` (from `#851`) — red for a reason outside this PR, and its assertion (line ~964) is not in any hunk. The §10 `T7` totals 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).
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!853
No description provided.