Skip to content

fix(cli): make batch validate like render and report what actually happened - #146

Merged
LeadcodeDev merged 1 commit into
chantier/audit-remediationfrom
fix/audit-lot4-batch
Aug 8, 2026
Merged

fix(cli): make batch validate like render and report what actually happened#146
LeadcodeDev merged 1 commit into
chantier/audit-remediationfrom
fix/audit-lot4-batch

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Refs #142 — CLI workstream, second of three. Four confirmed findings, all in commands/batch.rs.

batch renders N videos in one shot. It was the one command path that told the truth about none of them.

Silent success on worker panic

let _ = h.join(); carried the comment "panics in threads are surfaced as failures below". Nothing below ever inspected the join result, and the worker closure's bookkeeping runs only on the Ok/Err return path — which a panic skips entirely. So a batch where every render panicked printed Batch complete: 0/2 succeeded., left an empty output directory, and exited 0.

Panics are now pushed onto failures with a readable payload. A final guard fires if success_count + fail_count != total — defence in depth against a future row going neither counted nor reported.

Geometry validation bypassed

Preflight ran a parse-only dry load, so batch skipped the schema + geometry pass render applies. The same template that render rejects with 1 geometry violation(s) sailed through batch and produced an overflowing video, reported as success.

validation::run_checks now runs per row with render's no-flag defaults, failing the whole batch before a frame is drawn. This is the mode that renders in bulk — it must not be the one that skips the gate CLAUDE.md makes mandatory.

Writes outside --output-dir

--name-template interpolates values straight from the data file, so a row carrying {"id":"../secret/escaped"} or {"id":"/tmp/absolute"} wrote outside the output directory. Names are rejected in preflight when they contain a parent-dir, root or prefix component.

Lexical, not canonicalising — deliberately. canonicalize() fails with ENOENT on a subdirectory that does not exist yet, which would have broken the legitimate {lang}/{id}.mp4 form the existing field_placeholder_resolved test covers. A regression test pins that nested-output case alongside the two escape cases.

Notes for the reviewer

The two fixes interact, and the interaction is why one test looks different from the audit's reproduction. Wiring geometry validation into preflight made the audit's original panic repro (odd 321×241 dimensions) unreachable — batch now rejects those scenarios before spawning a thread. The panic-capture test therefore uses a different trigger (70000×70000, which overflows u32 arithmetic in the openh264 path) to exercise the same mechanism. The batch path gained a second closed panic vector as a side effect.

No --lenient / --no-validate / --strict-anim parity with render. The audit suggested it; adding it needs new fields on Commands::Batch in lib.rs. Deliberately not done: bulk rendering is exactly where an escape hatch from validation costs the most, so batch is always strict. Additive later if a real need appears.

Verification

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — 752 passed, 0 failed, re-run by the orchestrator on the integrated tree. 9 new tests, each written red first against the audit's reproduction.

…ppened

`batch` produces N videos in one shot, and it was the one path that told
the truth about none of them.

With `--jobs > 1` a worker panic was swallowed: `let _ = h.join()` was true
to its comment ("panics are surfaced as failures below") in name only —
nothing below inspected the join result, and the closure's bookkeeping only
runs on the return path a panic skips. A batch where every render panicked
printed "0/N succeeded" and exited 0. Panics are now recorded as failures,
with a final guard should a row ever go neither counted nor reported.

Preflight ran a parse-only dry run, so `batch` skipped the schema and
geometry pass `render` applies — the viewport-overflow gate CLAUDE.md makes
mandatory was bypassed by the very mode that renders in bulk. It now runs
`validation::run_checks` per row, with render's no-flag defaults, and fails
the batch before a single frame is drawn.

`--name-template` interpolates values straight from the data file, so a row
carrying `../escaped` or `/tmp/absolute` wrote outside `--output-dir`.
Names are now rejected in preflight when they contain a parent-dir, root or
prefix component. The check is lexical rather than canonicalising: a
canonicalize-and-compare fails with ENOENT on a subdirectory that does not
exist yet, which would have broken the legitimate `{lang}/{id}.mp4` form.
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Aug 8, 2026
@LeadcodeDev LeadcodeDev self-assigned this Aug 8, 2026
@LeadcodeDev
LeadcodeDev merged commit 025af5a into chantier/audit-remediation Aug 8, 2026
3 checks passed
@LeadcodeDev
LeadcodeDev deleted the fix/audit-lot4-batch branch August 8, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant