fix(#946): expand writes_sp's 175-of-222 wildcard into audited arms + no-wildcard tripwire - #969
Merged
Merged
Conversation
`writes_sp` claimed exhaustiveness over the priced instruction set while a single `_ => false` absorbed 175 of `ArmOp`'s 222 variants — the #615 silent-NOP shape (a function that enumerates a space while a wildcard silently answers for most of it). Measured on origin/main: 222 variants, 47 named, 175 absorbed. Renamed to `may_move_sp` — both call sites use it as a GIVE-UP predicate, so `true` is the sound direction and `false` is what must be earned. The name `writes_sp` could not honestly cover the "unaudited" answers. Classification, by reason rather than by answer: - 30 of the 175 are PRICED by `op_cost`, so they really do reach here. `I64Popcnt`/`I64Rotl`/`I64Rotr` expand to a fixed-register core wrapped in PUSH/POP (`0xB438`/`0xBC38`, `emit_i64_fixed_abi_entry`/`_exit`) — the wildcard's `false` was a WRONG answer, not merely an absent one. They now answer `true`. The old doc comment excused them as "a whole-function LoopedExpansion decline anyway", which was FALSE: `op_cost` prices `I64Popcnt` as `Cycles`. - The i64 ops with a register destination get the same precise `rd == SP` test as their i32 counterparts (`I64Const`/`I64Ldr` are the #936 ops that became reachable here and silently inherited the wildcard). - The reachable counted-loop vocabulary (`Str`/`Cmp`/`BOffset`/ `BCondOffset`/`Bx`/`Bl`/`Label`/`Nop`/`Udf`/`I64Str`) stays `false` with the reason recorded — `true` there would decline EVERY proven loop. - The other 145 are pre-declined by `scan_for_decline` before `analyze_loops` ever runs. They answer `true` so that pricing one later yields a loud decline instead of a silently inherited `false`. Behavioural pins added, including an explicit non-vacuity list of ops whose `true` a re-added `_ => false` would flip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Two halves, because either alone is bypassable:
STRUCTURAL (tests/wcet_sp_no_wildcard_946.rs) — scans the real source and
fails if `may_move_sp` grows a catch-all arm, or if any of the 222 `ArmOp`
variants stops being named. The arm scanner rejects bare-identifier bindings
(`op =>`, `op if pred(op) =>`) as well as `_ =>`: `wcet_loops.rs` uses both
shapes legitimately elsewhere, so a `_`-only scan would be bypassable.
ARM_OP_VARIANT_COUNT = 222 is deliberately the same number
a32_no_silent_nop_615.rs pins — two independent readings of one enum.
BEHAVIOURAL (wcet_loops.rs unit tests) — pins the answers, in both
directions: the `true`s a re-added `_ => false` would flip, and the reachable
counted-loop vocabulary (`Str`/`Cmp`/`BOffset`/`BCondOffset`/`Bx`) where a
careless `_ => true` would decline every proven loop.
GATE POTENCY, measured rather than assumed: replacing the 142-line
unreachable-bucket arm with `_ => false,` in the real file turns
`may_move_sp_has_no_catch_all_arm`, `may_move_sp_names_every_arm_op_variant`
(naming all 142) and the non-vacuity behavioural pin RED; restoring it turns
them green. The behavioural half only bites because that bucket answers
`true` — had it answered `false` to match the old wildcard, the wildcard
would be behaviourally identical and that half would be vacuous.
Three synthetic negative controls prove the scanner itself can go red, and
`may_move_sp_has_no_catch_all_arm` asserts the extracted body really contains
`match op {` before scanning it — a scan over nothing passes silently.
Also pins the REACHABILITY premise the `true` bucket rests on: `op_cost` has
no catch-all, `scan_for_decline` runs before `analyze_loops`, and
`analyze_loops` has exactly one call site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…onale Found while auditing every SP-touching encoder emission for #946. `STRAIGHTLINE_CEIL_PER_HALFWORD`'s doc claimed "the audited expansions push at most 3 registers, so this holds with margin; the i64 software div/rem, which pushes 4, is a LoopedExpansion decline and is never priced here". Enumerating every 16-bit PUSH/POP in arm_encoder.rs and its owning arm: I64Popcnt 0xB438/0xBC38 3 regs PRICED I64DivU 0xB4F0/0xBCF0 4 regs LoopedExpansion (declined) emit_i64_fixed_abi_entry 0xB40F 4 regs PRICED via I64Rotl/I64Rotr So a 4-register push DOES occur in a priced arm: 1+4 = 5 cycles in one halfword, exactly the ceiling, zero margin. The bound is unchanged and still sound (5 >= 5), and the finding strengthens rather than weakens the model — it is precisely why the constant had to go 4->5, since at 4 the priced I64Rotl/I64Rotr prologue push would have been UNDER-counted. The claims.yaml pin is on the constant (`STRAIGHTLINE_CEIL_PER_HALFWORD = 5`), which does not move; only the rationale comment beside it was stale. Ledger still 43/43. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…dcard hole The 175-variant wildcard was not purely latent. Measured, same two modules, same flags (-t cortex-m4 --emit-wcet), only wcet_loops.rs differing: i64.rotl in a trip-8 counted loop: main BOUNDED 3120 cyc -> now declines i64.popcnt in a trip-8 counted loop: main BOUNDED 4702 cyc -> now declines Both bodies contain a PRICED op whose encoder expansion emits PUSH/POP, and `writes_sp` answered `false` for it via the wildcard. So the misclassification was reachable and reached, on ordinary WASM. Honest about what that does and does not mean: those two bounds were not demonstrably WRONG. The push/pop is net-zero across the expansion and writes strictly below the incoming SP, so neither the trip count nor the cycle sum is corrupted. They were accidentally sound — resting on an unenforced premise that no tracked counter slot lives at a negative SP offset (the walk tracks slots by raw signed `addr.offset` and never constrains the sign). The answer moved to the sound side rather than staying accidentally right. Coverage cost stated, not hidden: an i64 rotate/popcnt inside an otherwise provable counted loop is no longer bounded. Re-earning it means enforcing the non-negative-slot premise in the walk, not relaxing the predicate. Includes a NON-VACUITY control: the same loop shape with `i64.and` (an i64 op that does not touch SP) must stay BOUNDED, so the two decline assertions cannot be satisfied by a future change that declines every i64 loop. wcet_bound_gate: 44 -> 47 executed tests, all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Follow-up to the classification commit. The three PRICED expansions that wrap
a fixed-register core in PUSH/POP (I64Popcnt, I64Rotl, I64Rotr) were given the
conservative `true`, which cost two real bounds. That was the wrong trade: the
answer was not undeterminable, it was determinable-but-unenforced.
The `false` argument has two parts. NET-ZERO was already verifiable by reading
each arm (entry -16-12+12 = -16, exit +16). NO-ALIASING needed a premise the
walk did not enforce: that nothing tracked lives at a negative SP offset. It
now does — `read_slot` returns Top below SP, `write_slot_word` taints instead
of recording, and `shift_slots` drops any entry a re-base pushed below SP (an
epilogue `pop {r4-r7}` shifts by -16, so this case is real). All three are
conservative in one direction only: Top and taint can cost a bound, never
invent one.
So the premise is now a WalkState invariant, `false` is derived rather than
assumed, and the bounds come back:
i64.rotl trip-8 loop: 3120 cycles (identical to main)
i64.popcnt trip-8 loop: 4702 cycles (identical to main)
Net effect of the lane on emitted bounds: none. The wildcard's answers were
right; nothing justified them. Now something does.
Gate fixtures rewritten from decline-assertions to EXACT bound pins (3120 /
4702, trip 8, source static) plus an SP-free `i64.and` control, so the lane
cannot silently trade Track D coverage in either direction. New unit test
`walk_state_never_tracks_a_slot_below_sp` pins the invariant directly.
Also corrects the reachability comment: `Call` bypasses the `op_cost` check
entirely (`classify_call` returns Direct and continues); it is unreachable
because `encode_thumb` refuses it with a typed Err, not because op_cost
declines it. The `true` answer is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…d-946 # Conflicts: # claims.yaml # crates/synth-backend/src/wcet.rs
avrabe
added a commit
that referenced
this pull request
Aug 14, 2026
…MS (#970) (#972) Six artifacts still read `proposed` while their work is on main. Re-graded to `implemented`, so the release-readiness query means something: RQ-57-SKIPEXIT #952 (#960) declined REQUESTED export exits non-zero RQ-57-PROVGAP #944 (#967) verified origins for introduced branches RQ-57-DOCSWEEP #946 (#968) Tiers 2+3 honesty sweep RQ-57-SPWILD #946 (#969) writes_sp 175-of-222 wildcard expanded RQ-57-A64PARAM #851 (#971) aarch64 written-param homing RQ-561-ZEROMEM #953 (v0.56.1) shipped two releases ago, never re-graded NEW: RQ-57-COUNTPARAMS (#970). It has no artifact because nobody planned it — the #851 lane found it while fixing the aarch64 instance of the same shape. An unplanned finding with no artifact is invisible to the release query, which is exactly how #933 slipped a release, so it gets one now. What it records that the issue alone does not: * The severity differs by BACKEND and both halves are stated at the confidence they were established. RISC-V is EXECUTED-confirmed: with a poisoned stack the function returns 0xDEADBEEF — an UNINITIALISED stack slot, i.e. previous frame contents. That is information disclosure, not a wrong value. ARM's exact wrong value was INFERRED FROM DISASSEMBLY, never executed; the artifact says so, because the #851 lane's first draft stated it as measured and the advisor caught it. * ARM is only INCIDENTALLY correct on the simple shape (a merge-point `str r1,[sp]` catches the still-live param) and breaks once a call clobbers the param register — so "ARM looks fine" is not evidence. * Red-first must be per-backend BY EXECUTION. Assuming one backend's evidence transfers is what left this latent after the aarch64 fix. * The named residual survives: the `None` branch of `current_func_param_count` still uses the unsound heuristic — unreachable from the CLI, reachable via the direct `compile_function` API. Per the user's direction this ships in v0.57 rather than as a v0.56.3 patch. rivet: errors unchanged (50 before and after); warnings 164 -> 166, which is the two every artifact in this file carries (trailer-reference naming, and the `verifies` link that lands with the test) — verified identical for RQ-57-SPWILD and RQ-57-GPIO, so this is one more artifact of the same shape, not a new defect. claim_check 43/43. Refs #970 Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
avrabe
added a commit
that referenced
this pull request
Aug 14, 2026
* chore(release): v0.57.0 assembly — "the checkers were the defects" Nine artifacts. In five of them the bug was in the machinery that checks the compiled code, not in the compiled code: #975 ArmSemantics silently no-oped 87 of 222 ops — a Rocq-proved, default-on rotl rule was "validated" by a model that executed neither of its instructions #976 the gpio differential CANNOT discriminate the miscompile it guards — complementary conditions, so no input to that driver can #969 writes_sp claimed exhaustiveness over a wildcard absorbing 175 of 222 #967 the "9 unattributed branches" were manufactured by witness's own hardcoded divergence text #979 the prescribed release: back-fill would have written 32 false entries, with the one correct pre-existing value beside them as the disproof The unifying property is that each of those checks COULD NOT FAIL. This release makes them able to fail and proves it by making them fail on purpose. Also fixed, and the most severe item: #974 — a conditionally-written parameter was demoted to a zero-init local on ARM and RISC-V. Exit 0, no decline, wrong code; on RISC-V it reads an UNINITIALISED stack slot (0xDEADBEEF under a poisoned stack), an information-disclosure shape. ARM behaves identically — which the issue predicted otherwise, and only execution settled. Release surfaces, all four swept and checker-confirmed at 0.57.0: Cargo.toml [workspace.package] + 10 path-dep pins MODULE.bazel, npm/package.json, Cargo.lock (cargo metadata) scripts/check_version_pins.py: OK Derived artifacts regenerated (--emit-status): artifacts/status.json, docs/status/FEATURE_MATRIX.md. Claim gate: 43/43. Open by design, named not hidden: #973 (ARM select miscompile, found only because a lane compiled ARM fixtures — which CI never does), #977 (ELF-magic flake, second sighting), #938 (breaking object 0.x-minor bump, auto-merge disabled), #912 (open with four remaining: items). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(release): act on the v0.57.0 cold review — 8 accuracy defects, 4 of them mine Cold review of the assembled release. Nothing blocked the tag; everything below is accuracy. Four of the eight were errors in the CHANGELOG I had just written, which is the reason the review exists. THE GENERALIZABLE FINDING, and it is pointed given this release's theme: `check_generated_fresh` byte-compares the RENDERED FEATURE_MATRIX against the TEMPLATE. `render_feature_matrix` only substitutes `{{...}}` fields, so the gate proves the render is faithful to the template — and NEVER that the template is faithful to the code. Every stale number below lives in template prose no substitution touches. In a release titled "the checkers were the defects", that is the checker that cannot fail. Three independent stale numbers survived a green 43/43. USER-FACING FALSE, verified by compiling rather than by reading: FEATURE_MATRIX listed "writing a PARAM local in a LEAF function" as a LOUD DECLINE on aarch64. #971 shipped exactly that. A leaf `local.set` on a param compiles: 32 bytes of machine code, exit 0. Also corrected in the same row: homing is no longer non-leaf-only, and the float-param decline widened with it. Fixed in the TEMPLATE (the render is generated) + regen. MY CHANGELOG ERRORS: * "145 of the 175 pre-declined / 30 reachable" matched no partition. The shipped source (wcet_loops.rs:1232) says 142 give up with `true`, leaving 33. Re-derived: 142/33. Corrected. * "demoted to a zero-initialised local" is wrong for the two backends the entry is about — zero-init is gated on first-access-being-a-READ, and in the cond-write shape the first access IS the write, so nothing initialises the slot. That is WHY it reads poison; the old wording made an information-disclosure bug sound like a benign wrong value, and contradicted the entry's own next sentence. * "Nine artifacts" — there are ten, and RQ-57-DOCSWEEP (#946/#968) had NO CHANGELOG entry at all despite touching CLAUDE.md, coq/STATUS.md, PROJECT_STATUS.md, the matrix template and eight source files. Added. * "Five in-tree oracles took that opt-in" — eight scripts plus three Rust tests. All eight carry floors, but `i64_param_518_riscv_loudskip`'s is `compiles >= 1`, which is a floor and NOT the "tight" one the paragraph claimed for the set. Named rather than folded into the claim. STALE COUNTS (the template-prose class above): ORACLE_WIRING.md, the matrix template and claims.yaml all said "137 oracles / 295,621 emulator entries". Re-derived independently — and the reviewer's number and mine agree exactly: 144 oracles / 296,059. Both `count-min` pins moved 137 -> 144 with them (same `emulations >=` pattern, two sibling claims); the pinned verbatim texts moved too, or the ledger would have gone red against its own corrected doc. REVERSE STALENESS (a doc calling SHIPPED work missing): `synth verify` declines shift rules citing "SMT modeling of the variable-shift register encoding is an open gap". #975 CLOSED that gap — it modelled LslReg/LsrReg/AsrReg/RorReg as Rm<7:0> (ARMv7-M A7.7.68/70/12/117) and moved five lowerings Invalid -> Verified. Both comments corrected to say what is true: the modelling gap is closed, the remaining decline is a WIRING residual. Behaviour deliberately unchanged — rewiring the rule table is a verification-surface change, not release assembly. Filed as #981. ARTIFACT: RQ-57-PROVGAP still asserted "9 object branches with no WASM origin" as fact while its own PR disproved it. Outcome recorded, as RQ-57-BACKFILL already did. docs/architecture/CRATE_STRUCTURE.md said 18 crates; there are 19. A RECURRENCE — PROJECT_STATUS.md cites this exact drift as why it was gutted in the #946 sweep, and one file over it was live again. Gates after: claim_check 43/43, check_version_pins OK at 0.57.0, cargo check -p synth-cli rc=0. Refs #980, #981 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * style: rustfmt the #975 decline-reason comment (indent 14 -> 12) My own miss: I ran cargo check on the edited file but not cargo fmt, and Format is a required context. The comment content is unchanged — only the indentation rustfmt wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #946 (Tier-1 D). Artifact RQ-57-SPWILD.
The measurement, re-taken on
origin/mainThe issue's numbers are accurate, not stale:
ArmOpvariantsa32_no_silent_nop_615.rs's pin)writes_sp_ => falsewrites_spdocumented itself as "exhaustive over everyrd-carrying op the pricedinstruction set can contain" — the #615 shape exactly: a function that claims to
enumerate a space while a wildcard silently answers for most of it.
Reachability: 30 of the 175 actually reach the predicate
analyze_loopshas exactly one caller,wcet::function_wcet_intermediate, andscan_for_declineruns before it. That scan declines everythingop_costcallsUnmodeled(141) orLoopedExpansion(4). So the stream reachingwrites_spcontains only
OpCost::Cyclesops and direct calls:All three legs of that argument are now pinned by tests, not asserted in a comment
(
op_cost_still_has_no_catch_all_arm,scan_for_decline_runs_before_analyze_loops,analyze_loops_has_exactly_one_call_site).One correction to the chain:
Callbypasses theop_costcheck entirely —classify_callreturnsDirectandcontinues. It is unreachable for a differentreason (
encode_thumbrefuses it with a typedErr, #615). The comment now says so.Was anything actually misclassified? Yes — live, and measured
I64Popcnt/I64Rotl/I64Rotrare priced and their expansions really do emitPUSH/POP(0xB438/0xBC38;0xB40F=PUSH {R0-R3}viaemit_i64_fixed_abi_entry). Two ordinary WASM modules,-t cortex-m4 --emit-wcet,only
wcet_loops.rsdiffering:So the wildcard was reached and answered on real compilations — not latent.
But the numbers were not wrong. The push/pop is net-zero across the expansion and
writes strictly below the incoming SP, so neither the trip count nor the cycle sum is
corrupted. The bounds were right for a reason nothing checked: they needed the premise
that no tracked counter slot lives at a negative SP offset, and the walk took
addr.offsetraw and signed.I first took the conservative
true(declining both loops). That was the wrong trade —the answer was determinable, just unenforced, and Track D's whole arc is
decline→bound. The premise is now a
WalkStateinvariant instead:read_slot(off < 0)→Topwrite_slot_word(off < 0)→ taint rather than recordshift_slotsdrops any entry a re-base pushed below SP (an epiloguepop {r4-r7}shifts by −16, so this case is real)
All three are conservative in one direction only. With the premise enforced,
falseisderived, and the bounds come back byte-for-byte identical to main (3120 / 4702).
Net effect of this PR on emitted bounds: none.
frozen_codegen_bytesgreen —.textcannot move, this is analysis-only.The
_ => false→ explicit armsGrouped by reason, not by answer:
rd == SPfor the priced i64 ops that have a destination(
I64SetCond,I64Clz,I64Const,I64Ldr,I64Mul,I64Shl, …) — the same testtheir i32 counterparts already got.
I64Const/I64Ldrare the wcet: two opcodes (I64Const, I64Str) account for all 9 unmodeled-op declines on a whole OS — and 11 cascades behind them #936 ops that becamereachable here and silently inherited the wildcard.
falsefor the net-zero PUSH/POP group, with both facts spelled out.falsefor the reachable counted-loop vocabulary (Str,Cmp,BOffset,BCondOffset,Bx,Bl,Label,Nop,Udf,I64Str).trueon any of thesewould decline every proven loop. Two traps recorded in-place:
Str'srdis thestored value (a source) and
MemAddrhas no writeback; andBxmust stayfalsebecauseresolve_toplevel_initsmatches the guard arm before itsBxarm,so a
truewould shadow the return handling with no compiler warning.truefor the 142 unreachable variants (VFP, MVE, i64 pseudo family, off-pathpseudo-ops). Deliberate:
trueis the give-up direction, and when wcet: two opcodes (I64Const, I64Str) account for all 9 unmodeled-op declines on a whole OS — and 11 cascades behind them #936 priced threeops they instantly became reachable and inherited an unaudited
falsewith nobodyrevisiting. Now pricing an op yields a loud decline until its SP effect is audited.
Renamed
writes_sp→may_move_sp: both call sites use it as a give-up predicate,and the old name could not honestly cover the unaudited answers.
Tripwire (#615 style), proven potent rather than assumed
Structural (
tests/wcet_sp_no_wildcard_946.rs) — fails ifmay_move_spgrows acatch-all, or if any of the 222 variants stops being named. The scanner rejects
bare-identifier bindings (
op =>,op if pred(op) =>) as well as_ =>, becausewcet_loops.rsuses both shapes legitimately elsewhere and a_-only scan would bebypassable. It asserts the extracted body really contains
match op {before scanningit — a scan over nothing passes silently.
Behavioural (unit tests) — pins the answers in both directions, plus an explicit
non-vacuity list.
Measured potency (not claimed):
_ => false,read_slot'soff < 0guardwalk_state_never_tracks_a_slot_below_spRED; restore → greenThe behavioural half only bites because the unreachable bucket answers
true— had itanswered
falseto match the old wildcard, the wildcard would be behaviourallyidentical and that half would be vacuous. Three synthetic negative controls prove
the scanner itself can go red.
Bonus finding: a stale audit in a claims-pinned area
Auditing every SP-touching emission turned up
STRAIGHTLINE_CEIL_PER_HALFWORD'srationale claiming "the audited expansions push at most 3 registers, so this holds with
margin". Enumerating every 16-bit PUSH/POP and its owner:
I64Popcnt0xB438/0xBC38I64DivU0xB4F0/0xBCF0emit_i64_fixed_abi_entry0xB40FI64Rotl/I64RotrThe margin does not exist: 1+4 = 5 in one halfword, exactly the ceiling. The bound is
unchanged and still sound, and the finding strengthens the model — it is precisely
why the constant had to go 4→5. The
claims.yamlpin is on the constant, which doesnot move; only the rationale comment beside it was stale.
Verification
cargo test --workspace— no failurescargo clippy --workspace --all-targets -- -D warnings— cleancargo fmt --check— cleanpython3 scripts/claim_check.py claims.yaml— 43/43wcet_bound_gate— 44 → 47 executed tests, all greenfrozen_codegen_bytes— green (byte-invisible)No
[Unreleased]edit, no version bump, noartifacts/status.jsonregen.Named follow-up
scan_for_declinereports only the first decline per function, so the 30-reachablefigure is a static-set argument rather than an observed one. A whole-object
--emit-wcetrun would confirm which of the 30 occur in practice.🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L