Skip to content

fix(#946): expand writes_sp's 175-of-222 wildcard into audited arms + no-wildcard tripwire - #969

Merged
avrabe merged 6 commits into
mainfrom
fix/writes-sp-wildcard-946
Aug 14, 2026
Merged

fix(#946): expand writes_sp's 175-of-222 wildcard into audited arms + no-wildcard tripwire#969
avrabe merged 6 commits into
mainfrom
fix/writes-sp-wildcard-946

Conversation

@avrabe

@avrabe avrabe commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Closes #946 (Tier-1 D). Artifact RQ-57-SPWILD.

The measurement, re-taken on origin/main

The issue's numbers are accurate, not stale:

ArmOp variants 222 (matches a32_no_silent_nop_615.rs's pin)
named by writes_sp 47
absorbed by a single _ => false 175 (79 %)

writes_sp documented itself as "exhaustive over every rd-carrying op the priced
instruction 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_loops has exactly one caller, wcet::function_wcet_intermediate, and
scan_for_decline runs before it. That scan declines everything op_cost calls
Unmodeled (141) or LoopedExpansion (4). So the stream reaching writes_sp
contains only OpCost::Cycles ops and direct calls:

  • 30 of the 175 are priced → genuinely reachable
  • 145 are pre-declined → unreachable

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: Call bypasses the op_cost check entirely —
classify_call returns Direct and continues. It is unreachable for a different
reason (encode_thumb refuses it with a typed Err, #615). The comment now says so.

Was anything actually misclassified? Yes — live, and measured

I64Popcnt / I64Rotl / I64Rotr are priced and their expansions really do emit
PUSH/POP (0xB438/0xBC38; 0xB40F = PUSH {R0-R3} via
emit_i64_fixed_abi_entry). Two ordinary WASM modules, -t cortex-m4 --emit-wcet,
only wcet_loops.rs differing:

i64.rotl   in a trip-8 counted loop   main: bounded 3120 cyc
i64.popcnt in a trip-8 counted loop   main: bounded 4702 cyc

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.offset raw 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 WalkState invariant instead:

  • read_slot(off < 0)Top
  • write_slot_word(off < 0) → taint rather than record
  • 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. With the premise enforced, false is
derived, and the bounds come back byte-for-byte identical to main (3120 / 4702).

Net effect of this PR on emitted bounds: none. frozen_codegen_bytes green —
.text cannot move, this is analysis-only.

The _ => false → explicit arms

Grouped by reason, not by answer:

  • Precise rd == SP for the priced i64 ops that have a destination
    (I64SetCond, I64Clz, I64Const, I64Ldr, I64Mul, I64Shl, …) — the same test
    their i32 counterparts already got. I64Const/I64Ldr are 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 became
    reachable here and silently inherited the wildcard.
  • Earned false for the net-zero PUSH/POP group, with both facts spelled out.
  • false for the reachable counted-loop vocabulary (Str, Cmp, BOffset,
    BCondOffset, Bx, Bl, Label, Nop, Udf, I64Str). true on any of these
    would decline every proven loop. Two traps recorded in-place: Str's rd is the
    stored value (a source) and MemAddr has no writeback; and Bx must stay
    false because resolve_toplevel_inits matches the guard arm before its Bx arm,
    so a true would shadow the return handling with no compiler warning.
  • true for the 142 unreachable variants (VFP, MVE, i64 pseudo family, off-path
    pseudo-ops). Deliberate: true is 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 three
    ops they instantly became reachable and inherited an unaudited false with nobody
    revisiting. Now pricing an op yields a loud decline until its SP effect is audited.

Renamed writes_spmay_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 if may_move_sp grows a
catch-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 _ =>, because
wcet_loops.rs uses both shapes legitimately elsewhere and a _-only scan would be
bypassable. It asserts the extracted body really contains match op { before scanning
it — 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):

mutation result
replace the 142-line arm with _ => false, 3 tests RED (structural ×2 naming all 142, behavioural ×1); restore → green
delete read_slot's off < 0 guard walk_state_never_tracks_a_slot_below_sp RED; restore → green

The behavioural half only bites because the unreachable 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.

Bonus finding: a stale audit in a claims-pinned area

Auditing every SP-touching emission turned up STRAIGHTLINE_CEIL_PER_HALFWORD's
rationale claiming "the audited expansions push at most 3 registers, so this holds with
margin". Enumerating every 16-bit PUSH/POP and its owner:

owner encoding regs status
I64Popcnt 0xB438/0xBC38 3 priced
I64DivU 0xB4F0/0xBCF0 4 LoopedExpansion (declined)
emit_i64_fixed_abi_entry 0xB40F 4 priced, via I64Rotl/I64Rotr

The 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.yaml pin is on the constant, which does
not move; only the rationale comment beside it was stale.

Verification

  • cargo test --workspace — no failures
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo fmt --check — clean
  • python3 scripts/claim_check.py claims.yaml43/43
  • wcet_bound_gate44 → 47 executed tests, all green
  • frozen_codegen_bytes — green (byte-invisible)

No [Unreleased] edit, no version bump, no artifacts/status.json regen.

Named follow-up

scan_for_decline reports only the first decline per function, so the 30-reachable
figure is a static-set argument rather than an observed one. A whole-object
--emit-wcet run would confirm which of the 30 occur in practice.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 5 commits August 14, 2026 05:58
`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

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.66082% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-backend/src/wcet_loops.rs 97.66% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

…d-946

# Conflicts:
#	claims.yaml
#	crates/synth-backend/src/wcet.rs
@avrabe
avrabe merged commit c1c40f1 into main Aug 14, 2026
57 checks passed
@avrabe
avrabe deleted the fix/writes-sp-wildcard-946 branch August 14, 2026 13:10
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

doc-vs-source sweep: 30 mechanically-verified disagreements, 4 of them possible defects (not doc staleness)

1 participant