Retire the Solid 1.x dialect (ADR 0110) - #62
Open
yumemi-thomas wants to merge 352 commits into
Open
yumemi-thomas wants to merge 352 commits into
yumemi-thomas wants to merge 352 commits into
Conversation
…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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope, stated first
This branch is 343 commits ahead of
main, and they are two differentbodies of work:
after PR Complete authenticated proof-policy refusal reductions #61 merged. This description does not summarize them, because
they are not mine to characterize.
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-jsis a major this build has no vocabulary for gets one project-levelSC9013 unsupported-solid-runtimeresult naming thepackage.jsonthat decidedit, 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, thedialect-v1feature,docs/rules/v1/, the v1 declaration index, the twoupstream_compatmodulesthat 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 unreachableunsupported-runtimepackage status, theOn/MergePropsschedule rows, andpkg/contracts/bundled/solid-v1/plus the phase-14 authority — audit records ofpublished 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:
--check-contractsemitsa findings snapshot under a refused runtime, and the sweep read
report.packagesthroughArray.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.
chain's. The direct-invocation rung composed a wrapper chain to a word and
discarded the wrappers, so
trackedreached the row with no schedule andtook
queued. Under 1.x that was usually right; after the retirement it waswrong for every 2.0 tracked primitive except
createTrackedEffect.solid-jsre-declares six primitives from
./client/hydration.jswhose rows theaudit withholds on purpose; they were answered out of
@solidjs/signals'rows. Now keyed on the package that declares the callee, via
ResolvedDeclaration::source_file.<Index />; 2.0's index-oriented component isRepeat. Nothing pinned it.The obvious fix for (3) was wrong and measuring caught it:
origin_moduleisthe import specifier's module, so keying on it declined ten legitimate
re-exports to fix six re-declarations — a regression
callback-reactive-argumentscaught.Also fixed here
A producer crash during the Type Facts symbol phase was unrecoverable
(
analyzefailed on a crash it was meant to survive). Diagnosed byinstrumenting all eleven exchange sites under contention: 28 of 28 observed
failures escaped from
close_symbols, none from the analyze exchange or itsretry. 28 failures in 84 runs before, 0 in 180 after.
Session::closealso nolonger reports an already-dead producer as a failure to close it; it returns
CloseOutcomeinstead, 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-wrapperandcallback-deferred-untracked-chainauthored against 2.0 andescaping-private-helperrestored — that one a true port, all 24 of itsentrypoint/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 verifygreen at the tip (240 s, 0 failures). Every fixture stub wastype-checked under
--strictagainst the realsolid-js@2.0.0-rc.3install aswell as against the stub, so no claim rests on a stub being looser than the
package.
Known remaining
SC9013cannot be written yet: the shippedbin/solid-checker-rustpredates the retirement and still analyzes with--dialect solid-v1, so the pin would not exercise the refusal. It waits on arelease that rebuilds that binary.
solid-js'screateSignal,createEffectand their nine./client/hydration.jssiblings carry nocreatesstatement. Open item inADR 0007.
🤖 Generated with Claude Code