Skip to content

RQ-59-GLOBALINIT (#1052): ARM relocatable path REFUSES global initializers it does not materialize — the fourth silent drop, plus teeth for the harness that was green over it - #1058

Merged
avrabe merged 5 commits into
mainfrom
fix/global-init-silent-drop-1052
Aug 26, 2026

Conversation

@avrabe

@avrabe avrabe commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes the fourth silent drop of the #1041 class: the ARM plain --relocatable path silently discarded nonzero global initializers — compiled exit 0, 0x2A nowhere in the ELF, wrong value at runtime. The fix is the house refusal (#851 / #1049 / #1053 shape). Materializing the initializers on this path is capability work (v0.60, alongside VCR-REACH-002) and is explicitly NOT this artifact — this PR is the honest-frontier minimum plus the harness that should have caught it.

Rivet: RQ-59-GLOBALINIT (artifact entry on PR #1056, plan/v59-globalinit; this branch is off main and does not duplicate it).

Red-first transcript

Before (origin/main) — the filed repro, executed:

$ synth compile globals_repro.wat --all-exports --relocatable --target cortex-m4 -o glob_arm.o
INFO Generated 431 byte ELF file
exit=0                                 # no warning, no mention of the initializer
$ objdump -s glob_arm.o
Contents of section .text:
 0000 10b5d9f8 000010bd            # push {r4,lr}; ldr.w r0,[r9]; pop {r4,pc}
# 0x2A appears NOWHERE in the ELF — no .data, no __synth_globals

wasmtime: get() == 42. The object returns whatever the embedder left at R9.

After (this branch):

$ synth compile globals_repro.wat --all-exports --relocatable --target cortex-m4 -o glob_arm.o
Error: module declares 1 global initializer(s) whose value(s) do not reach the emitted
object (global 0 = (i32.const 42)) — the ARM relocatable path does not materialize
global initializers (the R9 globals table is embedder-mapped, and the documented
contract assigns only the table BASE to the embedder, never initializer evaluation),
so every global.get would silently read whatever the embedder's memory happens to
hold; refusing (#1052). Compile self-contained (drop --relocatable; the Cortex-M
image materializes global initializers at reset, #649), use --native-pointer-abi
with a linear memory (the __synth_globals region ships the init values as .data,
#237), or pass --embedder-global-init to declare that your embedder evaluates the
module's global initializers and seeds the R9 table before any export runs.
Shipping the initializer image on this path is a documented capability follow-on (v0.60).
exit=1

The assertion shape is the point (commit 1, red on its parent): the tests assert a non-zero exit + a reason string naming the global initializers. They deliberately do NOT assert "the initializer value is absent from the object" — that was already true on the broken behaviour and would have been vacuously green, which is exactly how this bug survived. 6 refusal tests red on main (nonzero i32, nonzero i64, non-const init expr, cortex-m4 + cortex-r5, import-forced ET_REL, --native-pointer-abi without memory); 4 green controls passed throughout; all 11 pass now.

The contract language, re-verified (not inherited from the finding lane)

  • crates/synth-synthesis/src/instruction_selector/select_with_stack.rs:5545: "Load global value from globals table (R9 = globals base)." — the BASE register, nothing more. Same at :5629 and in contracts.rs ("R9: globals base pointer").
  • scripts/repro/multi_segment_static_data_differential.py:20: "and the embedder populates its init segments (the object carries no ROM image on this path)" — the data-segment sentence that made ARM --relocatable SILENTLY DROPS active data segments — exit 0, no bytes, no warning (explains gale#278 state(0)=255) #1041 a refusal-with-flag exists for data segments only. A repo-wide grep for any sentence assigning global-initializer evaluation to the embedder finds none.
  • README:119: "R9-based globals" — mechanism, not init ownership.

So: outside the contract, confirmed. Every other path ships the inits (--native-pointer-abi #237, aarch64 #851), materializes them at reset (self-contained — #649 fixed exactly this class there as a BUG), or loud-skips (RV32 #643). The plain relocatable path alone did none of the three.

The guard

Predicate is "does the initial VALUE reach the object", not "which flag was passed" (in build_relocatable_elf, after emit_wasm_data is known):

--embedder-global-init is the explicit escape hatch (the #952/#1049 shape). Bytes identical either way, pinned by test: the flagged nonzero-init object is asserted byte-identical to a zero-init twin's unflagged object — the init value reaches no emitted byte, which is precisely the contract the flag acknowledges. Frozen anchors: unmoved (refusal only; full frozen_codegen_bytes suite green).

The strengthened harness, and proof it has teeth

Why the bug survived: the sole relocatable-path globals harness (i64_globals_643_differential.py) maps a zeroed region annotated # zeroed globals table (inits are 0), and its fixture's inits ARE 0 — it structurally could not distinguish "initializers written" from "initializers were all zero anyway". Same weak-checker family as #1055 ("before FIRST read") and #1054 ("may clobber it freely").

Strengthened (check_nonzero_init_contract_1052, new fixture globals_init_1052.wat with inits 42 and 0x1122334455667788), three legs:

  1. refusal — plain --relocatable must exit non-zero naming the global initializers;
  2. contract — with --embedder-global-init the harness acts as the acknowledged embedder: it seeds the R9 table from the module's own initial values, read back through wasmtime's exported globals (the VCR-VER-003 served-vs-runtime shape — no second hardcoded copy), then the execution differential must agree with wasmtime;
  3. non-vacuity — the SAME object on a zeroed table must diverge from wasmtime; if it ever stops diverging, the fixture has gone vacuous and the harness fails itself.

Teeth proven by execution against the pre-fix compiler (origin/main sources, rebuilt):

=== #1052 nonzero-initializer contract (plain --relocatable) ===
  [BUG] refusal: exit=0 reason missing/unnamed (SILENT-DROP: compiled exit 0)
  [BUG] --embedder-global-init compile failed: For more information, try '--help'.
ORACLE: FAIL (2 divergences)

Against the fixed compiler: all legs pass (seeded get_a=0x2a, get_b_lo=0x55667788, get_b_hi=0x11223344; zeroed-table get_a -> 0x0 DIVERGES). The original #643 sections and its ci-checks: emulations >= 28 floor are untouched (the new legs only add emulations).

Affected-harness count (measured, not assumed — and re-measured)

First measurement said "2 tests in 1 file" and was wrong: cargo test stops at the first failing binary, so the first run's failure list was truncated. The honest number comes from --no-fail-fast plus executing every CI synth compile invocation extracted from ci.yml against the fixed binary. Measured fallout, all converted per the #1049 shape (default refuses; the flag acknowledges; bytes identical):

(#1049 found ~30 affected, #1053 found 0 — this one measured ~30 across 9 test files + 2 harnesses + 3 CI steps.)

Gates

Refs #1052

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 4 commits August 26, 2026 03:28
…dropped global initializers

Six refusal tests FAIL on current main (exit 0 where a loud refusal is
required): nonzero i32/i64 const inits, non-const integer init, cortex-m4 +
cortex-r5, import-forced ET_REL, and --native-pointer-abi with no linear
memory (region never emitted). Four green controls pass: zero inits, the
self-contained materialization path (#649), --native-pointer-abi with
memory (#237), and an untouched float global (GI-FPU-001 lane).

The assertion shape is deliberate: NON-ZERO EXIT + a reason naming the
global initializers — NOT "the init value is absent from the object",
which was already true on the broken behaviour and is exactly how the bug
survived the #643 harness's zeroed-globals fixture.

Refs #1052

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…tializers it does not materialize

The plain --relocatable path compiled (global (mut i32) (i32.const 42)) +
a global.get export to an object whose entire text was push; ldr.w
r0,[r9]; pop — 0x2A nowhere in the ELF, exit 0. OUTSIDE the documented
embedder contract: R9 is documented as the globals-table BASE only
(select_with_stack.rs "R9 = globals base"); no sentence assigns
initializer EVALUATION to the embedder, in contrast to the data-segment
sentence behind #1041's refusal. Every other path ships inits (#237,
#851), materializes them at reset (#649), or loud-skips (#643).

The guard's predicate is "does the initial VALUE reach the object", not
"which flag was passed": it refuses nonzero i32/i64 const inits and
non-const integer init exprs on the plain path, AND --native-pointer-abi
without a linear memory (no __synth_globals region is emitted there — the
object shipped an undefined symbol and no init image). All-zero const
inits never refuse (a zeroed embedder table is their correct initial
state — the #643 fixture and every zeroed-scratch harness stay green).
Float/v128 globals stay the GI-FPU-001 (#369)/#680 loud-skip lane; a new
WasmGlobal::float_or_v128 field lets the guard tell an integer non-const
init (silent-drop class) from a float None (access already loud-skips).

--embedder-global-init is the explicit escape hatch (the #952/#1041
shape): it declares the embedder evaluates the module's global
initializers and seeds the R9 table before any export runs. Emitted
bytes are identical either way — the refusal-test suite pins the flagged
object byte-identical to a zero-init twin's object.

Fallout measured, not assumed: 2 tests in cabi_arena_bind_418.rs (the
two ET_REL seam tests, which model exactly the host-instantiates-the-
module contract and already passed --embedder-data-init) gain the new
flag; 0 python harnesses (every nonzero-init repro fixture compiles
--native-pointer-abi or self-contained). Materializing the inits on this
path is documented capability follow-on work (v0.60), not this fix.

All 11 refusal tests green (6 were red on the parent commit).

Refs #1052

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… could notice

The sole relocatable-path globals harness mapped a ZEROED region
("# zeroed globals table (inits are 0)") and its fixture's initializers
ARE zero — so it was green over the #1052 silent drop for its entire
existence: it structurally could not distinguish "the initializers were
written" from "the initializers were all zero anyway" (the #1055/#1054
weak-checker family).

New section check_nonzero_init_contract_1052 with the NONZERO-init
fixture globals_init_1052.wat, three legs:
  1. refusal — plain --relocatable must exit non-zero naming the global
     initializers (NOT "the value is absent from the object", which was
     true on the broken behaviour too);
  2. contract — with --embedder-global-init the harness acts as the
     acknowledged embedder: it seeds the R9 table from the module's OWN
     initial values (read back via wasmtime exported globals — the
     VCR-VER-003 served-vs-runtime shape, no second hardcoded copy) and
     the execution differential must agree with wasmtime;
  3. non-vacuity — the SAME object on a zeroed table must DIVERGE from
     wasmtime; if it ever stops diverging the fixture has gone vacuous
     and the harness fails itself.

TEETH PROVEN by execution: against the pre-fix compiler the harness reds
("[BUG] refusal: exit=0 ... SILENT-DROP: compiled exit 0"); against the
fixed compiler all legs pass (seeded get_a=0x2a, get_b=0x1122334455667788
both words, zeroed-table divergence confirmed). ci-checks floor
(emulations >= 28) untouched — the new legs only add emulations.

Refs #1052

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…s truncated by fail-fast

The initial fallout count (2 tests) was WRONG: cargo test stops at the
first failing binary, so the first suite run's failure list was cut off.
Re-measured with --no-fail-fast plus executing all 37 unique ci.yml
'synth compile' invocations against the fixed binary:

- 9 test files (~28 tests) compile frozen SP-global fixtures as
  harness-embedders and gain --embedder-global-init (the #1049 shape):
  cmp_select_fusion_census, cmp_select_two_move_coverage,
  dwarf_debug_line_emit_394, flag_flip_wave_242, frozen_codegen_bytes,
  recovery_stats_242, shift_mask_elide_686, spill_baseline_pressure_390
  (+ cabi_arena_bind_418 from the previous commit).
  frozen_codegen_bytes green with the flag IS the frozen-anchor proof:
  the bit-identical oracle passes, so no emitted byte moved.
- 2 python harnesses: arm_corpus_sweep_973 (corpus compile coverage
  restored to 150/162 over its 144 floor) and gpio_thin_846_differential
  (loom $__stack_pointer; runner maps R9 itself). Same acknowledgment
  comment shape as #1041's.
- 3 ci.yml compile steps: flight-seam (x2), control-step, call_6_7args.
  Re-probe after: 0 refusals, 0 other failures.
- Guard ORDER fix: the #1052 guard now runs AFTER the #739
  shadow-stack-size flag-honesty guard so that more specific refusal
  keeps its message (shadow_stack_size_without_region_refuses_739).
- Over-refusal refined away: native-pointer-abi fixtures whose globals
  are declared but never accessed (no __synth_globals relocation, no
  region emitted — the call_5args shape) are INERT: the object has no
  globals surface, nothing can read the dropped value, so they compile.
  A module that DOES access globals with no region still refuses
  (pinned by native_pointer_abi_without_memory_refuses_loudly). On the
  plain path R9 accesses carry no relocation, so presence-based refusal
  stands, matching #1041.

Gates: cargo test --workspace --no-fail-fast TRUE_EXIT=0 (149 green
suites, 0 failing binaries); clippy -D warnings clean; fmt clean;
claim_check 50/50; model_coverage_audit ok; corpus sweep PASS;
gpio/643/call_5args harnesses PASS.

Refs #1052

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@avrabe
avrabe force-pushed the fix/global-init-silent-drop-1052 branch from eab2c6f to 63e9299 Compare August 26, 2026 01:29
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.14815% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-cli/src/main.rs 98.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe

avrabe commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

[coordinator] Holding this PR — it reddens VCR-DEC-001 graph-alloc spike differential (#242, thumb2), and the failure is real, not environmental.

All 9 required contexts are SUCCESS, so this would have merged under a count-based rule. It is held because the failing check is not on the advisory allowlist (only codecov* is).

Diagnosis

The failing job is titled trap-semantics oracle (#665 unreachable + #666 rem_s), but no trap oracle failed — the job name is just its title. 40 oracles pass, then:

step 41  FAILURE  Run VCR-DEC-001 graph-alloc spike differential (#242, thumb2)
step 45  FAILURE  Differential evidence ledger (#910)      <- downstream of 41

All four trap oracles reproduce GREEN locally on this branch, each at its non-vacuity floor exactly: 8/8, 8/8, 28/28, 49/49 emulations.

Baseline comparison — this is a regression, not pre-existing

Same script, same command, same host, main vs this branch:

MAIN    ORACLE-EVIDENCE ... mode=compiles floor=27 measured=27 compiles=27 exit=0
BRANCH  ORACLE-EVIDENCE ... mode=compiles floor=27 measured=27 compiles=27 exit=1

Both met the floor — the branch ran the work and the assertions failed.

What actually breaks

Six fixtures now refuse to compile, on this PR's own new refusal:

control_step.wasm      COMPILE FAIL: module declares 1 global initializer(s) whose
flight_seam.wasm       value(s) do not reach the emitted object (global 0 = (i32.const 655…
flight_seam_flat.wasm
gust_kernel.wasm
sret_decide.wasm
msgq_put_359.wasm

And the consequence is worse than six failures. The oracle's own output:

== diverging fixtures: 0 (must be ≥ 2 for increment-2 non-vacuity) ==
  VACUOUS: no fixture's bytes changed — the join colouring (increment 2)
  no longer reaches this corpus.

The refused fixtures were exactly the ones exercising the join colouring. So the refusal does not merely break compiles — it hollows out an unrelated allocator oracle, which then correctly refuses to pass rather than reporting a green vacuous run. That is the #910 non-vacuity design doing its job, and the reason this must not be merged and patched afterwards.

The refusal is right; the collision is what needs deciding

Nothing here argues against the refusal. Silently dropping (global (mut i32) (i32.const 65535)) is a miscompile, and #1052 is a real defect. The question is what these six fixtures should do now, and it is the same question #1049 already answered for data segments.

Options, in the order I'd rank them:

  1. Escape hatch, matching the house style — the --embedder-data-init shape from RQ-59-DATASEG (#1041): ARM relocatable path REFUSES active data segments it does not materialize — silent drop becomes a loud decline #1049 (and A declined export exits 0 with the symbol simply absent — the decline is honest to stderr but invisible to the build system #952's --allow-skipped-exports): an explicit flag declaring the embedder populates globals, changing no emitted byte. The allocator differential passes it, keeps its fixtures, and keeps its teeth. This is the third use of a pattern already established twice.
  2. Zero the fixtures' globals — cheapest, but verify the fixtures still exercise the join colouring afterwards. If they don't, this trades a red oracle for a vacuous one, which is strictly worse and is the failure mode this repo has found in five consecutive releases.
  3. Update the oracle's expectations — only if 1 and 2 are both wrong; a differential that expects a compile failure has stopped being a differential.

Option 2 needs an explicit "still non-vacuous" demonstration, not an assertion.

Reproduce

python3 scripts/oracle_run.py scripts/repro/vcr_dec_001_graph_alloc_differential.py ./target/debug/synth

(Trap for anyone reproducing: do not redirect CARGO_TARGET_DIR — the scripts hardcode ./target/debug/synth. Symlink target at your scratch dir and mkdir -p it first, or every oracle "fails" with FileNotFoundError and measured=0. The non-vacuity floor is what makes that mistake visible.)

Everything else on this PR looks good — the refusal test and the extended i64_globals_643_differential both pass locally, and the latter carries its own teeth check (zeroed-table get_a -> 0x0 DIVERGES from wasmtime).

… embedder-global-init contract — non-vacuously

The #1052 refusal reddened vcr_dec_001_graph_alloc_differential.py: six
fixtures (control_step, flight_seam, flight_seam_flat, gust_kernel,
sret_decide, msgq_put_359) carry nonzero global initializers, and they were
precisely the fixtures exercising the join colouring — the oracle correctly
reported itself VACUOUS (diverging=0) rather than passing hollow.

Fix is the established escape-hatch shape (#1041 --embedder-data-init, one
line above): the harness owns the R9 globals table, so it acknowledges
initializer evaluation explicitly with --embedder-global-init. Evidence:

  * bytes unmoved: section (1) still pins every fixture's flag-off text
    against the frozen goldens (control_step 8b3f1f6fe3a4 len=288 OK, ...),
    and sha256(with flag) == sha256(without) on a module that compiles
    either way (signed_div_const, 9d85e7b4...c57 both).
  * teeth restored, not silenced: applying fixtures 9 (>= 2), diverging
    fixtures 4 (>= 2), exit=0 — the join colouring reaches the corpus again.
  * arm_reloc_globalinit_refusal_1052 (11 tests) and
    i64_globals_643_differential.py stay green (refusal remains the default;
    the flag is opt-in acknowledgment only).

Refs #1052

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@avrabe
avrabe merged commit e1a7b57 into main Aug 26, 2026
63 checks passed
@avrabe
avrabe deleted the fix/global-init-silent-drop-1052 branch August 26, 2026 08:19
avrabe added a commit that referenced this pull request Aug 26, 2026
#1058 merged as e1a7b57. Verified the fix is ON MAIN rather than inferred from
the merge list: `embedder_global_init` appears 9x in crates/synth-cli/src/main.rs
and crates/synth-cli/tests/arm_reloc_globalinit_refusal_1052.rs exists there.

Second flip this release that NO PR owned (RQ-59-TIERCENSUS was the first).
That is the shape v0.58 warned about — a status left `proposed` over shipped
code makes the release-readiness query under-report scope at the cut — and it
is worth noting that it went unnoticed twice in one release even with people
watching for it.

Folded HERE rather than into a new PR for two reasons: GitHub-hosted CI capacity
is the binding constraint (#1062), and this PR already rewrites every line of
release-v0.59.yaml. The PR that makes v0.59 QUERYABLE should also make it
ACCURATE.

Not duplicated: RQ-59-TIERCENSUS and RQ-59-I64SHIFT are already carried by
#1051, so flipping them here too would put competing edits to the same file in
two open PRs.

Verified the way #1064 taught — BY ID, not by absence of an error:
  RQ-59-GLOBALINIT -> (system-req) [implemented]
  artifacts measured 464 = ARTIFACT_FLOOR 464 (a status flip moves no count)
  CI-filter OURS=0, claim_check exit 0

v0.59 now reads 14/17 on this branch; PARTIALCENSUS, TIERCENSUS and I64SHIFT
land with #1051.

Refs #1052, Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe added a commit that referenced this pull request Aug 26, 2026
#1058 merged as e1a7b57. Verified the fix is ON MAIN rather than inferred from
the merge list: `embedder_global_init` appears 9x in crates/synth-cli/src/main.rs
and crates/synth-cli/tests/arm_reloc_globalinit_refusal_1052.rs exists there.

Second flip this release that NO PR owned (RQ-59-TIERCENSUS was the first).
That is the shape v0.58 warned about — a status left `proposed` over shipped
code makes the release-readiness query under-report scope at the cut — and it
is worth noting that it went unnoticed twice in one release even with people
watching for it.

Folded HERE rather than into a new PR for two reasons: GitHub-hosted CI capacity
is the binding constraint (#1062), and this PR already rewrites every line of
release-v0.59.yaml. The PR that makes v0.59 QUERYABLE should also make it
ACCURATE.

Not duplicated: RQ-59-TIERCENSUS and RQ-59-I64SHIFT are already carried by
#1051, so flipping them here too would put competing edits to the same file in
two open PRs.

Verified the way #1064 taught — BY ID, not by absence of an error:
  RQ-59-GLOBALINIT -> (system-req) [implemented]
  artifacts measured 464 = ARTIFACT_FLOOR 464 (a status flip moves no count)
  CI-filter OURS=0, claim_check exit 0

v0.59 now reads 14/17 on this branch; PARTIALCENSUS, TIERCENSUS and I64SHIFT
land with #1051.

Refs #1052, Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe added a commit that referenced this pull request Aug 26, 2026
…pervised release

Measured on v0.59, in a release where I was EXPLICITLY watching for this class
because v0.58 had been burned by it:

  RQ-59-TIERCENSUS    #1047 merged; no open PR flipped it
  RQ-59-GLOBALINIT    #1058 merged; no open PR flipped it
  RQ-59-PARTIALCENSUS #1051 merged and did not flip its OWN status

All three caught by hand and folded into unrelated PRs. Three misses in one
supervised release is a coupling problem, not an attention problem: nothing
mechanically ties "the code landed" to "the artifact says so".

It fails SILENTLY and in the direction that looks like LESS work — a stale
`proposed` makes v0.58's release-readiness query under-report scope at the cut,
so a shipped artifact can be omitted from the notes and the evidence package.

The compounding factor is why this is not just "add a checklist": #1064 showed
the v0.59 file was never LOADED by rivet, so for most of the release the
readiness query could not have caught a stale status even if run. The two
defects hid each other — the file was invisible, so the statuses inside it were
unfalsifiable.

Three candidate fixes recorded with their tradeoffs rather than one asserted;
deriving status from evidence is most in keeping with this release's theme, a
CI check that a MERGED-PR artifact is not still `proposed` is the cheapest thing
that would have caught all three. Kill-criterion: replay v0.59's three misses —
a fix that would not have caught all three has not addressed the measurement.

Validated BY ID: RQ-60-FLIPCOUPLE resolves; CI-filter OURS=0.

Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe added a commit that referenced this pull request Aug 26, 2026
…he 8 links it hid, and a load floor (#1064) (#1065)

* fix(rivet): release-v0.59.yaml was never in the graph — convert to the generic-yaml schema

rivet's generic-yaml source denies unknown top-level fields and accepts
only 'artifacts:'. release-v0.59.yaml carried 'metadata:' +
'requirements:', so the whole file was skipped and all 16 RQ-59-*
artifacts were invisible — the release-readiness query answered about
artifacts rivet could not see. Fold the metadata block into comments
(the v0.56-58 shape) and rename requirements: -> artifacts:.

This EXPOSES 8 pre-existing wrong-type derives-from links (fixed in the
next commit — this commit alone would turn Rivet Validation red).

Refs #1064

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* fix(rivet): repair the 8 wrong-type derives-from links Part 1 exposed

A system-req must derive-from a stakeholder-req. The newly-visible v0.59
artifacts carried 8 derives-from links at sw-req / system-req /
sys-verification targets. Per-artifact disposition (reason inline in the
file next to each link):

  RQ-59-REACH         -> BR-001, VCR-REACH-001 kept as refines
  RQ-59-MEASURE       -> BR-002 (flip verdict = performance dominance),
                         VCR-DEC-001 kept as refines
  RQ-59-WCETI64       -> BR-001 (DAL-A timing artifact), VCR-WCET-001
                         kept as traces-to (it is a sys-verification;
                         refines would invert the V)
  RQ-59-CRSWEEP       -> BR-001, NFR-002 DROPPED (board hygiene is
                         evidence integrity, not product reliability)
  RQ-59-MINORHOLD     -> BR-001 (qualified toolchain / deterministic
                         builds), NFR-002 DROPPED
  RQ-59-FRESHNESS     -> BR-001 (stale-read-as-fresh forges evidence),
                         NFR-002 DROPPED
  RQ-59-PARTIALCENSUS -> BR-001 (the policy fork is a safety question),
                         NFR-002 KEPT as refines (it measures NFR-002's
                         real-world shortfall)
  RQ-59-TIERCENSUS    -> BR-001 (where formal verification does not
                         reach), NFR-002 KEPT as refines

rivet validate: exit 0, Result: PASS, 0 errors; CI-filter OURS = 0.

Refs #1064

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* ci(rivet): non-vacuity floor on artifacts loaded — a skipped source file can no longer pass

Part 3 of #1064. The Rivet Validation gate greps validate output for
ERROR: lines, so a release file rivet SKIPS entirely (non-schema
top-level keys) contributes zero errors and the gate stays green
precisely because the file is invisible — validation did LESS work and
nothing noticed (same shape as #1012 one level up, and the sixth
consecutive release where the defect was in a checker).

Pin a floor on artifacts loaded (the scripts/oracle_run.py discipline:
floor=N measured=N, the number may only rise). Measured under the
CI-pinned rivet v0.23.0 with the externals block stripped — this job's
own conditions: 463 local artifacts. Fails closed if the count line
ever disappears from 'rivet list' output.

RED-FIRST, replayed against the step's exact logic under rivet 0.23.0
(and cross-checked under 0.32.0, identical):

  break:   sed 's/^artifacts:/requirements:/' release-v0.59.yaml
  red:     artifact-load floor=463 measured=447 -> exit 1
           (the OURS error-grep stayed at 0 — the old gate is
           structurally blind to this class)
  restore: artifact-load floor=463 measured=463 -> exit 0

When a PR adds artifacts, raise the floor in the same PR (visible-diff
rule); a fall means a source went invisible — find it, never lower the
floor.

Refs #1064

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* ci(#1064): tighten ARTIFACT_FLOOR 463 -> 464 after #1056 landed GLOBALINIT

The floor was measured before #1056 merged RQ-59-GLOBALINIT to main. A floor
only fails BELOW itself, so 464-measured against 463-pinned would have passed —
but loose. The guard's own rule is that a PR adding artifacts raises the floor
in the same PR, so leaving it slack by one would be the first crack in exactly
the discipline this gate exists to establish.

Re-measured on this branch rebased onto main (17 v0.59 artifacts, GLOBALINIT
included): 464. Re-verified the guard is still potent at the new value by
reintroducing the original #1064 defect:

  GREEN: measured=464 floor=464
  RED  : metadata:/requirements: restored -> measured=447 -> FIRES

CI-filter OURS=0, claim_check exit 0.

Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* chore(rivet): RQ-59-GLOBALINIT is implemented — the second orphaned flip

#1058 merged as e1a7b57. Verified the fix is ON MAIN rather than inferred from
the merge list: `embedder_global_init` appears 9x in crates/synth-cli/src/main.rs
and crates/synth-cli/tests/arm_reloc_globalinit_refusal_1052.rs exists there.

Second flip this release that NO PR owned (RQ-59-TIERCENSUS was the first).
That is the shape v0.58 warned about — a status left `proposed` over shipped
code makes the release-readiness query under-report scope at the cut — and it
is worth noting that it went unnoticed twice in one release even with people
watching for it.

Folded HERE rather than into a new PR for two reasons: GitHub-hosted CI capacity
is the binding constraint (#1062), and this PR already rewrites every line of
release-v0.59.yaml. The PR that makes v0.59 QUERYABLE should also make it
ACCURATE.

Not duplicated: RQ-59-TIERCENSUS and RQ-59-I64SHIFT are already carried by
#1051, so flipping them here too would put competing edits to the same file in
two open PRs.

Verified the way #1064 taught — BY ID, not by absence of an error:
  RQ-59-GLOBALINIT -> (system-req) [implemented]
  artifacts measured 464 = ARTIFACT_FLOOR 464 (a status flip moves no count)
  CI-filter OURS=0, claim_check exit 0

v0.59 now reads 14/17 on this branch; PARTIALCENSUS, TIERCENSUS and I64SHIFT
land with #1051.

Refs #1052, Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* chore(rivet): RQ-59-PARTIALCENSUS is implemented — v0.59 reads 17/17

#1051 merged as f1e2e7b but did not flip its OWN status. THIRD orphaned flip
this release (RQ-59-TIERCENSUS, RQ-59-GLOBALINIT, now this one), all in a
release where I was explicitly watching for the class. Three in one release says
the coupling between "the fix merges" and "the status flips" is too manual to be
reliable — filed as a v0.60 concern alongside RQ-60-ARTIFACTSPLIT rather than
noted and forgotten again.

Verified the work is ON MAIN, not inferred: scripts/repro/partial_census_1017.py
exists there and carries its `# ci-status: manual (measurement)` declaration.

WITH THIS, v0.59 IS 17/17 — and for the first time that number is a QUERY
rather than a YAML read. Per-artifact, via `rivet validate --explain <id>`:

  resolved AND implemented: 17 / 17   (failures: 0)

That distinction is the whole of #1064: before this PR's schema fix, the file
was skipped entirely and `rivet validate --explain RQ-59-POPCNT` answered
"artifact not found" while a YAML read happily reported statuses. A count read
out of a file rivet never loaded is not a readiness metric.

  artifacts measured 464 = ARTIFACT_FLOOR 464 (status flips move no count)
  CI-filter OURS=0, claim_check exit 0

Refs #1017, Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 26, 2026
…pervised release

Measured on v0.59, in a release where I was EXPLICITLY watching for this class
because v0.58 had been burned by it:

  RQ-59-TIERCENSUS    #1047 merged; no open PR flipped it
  RQ-59-GLOBALINIT    #1058 merged; no open PR flipped it
  RQ-59-PARTIALCENSUS #1051 merged and did not flip its OWN status

All three caught by hand and folded into unrelated PRs. Three misses in one
supervised release is a coupling problem, not an attention problem: nothing
mechanically ties "the code landed" to "the artifact says so".

It fails SILENTLY and in the direction that looks like LESS work — a stale
`proposed` makes v0.58's release-readiness query under-report scope at the cut,
so a shipped artifact can be omitted from the notes and the evidence package.

The compounding factor is why this is not just "add a checklist": #1064 showed
the v0.59 file was never LOADED by rivet, so for most of the release the
readiness query could not have caught a stale status even if run. The two
defects hid each other — the file was invisible, so the statuses inside it were
unfalsifiable.

Three candidate fixes recorded with their tradeoffs rather than one asserted;
deriving status from evidence is most in keeping with this release's theme, a
CI check that a MERGED-PR artifact is not still `proposed` is the cheapest thing
that would have caught all three. Kill-criterion: replay v0.59's three misses —
a fix that would not have caught all three has not addressed the measurement.

Validated BY ID: RQ-60-FLIPCOUPLE resolves; CI-filter OURS=0.

Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe added a commit that referenced this pull request Aug 26, 2026
…rtifacts (#1070)

* plan(v0.60): scope the release — "Derive what you check against"

Five artifacts. Both halves of the theme were EARNED during v0.59 rather than
asserted, which is why this file leads with the evidence:

  RQ-60-CANARY       VCR-TIER-001 increment 1 (delivered, PR #1061). Its
                     immediate finding is the theme's best argument: the
                     census's OWN hand-maintained `declared_temps` column
                     declared the #1048 miscompile LEGAL — `&[R5]` (rm_hi) for
                     the i64 shifts, `&[R3]` (rnhi) for the bit-counts, exactly
                     the registers the defect clobbered. A checker that mirrors
                     the defect it exists to catch.

  RQ-60-CFOBLIG      #1057 (gale). 48% of a real object is covered by NEITHER
                     proof half, and the cause is one level deeper than the
                     issue could see: `Inductive wasm_instr` has NO control-flow
                     constructor at all. There is no obligation for `BrIf`
                     because the model has no `BrIf`. Verified before scoping —
                     gale asked to be corrected and was right to.

  RQ-60-A64IMPORT    VCR-REACH-002. AArch64 accepts 13/805 real modules (1.6%).
                     Import dispatch (~121 modules) is synth's OWN ARM
                     `--relocatable` undefined-symbol design ported, so it is a
                     port of a shipped pattern, not a design question.

  RQ-60-RACOST       VCR-RA-011. Cost model + tied operands, NOT a better
                     search — with the ruled-out alternatives recorded so they
                     are not re-litigated.

  RQ-60-ARTIFACTSPLIT #1059. The single-file artifact write surface that
                     silently dropped a trace link during the v0.59 wave.

Validated with the TYPED oracle, not a permissive one — the lesson from the
v0.59 corruption this file's last artifact is about:
  rivet validate: OURS=0 (main baseline 0; rivet FAILs on main by construction)
  duplicate-key-STRICT loader: 5 artifacts, no duplicate ids
  structural: every artifact carries its own links: and a non-empty issue:
  release field consistent: {v0.60}

The structural check is the mitigation RQ-60-ARTIFACTSPLIT proposes, run here
by hand against this file.

Refs #1021, Refs #1057, Refs #1017, Refs #242, Refs #1059

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* plan(v0.60): fix the schema — this file had the #1064 defect I filed

I wrote release-v0.60.yaml by copying v0.59's shape, so it carried the exact
defect #1064 is about: top-level `metadata:` + `requirements:` instead of
`artifacts:`. rivet's generic-yaml DENIES unknown top-level fields, so the whole
file was skipped with a WARN carrying no `ERROR:` prefix.

Which means my own validation of this file was VACUOUS. I ran `rivet validate`,
got OURS=0, and reported it clean — but rivet had skipped the file entirely.
Zero errors from a file that never loaded is not zero errors. Same shape as
#1012 one level up, and the reason #1064's fix pins a FLOOR on artifacts loaded
rather than only grepping for errors: a validator doing LESS work must fail, not
pass quietly.

Converted metadata: -> comments (v0.58's shape), requirements: -> artifacts:.

Verified BY ID rather than by absence of an error — the distinction that hid
this in the first place:

  RQ-60-CANARY        -> RQ-60-CANARY (system-req)  [proposed]
  RQ-60-CFOBLIG       -> RQ-60-CFOBLIG (system-req)  [proposed]
  RQ-60-A64IMPORT     -> RQ-60-A64IMPORT (system-req)  [proposed]
  RQ-60-RACOST        -> RQ-60-RACOST (system-req)  [proposed]
  RQ-60-ARTIFACTSPLIT -> RQ-60-ARTIFACTSPLIT (system-req)  [proposed]

CI-filter OURS = 0. Artifact count on this branch 447 -> 452 (+5); once #1065
lands the v0.59 fix the same 5 lift 463 -> 468, so this PR must raise
ARTIFACT_FLOOR to 468 when it rebases onto #1065.

All 5 already derive-from BR-001 (the correct stakeholder-req type), so unlike
v0.59 no link retargeting is needed.

Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* plan(v0.60): triage RQ-60-WCETKEY (#1063) — the hints seam has no key for internal functions

gale (fathom) measured it on the E2 dissolved gust:os composite: synth-wcet-v1
takes its `name` from the EXPORT SECTION and falls back to `func_<index>` for
every internal function, so 7 of 13 `loop` declines cannot be hinted at all —
and `loop` is 13 of 28 declines with 11 `callee-unbounded` cascades behind it.

WORSE THAN "NOT HINTABLE": a hint keyed on `func_22` SILENTLY RETARGETS when an
edit shifts the index space. It still parses and still keys onto a function —
just not the one the author verified. synth always emits its own DERIVED ceiling
rather than the raw hint, so the bound stays synth-derived; what a mis-keyed
hint corrupts is the OPT-IN, converting a decline for a function whose shape
nobody looked at. An index is not an identity.

gale pre-empted the obvious dismissal: rebuilding with `meld fuse
--preserve-names` yields a BYTE-IDENTICAL .wcet.json while populating the name
section 0 -> 59 of 68. synth carries the names and ignores them.

Recorded with the caveat they volunteered, because it must shape the design: the
names are v2 Rust mangling carrying a NON-content-derived crate disambiguator,
and scry measured 43-45% of their function identities churning per build for
exactly that reason (scry#123/#137). Better than an index, still not stable —
fixing the blocker with a key that churns every build would trade an
unaddressable decline for an unreliable one.

Their kill-criterion adopted verbatim: a hints file keyed on the name-section
name of one of those seven converts its `loop` decline to `hint-verified`, or is
rejected with a NAMED reason. Being ignored because the key never matches is the
failure.

Validated the way #1064 taught — BY ID, not by absence of an error:
all 6 v0.60 artifacts resolve via `rivet validate --explain`; CI-filter OURS=0;
duplicate-key-strict loader clean; every artifact carries its own links: and
issue:.

Refs #1063
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* plan(v0.60): RQ-60-FLIPCOUPLE — three orphaned status flips in one supervised release

Measured on v0.59, in a release where I was EXPLICITLY watching for this class
because v0.58 had been burned by it:

  RQ-59-TIERCENSUS    #1047 merged; no open PR flipped it
  RQ-59-GLOBALINIT    #1058 merged; no open PR flipped it
  RQ-59-PARTIALCENSUS #1051 merged and did not flip its OWN status

All three caught by hand and folded into unrelated PRs. Three misses in one
supervised release is a coupling problem, not an attention problem: nothing
mechanically ties "the code landed" to "the artifact says so".

It fails SILENTLY and in the direction that looks like LESS work — a stale
`proposed` makes v0.58's release-readiness query under-report scope at the cut,
so a shipped artifact can be omitted from the notes and the evidence package.

The compounding factor is why this is not just "add a checklist": #1064 showed
the v0.59 file was never LOADED by rivet, so for most of the release the
readiness query could not have caught a stale status even if run. The two
defects hid each other — the file was invisible, so the statuses inside it were
unfalsifiable.

Three candidate fixes recorded with their tradeoffs rather than one asserted;
deriving status from evidence is most in keeping with this release's theme, a
CI check that a MERGED-PR artifact is not still `proposed` is the cheapest thing
that would have caught all three. Kill-criterion: replay v0.59's three misses —
a fix that would not have caught all three has not addressed the measurement.

Validated BY ID: RQ-60-FLIPCOUPLE resolves; CI-filter OURS=0.

Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* plan(v0.60): RQ-60-VFPPRESSURE (#1069) — three named functions block a complete falcon M7 cascade

jess filed it with a fully public repro and measured it on a real RT1176 Renode
model (memory byte-exact, 214,696 instructions retired). 2 of 5 cascade stages
export; the loop cannot close on target without the other three.

VERIFIED ALL THREE CLAIMS against current code before scoping — they measured
0.55.0, we shipped 0.59.0:
  * the S pool IS `[bool; 16]` = S0..S15; callee-saved S16..S31 / D8..D15 are
    absent and would roughly double it
  * NO __aeabi_ul2f/l2f/ul2d/l2d routing exists anywhere; #869's acceptance
    comment described exactly that route and inline-f64 shipped instead
  * has_double_fpu() gates f64 on FPUPrecision::Double, i.e. m7dp only

REPRODUCED ON 0.59.0, minimal case sharper than the cascade:

    (func (param i64) (result f32) (f32.convert_i64_u (local.get 0)))
    m4f  -> DECLINED "scalar f64 requires a double-precision FPU target"
    m7dp -> compiles

A function with NO f64 in its signature is refused for needing f64, because our
own lowering introduces it. A reach failure of our own making.

HONEST RESIDUAL IN MY OWN VERIFICATION, recorded so a lane does not inherit a
false premise: sub-problem 1 (S-exhaustion) is confirmed by CODE INSPECTION
ONLY. My deep-f32 fixture compiled fine at 60 bytes — not deep enough to exhaust
S0..S15. A lane must build a fixture that actually reddens phase 1 before
claiming to fix it.

Refs #1069, Refs #869
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* ci(#1064): raise ARTIFACT_FLOOR 464 -> 472 for v0.60's 8 artifacts

The gate's own rule: a PR that ADDS artifacts raises the floor in the SAME PR,
so every movement is a visible diff. 464 (main) + 8 (release-v0.60.yaml) = 472,
measured under the job's own conditions rather than assumed.

Re-verified the guard is still POTENT at the new value rather than assuming
potency carried over — a raised gate that no longer fires is worse than the
omission it was raised for:

  GREEN: measured=472 floor=472
  RED  : reintroduce the #1064 metadata:/requirements: schema defect
         -> measured drops below 472 -> FIRES

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

---------

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.

1 participant