Skip to content

Retire the Solid 1.x dialect (ADR 0110) - #62

Open
yumemi-thomas wants to merge 352 commits into
mainfrom
codex/phase19a-authenticated-proof-policy
Open

yumemi-thomas wants to merge 352 commits into
mainfrom
codex/phase19a-authenticated-proof-policy

Conversation

@yumemi-thomas

Copy link
Copy Markdown
Owner

Scope, stated first

This branch is 343 commits ahead of main, and they are two different
bodies of work:

  • 308 commits, 2026-09-02 → 2026-09-16 — the continuation of phase 19a
    after PR Complete authenticated proof-policy refusal reductions #61 merged. This description does not summarize them, because
    they are not mine to characterize.
  • 35 commits, 2026-09-16 → 2026-09-17 — retiring Solid 1.x, described
    below. These are the ones this PR can vouch for.

Split the PR if the first group wants its own review.

Retiring Solid 1.x (ADR 0110)

The checker now analyzes Solid 2.0 only. A project whose resolved
solid-js is a major this build has no vocabulary for gets one project-level
SC9013 unsupported-solid-runtime result naming the package.json that decided
it, and no other findings — rather than being analyzed under a language it does
not run. An absent or unclassifiable install still gets the 2.0 default, because
that is an absence rather than a contradicted answer.

The refusal landed before the deletion, deliberately: the window between
them would have been exactly the silent-wrong-language behaviour ADR 0110 § 1
forbids.

Deleted

rust/dialects/solid-v1/, solid_1x.rs, the dialect-v1 feature,
docs/rules/v1/, the v1 declaration index, the two upstream_compat modules
that only 1.x ran, and the 254 transcribed eslint-plugin-solid parity cases
(upstream targets 1.x; re-pointing them at the 2.0 catalog would have been a new
claim wearing upstream's name).

Kept deliberately, each with its reason in the code

Version::V1 (the refusal must recognize 1.x to refuse it), the unreachable
unsupported-runtime package status, the On/MergeProps schedule rows, and
pkg/contracts/bundled/solid-v1/ plus the phase-14 authority — audit records of
published artifacts, which describe bytes that still behave as audited and
outlive the dialect (ADR 0110 § 4).

Defects the retirement surfaced

Four, none of which was the retirement itself:

  1. A silent false negative in the contract sweep. --check-contracts emits
    a findings snapshot under a refused runtime, and the sweep read
    report.packages through Array.isArray(…) ? … : [] — so it reported
    "nothing to generate" for a project it had never analyzed. Exit code could
    not catch it either. Now refused, quoting the refusal's own message and hint.
  2. A tracked callback's schedule was the consumer's default, not the
    chain's.
    The direct-invocation rung composed a wrapper chain to a word and
    discarded the wrappers, so tracked reached the row with no schedule and
    took queued. Under 1.x that was usually right; after the retirement it was
    wrong for every 2.0 tracked primitive except createTrackedEffect.
  3. The negative-authority lookup ignored a row's package. solid-js
    re-declares six primitives from ./client/hydration.js whose rows the
    audit withholds on purpose; they were answered out of @solidjs/signals'
    rows. Now keyed on the package that declares the callee, via
    ResolvedDeclaration::source_file.
  4. A message naming an API 2.0 does not export. SC8014 offered
    <Index />; 2.0's index-oriented component is Repeat. Nothing pinned it.

The obvious fix for (3) was wrong and measuring caught it: origin_module is
the import specifier's module, so keying on it declined ten legitimate
re-exports to fix six re-declarations — a regression
callback-reactive-arguments caught.

Also fixed here

A producer crash during the Type Facts symbol phase was unrecoverable
(analyze failed on a crash it was meant to survive). Diagnosed by
instrumenting all eleven exchange sites under contention: 28 of 28 observed
failures escaped from close_symbols, none from the analyze exchange or its
retry. 28 failures in 84 runs before, 0 in 180 after. Session::close also no
longer reports an already-dead producer as a failure to close it; it returns
CloseOutcome instead, so the observation survives without the false error.

Coverage

Honest about both directions. Lost: the tsc oracle corpus 161 → 89 cases,
and 254 upstream parity ledger rows dropped with an ADR 0110 reason (all 465
identities still accounted for). Regained: the package-contract corpus is
94 → 97 fixtures, with callback-untracked-wrapper and
callback-deferred-untracked-chain authored against 2.0 and
escaping-private-helper restored — that one a true port, all 24 of its
entrypoint/export rows byte-identical to the 1.x original, which is its own
evidence that the call-graph claim never depended on a dialect.

Two fixtures had pinned defect behaviour and now pin the corrected behaviour.

Verification

make verify green at the tip (240 s, 0 failures). Every fixture stub was
type-checked under --strict against the real solid-js@2.0.0-rc.3 install as
well as against the stub, so no claim rests on a stub being looser than the
package.

Known remaining

  • The CLI exit-status pin for SC9013 cannot be written yet: the shipped
    bin/solid-checker-rust predates the retirement and still analyzes with
    --dialect solid-v1, so the pin would not exercise the refusal. It waits on a
    release that rebuilds that binary.
  • The negative-authority table is not condition-aware, which is why
    solid-js's createSignal, createEffect and their nine
    ./client/hydration.js siblings carry no creates statement. Open item in
    ADR 0007.

🤖 Generated with Claude Code

yumemi-thomas and others added 30 commits September 11, 2026 08:35
…easured

The chain § 27 proposed -- certify utils, supply it to memo -- stops at the
first step. `@solid-primitives/utils` refuses on a named demand:
`createHydratableSignal`'s returned value root shape has no verifiable
premise, the producer's root observation being open. `--recover-entrypoints`
does not help; it isolates refused artifact cases, and here one export's
demand refuses the whole transaction.

memo is unchanged, exactly as predicted: hazard records 21 -> 7 per domain
(the two core-runtime frontiers cleared, @solid-primitives/utils remains),
closure candidates still 0.

The part worth recording is what this says about § 27. Before the exemption
utils *certified*, vacuously, with a contract stating nothing. After it,
utils refuses. That is the right direction on the precision contract -- a
receipt binding an empty document is worth nothing and a named refusal is
worth something -- but it is a behaviour change I did not measure before
landing. `make verify` excludes the ecosystem benchmark, the checked-in
report holds 381 rows with contract content, essentially all of which import
solid-js, and how many go from certifying vacuously to refusing is unknown.

The refusal itself looks pre-existing: a generator declaring a value root
shape the producer leaves open is a mismatch the frontier was hiding for
every Solid-importing package. Unmasking it is progress. Paying for it
ecosystem-wide in one step was not measured.

cf905a5 should be read as sound but unmeasured at ecosystem scale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…egresses 0

§ 28 called cf905a5 "sound but unmeasured at ecosystem scale" and named
@solid-primitives/utils as a data point going the wrong way. Took the
measurement with `make ecosystem-regression`, the gate built for exactly
this question. § 28's worry was wrong in the aggregate.

418 probes, 349 complete contracts, 32 partial, 531 s.
certificationRegressionCount 0, certificationFixCount 13.

Twelve rows go refused -> certified, including solid-js@1.9.14 itself,
@solidjs/web, @solidjs/element, four TanStack packages, both corvu popovers,
solid-devtools and @solid-primitives/visibility-observer; @kobalte/solidbase
goes not-attempted -> certified.

Not a vacuous pass: the threshold is maxCertificationRegressions 0, the
report records baseline.provided true, and report.test.mjs already pins that
a run supplying no baseline *fails* the threshold rather than passing
silently.

utils reconciles: it is in neither list because it was not certifying before
the exemption either. Its `recursive-value-shape` refusal is a real gap the
exemption unmasked rather than caused, and it now has thirteen
counterexamples arguing the unmasking is worth having.

One consequence worth naming: benchmarks/ecosystem/report.json still records
those thirteen rows as refused, so a future change taking one back to
refused would match the stale pin and pass. Repinning closes that hole but
rewrites an artifact the phase ledgers read, so it is left as a deliberate
separate step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ark wall

Asked to repin benchmarks/ecosystem/report.json with § 29's improved run.
Produced the candidate to scratch first rather than writing the pin and
finding out afterwards, which is what caught this.

durationMs 116,582 -> 527,689. `performance-budget.test.mjs` asserts the
pinned report is strictly below 150,000 ms, so the candidate is 3.5x over
its own ceiling. A pin that fails its budget test is worse than a stale pin;
the file is unchanged.

Not contention and not the host: the uncontended rerun with a warm cache
measured 527 s, and this is the 14-core authority host the budget was
calibrated on (docs record 116.6 s there).

The cause is the exemption. Harness time is up 4.2x while generation moved
only 1.4x, and the harness is the probe worker: every closed claim domain
schedules a mandatory contradiction veto, so +117 certified entrypoints
means far more gates execute. The cost sits exactly where the benefit does.

Worth naming separately: no gate caught this. `make verify` excludes the
benchmark, `make ecosystem-regression` measures outcomes rather than time
(its thresholds carry only maxCertificationRegressions), and
performance-budget.test.mjs reads the *pinned* report so it cannot fire
until somebody repins. A change can quadruple corpus wall time and be green
everywhere that runs before it lands.

§ 30.4 lays out the four options -- raise the budget, batch the synthesized
vetoes, narrow the exemption, or revert cf905a5 -- and none is mine to take
unilaterally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…is a better lever

§ 30.4 proposed batching synthesized vetoes per artifact case. Mapping it
found that proposal was under-informed, in a way worth recording before
anyone acts on it.

Launches cannot be shared. `isolation_collisions` flags two runs sharing a
process, realm or module instance and *refuses the mode* -- that check exists
to catch a shared realm, so a shared launch would defeat the property it
guards. And emitting one module for an export's several domains changes
nothing on its own, because sessions are keyed per claim.

So fewer launches means fewer targets, which means a gate becomes one
isolated observation of one export attesting several of its claims. Coherent
-- one run really does observe all three domains, and the generated body
already computes them -- but it moves `probe_gate_root` and therefore every
policy-2 receipt in the repository, changes what a hand recipe addresses
(the corpus is claim-keyed, as is § 22's scaffold), and changes withheld
attribution. Sized at 395 gates -> 201, ~49% fewer launches, ~527 s -> ~265 s,
still 1.8x over the ceiling.

The better lever is smaller: the sequencing requirement is only that session
N+1 must not *read* the workspace until N's census passes, not that its
interpreter cannot already be booting. A pool of pre-booted workers, each
handed one session after the preceding census and each still getting a fresh
process and realm, amortises boot without weakening isolation and without
changing what a gate means. It needs a worker-protocol bump and is confined
to the harness launcher.

Recommendation in § 31.4: do the pre-boot first, measure, then decide whether
the gate-model change earns a repository-wide receipt migration.

Neither is started; § 30's budget breach stands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
….3 is dead

Measured the probe-gate batch before building the pre-boot worker pool
§ 31.4 recommended. The measurement killed it.

One batch, two sessions, debug: census 2.835 s (59%), workspace 0.957 s
(20%), pin verification 0.913 s (19%), **launch 0.061 s (1.3%)**, conditions
0.039 s. Per session that is census 1418 ms against launch 31 ms.

So a pool that amortised Node boot perfectly would remove at most 1.3%.
§ 31.3 was optimising the one phase that costs nothing.

What costs is the watched census, which runs after every session and hashes
the pinned images -- 117 MB of Node executable and 24.7 MB (release) of
verifier image on this host. It is 98% of a session. That also explains § 30
precisely: the exemption did not make launches slower, it made more
sessions, and every session buys another full census.

This reframes § 31.2 too. Halving sessions halves the *census*, not the
launches, so the gate-model change is worth ~30% of the harness -- right
arithmetic, wrong cost, in the original note.

The untried lever is hashing the large immutable images once per batch
instead of once per session. Not obviously sound: it reopens exactly the
tamper-then-restore the intermediate census exists to stop, and the Node
executable is the extreme case. Left as a security-model question, § 32.3.

Nothing built; § 30's budget breach stands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the ranking

§ 32 concluded launches are 1.3% of a probe-gate batch and abandoned the
pre-boot pool. It measured `cargo test`, which is a debug build, and the
debug profile distorts exactly the phase the comparison rested on:

  * `verifier-image` hashes `current_exe()`, which under `cargo test` is the
    116.3 MB test executable rather than the 23.6 MB release checker;
  * debug SHA-256 runs at 124 MB/s against 510 MB/s measured with `shasum`
    on the same files and host.

Together those inflate the census ~19x, which is the gap between § 32's
1418 ms/session and release. Launching is unaffected by the checker's build
profile, so it was compared against a census nineteen times too expensive.

Release, `@kobalte/utils`, 313 batches / 6,198 sessions: launch 47.4%
(80 ms/session), census 43.3% (73 ms/session). The two phases swap places,
so § 31.3's pre-boot pool is reinstated as the largest measured lever and
§ 31 is vindicated rather than refuted.

Also records that no single package generalises — `solid-js` runs zero probe
sessions and spends 95.7% in Type Facts export-value acquisition — and that
§ 30's breach survives the correction: install time, the environmental
control, is flat while certification is 4.17x and 95% of the increase.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…otocol v7)

The recipe module was a spawn-time environment variable, which fixes it at
process creation and is what makes a worker un-poolable: a booted worker
cannot be told afterwards which recipe to run. It now travels in the session
frame, read next to `id` and `mode.environment` — before the recipe, and
therefore the package, runs — and fails closed when absent so a pooled worker
can never fall back to a value inherited from an earlier session.

The launch nonce deliberately stays in the environment. It binds the *process*
to the harness that spawned it, which is true from boot and is what the
startup frame answers with before any session exists. A parked worker keeps
exactly the binding it has today, so nothing about what the harness proves
changes here.

Both launchers move together — the certification path in `probe_harness.rs`
and the audit path in `contract-probe-driver.mjs` — and ADR 0006's
environment allowlist drops `SOLID_CHECKER_PROBE_RECIPE`.

No behaviour change; this only makes § 31.3's pre-boot pool expressible.

make test-probe-harness: 125 passed, 0 failed (real launches, so a worker
that failed to receive its recipe would fail every one of them).
make test-cli: 217 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…re-boot pool)

`launch` splits into `spawn_parked` — pinned Node, allowlisted environment,
nonce, startup frame verified, all session-independent — and `run_parked`,
which writes the session and reads the one run frame. The next session's
worker is booted on a scoped thread *concurrently with* `verify_unchanged`.

The census is the one window where booting ahead is sound. The finished
session's process group is already dead, and the parked worker has no session,
has read nothing a session names, and is blocked on `stdin` until `run_parked`
writes to it. The census still stands between one session's run and the next
session's first read, which is the property the ordering exists for: parking
changes when a process exists, not when it reads. Booting during the previous
session's *run* would instead place a parked process beside hostile package
code, and is not done.

The policy timeout now starts at dispatch rather than at boot, so a worker
parked early cannot spend a session's budget waiting; the startup frame gets
`STARTUP_BUDGET` on its own. A refused or panicked pre-boot is discarded and
the next session boots inline, surfacing the real error there; the census
verdict always takes precedence. `SOLID_CHECKER_PROBE_NO_PREBOOT=1` disables
the pool and is the A/B control.

Measured on `@kobalte/utils`, release, 313 batches / 6,198 sessions per run,
in both orders to rule out an order effect:

  pool first     launch 169.5s vs 498.0s   wall 208.7s vs 247.7s   -15.7%
  no-pool first  launch 155.2s vs 521.7s   wall 210.2s vs 252.2s   -16.7%

Launching falls ~68%; end to end a certification is ~16% faster. Zero
outcome, status, or exit-status differences across 12,396 sessions.

This does not close § 30: ~16% off 527 s is ~440 s against a 150 s ceiling.

make test-probe-harness: 125 passed, 0 failed.
clippy -p solid-facts-backend --lib -D warnings: clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§ 34.3 measured the pre-boot pool on a single package and reported ~16%;
`f0de1b0e`'s commit message repeats it. A controlled corpus A/B — full
418-row runs back to back, no-pool first, `SOLID_CHECKER_PROBE_NO_PREBOOT=1`
as the control — gives −1.5% wall (548.0s vs 556.5s) and +9.3% row-time sum
(7713.0s vs 7059.0s), with zero outcome, status, or exit-status differences.

The single-package figure did not transfer because that package had the host
to itself, so a boot beside the census ran on an idle core. The benchmark
runs 20 certification children on 14 cores, so the boot does not hide, it
competes — which is what the extra row time is, and is the same saturation
argument that killed the parallel-producer-session proposal earlier.

§ 33.3 had already recorded that no single package generalises. It was
written and then not applied.

The pool stays on: the unsaturated case — one package, interactively, which
is the case a person waits on — really is ~16% faster, it is semantically
neutral in both configurations, and the switch turns it off where the extra
CPU is unwanted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…(§ 27.2)

§ 27.2 named a class to watch and left it an argument: after the core-runtime
exemption a generator proposes `reads: []` for an export whose only read is a
property access on a core-runtime import, and that is admissible only because
the certifier's census refuses the form. Nothing tested it.

The proposal is real. A throwaway package importing `sharedConfig` from
`solid-js` and returning `sharedConfig.hydrating` generates a contract where
that export and an own-literal control share one summary — `closed: [reads,
creates]`, `reads: []` — with an empty refusals sidecar.

Of the two guards it depends on, only one was covered. The census side
already refuses an own-literal whose declaration falls outside the artifact's
runtime source (`type_facts.rs`). The producer side was untested, and the
shape that matters is not the one § 27.2 anticipated: the producer roots an
*imported* binding as `own-literal` whenever its declaration carries a
visible object literal — `importedTableRead` is imported and is rooted,
legitimately, because ADR 0044's premise is about the literal and not about
the import. What nothing asserted is the case where the declaration has no
initializer, which is exactly what a core-runtime package ships
(`declare const sharedConfig`). Had that rooted, the form would reach the
census carrying a clearing premise and `runtime_sources` would be the only
thing left standing.

Adds `TestDeclaredImportedReceiverIsNotRootedAsAnOwnLiteral`: the form must be
recorded, so the census has something to refuse, and none of them may be
rooted. It carries `importedTableRead` as an in-test control so it cannot
pass vacuously. Confirmed falsifiable — pointed at that control it fails with
`a form is rooted "own-literal"; a declaration with no initializer states no
premise`.

The class is sound; the gap was in the evidence. § 36 records it and § 27.2
points at it.

make verify: exit 0, 197.94s, 0 failed steps.
go test ./internal/typefacts/...: ok. gofmt, go vet: clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-measured against the current release binary, the `recursive-value-shape`
refusal § 28 recorded for `@solid-primitives/utils` no longer occurs. The
package emits three artifact cases, certifies all three with none refused,
and declines only `unresolved-callee: 4` — every `unaccepted-external-
dependency` decline is gone, which is § 27's exemption working.

Its `reads` domain is unknown for **zero of 99 exports**: the first real
published package whose `reads` closes completely.

The `@solid-primitives/source` entries are inapplicable artifact cases, not
refusals — that condition routes to sources the tarball does not ship.
`artifactCasesTotal` counts emitted cases, and an earlier reading of it here
wrongly concluded the package had nothing to certify.

`memo` is therefore not blocked by `utils`. It emits one real case and
declines 21 `unaccepted-external-dependency` records against the
`@solid-primitives/utils` specifier. Certifying utils does not help, because
the benchmark runs each package independently and never supplies one row's
contract to another's generation; naming both packages in one invocation
leaves `dependencyPlan: null` on both and memo's declines untouched.

So the chain § 27 proposed is sound and its first step now works. The missing
piece is the step that hands step one's receipt to step two — the generator
accepts `--accepted-contracts`, the benchmark does not drive it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d to refusals

§ 37 left the chain needing one step: hand `utils`' receipt to `memo`'s
generation. There is no path for it today, and this records why, including
two things that were tried and rejected.

An accepted-catalog entry is resolution-bound — `importer`, `specifier`,
`resolvedImportRoot`, `semanticDigest` — so supplying the edge is not a file
copy; a hand-built entry would forge a binding for a resolution never
performed.

The lane that exists for this cannot be requested. `certificationLaneRequest`
needs `class: "partial-success"` and a frontier in *artifact-case refusals*.
`memo` is `success` with zero refused cases and a decline census instead: 21
`unaccepted-external-dependency` records, seven exports unknown in every
domain. Measured, this is not something § 27 introduced — the pinned
pre-exemption report classes memo `success` with zero refused cases too, and
the corpus `partial-success` count is 32 of 418 both before and after. The
policy has never covered this row, which is what made § 26's graph-lane
attempt a silent no-op.

Extending the predicate so an explicit `--dependency-graph-lane` accepts a
decline-based frontier was implemented, measured, and reverted. It routes the
request — memo then reports `laneRequested: published-graph` — but certify
composes nothing: `graphPreparation: {reusedProposal: false}` with no
`rootCases`, lane `generated-proposal`, declines and unknowns unchanged. The
frontier-only lane publishes refused cases *instead of* generated ones, so
with zero refused cases it degenerates. Keeping the change would have made the
report claim a lane that never engaged.

Closing it needs a composing path for decline-based frontiers in the
certifier, not in the benchmark.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eneck

Read off the 418-row report (381 rows with a contract, 8,950 exports), not
from one package at a time.

`reads` is closed for 11.4% of exports and fully closed for 63 of 381 rows;
`creates` 5.9%, `returns` 1.6%, every other domain 0%.

Of 411,906 declined closures, **86.8% are `unaccepted-external-dependency`**
across 273 rows — the decline-based dependency-edge composition § 38.4
describes. The next kinds are far behind: unresolved-callee 4.5%,
runtime-accessor-installation 3.3%, refusing-callee-fixpoint 3.0%,
dialect-silent 2.2%. The review plan holds 3,000 closure candidates and
**zero probe candidates**: nothing in the corpus is asking for a
hand-authored recipe.

Partitioned by whether a row has a dependency frontier: with one, 273 rows /
7,720 exports close 4.7% of reads and **none** close fully; without one, 108
rows / 1,230 exports close 53.7% and 63 close fully. Recorded with the caveat
that this is a selection effect — dependency-free packages are simpler — so
53.7% is not a forecast for the other 273.

This corrects the framing used throughout this document and this session,
which put the human cost at one hand-authored observation per export whose
reads you want closed. `@solid-primitives/utils` closes reads on 99 of 99
exports with zero recipes and has none in the corpus; § 6's argument is about
exports that genuinely read a source they own, and most do not. Hand
observations are a real cost but a small one, and are not what is holding the
corpus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ting silently

§ 39 measured 86.8% of all 411,906 corpus declines as
`unaccepted-external-dependency`, behind one missing capability. This does not
build that capability; it stops the gap being invisible.

`preparedGraphForPartialProposal` returned `{graph: null, trace: null}`
whenever the frontier was not in the refusals array. For a row whose frontier
is a decline census — `@solid-primitives/memo`: 21 records, zero refused
cases — that is an early exit with no record, which is precisely what the
same function's other exits avoid ("it must leave a trace, or the audit reads
exactly like a row that never wanted the lane"). That silence is § 26's
unexplained no-op.

memo's audit now reads `partialProposalFrontier: "declined-only"` with the
reason, `declinedDependencyRecords: 21`, and
`declinedDependencySpecifiers: ["./dist/index.js:@solid-primitives/utils"]`.

Also restores the benchmark lane routing that § 38.3 reverted. That revert was
right when the change stood alone: it made `laneRequested` claim a lane that
never engaged. Paired with the trace it is a different change — routing is
what lets certify reach the point where it can say why. Both halves are gated
behind the explicit `--dependency-graph-lane`; the default policy does not
move, and a test asserts that.

Tests carry controls: declines naming no dependency stay silent, and the
default policy is asserted unchanged. The trace assertion was falsified by
breaking its expected count.

Composes nothing — memo still has 21 declines and seven unknown exports.
§ 38.4 remains the work.

make verify: exit 0, 114.61s, 0 failed steps.
ecosystem-benchmark-test: 270 passed. test-cli: 217 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lane)

§ 38.4 named this as the missing capability and § 39 measured it at 86.8% of
every declined closure in the corpus: a row can want an accepted dependency
contract while every one of its artifact cases generated, and the want is then
recorded as `unaccepted-external-dependency` declines rather than as a refused
case. `partialProposalHasDependencyFrontier` reads refusals, so the lane could
not see it and § 40 could only name it.

It turned out to be one derivation. `preparePublishedGraphCases` already says
"exact case coordinates are acquisition requests, not claims that generation
refused them", and `writeProposalRefusalAudit` has recorded `entrypoint` and
`conditions` on every declined-closure row since 2ee29d2. So
`declinedDependencyGraphCases` reads the distinct coordinate pairs those
declines name -- `import` folded in as every other request here folds it,
cases whose only declines are of another kind excluded -- and
`preparedGraphForPartialProposal` hands them to the graph instead of
returning a trace. A preparation that fails still falls back to the partial
proposal and still names the specifier it was reaching for.

Measured on `@solid-primitives/memo@2.0.0-next.2` with `--dependency-graph-lane`:
zero closure candidates become nine (seven `reads` withheld `noRecipe`, two
`creates` `censusRefused`) behind a seven-node graph, at 291 ms -> 17,275 ms
for the row. Nothing closes. The default policy still never routes here, and
`make ecosystem-regression` is 0 regressions / 0 certification regressions
against the pin -- which exercises the path not taken, and says only that the
capability costs nothing while it is off.

Recorded as § 41, including the correction it forces on § 39: behind the
dependency edge is the recipe requirement, and the corpus-wide
probe-candidate count of zero was an artifact of where the pipeline stopped.

Gates: make verify 118.75s exit 0; ecosystem-regression 0/0; CLI 218;
ecosystem-benchmark-test 270.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hind the edge

§ 41.6 left the corpus cost unmeasured with one row as the only datum. This
runs the same ten probes twice, control and `--dependency-graph-lane`, on the
deciles of the 256-row decline-only population (memo's 21 declines sit just
below the population median of 24, so it was a typical row, not a favourable
one).

Result: 0 -> 431 closure candidates, 8.5 s -> 202.8 s, and **10/10 rows plus
11/12 entrypoints still certified in both arms**. No receipt lost -- unlike
the frontier-only lane on refusal frontiers, this path composes cases that
generated, so there is nothing to trade. Both non-composing rows fell back to
their proposal and named the cause (an uninstalled dependency; a transitive
`dayjs` with no runtime ESM exports).

83% of the 431 candidates are blocked on `noRecipe`. That retracts § 39.4's
conclusion, which I also gave as this session's answer: hand-authored
observations are *not* a small cost once the edge is crossed, and the
corpus-wide probe-candidate count of zero was an artifact of where the
pipeline stopped. The ordering in § 39 survives; the conclusion about where
the human cost sits does not.

The size of that cost, measured for the first time: 358 candidates are 160
distinct claims, 130 of them in `@solid-primitives/utils` alone, and 88 of the
160 are wanted by two or more rows. The unit of work is one shared package's
claims unblocking many dependents, not a recipe per export.

Also records the amortization nobody has taken: eight composing rows ran 55
proposal generations, mostly the same `utils` and `solid-js` nodes re-acquired
per row. Nodes are shared within a graph and not between rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…and what they measure

§ 42.6 costed the recipe work as ~130 claims on one shared package and called
it the first real number for it. This pays a small part of that bill to test
the number: five hand-authored `reads: []` recipes for
`@solid-primitives/utils@7.0.0-next.4` on the `.` case four sampled rows
share, scaffolded with `probe-recipe-scaffold.mjs` from the `props` row's
certification audit.

**Nothing closes**, and the three reasons are each worth more than the recipes:

1. `no recipe in corpus` masks a census refusal. `arrayEquals`' proposal names
   an operation so it is not the empty closure `PROPOSABLE` admits, and
   `compare`'s `a < b` is a coercion form with no reviewed subject root. No
   recipe can serve either, and both reported as `noRecipe` until one was
   written. So § 42.4's "83% blocked on a recipe" is an upper bound on
   recipe-serviceable candidates, not a count of them — corrected in § 43.3.
   Five is too small to put a fraction on 358 and the section says so.

2. The chain does not terminate at `utils`. `clamp`, `trueFn` and `access` run
   clean and move to `dependencyWithheld`: `utils`' own reads closures compose
   from `solid-js` claims that are withheld too. Recipe work has to go
   bottom-up from `solid-js`, which reorders § 42.6's plan.

3. The diagnostic that says so names the wrong claim. All six
   `composed from a withheld dependency claim: <id> of solid-js` records carry
   the candidate's *own* `semanticClaimId` as `<id>`, on the full 64-hex
   digest, which `NormalizedContract::claim_id` makes impossible across
   packages. Recorded in § 43.5, not fixed blind.

The two recipes that can never fire are kept deliberately: without them those
candidates report `noRecipe` and the census refusal underneath stays hidden.

`access` is the one that earns its length — it executes caller code, so it
passes an accessor reading a getter the recipe owns, asserts the getter ran,
and does not emit, because that read is the caller's under ADR 0034.

Gates: make verify 130.86s exit 0; ecosystem-probe-recipes 8 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Traced the `composed from a withheld dependency claim: <id> of solid-js`
defect § 43.5 filed. It is not a message defect.

`ProofDemandSubject::DependencyClosure::semantic_claim_id` is built at
contract_semantics/certification.rs:472 from `candidates.proposal.claim_id`
over the node's *own* closure candidate, so it is the parent's claim id by
construction. Neither `MissingClosedClaim` site is mislabelling it. Two of the
field's three readers agree -- `creates_census` and `dependency_creates_claims`
both resolve it against the parent's own `DomainClosure` demand. The third,
`dependencies.rs:3137`, asks the *dependency's* receipt whether it closes it,
and `NormalizedContract::claim_id` digests package identity, so that
comparison has no satisfying assignment.

Consequence, deduced from the code and consistent with all six observed
records: a `reads` or `returns` closure candidate on a node with any accepted
dependency cannot compose. `creates` escapes only via ADR 0020's live census,
which is the condition's one guard.

That withdraws § 43.4. The three serviceable recipes' candidates are not
waiting on a `solid-js` claim -- none was ever identified -- so
"recipes have to be written bottom-up from solid-js" was inferred from an
artifact. § 43.3's masking argument is unaffected.

Not fixed here, and § 44.4 says why: deleting the check loosens a
trust-boundary condition, carrying the real dependency claim needs the census
to emit them for reads/returns the way it already does for creates, and
correcting only the message means admitting the pointer the defect report
asked to preserve does not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the parent's claim

§ 44 derived the defect from the code and said so. This measures it.

`one_dependency_receipt_cannot_exchange_callbacks_for_throws` already proves
the composition check is correct and satisfiable -- the leaf's `callbacks`
claim passes, the leaf's `throws` claim refuses. It reaches the check through
`authenticate_dependency_claim_for_test`, which *overwrites*
`requirement.semantic_claim_id` with an id the test computed from the leaf's
plan. That overwrite is the blind spot: nothing asserted what the field holds
when demand planning fills it.

On a two-node graph with callbacks closed on *both* nodes, planning gives the
requirement the parent's claim (60736bba…) rather than the dependency's
(00023cbc…); the dependency's receipt does close its own callbacks claim; and
composition refuses anyway with `MissingClosedClaim`. The dependency closed
exactly the domain the parent's candidate is about and nothing it could
publish would satisfy the check.

The vehicle is `callbacks`, which widens § 44: `closure_candidates` is every
closed domain, not the proposable three. A proposable one is withheld (§ 43);
anything else fails `composed_from_withheld_dependency`'s `is_proposable`
guard and the error propagates, refusing the row. That path is unreached in
the corpus only because no row closes a non-proposable domain, not because it
is guarded.

Committed green as a characterization: it asserts the current meaning, names
the defect at the assertion, and fails deliberately under any of § 44.4's
three fixes. Falsified before keeping -- swapping in the dependency's id
fails with both digests shown.

Gates: make verify 168.34s exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sition requirement carries

§ 44 filed this as a diagnostic naming the wrong claim; § 45 measured it and
found the check correct and satisfiable, with the field feeding it carrying
two different claims for three different readers. Planning fills
`DependencyCompositionRequirement::semantic_claim_id` from the parent's own
proposal -- which is what `creates_census` and `dependency_creates_claims`
want -- and the receipt check then asked the *dependency's* contract to
contain it. `NormalizedContract::claim_id` digests package identity, so that
comparison had no satisfying assignment and every closure candidate on a node
with a dependency was refused by it.

Splits the field: `semantic_claim_id` (the parent's, for the census lookups)
and `dependency_semantic_claim_id` (the dependency's, for the receipt check).
Planning names no dependency claim, and that is a fact about the domains
rather than a gap -- `creates` is the one domain whose census follows callees,
and it names its dependency claims exactly through `dependency_creates_claims`,
checked against the same receipt by the caller. `census_reads_domain` and
`census_returns_domain` have no callee walk by construction.

A first cut guarded the condition on "closed in the dependency's accepted
contract" as a proxy for "is a dependency claim";
`one_dependency_receipt_cannot_exchange_callbacks_for_throws` failed it
correctly, because a claim can belong to the dependency and not be closed.

Two tests were green for the wrong reason and are re-pointed:
`dependency_composition_requires_the_receipt_to_close_the_exact_claim` now
asserts against a real dependency claim, and § 45's characterization test
inverts into asserting composition accepts a dependency that closed what it
was asked for.

Measured on the four rows sharing `@solid-primitives/utils@7.0.0-next.4`:
`dependencyWithheld` 6 -> 0 per row with nothing reappearing, and `clamp`,
`trueFn` and `access` close `reads` in the published contract -- the first
`reads` closures on a real package (§ 12's "Still open" is now false). The
negative controls hold: `arrayEquals` and `compare` carry recipes and stay
open because their census refuses, and `noop` closes `creates` but not
`reads` because nobody wrote it one.

Gates: make verify 173.92s exit 0; ecosystem-regression 0 regressions /
0 certification regressions, 381 rows certified, corpus withheld 11833 ->
11665 with `dependencyWithheld` gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nt the two-pass run

§ 43.3 found that `no recipe in corpus` masks a census refusal underneath it
and could only say the real backlog was smaller than 358. This measures the
fraction and turns the finding into a workflow.

The mechanism: `recipe_gated` weakens a `no recipe in corpus` candidate out of
the plan before its demands are discharged, so its census never runs and a
refusal that would have decided it unconditionally is never computed. A
*throwing* scaffold is a recipe as far as the gate is concerned -- the
candidate stays in the plan, its census runs, and `census refused: …` surfaces
ahead of the incomplete gate. A throw withholds exactly as no recipe does
(`a_recipe_that_throws_withholds_its_candidate_rather_than_certifying_it`), so
the pass risks nothing and costs no hand-authoring.

Measured on `@solid-primitives/utils@7.0.0-next.4`'s `.` case: of 45 `reads`
scaffolds, **24 are `census refused` and 21 are worth finishing**. 53% of the
batch was never worth writing, and one run finds out. The § 43 sample put it
at 2 of 5 and was too small to say so.

`censusRefusedCandidates` reports them, before the no-gaps exit -- a second
pass is exactly when there are no gaps left and the report is all there is to
say. Two-pass workflow documented in the script header and the corpus README.

Deliberately not extrapolated: 53% is one artifact case of one package, and
§ 42.4's 358 stays an upper bound with no fraction attached until a lane-on
corpus pass measures one.

Gates: make verify 116.67s exit 0; scaffold + corpus suites 28 passed.
New assertions falsified before keeping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every closure number in §§ 41-47 was read out of a run's catalog by hand,
because `certifiedClosures` and `closureCandidates` were `null` on all 418
benchmark rows -- which made the lane-on corpus pass pointless, since it could
be paid for and then not read. Two separate gaps produced that one `null`.

`main.rs` has four certification entry points. The two value-only ones report
closure candidates, certified closures and withheld closures; **both graph
ones reported only the third**, so a composed row said what gating took away
and never what the planner derived or the receipt binds. Both now emit all
three, per node, with the node's package and version on the record --
attribution is the point, because a composed row carries several packages'
closures.

And `run.mjs` never read those fields at all, so the field was *absent* rather
than empty and `jq` answers `null` for both. That fooled § 46.5, which read the
`null` and concluded yield was unreadable: right conclusion, evidence that
could not tell "the certifier emitted nothing" from "the runner never asked"
when both were true. `closureAccounting` carries them through and preserves
`null` rather than defaulting, because those two remain different answers.

`closed` is capped at 64 rows by the consumer, so the record also carries
`closedByDomain`, tallied natively and summed across nodes -- a corpus pass
reading a breakdown off a capped list would report a smaller yield rather than
a partial one, which is this section's whole failure mode. Nine domains bound
it.

Measured on `@solid-primitives/props@4.0.0-next.3` with the graph lane, read
straight off report.json: 25 closed domains over 5 cases, of which
`@solid-primitives/utils` creates 24, reads 3, returns 3. Those three reads
are `clamp`, `trueFn` and `access`.

Gates: make verify 114.96s exit 0; ecosystem-benchmark-test 270; CLI 98.
New assertions falsified before keeping. Two self-inflicted failures on the
way -- rustfmt on a reshaped match arm, and the runner test's exhaustive
`deepEqual` -- both fixed rather than worked around.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lane-on pass § 48 made readable, run over all 418 probes with the release
checker and the checked-in recipe corpus.

**1,515 distinct claims need a hand-authored recipe** (1,486 `reads`, 29
`returns`), against 466 the census refuses and 76 whose veto ran and failed.
They concentrate: 64 packages hold all of them, the top 5 hold 62% and the top
20 hold 88%.

What closes today, deduplicated: 362 `creates`, 26 `returns`, **3 `reads`** --
the § 43 recipes, and the corpus's only `reads` recipes. The per-row figure for
the same three is 300, because `@solid-primitives/utils` is a dependency of
about a hundred rows. Every per-row total in this document is inflated the same
way and § 49.2 says so.

Two corrections:

- § 42.7 projected four hours by applying a per-row multiplier to the corpus's
  summed certification time. The sum was right (180 min) and the projection
  was not: the runner executes rows concurrently, so it took **10 minutes**
  wall on 14 cores. The estimate assumed serial execution and never said so.
- § 47's "53% unserviceable" was one artifact case. Corpus-wide it is 23.5%,
  so the backlog is more serviceable than that sample suggested. § 47 declined
  to extrapolate and was right to.

Two things surfaced and deliberately not investigated: 76 distinct claims
whose veto exists and failed (a correctness signal, not backlog), and 461
claims carrying no dependency node -- the largest single group, 30% of the
total, uncharacterised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§ 49.6 called these "a recipe exists for each and it broke" and flagged them
as a correctness signal. Wrong on the first word: the corpus holds 19 recipes
and none appears here. All 76 are ADR 0036 synthesized vetoes, failing on
properties of the artifact or the harness.

Five causes: TypeScript source under node_modules (35 claims, the trap
`kobalte-utils-noop.mjs` documented in 2026-09-04), a scoped custom export
condition Node's `--conditions` cannot take (20), an export-condition binding
(9), a package missing from the probe workspace (9), and `.jsx` (3).

Not a correctness problem: an incomplete veto withholds, exactly as no recipe
does, so none of these certified anything. What they cost is closure -- a
synthesized veto that ran and did not contradict closes on the census alone,
with no hand authoring.

So they are a separate population from § 49's 1,515 with a far better ratio:
76 claims behind three or four harness fixes rather than 76 hand-written
observations. Two of the five causes are honest interpreter limits; the
missing-package one is the only thing left in this document that still looks
like a bug, and it carries its own clue -- the probe resolved solid-js' server
entry, whose runtime dependency the workspace does not carry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§ 50 left `Cannot find package 'seroval'` as the last thing looking like a
defect. It is one, and not where the error points.

`solid-js`' `./web` resolves `node` -> `web/dist/server.js` (which imports
seroval) and `browser` -> `web/dist/web.js`. The six affected rows carry
artifact cases with no conditions, so the probe worker -- being Node -- selects
the server build, and seroval is not in the private workspace.

The gate should have refused by name. `require_authenticated_dependency_closure`
iterates the *analyzed package's own* declared dependencies, one level;
seroval is a dependency of solid-js, which is itself authenticated and copied
in, so the precheck passes and the import fails a level deeper. The precheck is
one level deep and the runtime closure is not.

It costs diagnosis, not safety: incomplete veto either way, candidate withheld
either way, nothing wrongly certified.

Three fixes of very different size are recorded rather than taken -- matching
the precheck to the runtime closure (small, closes no claim), authenticating
the closure transitively (closes the 9, widens trust scope), or reconsidering
the probe's conditions.

And one open question stated as a question: the census analysed one resolution
of solid-js/web and the probe loaded whichever Node's defaults selected.
Whether those are the same bytes is not established, and if they are not it is
a soundness question rather than a diagnostic one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…g Node

§ 51.4 asked whether the census and the probe could resolve differently, and
called it a possible soundness question. For the *subject* it is already
checked: `verify_reported_resolution` compares the worker's resolution against
the artifact case with `names_same_file` and refuses with "the probe ran
against a different file than the Type Facts witness read". Answered, and
sound.

The gap that is real is one level down: a declared dependency is checked with
`path_is_inside` -- containment in the authenticated copy, not file identity --
so which build within `solid-js` a probe resolves to is not pinned. Recorded
in § 52.2, not fixed here.

§ 51.3's fix (1) is deliberately not taken. Walking declared dependencies
transitively would refuse gates that work today, because what an artifact case
imports is a subset of what its closure declares: `web.js` needs no `seroval`
even though `server.js` does. It trades a bad diagnostic for lost capability.

Instead a worker throw matching `Cannot find package '<name>'` becomes a named
incompletion saying the workspace carries only this transaction's authenticated
closure and the package is reached transitively rather than declared. It cannot
over-refuse -- the resolution has already failed by the time it runs -- and
every other throw keeps its own text, because rewriting one the classifier does
not understand would replace a real diagnosis with a guess.

Outcome unchanged: an incomplete veto withholds its candidate. Only the reason
improves.

Gates: make verify 176.33s exit 0; new test falsified before keeping, with
controls for both quote spellings and the truncated-frame cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… composed build

A probe launch asked the worker to resolve only what its *recipe* declared.
A synthesized veto (ADR 0036) declares nothing, so on all of those the
containment check ran on nothing at all. The independently replayed closure's
accepted edges are now asked about on every launch — the set the replay proved
this artifact case's modules actually import — carried as
`RequestedDependency { specifier, package_name }`, because an edge may name a
subpath while `dependency_roots` is keyed by package.

And containment is no longer the whole check where this transaction certified
the dependency itself. A `graph_dependencies` entry is a full plan whose
`verified_resolution` names the exact runtime file its own certification
selected; the workspace materializes it and requires `names_same_file`, as the
subject has always been checked. A declaration-only `certification_sources`
snapshot has no runtime case bound and keeps containment.

Measured with `make ecosystem-regression` at HEAD, with the first check, and
with both: 192 packages and 4,478 closures certified in all three, every
withheld-reason count identical, `vetoRunRefused` zero throughout.

That zero has one explanation, recorded in § 54.3: `module_closure.rs` records
no dependency edge for the built-in runtime, so solid-js — the case in § 53
that motivated this — is invisible to both checks. What landed is real, and it
does not cover that case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR 0037 already knew how to handle this. `require_condition_neutral_closure`
walks every `exports` and `imports` object in the authenticated closure and
compares the target selected under the requested conditions against the one the
pinned interpreter actually applies — but it runs only for a condition this
verifier *adds*. When the requested set reproduces every planned artifact case,
`reproduce_artifact_cases` returns before it.

That exemption is sound for anything with a plan or an accepted edge, which
ADR 0006's per-node replay dispositions. The built-in runtime has neither:
`module_closure.rs` records no dependency edge for solid-js by design, so
nothing checked which of its builds the interpreter selects. It selects the
server build — `node` outranks nothing in a bare Node set, and `exports["."]`
answers `node` with `server.js` where a bare `import` answers with the client
build.

`require_condition_neutral_unplanned_dependencies` covers exactly that gap:
closure entries with no certified entry and no accepted edge.

Measured with `make ecosystem-regression`, control versus this change, keyed on
probeId: 9 rows moved, all one way. Certified closures 5,174 -> 5,187,
`vetoIncomplete` 19 -> 4, details naming `seroval` 16 -> 1, `vetoRunRefused` 0
throughout, every other reason bucket identical. Refusing the requested set
makes ADR 0037 try `browser`, which solid-js lists before `node`; both reach the
client build, so `server.js` — the only file that wants `seroval` — is never
loaded.

This withdraws § 51.3's fix (2) rather than deferring it: authenticating
`seroval` would have made the wrong build load successfully.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every measurement since § 48 counted detail rows, which are (package x artifact
case x claim) and over-count about fivefold. By distinct semanticClaimId the
corpus withholds 1,489 noRecipe, 397 censusRefused and 99 vetoIncomplete claims
against 5,187 certified closures.

Two things change under that unit. The noRecipe bucket is reads 1,411 / returns
78, and the reads half has no synthesized veto by the recorded decision of § 4 —
so 71% of what remains is hand-recipe labor by design, not a gap to close. And
the census bucket is 99% creates, top family property-access-unknown-accessor at
122 claims across 32 packages; coercion, which looked second-largest at 364
details, is 41 claims.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… for

`refuse_form` printed the form kind, node kind, location and reach — and not
`subject_root`, which is the field `census_form_disposition` switches on. Every
boundary of ADR 0034 therefore read alike: a module-level receiver, a parameter
the producer could not identify, and a form that never reads its subject all
arrived as one undifferentiated "uncensused invoking form", so choosing which
premise to review next meant reading the package rather than the refusal.

It now names the derivation offered and why it was not enough, plus write
position.

Behaviour-neutral by measurement: `make ecosystem-regression` against the
previous run moves no row and certifies the same 5,187 closures.

What it found immediately: all 122 property-access-unknown-accessor claims,
across all 32 packages, are one premise — "the producer offered no subject
derivation". None is an ADR 0034 boundary case. The verifier's `own-literal`
arm for this form family is already reviewed and reachable; the producer just
never roots an accessor subject that way. § 57.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
yumemi-thomas and others added 30 commits September 16, 2026 23:06
…gation corpora

Three more corpora carried Solid 1.x rows that the retirement makes
unanswerable, each found by a separate `make verify` step rather than by
grepping:

- `dialect-authority.test.mjs` asserted the `audited-archives.json` mirror
  names a `solid-v1` entry. The mirror carries one entry per dialect the Rust
  tables define, so that entry went with the dialect. It now asserts the whole
  id list instead of `find`ing one entry, because an entry appearing or
  vanishing is exactly what this pin exists to catch and a `find` that returns
  `undefined` would not.
- The obligation audit's one v1 case: a 1.x-specific claim that an exported
  function returning JSX is not yet component identity, so the read's execution
  model stays unknown. 2.0 decides it, so the obligation cannot be raised and
  the case is removed rather than re-pinned to zero findings.
- `obsoletePolicy1Catalogs` 22 -> 21, one catalog having gone with the
  1.9.14-stub fixtures. Like the main-document count beside it, this enumerates
  checked-in files rather than measuring an analysis answer.

Green: scripts suite 34 files / 223 tests, ecosystem runner 17 files / 282
tests, obligation audit 6 obligations, tsc oracle gate 89 cases, ownership gate
37 cases, conformance, and `sh -n` / `jq empty` over the shell and JSON.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ming a 2.0 API that does not exist

The slice the retirement plan asked to keep separate, because resolving
`carries_eslint_era_rules()` to its 2.0 branch *could* move findings. It did
not: the trait default is `false` and `Solid2` never overrode it, so every one
of the four call sites was already taking the branch it now takes
unconditionally. Coverage is unmoved at 80 projects, which is what makes this a
refactor rather than a semantic change.

Removed with the predicate: `solid1x_attributes` and `solid1x_undef`, the two
modules it gated wholesale. The surviving modules lose the historical prefix --
`solid1x_syntax` -> `syntax`, `solid1x_structure` -> `structure`,
`solid1x_options` -> `rule_options` -- because nothing in them is 1.x any more,
and `Solid1xRuleOptions` with them.

**The dialect-specific rule-options mechanism is gone, not renamed.** Its only
content was `v1/prefer-classlist`'s `classnames` list, and with that rule
retired the parse path was unreachable: an `owns_dialect_options` predicate
that nothing answers true for, threaded through the pipeline into a struct
nothing reads. What is kept is the *validation* it hung off -- a rule entry
with any key other than `enabled` is still refused, because this module's whole
premise is that a typo must not silently mean "defaults". When a 2.0 rule grows
an option, it is that arm that grows a parser.

**And a real wording bug, found by resolving the gate rather than by looking
for it.** SC8014's second message -- the one shown when the callback has no
semantics-preserving rewrite -- read "Use Solid's `<For />` component or
`<Index />` component". Solid 2.0 does not export `Index`; its index-oriented
component is `Repeat`. That was harmless while both vocabularies shipped and is
not now, so the message names `<Repeat />`.

Nothing pinned it. The only fixture reporting SC8014 used a one-parameter
arrow, which takes the *other* branch, and neither the ownership gate nor the
oracle asserts message text. `ported-structure-v2` now carries an
index-parameter call too and joins `KEEPS_WORDING`, so both of SC8014's
messages are pinned where they can be read -- one with a fix, one without.

Green: workspace clippy and fmt clean, coverage 80 projects / 433 findings
(the new one), ownership gate 37, tsc oracle gate 89, CLI 234, and every Rust
target except the three probe-gate tests that need the certification pins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…etirement

`make contract-coverage-census` ran `--solid 1`, which after the Solid 1.x
retirement refuses all 18 packages with SC9013 and reports nothing. It is
`--solid 2` now.

That retarget is the dangerous kind, and the guard matters more than the flag.
A `solid2` run measures a *narrower* corpus -- 75 of the benchmark's 214
packages are solid1-only, including SolidStart itself -- so comparing it to the
pinned baseline raises every percentage without proving one new statement. ADR
0110 s 5 names that explicitly and freezes the denominator.

So the pin now records which corpus measured it, read from the run's own
`scope.solidTargets` rather than from a flag, and a run whose denominator
differs from the pin's is refused with that reason instead of compared. An
unrestricted run, which has no single denominator to pin, is refused too. The
existing pin is stamped `solid1`, which is what it was measured over; the first
`solid2` run therefore fails until someone re-pins it deliberately and records
the new baseline beside the old rather than as a continuation of it.

Verified both ways: the 1.x-era run still compares green against the 1.x pin
(exit 0), and the same run relabelled `solid2` is refused (exit 1) with the
denominator message.

Also records in the plan that ADR 0110 superseded most of step 2 -- sub-steps 3
and 5 withdrawn by s 4, sub-step 1 refused by s 5 -- leaving only this and the
`hydration.d.ts` resolution defect, which is a precision bug rather than a step
of the retirement.

Green: scripts and ecosystem-benchmark suites, 51 files / 505 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ults

`exports::solid_v1_solid_js` is the frozen record of where
`solid-js@1.9.14` exports each name. It answered `Solid1x::export_modules`,
which is how the wrong-module import check reached the ten `solid-js/web`
names the primitive vocabulary deliberately does not carry.

With `Solid1x` gone the table has exactly one reader left: the test
asserting it is sorted. That is a table proving its own invariant and
nothing else, so the retirement takes it too (ADR 0110).

`export_modules` itself is untouched -- it is a `Dialect` method, and
`Solid2` answers it out of its own two tables. Coverage is unmoved because
nothing outside this crate ever named the deleted module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan's step 2.5 ends "then delete pkg/contracts/bundled/solid-v1/ and
benchmarks/package-contract-v2/phase14/solid-v1-authority/". It should not
be done, for the reason ADR 0110 s 4 already gave about the accepted tier:
those documents record what published *artifacts* do, and a change to what
this checker analyzes does not make bytes stop invoking what they invoke.

Three checked facts behind the refusal, recorded in the plan:

- Neither directory is an analyzer input. Both bundle indexes are empty,
  both embedded-bundle lists are empty, and the seam whose "solid-v1" arm
  reads the authority has no caller in the workspace -- ADR 0027 retired it
  before this plan existed.
- Both are live gate inputs. All 19 authority documents are decoded,
  normalized and closure-digest-checked against the index on every run, and
  four bundled documents are measured by the phase 0 gate.
- Two of them are include_bytes! fixtures for dialect-neutral code, in the
  wasm crate and the policy-2 receipt tests. Deleting them costs those
  tests their input and buys nothing.

The genuinely dead thing here -- the "solid-v1" arm of that uncalled seam --
is left alone and said so: it was unreachable before this plan, so removing
it is ADR 0027 cleanup rather than retirement work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ract"

`--check-contracts` emits two unrelated documents. Normally it is a contract
report, `{missing, packages, stale}`. When the installed runtime is one this
build carries no dialect for, the step 4 refusal replaces the analysis and it
is the ordinary findings snapshot carrying SC9013, at exit 0.

The contract sweep read `report.packages` through
`Array.isArray(...) ? ... : []`, so it read that snapshot as an empty report
and told the operator there was nothing to generate -- for a project it had
never analyzed. The exit code could not catch it: the report exits 1 when a
package needs action, so the sweep deliberately accepts 0 and 1, and the
refusal exits 0.

The shape stays as it is. An absent report is the honest answer, and emitting
`{ packages: [] }` with a flag beside it would move the same false negative
one layer down, to every consumer that does not read the flag. The consumer
refuses instead, and quotes the refusal's own message and hint so the operator
learns what to do rather than that something went wrong.

Both sides are now pinned, because either one alone would let the other drift:
`a_refused_runtime_answers_check_contracts_with_the_refusal_not_a_report`
asserts the native document has no `packages`/`missing`/`stale` key and
carries SC9013 at exit 0, and the CLI test asserts the sweep refuses it --
including the shapes with no refusal to quote, which must still fail closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan calls the Solid 2 creates census refusing callees resolved into
`solid-js/types/client/hydration.d.ts` a resolution defect to fix before the
first solid2 census is worth pinning. That is wrong, and I repeated it.
Traced end to end instead:

- `solid-js@2.0.0-rc.3`'s `types/index.d.ts:8` re-*declares* createSignal and
  ten siblings from `./client/hydration.js`; line 1 re-exports a different
  set from `@solidjs/signals` and createSignal is not in it. Checked against
  the audited install. So resolving into hydration.d.ts is the right answer.
- `NEGATIVE_ROWS` carries createSignal/creates for `@solidjs/signals` only.
  `solid-js`' re-declaration is withheld on purpose: the node/worker/deno
  body reaches `ctx.serialize(...)`, which semantic-model.md s creates
  [Decision 2026-09-04] settles is a create.
- `census_dialect_axiom` binds the archive the declaration resolves into and
  asks `primitive_performs_no_operation`, which is false for a withheld row.
  No terminator, claim stays open. That is the refusal.

A (package, export, domain) row carries no condition, so closing it would
state something false for every SSR consumer. The fix is a condition-aware
table -- an open ADR 0007 item -- not a resolver change, and the census
numbers are readable now with the hole named. The withholding itself was
already recorded in docs/precision-backlog.md; only the plan was out of step.

Recorded beside it, deliberately unchanged: `some_audit_denies_primitive`
matches export and domain but never `row.package`, so the proposal generator
reads the `@solidjs/signals` row as covering `solid-js`' withheld one. Its
doc comment argues that is safe because the archive-bound census proves or
refuses the proposal later -- which is exactly the trace above -- but it is
the same package-blind name match whose cross-dialect form was a real bug
before the retirement, so it wants a deliberate look.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deleting the four non-portable package-contract fixtures left references
behind that read as guarantees. Each is corrected where it stands, and none
is simply removed -- the history is why the code looks the way it does.

- `interproc.rs`' `primitive_callback_execution` cites
  `callback-deferred-untracked-chain`'s `unestablishedScheduleShape` as
  pinning that a missing row makes the wrapper chain refuse. The behaviour is
  unchanged; the pin is gone, and the comment now says so.
- `unresolved-dispatch-reachability`'s README says
  `escaping-private-helper` "is the fixture that makes the sentence true" and
  then names seven arms as pinning the exact call-site enumeration. Every one
  of those arms is currently unpinned. The README now says which sentences
  describe intent rather than a guard, and what porting it costs: an
  `@solidjs/web` stub and a `jsxImportSource` arrangement across thirty JSX
  files, because 2.0 moves `namespace JSX` out of `solid-js` and `For` gains a
  `keyed: false` overload that inverts its children callback.
- `callback-slot-derived-store`'s README opens "read that fixture's README
  first -- it states the defect and the rule", pointing at the deleted 1.x
  twin. The defect statement is quoted in place instead. This fixture's own
  pin survives intact; what was lost is only the contrast with a dialect that
  had no compute form at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pper

The first of the package-contract fixtures the retirement deleted without a
replacement. Its claim is dialect-neutral and load-bearing: `inline` promises
the export invokes the callback before it returns, `deferred` promises the
opposite, and a *clearing* wrapper is inline -- the clearing travels on the
tracking axis, not the schedule one.

Four of the five exports transcribe unchanged. `untrack`, `createRoot`,
`runWithOwner` and `onCleanup` all survive 2.0 with the shapes the claims rest
on, and `runWithOwner`'s callback is still at index 1, which is the whole point
of `ownerWrapper`. The generated contract answers same-stack/untracked at slot
0 for `untrackedWrapper` and `rootWrapper`, same-stack/untracked at slot **1**
for `ownerWrapper`, queued/**tracked** for `trackedWrapper` and
queued/untracked plus a cleanup for `deferredWrapper` -- the two negatives
differing from the three positives is what makes this a rule rather than
"answer inline for anything wrapped in a call".

`trackedWrapper` is the one that moved, onto 2.0's two-function `createEffect`.
Checked rather than assumed, and it corrects something I had repeated: the 1.x
spelling `createEffect(compute)` is **not** a type error in statement position.
The deprecated overload returns `never`, `tsc --noEmit --strict` is silent on
discarding it, and only a use of the result would fail. So the two-argument
form is used because it is the supported one, not because the alternative
fails to compile.

`index.ts` type-checks clean under --strict against the real
solid-js@2.0.0-rc.3 install as well as against the stub, so no claim rests on
the stub being looser than the package; the stub cites every declaration it
carries by file and line and keeps both `createEffect` overloads so the
deprecated arm resolves here exactly as it does against the package.

Not vacuous, and the gaps are pinned too: one withheld owner requirement
(schema 1 has no operation kind for a free-standing one) and two
`dialect-silent` `creates` declines, for `runWithOwner` and `createEffect`
against `solid-js`. Both are correct -- `runWithOwner` is outside the audited
document's ten, and `createEffect`'s row was withdrawn 2026-09-04 because the
server body can reach `ctx.serialize`. `untrack`, `createRoot` and `onCleanup`
carry rows and do not decline, so the fixture also pins that the difference is
the table rather than the wrapper shape.

Corpus: 94 -> 95 fixtures; no existing snapshot moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`primitive_callback_execution` answers per `(Primitive, parameter)`, and
`Primitive` is the shared vocabulary rather than one dialect's. After ADR 0110
exactly two of its arms are unreachable: `On` and `MergeProps` are Solid 1.x
names, and `Solid2` mentions neither -- 2.0's nearest relative to `mergeProps`
is `merge`, which this dialect does not model as a callback-taking primitive,
so it does not arrive under another spelling either.

Kept rather than deleted, for the reason `Version::V1` is kept: what each arm
says is still true of the primitive it names, and a future dialect carrying
either name should find the reasoning instead of re-deriving it. The comment
now says which part is gone -- the reaching, not the claim -- so neither reads
as live behaviour.

The third `MergeProps` site, in `primitive_slot_roots_parameter_invoke`,
already said "2.0 does not carry the name at all" and is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`analyze` could fail outright on a producer crash it was supposed to survive,
and the test that exists to catch that went green on every idle run.

`exchange` restarts and replays around one failed request, which covers the
analyze exchange. It does not cover the `Operation::Symbols` exchanges
`close_symbols` and `patch_stable_symbols` issue afterwards, and those use
`exchange_once` for a good reason: each carries the `state_token` and the
release/reference flags of the analysis that just ran, so re-sending one to a
freshly spawned producer would name retained state that process never had.
Recovery has to redo the analysis rather than the request, so it goes in
`analyze_groups` where the demand set is still known -- the same shape as the
`state-mismatch` arm beside it, plus the restart that arm does not need, and
applied exactly once so a producer that keeps dying still surfaces.

Diagnosed rather than guessed. `kill -9` returns when the signal is queued, so
where the teardown lands inside `analyze` is a scheduling outcome: idle, the
producer is gone before the analyze exchange and the existing restart handles
it; loaded, the analyze exchange completes first and the producer dies in the
symbol phase, where nothing recovered. Temporary instrumentation on all eleven
exchange sites, run under contention, put every one of 28 observed failures in
`close_symbols` -- 23 at the release exchange, 5 at the reference-changes one
-- and none at the analyze exchange, the restart's own open/replay, or the
retry. The instrumentation was removed before this commit; the tree is
otherwise untouched.

Measured on a 14-core host with six concurrent copies of the target:
28 failures in 84 runs before, 0 in 180 after. The test's doc comment now says
which of the two crash windows an idle run actually exercises, so a green run
is not mistaken for coverage of both.

One residual, observed once in 84 post-fix runs and not again in 180: the kill
can land after the whole analysis, leaving `close` to meet the dead producer.
That is not this defect -- `close` refusing there is deliberate and pinned by
`close_is_bounded_when_the_producer_exits_between_requests` -- so it is left
alone rather than absorbed, and the crash test's final `close().unwrap()`
remains the one place that window can still show.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lose

`Session::close` sent its goodbye, waited up to 250 ms, and returned the
transport error if the producer had died -- after having marked the session
closed and terminated the child on every path above. So it reported "close
failed" about a close that had succeeded, and it was the only place in this
API where a producer death the session can absorb is an error: everywhere else
the restart machinery recovers one and answers Ok.

Decided on the callers rather than on taste, which is what the question turned
on:

- **No production caller reads the result.** `rg` across rust/ finds exactly
  two non-test callers: a bench, and `Drop for Session`, which discards it with
  `let _ = self.close();`. The backend adapter never calls `close` at all -- it
  relies on that Drop.
- **The test corpus expects success 28 times to once.** Twenty-eight
  `close().unwrap()` sites, including two in crash tests whose producer was
  deliberately killed and whose session then recovered, against the single
  `is_err()` in `close_is_bounded_when_the_producer_exits_between_requests` --
  a test whose stated claim is boundedness, and whose comment is about a
  previously stranded receiver.
- **Nothing is left behind when the goodbye is unacknowledged.** The child is
  killed and its reader joined by `Connection::terminate`, and a shared
  transition arena is removed by its own `Drop` on the client side. There is no
  cleanup an acknowledged close buys that an unacknowledged one loses.

So the refusal goes, but the observation does not: `close` now returns
`CloseOutcome::{Acknowledged, ProducerAlreadyGone}`. That keeps the one real
consumer of "the producer was already gone" -- the test that pins it -- with a
stronger assertion than `is_err()` rather than only a timing bound: reaching
`ProducerAlreadyGone` proves close went through the send/wait path and observed
the dead producer, without which the sub-second bound could pass on a `close`
that never attempted the exchange. Only transport failures become that variant;
a service-level refusal is the producer answering and declining, and still
propagates.

`CloseOutcome` is deliberately not `#[must_use]`. A caller that simply wants
the session shut down is right not to inspect it, the enclosing Result already
forces real failures to be handled, and marking it would put noise at the ~28
call sites that correctly do not care.

This also settles the residual left open by 067a189: the crash test's closing
`close().unwrap()` was failing about once in 84 runs under load when the
SIGKILL landed after the whole analysis instead of before it. It is now correct
by contract rather than by timing. 180 runs of the load harness: 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It still described step 3 as in progress and step 2 as undecided, and listed
three loose ends that have since closed: `docs/adding-a-dialect.md` no longer
mentions 1.x, the CLI README documents the SC9013 refusal and the retired
`v1/` rule names, and `test-backend-v1` went out of `scripts/verify.sh` with
the configuration it verified. Twenty-four commits carry the retirement,
08360f5 to 0854fdd, and verify is green at the tip.

What is left is reordered by what it risks rather than by plan order:

- `some_audit_denies_primitive`'s package-blind match goes first, because it
  is the only remaining item that could be a correctness issue rather than
  lost coverage.
- The two unauthored fixtures follow, each with its blockers named concretely
  enough to start from.
- The CLI exit-status pin is recorded as blocked on shipping a v2-only
  binary, not as outstanding work.

Two cost claims are now checked against the audited rc.3 install rather than
carried forward from the original estimate. `namespace JSX` is in
`@solidjs/web/types/jsx.d.ts` only and `solid-js` ships no `jsx-runtime`
subpath, which is what makes `escaping-private-helper` expensive; and `For`
has three overloads in 2.0, not the two the plan claimed -- only `keyed?: true`
keeps 1.x's raw `(item, index)` shape, `keyed: false` hands over an accessor,
and the predicate form hands over accessors for both arguments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ands

Investigated the package-blind match flagged during the census trace. It is a
real imprecision, the obvious fix is a regression, and the code is unchanged.

Demonstrated on a probe package built against the audited rc.3 install, which
separates a re-export from a re-declaration. `solid-js` re-declares
createSignal, createMemo, createStore, createProjection, createOptimistic and
createOptimisticStore from ./client/hydration.js, and the audit withholds rows
for those six implementations on purpose. They are answered out of
@solidjs/signals' rows anyway: the generator proposed `creates: []` for the
consuming export and emitted no dialect-silent decline record, which is what
blinds the "audit this primitive next" instrument exactly where a row is
missing.

Not a soundness hole. The proposal still has to survive the archive-bound
implementation census, which refuses it, so nothing false certifies -- the cost
is an open claim that can never close plus the missing decline record.

The obvious fix was tried and reverted, because the measurement contradicted
the assumption behind it. The only package identity at the call site is
`ResolvedDeclaration::origin_module`, which is the module the import
*specifier* resolved to, not the archive owning the declaration: probed against
the real install, a `solid-js` import of `untrack` or `createRoot` reports
`solid-js` there too, though solid-js re-exports both from @solidjs/signals.
Keying on it declines ten legitimate re-exports to fix six re-declarations, and
`callback-reactive-arguments` catches the regression -- so the gates agree it is
a net loss.

A correct fix has to discriminate on `ResolvedDeclaration::source_file`, the way
`census_dialect_axiom` already binds an archive. That field is not plumbed to
this call site, and the decline record's `package` field is pinned by
`creates-decline-records`, so both belong to that work rather than to this note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A negative-authority row is `(package, export, domain)`, and the proposal-side
lookup matched only the last two -- so one package's denial answered for every
package exporting that name.

It has a live consequence. 2.0's `solid-js` re-*declares* createSignal,
createMemo, createStore, createProjection, createOptimistic and
createOptimisticStore from ./client/hydration.js rather than re-exporting
@solidjs/signals', and the audit withholds rows for those six implementations
on purpose (solid_2.rs s 7.4: the node/worker/deno bodies reach ctx.serialize,
which is a create). They were answered out of @solidjs/signals' rows anyway,
so the generator proposed a closed `creates` the audit refuses to make and
emitted no decline record -- blinding the "audit this primitive next"
instrument exactly where a row is missing.

The obvious fix is wrong, and measuring is what caught it.
`ResolvedDeclaration::origin_module` is the module the import *specifier*
resolved to, not the archive owning the declaration: probed against the audited
rc.3 install it reports `solid-js` for `untrack` and `createRoot`, which
solid-js genuinely re-exports, as readily as for `createSignal`. Keying on it
declined ten legitimate re-exports to fix six re-declarations, and
`callback-reactive-arguments` caught the regression.

`ResolvedDeclaration::source_file` is the discriminator -- the file the
declaration is actually written in, which is how `census_dialect_axiom` already
binds an archive. It reaches the call site through a new
`callee_declaration_source_file` accessor and maps to its installed package by
the deepest `node_modules/` segment, matching Node resolution. Probed on the
real install: `untrack` from solid-js answers
@solidjs/signals/.../core/core.d.ts, `createSignal` answers
solid-js/types/client/hydration.d.ts. An unresolved declaration denies nothing,
which is the safe polarity -- silence means "do not propose".

Measured effect: declined closure proposals 227 -> 312 across 15 corpus
fixtures, and the direction is uniform. No export closed a domain it had not
closed before, and no export's summary changed outside the closed/creates
domains -- checked per export rather than per summary, because summaries are
content-addressed and shared. The decline record's own `package` field is
untouched, so `creates-decline-records` is unaffected. Coverage did not move.

It also settles a fixture that contradicted itself: callback-reactive-arguments'
README says "the `creates` domain is not closed, so the package makes no claim
that it creates nothing", while its snapshot closed `creates: []` -- granted on
@solidjs/signals' row, for a two-line local stub that plainly does create. The
snapshot now matches the README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…imation

Tried to author the 2.0 counterpart of callback-deferred-untracked-chain and
probed every candidate export against the audited rc.3 install first. It is not
a port, and the probe is worth more than the attempt would have been.

Three of its claims do not survive. `mergePropsShape` has no counterpart --
2.0's `merge` is not modelled as a callback-taking primitive. `mountShape`
answers same-stack/untracked in 2.0 against queued/untracked in 1.x, and that
is a real dialect divergence rather than a regression: Solid2 audits 2.0's
createEffect compute as DuringCall, citing that it reaches effect(), which
calls recompute(node, true) before queueing the effect function. And
`unestablishedScheduleShape` -- the export interproc.rs cites by name as the
pin that a missing row makes the wrapper chain refuse -- loses its premise
entirely: 1.x refused because createSignal(fn) *stores* the function, while 2.0
models it as a compute slot with a Tracked row, so the chain answers instead of
refusing. Restoring that pin needs a primitive the dialect states no timing
for, and the documented candidates are createStore/createOptimisticStore.

The probe also widened something already on the books. precision-backlog.md
records the direct-invocation rung's `queued` default as a known approximation
and names this fixture's memoInsideUntrack, then adds that its sibling
trackedShape publishes queued correctly. That parenthetical was true only of
1.x, whose createEffect is AfterCall. Solid2 states DuringCall for createMemo,
createSignal, createOptimistic, createProjection, createEffect and
createRenderEffect, and AfterCall for createTrackedEffect alone -- so with 1.x
retired the default is wrong for every tracked primitive but that one. Measured:
all four of createMemo, createSignal, createEffect and createRenderEffect
publish at.schedule "queued" for a directly invoked parameter where same-stack
is the audited answer. The chain rung is unaffected and already correct.

Which makes the fixture worth authoring for a better reason than coverage: it
is the thing that distinguishes the two rungs, so it is how that fix gets
pinned. Scoped here rather than written, because the claims have to be
re-derived against 2.0 rather than translated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The code deletion landed in 69f7e6e; the documentation did not follow it, and
one of the gaps was a user-facing defect.

- **README.md** presented a two-column dialect table and documented
  `--dialect <solid-v1|solid-v2>` as a supported flag. The shipped binary
  answers `unknown dialect "solid-v1"; known dialects: solid-v2`, so the
  product rejected a command its own README taught. The table is now Solid 2.0
  only, the refusal is described where a reader meets the flag, and two numbers
  that had drifted independently are corrected against the repository: the
  audited runtime is rc.3 (not rc.0) and the catalog is 27 rules (not 26).
- **rust/ARCHITECTURE.md** claimed `dialect-v1` and `dialect-v2` are both
  exposed and default, and that verification compiles both single-dialect
  variants. Neither is true. Its "Version ownership at a glance" table still
  had a Solid 1.x column naming `solid_1x.rs`, `dialects/solid-v1/compiler`
  and `solid1x_*` modules, and the crate tree still drew `dialects/solid-v1/`.
  It also described `RuleOptions` as nesting a `Solid1xRuleOptions` member,
  removed in 13d549f.
- **AGENTS.md** named the deleted crate in the repository-shape list and told
  agents to "preserve exact Solid 1.x and Solid 2.0 behavior". Its
  upstream-parity trap was the edit ADR 0110 s 3 named in advance and step 3
  missed: it still required divergences to be pinned against a corpus that went
  with the dialect. The ported code stayed and now runs unconditionally, so it
  is retained behaviour with no upstream control -- which the trap now says,
  along with what to write in a product-owned case instead.
- **rust/README.md**, **CONTRIBUTING.md**, two `solid_2.rs` doc comments and
  `audited-slices/README.md` each cited a crate, module or test that no longer
  exists.

Two things are kept rather than cut, and say so: the "How the Solid 1.x dialect
landed" section, which is the only worked account of what a second dialect
costs seam by seam and is the evidence behind docs/adding-a-dialect.md; and the
`solid-v1/` prefix on retained artifact directories, which are audit records of
published packages and outlive the dialect by ADR 0110 s 4.

Verified against the product rather than asserted: `--dialect solid-v2` is
accepted, `--dialect solid-v1` is refused, the catalog is 27 rules, and both
ADR links resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The direct-invocation rung of `interprocedural_contributions` computed a
wrapper chain, composed it to a word, then threw the wrappers away -- so a
`tracked` word reached `ContractCallback` with `schedule: None` and took the
consumer's historical `queued` default. The comment there asserted that "only
`inline` and `deferred` reach here, and both carry their schedule in the word",
which was false: an enclosing chain composes to `tracked` routinely.

Every tracked callback therefore published "runs after the export returns".
Under 1.x that was usually right, because 1.x's createEffect is AfterCall. With
1.x retired it is wrong for every 2.0 tracked primitive except
createTrackedEffect -- measured against the audited rc.3 install, createMemo,
createSignal, createEffect and createRenderEffect each published `queued` where
`same-stack` is the audited answer (Solid2::tracked_callback_timing cites the
bytes: both effects reach effect(), which calls recompute(node, true) before
queueing the effect function).

The rung now recomposes the schedule from the chain that produced the word,
exactly as the primitive-slot rung does. Deliberately narrow: only a word the
chain produced gets a schedule. The `ExecutionRole::TrackedJsx` arm -- the
other producer precision-backlog.md names, a compiler-lowering role with no
chain -- keeps the default, because `composed_tracked_schedule(&[])` answers
`same-stack` and nothing there proves it.

Pinned by the re-authored `callback-deferred-untracked-chain`, which is not a
port: three 1.x exports lose their premise under 2.0. `mountShape` answers
same-stack because the compute is DuringCall; `unestablishedScheduleShape`
resolves because 2.0 models createSignal(fn) as a compute slot rather than a
stored value; `mergePropsShape` has no counterpart, 2.0's `merge` not being a
callback-taking primitive. The 2.0 fixture is a four-cell grid over
schedule x tracking, so collapsing the axes cannot pass silently -- before the
fix `trackedShape` and `deferredTrackedShape` both answered queued and two
cells merged. Its `solid-js/runtime.ts` forwarding seam ported unchanged, both
of its path tricks being dialect-neutral.

Two existing fixtures moved, and one of them had pinned the defect:
`callback-untracked-wrapper`'s `trackedWrapper` asserted queued/tracked for
2.0's createEffect, inherited from the 1.x answer rather than measured. Its
README now says so. `multi-role-callback-parameter`'s three rows moved the same
way; its README described them as queued/tracked and now describes what
separates them, which is the tracking axis alone.

Corpus 95 -> 96 fixtures; both new documents type-check clean under --strict
against the real solid-js@2.0.0-rc.3 install as well as against the stub.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last package-contract fixture the retirement deleted, and the estimate for
it was wrong in the safe direction. The plan called it the expensive one --
"an @solidjs/web stub and a jsxImportSource arrangement across thirty JSX
files". That arrangement never existed: the fixture carries no jsxImportSource
pragma and no tsconfig of its own, because the generator supplies the analysis
config, and the stub's `namespace JSX` only ever made `For`'s declaration
self-contained. The port needed a new manifest dependency and a 2.0 `solid-js`
stub.

All 24 of its entrypoint/export rows come back byte-identical to the 1.x
original. That is the fixture's own evidence that its claim -- the call graph's
answer is fail-closed or exact: either every way of entering a function is one
of the call sites it enumerated, or emission marks every export -- never
depended on a dialect. The thirty `.js`/`.jsx` sources are unchanged.

Two 2.0 shapes did have to be met, and the stub keeps both whole rather than
reducing them. `For` has three overloads, of which only `keyed?: true` keeps
1.x's raw `(item, index: Accessor<number>)` children -- `keyed: false` hands the
callback an accessor and a plain number, and the predicate form hands it
accessors for both -- so `builtin.jsx` binding the first is a choice rather than
the only option on offer. And `For`/`Show` return `Element` from `solid-js`' own
types.js rather than `JSX.Element`, 2.0 having moved `namespace JSX` out of
`solid-js` into `@solidjs/web`; this stub therefore declares no JSX namespace at
all, because nothing here resolves one and inventing one would be wider than the
package.

The absolute-rule check is re-run rather than carried over, against the real
solid-js@2.0.0-rc.3 install and the generator's own tsconfig shape: clean under
`jsxImportSource: "@solidjs/web"`, which is the row that matters, and the
generator-config diagnostic is identical with the published package installed.
That diagnostic is TS2769 where 1.x reported TS2741 -- the same missing
`children`, reported as an overload-resolution failure because 2.0's `For` has
three overloads instead of one.

`unresolved-dispatch-reachability`'s README named seven of these arms as the pin
for the call-site enumeration and had to be amended to say they were unpinned;
that amendment is now withdrawn. The `interproc.rs` comment about
`unestablishedScheduleShape` is corrected too: 2.0 has no such shape, and the
re-authored chain fixture pins the resolved reading instead.

Corpus 96 -> 97 fixtures; no existing snapshot moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bin/solid-checker-rust` predates the retirement commits and still carries the
1.x dialect: run today it accepts `--dialect solid-v1` and analyzes with it. So
a CLI-level pin for the SC9013 refusal would not exercise the refusal -- the
shipped binary would analyze the project instead. Writing it against a stub
native binary would pin the launcher's exit-code forwarding, which
launcher.test.mjs already covers generically.

It therefore waits on a release that rebuilds the shipped binary, rather than
on anyone's attention. Recorded with what *is* pinned in the meantime: the Rust
process boundary, the ESLint adapter, and the contract sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Its doc comment said "an exact resolved fact and nothing weaker ... never
derived from the callee's spelling or from an import statement standing
nearby", and then named the decline record's package as what it is used for.
The first half is true of the accessor. The second is misleading about the
record, and measuring is what showed it: probed against the audited rc.3
install, every solid-js primitive callee answered None here, so the caller
reached its import-statement fallback and the record's package is usually the
written specifier's -- which for solid-js cannot tell a re-export of
@solidjs/signals' declaration from solid-js' own re-declaration.

Nothing decides an audit on it: the proposal-side denial lookup moved to
callee_declaration_source_file in 537e269 for that reason, and the census that
proves a claim binds an archive. What remains is that the decline record -- an
instrument for ranking which primitive to audit next -- can name a package
coarser than the one whose bytes the call reached. Recorded where the next
reader will meet it rather than left in a transcript.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`browser.rs` imported `set_close_on_exec` unconditionally, but that helper is
`#[cfg(unix)]` and its only caller here -- `spawn_piped_browser`, which builds
the CDP pipe out of raw descriptors -- is already gated, with a
`#[cfg(not(unix))]` counterpart beside it. So every non-unix target failed to
compile with E0432.

Only the wasm build compiles a non-unix target, and `make verify` does not
build wasm, which is why this stood from 01b84ad (2026-09-05) until CI ran it
on PR #62 twelve days later. The import now carries the same gate as the
function that uses it.

Left alone: four "never used" warnings on non-unix for constants and a field
that only the unix path reads. They are the same shape as this, do not fail the
build, and widening the change into that subsystem to silence them is not worth
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 files, 961 MB, 21.3 M lines -- 94% of this branch's entire diff against
main, and not one of them is read by any code, script, gate or test. They are
machine-readable artifacts of a single measurement run each, cited as evidence
by ADRs and phase documents.

.gitignore already tried to keep them out and could not, for two reasons worth
recording so it does not recur: the rule was a name-based list that enumerated
21 of the 63 and missed the other 42, and .gitignore never untracks what is
already tracked, so the ones committed before it landed stayed forever. The
rule is structural now, and the tracked copies are out of the index.

`git rm --cached`, not `git rm`: the files stay on disk. They are expensive to
regenerate and are somebody's evidence; this removes them from the repository,
not from the machine.

One cost, stated rather than discovered later: roughly twenty ADRs and phase
documents link a dump by relative path, and those links now point at a local
artifact rather than a repository file.

Diff against main: 22,615,289 insertions -> 1,318,563.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three trees deleted outright, with the code that embedded them:

- benchmarks/package-contract-v2/phase14/solid-v1-authority/ (21 files) and
  first_party_bundles.rs's whole Solid 1 surface: the 20 include_bytes!
  documents, the authority index, `Solid1Authority`/`Solid1AuthorityCase`,
  `solid1_bundles`/`solid1_bundles_with_measurements`, the "solid-v1" arm of
  the retired bundle-loader seam, and its selector table. `NamedDocument`,
  `selector_selects` and `has_local_closure` went with them once nothing
  reached them.
- rust/crates/solid-dialect/audited-slices/solid-v1/ (96 files) and
  benchmarks/package-contract-v2/phase0/solid-1x/ (5 files), neither of which
  any code, script or gate read.
- Sixteen of the nineteen pkg/contracts/bundled/solid-v1/ documents, likewise
  unread: the four the phase-0 gate names are path strings compared against a
  frozen baseline, and the two check-bundled-contracts.test.mjs names are
  synthetic. Neither set exists on disk.

`phase16_benchmark` started from the v1 bundles and extended with v2; since
`solid1_bundles_with_measurements` returned an empty vector and only validated
the authority documents, it now starts from v2 directly, which is the same
report. `solid-contract-bundles` loses its "solid-v1" group.

Three files stay, and the README says why rather than leaving it to be
rediscovered: `bundle-index.json`, which the bundle walker requires of every
`solid-v*` directory, and two stable-v1 documents that `policy2_receipt`'s
tests compile in as fixtures for dialect-neutral receipt logic. Retargeting
those two onto 2.0 documents is not a substitution -- the test hardcodes the
closure identities of the package they describe, so it needs that fixture
rebuilt, and guessing at it risks a test that passes vacuously.

The wasm crate's two fixtures did move to 2.0 documents, because nothing there
depended on their contents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`first_party_bundles.rs` was pinned by the marker `contract_document::decode(`.
That call lived in the Solid 1 authority replay, which went with the 1.x
artifacts, so the gate failed on a file that had not stopped doing its job --
it had stopped doing it *that way*.

The file still owns stable-v1 bundle loading and now reaches it through
`load_receipt_issued_embedded_contract(`, which is the interface that decodes.
The marker follows the role. Dropping the entry would have been the other
option and the wrong one: it would stop auditing the file entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Thirty-six tracked main documents left with the Solid 1.x artifacts: twenty
from the phase-14 solid-v1 authority and sixteen from
pkg/contracts/bundled/solid-v1/, none read by any code, script or gate. The
pin follows, with the reason, so the drop reads as the removal it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eighteen more tracked documents, none read by any code, script or gate:
`rust/crates/solid-dialect/contracts/solid-v1/` and the three unused
`solid-1x-*` fixture trees under the backend's tests. The contracts README
says the directory was deleted rather than leaving a reader to wonder.

What still carries a 1.x name is deliberate and is not an artifact: the
retirement plan, one audit document, and
`fixtures/reactive-ir/solid-1x-leftovers`, which is a **Solid 2.0** fixture --
it pins that 1.x-only names written in a 2.0 project are caught, which is
exactly the coverage the retirement makes matter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last 1.x artifact that was not load bearing: 48 KB documenting
solid-js@1.9.14's runtime bytes, which established the 1.x negative-authority
rows. Those rows went with `solid_1x.rs`, and the slices and manifest it cites
went with `audited-slices/solid-v1/` and `phase0/solid-1x/`.

Three documents cite it. They are historical narrative that already points at
those deleted paths, so rather than rewrite phase history, precision-backlog.md
now says once, above the 1.x passages, that the whole chain is historical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ADR 0111)

`primitive_callback_execution` answered the package-contract word -- inline,
deferred, tracked -- from a hardcoded match on `Primitive` in shared
`solid-reactive-ir` code, delegating to the dialect for three primitives and
stating the rest itself. ARCHITECTURE.md says shared code "asks the selected
vocabulary and receives that dialect's answer", and AGENTS.md forbids
dialect-specific behaviour in shared code when the seam can express it. This
was the one layer that did not follow the rule, and the 1.x retirement is what
made it visible.

Three things made it worth fixing rather than noting. Three of its arms were
already dead -- `createResource`, `on` and `mergeProps` are 1.x names `Solid2`
does not carry, so shared code held answers no dialect in the build could
reach. It produced a real defect: the direct rung read this table while the
chain rung read the dialect's `tracked_callback_timing`, they disagreed, and
every tracked callback published `queued`. And `docs/adding-a-dialect.md`
listed neither, so a third dialect would have been written and shipped before
finding it could not state a contract word of its own.

`Dialect::contract_callback_execution_at` now owns it, defaulting to None --
which contract emission already treats as unknown -- with `Solid2` stating
2.0's answers. It is deliberately separate from `callback_execution_at`: that
one is attribution for the checker's analysis, this one is what a contract
promises a consumer, and they diverge exactly where it matters. `onCleanup`
carries no `callback_executions` row at all and still promises `deferred`,
which is why deriving one from the other would be wrong.

No contract changed: the corpus is byte-identical across the move -- 97
fixtures, 328 declined closures, 147 artifact cases, 174 operations, 1259 proof
candidates, 5023 open claims, before and after. A test pins the answers where
they now live, including the `onCleanup` case that proves the word is stated
rather than derived.

Also renames `fixtures/reactive-ir/solid-1x-leftovers` to `retired-1x-spellings`.
It is a Solid 2.0 fixture -- it pins that 1.x-only names written in a 2.0
project are caught -- and the old name read like an artifact of the retired
dialect.

The ADR is explicit that this is not a survey: shared code still holds
primitive-keyed logic in owners.rs, source_discovery.rs, static_api.rs and
cleanup.rs, and whether each is dispatch or version-specific behaviour has not
been audited. That audit should happen before a third dialect, not during one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The audit ADR 0111 asked for, and it found a defect rather than dead weight.

Mechanically: 24 of the 77 `Primitive` variants are named by no dialect this
build carries -- expected, since `Primitive` is the shared vocabulary and
`Version::V1` is retained so the SC9013 refusal can recognize 1.x, but worth
stating that a third of the enum cannot be produced. Only two shared-code sites
still matched an unreachable variant, both in `source_discovery.rs`, and both
were the same hardcoded list: `CreateSignal | CreateStore | CreateResource`,
deciding which primitives return a destructurable reactive tuple.

That is 1.x's list. `createResource` is 1.x-only, and the list missed 2.0's
`createOptimistic` and `createOptimisticStore` entirely -- so a 2.0 project
destructuring either was not recognized as binding a reactive source at those
two sites.

`Dialect::returns_reactive_tuple` already existed for exactly this, is used
elsewhere in the same file, and its own doc comment names the problem: "shared
code carried one hardcoded list that was neither dialect's". Both sites ask it
now, and the store-versus-accessor kind beside them asks
`Dialect::returns_store` rather than comparing to `CreateStore`.

Unpinned, and recorded as such: coverage is unchanged at 433 findings across 80
projects because no fixture destructures either 2.0 primitive. The fix is
latent precision -- correct, exercised by nothing. Pinning it needs a fixture
whose finding depends on source recognition, which is its own piece of work.

The remaining primitive-keyed sites -- owners.rs, static_api.rs,
server_rules.rs, cleanup.rs, indexes.rs -- match only primitives a carried
dialect names, so none is dead. Whether each should consult a seam instead is a
separate question the ADR now lists rather than settles: some are genuine
dispatch, and some encode role knowledge a future dialect might answer
differently.

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