Skip to content

ADR-0073: agent-first test runner on the query graph - #2239

Closed
DorianListens wants to merge 15 commits into
trunkfrom
claude/rue-test-runner-proposal-qouv7j
Closed

ADR-0073: agent-first test runner on the query graph#2239
DorianListens wants to merge 15 commits into
trunkfrom
claude/rue-test-runner-proposal-qouv7j

Conversation

@DorianListens

@DorianListens DorianListens commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Refs RUE-506.

Draft ADR proposing rue test, re-grounding the RUE-506 design capture against the compiler as it exists today. Intended for several rounds of iteration before acceptance; nothing is ratified.

What the proposal commits to

  • Tests are language items: test "name" { ... } blocks (contextual keyword, preview-gated as test_declarations), analyzed as ordinary demand-driven roots. Placement is the visibility model — tests beside the code see private items, tests in importing modules prove the public API. Executable requests never root them. Bodies are ()-typed, and ? in a test body's immediate block gets unwrap-and-report semantics: the failure arm emits a structured unhandled_error record — the error payload rendered by a compiler-synthesized structural printer, the ? site's span attached — and traps, so attribution matches @assert, each ? site stands alone (no identical-error-type constraint, no return-type annotation on the block), and the position it defines is a compile error today. Orphan-test detection is manifest-gated and backed by a bounded candidate-acquisition step in the ADR-0063 host input protocol: out-of-closure manifest entries are read and published as revisioned inputs (or typed absent/unreadable outcomes) and consumed by a parse-only query that never mints semantic roots.
  • rue test is the driver's first subcommand, emitting a versioned NDJSON event stream on stdout as the primary surface (per ADR-0061 §6 schema policy); the human renderer is a consumer of the same stream. Discovery-without-execution via --list, stable IDs, repro argv on every failure, asymmetric verbosity, byte-safe bounded capture. capability_summary is present from event-schema v1.0 with an explicit unavailable status until Phase 3 populates it, so the MVP never contradicts its own zero-claims posture. The structured failure channel is a dedicated inherited pipe (recommended over stderr framing, which arbitrary user bytes could forge), budgeted separately from user streams.
  • Execution is a contract, not a mechanism: MVP is one linked test image per target (compiler-synthesized dispatcher main, per-function CodegenUnit reuse) plus one process per test with process-group timeout/kill. Two inventories are pinned as contract values: the loader-visible one (constant argv[0], fixed-width selector, pinned environment vector, run-constant image spelling — initial-stack consumption becomes deterministic, making the pinned RLIMIT_STACK a real boundary) and the test-visible one (what std.env observes after the dispatcher normalizes argv).
  • Capability summaries are inferred, not declared: a five-piece configuration-keyed query split — body-local edge projections (drop glue expanded through the per-type facts family, since a DropGlue reference names a type, not a callee) and leaf projections; EffectGraph SCC condensation over the edge projections; ComponentEffect joins along the acyclic condensation; per-function summary stamps — riding ADR-0063 red/green cutoff so leaf-only edits are near-zero warm work. Grounded in the three effect chokepoints the language already has (typed runtime-ABI helper manifest, @syscall, extern "C"). With no traits, function pointers, or threads, the call graph is total — inference is sound today with FFI as the only opaque edge. Summaries stay out of the type system in this ADR (explicitly flagged as the highest-stakes maintainer call).
  • Hermetic-only caching and selection: verdict cache keyed on closure fingerprints from the ADR-0063 graph, the pinned execution values, and every verdict-determining runner policy (effective timeout, per-stream output limits — presentation-only settings stay out); --changed-only is the same predicate. Allocation determinism comes from a test-build budgeted page mapper that reserves the entire permitted arena at startup: in-budget allocations cannot fail ambiently, over-budget allocations fail by policy, so a null observed by a test is a deterministic function of its own allocation sequence and policy denial can never be confused with ambient mapping failure. syscall/ffi/random tests always run — unsoundness ejects, never degrades.

Key re-groundings vs. the original issue sketch

  • ADR-0063 (implemented) already provides the fingerprints, reachability, and early-cutoff economics the issue hoped for — §15 even names RUE-506 test selection as a planned consumer.
  • The "indirect calls are the real boundary" concern is currently vacuous; the @requires(...) declaration surface is reserved for when traits/function values land, with FFI as its first user.
  • No clock API exists anywhere — Rue tests are time-deterministic for free; the ADR records "any future time API is born behind a clock capability" as a standing constraint.
  • The abort-only runtime (everything exits 101, no unwinding) makes process isolation a correctness requirement, not a preference, and pins the pinned-stderr-message failure taxonomy.

Ratification gates

  • Phase 3 is gated on the @ptr_to_int disposition (RUE-967 — the strict-provenance intrinsic split is recommended, with the escape-scoped recognizer as fallback); without it the addr leaf would mark nearly all of std.
  • Phase 4 is gated on the allocation-determinism maintainer call (the budgeted page mapper with up-front arena reservation) and on the verdict-cache key audit, whose named hard case is demonstrating image-independence rather than assuming it.

Sequencing

Phase 1 (declarations) and Phase 2 (MVP runner, zero capability claims) ship value before any capability work; Phase 2.5 pulls structured assertion payloads ahead of the capability phases (unstructured failure output is the primary agent token sink); Phases 3–7 layer summaries, caching/selection, scheduling/flake policy, syscall-number refinement, and the public provider protocol (wire format deliberately deferred to align with RUE-505).

For reviewers

The Open Questions section is structured for iteration: explicit maintainer calls (test syntax; analysis-only vs typed capabilities; the allocation-budget mechanism; the RUE-967 provenance split; the dedicated-pipe failure channel; seedable @random_*; @assert stabilization; the scripts/rue test homonym; exit codes), spikes with defined outputs (provenance-split migration audit, syscall-number classification coverage, spawn-throughput baseline, abort-tolerant batching, verdict-cache key audit, memo-DB pressure under test-shaped root sets), and deferred questions (comptime tests, doctests/RUE-504, xfail metadata, workspace model). The Result-typed-test-bodies question is resolved in this revision: ()-only bodies with unwrap-and-report ? (see §1 and Rejected alternatives).

Copy link
Copy Markdown
Contributor Author

Adversarial review: ADR-0070

Scope of this review: the original RUE-506 capture (re-read in full), every load-bearing claim checked against trunk source (f428bc5), and an external landscape sweep (prior-art verification plus a search for better designs). Verdict up front:

The design shape survives adversarial review. No better overall shape was found in the landscape: language-item tests + compiler-verified hermeticity + sound fingerprint caching/selection + versioned NDJSON + a process-isolation contract is not implemented as a whole anywhere, each pillar individually matches the best existing practice, and the places where industry departs from soundness (Meta predictive selection, Google TAP's pivot to culprit-finding, Teamscale/Launchable) all departed because of dynamic-collection costs that Rue's static graph genuinely avoids. The re-grounding against ADR-0063 is broadly accurate and §15 does name this consumer.

But the document has two findings that go to its central claims (a hermeticity soundness hole in the current language, and an unstated conflict between EffectSummary and the query engine's cycle semantics), one missed piece of prior art that falsifies the novelty framing as written (Unison), and a substantial list of factual errors against current source. Details below, ordered by severity.


1. Hermeticity as specified is unsound today: @ptr_to_int observes ASLR

§4.1 derives hermetic as "no syscall, no ffi, no random", and §5 claims a verified-hermetic test "cannot be flaky through any channel the OS offers except resource exhaustion." That is false in the current language:

  • @ptr_to_int(p) returns the raw address as u64 (spec 9.2, docs/spec/src/09-unchecked-code/02-intrinsics.md:103-112; sema dispatch crates/rue-air/src/sema/analysis/intrinsics.rs:264,413-416).
  • The heap comes from mmap(NULL, …) — the kernel chooses the address (crates/rue-runtime/src/x86_64_linux.rs:346-371, consumed at crates/rue-runtime/src/heap.rs:66); stack addresses vary with ASLR and the env/argv block size.

So checked { @ptr_to_int(@raw(x)) % 2 == 0 } is a no-syscall/no-FFI/no-random expression whose value differs run to run. Under the §4.1 lattice that test is verified hermetic, and §5 will cache its pass and replay it. Unsoundness was supposed to eject, never degrade — this is a leaf the lattice is missing, not an edge case: checked blocks are not a lattice input at all.

Related memory-state channels, same family:

  • Pressure observed as a value, not a trap: raw @alloc/@alloc_zeroed/@realloc return null on failure and @resize returns false (spec 8.6:4, docs/spec/src/08-runtime-behavior/06-allocation-failure.md:31-38; 9.2:12a). Checked code can branch on machine memory state and pass differently without trapping — the §5 "resource exhaustion" parenthetical covers only the trap path.
  • Stack overflow depends on ambient RLIMIT_STACK, which is not in the pinned-environment set (the pin set is env vars only), and exits 101 like any trap (crates/rue-runtime/src/entry.rs:35-53,133).
  • Uninitialized reads: @alloc storage is uninitialized (9.2:10) and reading it is not on the UB list (docs/spec/src/appendices/B-undefined-behavior.md:167-183) — a defined read of unspecified bytes, outside the closure fingerprint.

Suggested disposition, for the ADR to take a position on:

  1. Add an address-observation leaf: @ptr_to_int joins a new bit (addr or nondet), ejecting from caching. It must be that intrinsic specifically, not "any checked block" and not the whole raw family — std's collections are built on @alloc/@ptr_write and a coarse bit would eject effectively every test that touches StrBuf.
  2. Consider moving uninitialized reads to the UB list (a spec change with independent merit); "caching is sound for UB-free programs" is the standard and defensible posture, and it disposes of the uninit channel cleanly.
  3. Decide the alloc-failure-observability posture explicitly: either a bit on raw-alloc-family use in user code (coarse, ejects legitimate tests), or extend the resource-exhaustion carve-out to "memory-pressure-dependent behavior" and have the runner pin rlimits (RLIMIT_STACK, RLIMIT_AS) as part of the environment contract and cache key. The current text silently claims more than it can hold.
  4. The verdict-cache key audit spike already exists — add rlimits and ASLR explicitly to its perturbation checklist.

2. EffectSummary collides with the query engine's cycle semantics, and with an ADR-0063 rejected alternative

§4.2 says summaries resolve recursion as "a least fixed point over the bitset join — bounded, monotone, and cheap." Three problems, none mentioned:

  • The engine has no fixpoint support. Cycles are unconditional aborts: QueryAbort::Cycle (crates/rue-query/src/lib.rs:2488-2492, detection at :9513-9526). No cycle-recovery/iteration hook exists anywhere in the tree; every family that meets a cycle today converts it to a diagnostic (const-eval, comptime specialization, layout).
  • ADR-0063 explicitly rejected this dependency shape: "Make body queries depend on callee bodies. Rejected. It turns legal source recursion into query cycles and invalidates callers on ordinary callee implementation edits" (docs/designs/0063-…md:944-948). EffectSummary(FunctionInstanceKey) joining its BodyReferences-resolved callees' summaries is that shape verbatim.
  • The reachability precedent doesn't transfer. Recursion is legal in reachability only because it's one coordinator query over the whole root set with an internal visited set (revisioned_query_database.rs:16117-16180). A visited set converges for a membership predicate; it does not converge for a joined lattice — an SCC needs iterate-to-fixpoint or Tarjan condensation plus one reverse-topological pass, neither of which exists in the compiler. And if the fix is "make EffectSummary a whole-closure coordinator query," the headline economics change: a single coordinator terminal has one stamp, so a per-function edit dirties the family unless the design also adopts §8's per-identity stamped-projection pattern (docs/designs/0063-…md:520-527; in code, BodyClosureOutput.bodies[i].bundle each carrying its own stamp).

This is solvable — SCC condensation inside a coordinator with per-identity stamped projections is a perfectly good design — but it is a real design, and Phase 3's estimate should reflect it. §4.2 needs a mechanism paragraph and a citation of the rejected alternative it is deliberately skirting.

Also in §4.2, a factual contradiction: "helper references are visible in the body's resolved references against the typed ABI manifest" is false — BodyReference has exactly four variants (Callable/Definition/Type/DropGlue, crates/rue-compiler/src/body_query.rs:195-204), none of them helpers. The leaves live in the canonical bodies: SemanticBodyInstData::RuntimeCall/Intrinsic (crates/rue-air/src/semantic_body.rs:319-334, RuntimeCallKind at crates/rue-air/src/runtime_call.rs:107-148). The other sentence in the same paragraph ("leaf extraction reads the canonical body artifacts") is correct — keep that one. Two notes that follow: the canonical_bodies family is currently allow(dead_code) outside tests (revisioned_query_database.rs:404-406), so EffectSummary would be its first production consumer, with retention consequences the "free when not demanded" argument should price in; and drop glue is a summary edge — a drop fn performing @syscall is reached via BodyReference::DropGlue, not an ordinary call, and a naive callee walk misses it. Worth recording as a Phase 3 obligation.

3. @syscall is not actually checked-gated — in the compiler or the spec

The Context bullet ("legal only inside checked {}, spec §9.2") is wrong twice over:

  • The shared checked-block gate deliberately excludes it — "@syscall has its own gating" (crates/rue-air/src/sema/analysis/intrinsics.rs:253) — but analyze_syscall_intrinsic never reads ctx.checked_depth (crates/rue-air/src/sema/analysis/pointers.rs:878-922). @syscall outside checked compiles today.
  • Spec legality rule 9.1:12 lists raw-pointer, allocation, and raw-byte intrinsics — @syscall is absent; only the informal §9.2 page heading claims the requirement. No spec case asserts E1300 for un-checked @syscall.

This doesn't break EffectSummary (leaf extraction is over AIR sites, which see @syscall wherever it appears) — but it does mean the "three doors behind checked" framing is partly decorative, and any implementation shortcut that ever used checked as an effect proxy would be unsound. Two follow-ups worth filing independently of this ADR: the missing gate/legality rule, and spec 8.5:1 ("Rue installs no signal handlers"), which is contradicted by the runtime's SIGSEGV stack-overflow handler (crates/rue-runtime/src/entry.rs:133).

4. Unison must be cited, and the novelty claim qualified

Unison has shipped exactly "hermetic verdicts are cacheable artifacts keyed on content fingerprints" for years: test results are cached unless a function in the test's dependency graph gets a new hash; IO-using tests are excluded (Unison testing docs). Its purity comes from a type-system ability check (the annotation tax the ADR's effect-systems bullet warns about), and it has no isolation contract, no verdict taxonomy, no event stream, no selection story. So the ADR's actual contribution is narrower and should be stated as such: inferring hermeticity in an effect-unannotated language, plus the isolation/verdict/stream contract around the cache. "Instead of importing the compensating machinery other ecosystems needed" survives; any implicit "no one caches verdicts by content hash" does not.

5. Agent-first gaps — the two the ADR's own source issue flags

  • Structured expected/actual is deferred too far. RUE-506: "Failure output as values, not prose — expected/actual as structured data with machine-computed diffs. This is where most agent token waste lives today." The MVP ships the §7.1 channel contract but the only producer is boolean @assert — every failure payload in Phase 2 is a pinned string plus the test-declaration span. As sequenced, the runner is agent-first in transport but not in content until an unscheduled Future Work item lands. Recommend pulling @assert_eq (or one structured-payload intrinsic) into Phase 2, or an explicit Phase 2.5. (Also: the Future Work line "today's @assert gives only a boolean and a fixed message" is wrong — 4.13:5b gives it an optional message argument. The gap is structure, not messages.)
  • No expectation-promotion story. The dominant agent iteration loop elsewhere is snapshot/expect testing with machine-applied updates (insta cargo insta accept, ppx_expect + dune promote, Jest -u); Ronacher's MiniJinja→Go port is a concrete 2026 datapoint that the snapshot suite was the agent harness. ADR-0070 has expected/actual as data but no way for a failure to carry a proposed fix and no accept verb. This rides existing seams — reserve a suggested_fix/promotion payload field in the §7.1 record and name a future rue test --accept — but it should be reserved now so the payload schema doesn't foreclose it. Note the architecture is unusually good for this: the runner applying promotions keeps snapshot tests hermetic (the test never writes files).
  • Smaller: per-test static reachability is the cheapest missed win. RUE-506 asked for "what tests exist for this function/module" as a query surface; --list answers only "what tests exist." The graph already computes per-root reached sets; the inverse query ("which tests reach item X") is nearly free, and it is the sound version of what Teamscale sells dynamically. Reserve it in --list/schema. Also consider a test_started event — Swift Testing/libtest both emit start events, and without one a stream consumer cannot attribute hangs or show progress.

6. Placement/visibility: right call, wrong description, one footgun

  • Visibility in Rue is directory-scoped (spec 10.3: a private item is visible throughout its containing directory). So the real model is: same-directory test files get private access (Go's foo_test.go-in-package shape), other-directory tests prove the public API. That's stronger than the ADR's text, which reads as if private-item tests must share the file. Say it explicitly — it's a selling point, and it changes the "production source files carry test text" consequence (they don't have to).
  • Discovery footgun: discovery is the root's transitive @import closure, so a sibling foo_tests.rue that nothing imports is silently invisible — no diagnostic, tests just don't exist. That is the same "typo becomes false evidence" class the ADR invokes for empty filters, and it will bite agents that add a test file and forget the import. Recommend: rue test warns when a .rue file under the root's directory tree contains test items but is outside the closure (or an equivalent lint).
  • The unused-warnings paragraph is wrong about the mechanism. Warnings are not computed against request roots: the session filters declarations against reached ∪ declaration-deps ∪ syntactic warning-references, where the third set comes from a whole-program syntactic scan over every body-owning declaration, reached or not (crates/rue-compiler/src/session.rs:6771-6841, :4813-4831). Consequence: whether test-only helpers warn depends on whether test bodies join that syntactic scan — if they do, helpers don't warn and "tests cost production builds nothing but parse time" is false (each executable request pays a per-test-body reference scan); if they don't, private helpers used only by tests warn in executable builds, which collides with the no-visibility-loosening principle. The ADR needs to pick one and say it; the current text describes a mechanism that doesn't exist.

7. Mechanical/dispatch corrections (each verified against source)

  • The dispatch rule breaks on flag values. Eleven flags take a following value (-o, --emit, --target, --linker, --preview, --log-level, --log-format, --error-format, -j/--jobs, --source-manifest, --link-archive). rue -o test prog.rue — naming your binary test — enters test mode under "first non-flag argument is exactly test." The rule must be "first argument that is neither a flag nor a flag's value," i.e. dispatch happens inside a value-aware scan, not before it. Also worth noting this is the driver's first subcommand ever (--watch is the only existing alternate mode, with its own flag-combination validator that test mode will need to join), and §4.2's "an executable or check request" references a check request that doesn't exist.
  • Contextual keyword: no contextual keyword exists anywhere in the lexer/parser today — test would be the first, and it's a live identifier (std/bitset.rue:63 has fn test(...)). The approach is right and necessary; "mitigated by its restriction to item position" understates that this is a new parser category with no in-repo pattern.
  • DirectiveArg is Ident-only (crates/rue-parser/src/ast.rs:73-76). Of the sketched directives, only @requires(fs) parses; @timeout(ms), @known_bug("RUE-NN"), @tag("..."), @group("name") all need new literal-argument grammar and AST. "Directive syntax fits, deferred" is a grammar change, not a scheduling choice. Also directives are currently rejected on enum/drop/extern items, and the item-grammar list in §1 omits extern items entirely.
  • Test identity needs a schema change the ADR doesn't mention: StableDefinitionKind is a closed, macro-checked taxonomy of exactly 8 kinds ("Adding a kind cannot compile until all taxonomy fields are supplied", crates/rue-air/src/semantic_identity.rs:437-452). A 9th Test kind plus a namespace decision is required — fine, but it's Phase 1 work, not free.
  • rue-test-runner attribution errors in the Context bullet: known_bug semantics live in rue-oracle-diff/rue-cli-tests, not rue-test-runner; SIGPIPE/141 verdict handling exists nowhere in the harness (it's a runtime/spec property) and is net-new runner work; and — notably — the empty-filter principle is inverted: rue-test-runner deliberately allows a zero-match user filter to pass and only fails on an empty corpus ("a user filter that matches zero cases may remain successful", crates/rue-test-runner/src/lib.rs:1743-1762; the strict behavior is rue-spec's own layer, RUE-1161). Exit-code 3 may still be the right call, but it's a departure from the in-tree harness presented as an adoption of it — it belongs squarely in maintainer calls (where it already sits) without the precedent claim.
  • Naming/availability: RootSetKey doesn't exist — the family is compiler.body-reachability keyed by BodyClosureQueryKey (crates/rue-compiler/src/body_query.rs:344-348; ADR-0063 §6 pre-authorizes name drift, but §Context asserts these names exist today). ProgramImagePlan is pub(crate) and not on the ADR-0061 facade — "through the ordinary ProgramImagePlan path" implies an availability that needs facade work. require_preview() names a function that doesn't exist (the pattern — call sites checking PreviewFeature membership — does).
  • Two better precedents to cite: multi-root requests already ship — every extern "C" export is a co-equal reachability root next to main (crates/rue-compiler/src/session.rs:6113-6127), which is exactly the test-request shape; and the synthesized dispatcher has two in-tree precedents (FunctionInstanceKey::DropGlue as a first-class synthesized instance, and ProgramImageExportThunk as a compiler-owned link input with its own plan entry, program_image_plan.rs:37-45).
  • Lattice corrections: the exit row is wrong — RuntimeHelperId::Exit is emitted only by the compiler's own main-return lowering; user-visible exit is std.exit@syscall (std/_std.rue:104-111), so the exit bit as specified is unreachable from user code (and, if leaf extraction is naive about the synthesized dispatcher, would be joined into every test). Either delete the row or define it against std.exit. "std.fs/std.net/std.exit never touch the helper manifest" is also false — they allocate through RuntimeHelperId::Alloc on most calls (std/fs.rue:534 etc.); harmless for the lattice (alloc carries no capability) but it's load-bearing rhetoric for "the leaves are exactly these three doors," so reword. extern "C" exports are ordinary unchecked Rue (9.3:1) — the ffi row should say "call." And traps write pinned stderr, so the "traps introduce no capability" line should lean explicitly on the verdict/captured-output carve-out.
  • Citation fixes: ADR-0038 contains no bug/error split — it's the Result/Option/must-check design; the Rejected Alternatives argument stands on its own four legs but must drop or re-source that appeal. ADR-0064 and ADR-0065 are accepted, not implemented (implemented: blank) — the Context reads as if FFI is shipped. ADR-0069 mentions a test runner only as future scope growth (twice), not as something its tiers "expect." "ADR-0061's RUE-439 rule applies verbatim" overstates — 0061 treats RUE-439 as future work, not a ratified rule. And the §14 retention-budget mention should not imply prior evidence about test-shaped root sets exists — the calibration table is all main-rooted; the Phase 2 spike is correctly scoped and is the first such measurement (note the budgets are soft: the failure mode is RSS growth, not rejection).

8. Prior-art updates from the landscape sweep

Worth folding into the Context section:

  • nextest now has its own machine-readable formats (stable list JSON; experimental libtest-json run output) and has said it will stabilize independently of upstream libtest — "reverse-engineered from libtest" is the origin story, not the current state. Upstream libtest JSON (RFC 3558) remains unstable as of mid-2026.
  • Go's cache is package-granular — worth saying, since item granularity is one of Rue's genuine differentiators.
  • Deno's per-test permissions can only narrow the process-level grant, never widen — "runtime-enforced per-test permission grants" should read "scoped-down grants."
  • Zig is converging on a public build-server protocol (post-0.15 build-system rework) but hasn't shipped it as a public contract — committing to a documented protocol from Phase 2 is a real differentiator, and Zig's known wart (user stdout writes corrupt the test protocol, prevent confusing debugging experience when users write garbage to stdout, interfering with the build runner / test runner protocol ziglang/zig#15091) is a concrete argument for Rue's stream-on-runner-stdout/tests-in-own-processes split.
  • Swift Testing's schema history is direct evidence for the ADR's v1-metadata choice: tags/timeLimits were omitted from ABI v0 and had to be pitched back in (v6.3/v6.4) after third-party tool pain. Keep capability summaries and structured identity in v1 events, as drafted.
  • Nim belongs in the effect-systems bullet: it's the one mainstream compiler doing zero-annotation bottom-up effect inference (exception + user-defined tag effects), and its effectsOf (RFC 404) is the ready-made design for effect-polymorphic function values — a better default than "⊤ or declared bounds" for the day function-typed values land. Cite it in "Constraints on future language evolution" too.
  • HyRTS (ICSE 2018) preempts the obvious objection to function-granular selection — dynamic method-level RTS often loses end-to-end to class-level because of collection overhead. Rue's answer (fingerprints are compilation byproducts; zero marginal collection) is exactly the dissolution of that tradeoff, and citing the literature makes the claim look considered rather than naive.
  • CTRF (Common Test Report Format) is the emerging cross-tool report standard with an AI-tooling ecosystem — wrong as the primary surface (it's a summary format, not an event stream), right as a Phase 7 reference adapter next to JUnit.
  • The MCP/agent-tooling ecosystem (2024-2026) is all lossy adapters reconstructing structure over prose-emitting runners — confirming the NDJSON+IDs+repro direction, and confirming that the two things adapters keep bolting on are exactly the two gaps in §5 above (applyable diffs, per-test coverage).

9. Smaller design notes

  • Phase 6 @requires(...) narrowing on extern blocks is honor-system. If a declared-narrowed summary can ever reach the hermetic predicate, a lying (or stale) annotation produces stale cached passes — recommend a floor: declared narrowing informs scheduling/reporting, but FFI-touching tests never become cacheable regardless of annotation. That keeps "unsoundness ejects" literally true.
  • exit(0) inside a test body is a spoofable pass (process exits 0 before later assertions). Every process-based runner shares this; worth one sentence, and it interacts with the dispatcher design (dispatcher could detect "returned vs exited," e.g. an epilogue sentinel write, if you ever care).
  • SIGPIPE: §4.1 calls stdout hermetic-compatible, but a stdout-writing test dies with 141 if the runner's reader closes early (8.5:3) — the execution contract should oblige the runner to keep pipes open until child exit.
  • Phase 4's "error-tolerant test images" has an obvious mechanism (exclude failed closures from the image; their tests get compile_error verdicts without stubs) — one sentence would keep someone from inventing stub synthesis.
  • Floats (ADR-0065, unimplemented) will need a determinism sentence in "Constraints on future language evolution" when they land — IEEE arithmetic is deterministic per target, but NaN payloads and @float_to_int edges are where hermeticity reasoning usually goes wrong.

Summary of requested changes

Blocking (go to the core claims): §1 hermeticity leaves (@ptr_to_int, memory-pressure observability, rlimits in the pin set/cache key); §2 EffectSummary mechanism paragraph (SCC/coordinator + per-identity stamps, cite the ADR-0063 rejected alternative, fix the BodyReferences leaf claim, record the drop-glue edge).

Strongly recommended: cite Unison and qualify novelty; pull structured expected/actual into Phase 2/2.5; reserve promotion payload + accept verb; reserve per-test reachability and test_started; fix the visibility framing and add the unimported-test-file lint; fix the dispatch rule.

Factual sweep: §3 @syscall gating (and file the compiler/spec bugs), the ~15 source corrections in §7, and the prior-art updates in §8.

Everything above was verified against trunk f428bc5 with file:line evidence; happy to expand any item into a follow-up.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

All findings addressed in 348023c (+406/−178). I re-verified every load-bearing claim against source before editing — including the two that contradicted my original research notes — and the review held up on all of them. Disposition by section:

1. Hermeticity unsoundness (@ptr_to_int/ASLR, memory pressure, rlimits) — accepted, blocking. §4.1 now has an addr bit scoped to exactly @ptr_to_int (not checked, not the raw family — the StrBuf-ejection trap you flagged is called out in the text), and the hermetic predicate is "no syscall, no ffi, no random, no addr". Memory-pressure observability (8.6:4 null/false returns, RLIMIT_STACK-dependent overflow) gets your option 2: the carve-out is extended and stated honestly — "deterministic given pinned limits on a non-exhausted machine" — with RLIMIT_STACK/RLIMIT_AS pinned in the execution contract and cache key. Uninitialized reads: adopted "caching is sound for UB-free programs" plus a new maintainer call to move uninit reads to the UB list. Rlimits + ASLR added to the cache-key audit spike checklist.

2. EffectSummary vs cycle semantics — accepted, blocking. §4.2 rewritten: the per-function callee-summary shape is named as ADR-0063's rejected alternative and avoided; the mechanism is now an SCC condensation coordinator over the reached call/drop-glue graph, one join per component in reverse topological order, publishing per-identity stamped projections (§8 pattern) so the coordinator terminal doesn't become a whole-closure stamp. Leaf claim corrected (canonical-body instruction payloads, not BodyReferences; references contribute edges). Drop-glue edges are an explicit Phase 3 obligation, canonical-bodies first-production-consumer retention is priced into Phase 3's measurement gate, and dispatcher code is excluded from summaries (which also resolves your exit-row point).

3. @syscall gating — accepted; bugs filed. RUE-1369 (missing sema gate + 9.1:12 omission, with a note that ADR-0070's analysis doesn't rely on checked) and RUE-1370 (the 8.5 "installs no signal handlers" sentence vs the SIGSEGV handler, related to RUE-707). The Context bullet now states the actual situation and cites RUE-1369.

4. Unison — accepted. New prior-art bullet; the novelty claim is now scoped to what's defensible: hermeticity inference in an effect-unannotated language plus the isolation/verdict/stream contract around the cache, not the cached-verdict idea.

5. Agent-first gaps — accepted, and I think this was the most valuable section. New Phase 2.5 pulls @assert_eq-family structured payloads and machine-computed diffs ahead of all capability work, with the rationale stated in your terms (agent-first in transport but prose in content doesn't meet the bar). §7.1 reserves the promotion payload (suggested fix + target span + replaced-content hash) and names rue test --accept, with the runner-applies-promotions hermeticity note. §2 reserves the inverse --list --reaches <item> query with the per-root reached sets named as why it's nearly free. test_started added. The @assert "fixed message" error is corrected (the gap is structure, not messages).

6. Visibility/placement — accepted. §1 now states the directory-scoped model (10.3) explicitly with the Go _test.go comparison, and the Neutral consequence no longer claims production files must carry test text. The unimported-test-file warning is in §1 and Phase 2, scoped to files the compilation may read (manifest-bounded — discovery must never widen the read set). The unused-warnings paragraph is rewritten around the mechanism that actually exists: the ADR now takes the decision (test bodies join the syntactic reference scan) and restates the executable-request cost honestly as parse + syntactic scan.

7. Mechanical corrections — all applied: value-aware dispatch rule (flag-value scan, watch-validator join, "check request" removed); first-contextual-keyword sizing with the live Bitset.test identifier named; identifier-only DirectiveArg acknowledged (literal-argument directives are grammar work; @requires(fs) parses today); StableDefinitionKind Test kind + namespace in Phase 1 scope; harness attributions fixed (known_bug → oracle/CLI harnesses, empty-filter → rue-spec/RUE-1161 with the shared-crate behavior stated, SIGPIPE verdicts marked net-new); conceptual-naming caveat for query families; image-planning facade work in Phase 2 scope; preview-gate wording fixed; extern-export multi-root and drop-glue/export-thunk synthesis precedents cited; exit row deleted with the dispatcher-exclusion rationale; "never touch the helper manifest" reworded (I/O bypasses; allocation doesn't); ffi row says call; traps text leans on the verdict carve-out explicitly; ADR-0038/0064/0069/0061-RUE-439/§14 citations all corrected as specified.

8. Prior-art updates — folded in: nextest's own formats + RFC 3558 status, Go package granularity, Deno narrow-only scoping, Zig protocol status + #15091 as the argument for our stream/stdout split, Swift Testing's v0-metadata history as evidence for rich v1 events, Nim + effectsOf in both the prior-art bullet and the traits subsection of the evolution constraints, HyRTS cited where selection granularity economics are claimed, CTRF as a Phase 7 reference adapter.

9. Smaller notes — all taken: declared-narrowing cacheability floor in §4.5 (annotations inform scheduling/reporting, never caching — "unsoundness ejects" stays literal against dishonest annotations); std.exit(0) blind spot + epilogue-sentinel option in §3; pipes-held-open-until-exit as a contract obligation; error-tolerant images specified as exclusion-not-stubbing; floats determinism subsection added to the evolution constraints.

One point of partial pushback, for the record: the §14 retention note — agreed the calibration table is main-rooted and the ADR now says the Phase 2 measurement is the first of its kind, but I kept it as a spike rather than a blocking concern precisely because the budgets are soft; the text now names RSS growth as the failure mode.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Adversarial review, round 2 (348023c)

Re-verified against trunk (now 59799c9 — moved by one commit, nothing touching this ADR; all round-1 evidence re-checked where relevant). Method: diffed 9d69b85..348023c hunk-by-hunk against the round-1 findings, pulled RUE-1369 and RUE-1370 from Linear and checked them against the source evidence, and re-attacked the new mechanisms the revision introduces.

Every round-1 finding was genuinely addressed — no claimed fix is missing or quietly reinterpreted. The addr bit, the rlimit carve-out and cache-key additions, the UB-free-caching posture with its two new maintainer calls, the SCC-coordinator rewrite of §4.2 (rejected-alternative citation, corrected leaf source, drop-glue obligation, canonical-bodies retention pricing, dispatcher exclusion), Phase 2.5, the promotion payload, --reaches, test_started, the directory-visibility framing, the unimported-file warning, the value-aware dispatch rule, the harness re-attributions, the narrowing floor, and all the prior-art and citation fixes are present and accurate as described. RUE-1369 and RUE-1370 are filed, correctly scoped, and match the source. The §14 pushback (spike, not blocker, because the budgets are soft) is reasonable and I accept it.

One new blocking finding, introduced by the round-1 fix itself, plus small notes.


1. The addr bit as specified ejects essentially all of std (blocking, Phase 3/4)

§4.1 now says the bit is "precisely @ptr_to_int … not allocation — std's collections live on @alloc/@ptr_write, and a coarser bit would eject every test that touches StrBuf for no soundness gain." The problem: std's collections also live on @ptr_to_int. 42 sites across std/:

  • std/strbuf.rue:51-53copy_packed_bytes forms sub-range pointers via @int_to_ptr(@ptr_to_int(p) + offset); this is on the copy path under StrBuf.from/append/grow.
  • std/strbuf.rue:180, std/rawbuf.rue:71, std/net.rue:305,382,413,440,566,572 — the idiomatic null test is @ptr_to_int(p) == 0 (it's even the spec's stated null test, 9.2: "@ptr_to_int(p) == 0 is the null test").
  • std/rawbuf.rue:15,69,74,120ptr mut u8ptr mut T type-punning via @int_to_ptr(@ptr_to_int(..)), documented at rawbuf.rue:15 as the sanctioned idiom (no pointer-cast intrinsic exists). ArrayBuf sits on RawBuf.
  • std/mem.rue:23-24swap addresses both bindings via @ptr_to_int; std/sort.rue:37 uses mem.swap, std/binary_heap.rue uses ArrayBuf.swap.

So under the revised lattice, any test touching StrBuf, ArrayBuf, RawBuf, mem.swap, sort, or binary_heap joins addr — the hermetic set collapses to roughly arithmetic-only tests, Phase 4's cache hit rate approaches zero, and Phase 3's day-one summaries would show addr on nearly everything. The ADR's own §1 example test (StrBuf.from("8080")) is non-hermetic under its own lattice. The precise-not-coarse framing doesn't survive contact with std, because the intrinsic conflates three different operations.

The saving observation: every one of these std uses is a deterministic idiom. Null testing (== 0 is ASLR-invariant), provenance-preserving rebase (the integer flows only into @int_to_ptr; the result is used as a pointer), and type-punning casts never observe the address. The nondeterminism is only in letting the integer escape (branching on its magnitude, storing it, hashing it, printing it).

This is exactly the problem Rust spent years untangling as pointer provenance, and its strict-provenance APIs are the ready-made fix shape: Rust split ptr as usize into addr() (observe — the operation that "counts"), with_addr() (rebase, provenance-preserving), and cast() (pointee change). Two dispositions for the ADR to choose between:

  1. (Recommended) Split the intrinsic, migrate std. Add @ptr_cast (pointee change), keep byte offsets on @ptr_offset over ptr u8 (stride is already 1 there — note strbuf.rue:47-49's comment claiming @ptr_offset can't form these sub-range pointers looks stale for the u8 case and is worth checking during migration), and either allow direct null comparison or add @ptr_is_null. After a mechanical ~42-site std migration, @ptr_to_int is genuinely rare — true address observation — and the addr bit works exactly as §4.1 specifies. Independent merit: it removes int-roundtrip provenance destruction from std ahead of any future alias-analysis or optimizer work, which is the same reason Rust did it.
  2. (Alternative) Escape-scoped addr. Join addr only when a @ptr_to_int result escapes the recognized deterministic shapes (flows anywhere other than @int_to_ptr operands and == 0/pointer-equality comparisons). All 42 std sites are body-local and would clear. Cost: a taint rule inside the summary extractor — soundness-critical dataflow where §4.1 currently promises a syntactic leaf, with the usual precision corner cases. Weaker on provenance hygiene, no std churn.

Either way, §4.1's "the bit is precisely @ptr_to_int" paragraph and the StrBuf sentence need rewriting, and the decision (not necessarily the migration) should land before Phase 3 ships summaries — otherwise the first visible output of the capability system is "everything is addr."

2. Small notes (non-blocking)

  • Future Work still says "JUnit and CI-surface adapters" while Phase 7 now says "JUnit and CTRF adapters" — harmonize.
  • Unimported-test-file warning: detecting "contains test items" means parsing files outside the compilation. Two Phase 2 details worth a sentence each: a parse failure in such a file should surface inside the warning (or be explicitly skipped), never as a compile error of the request; and directory trees can legitimately contain test-shaped files belonging to other roots (this repo's cli-test-fixtures/ shape) — the --source-manifest bound covers the disciplined case, but the warning may want a per-directory opt-out before it meets a monorepo.
  • mem.swap's address integers flow only into @int_to_ptr, so it's cleanly covered by either disposition above — noted so nobody special-cases it.

Nothing else new. With finding 1 dispositioned, this round finds no remaining gap between the document's claims and the source, and the design's core — contract-first execution, inferred hermeticity with eject-don't-degrade, sound fingerprint selection, versioned stream — remains, in my judgment, ahead of anything shipping elsewhere.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Round 2 addressed in a9fe199 (+62/−10).

Finding 1 (addr ejects std) — accepted, blocking, and confirmed against source before editing: 42 @ptr_to_int sites across std/, the three idioms are exactly as described, spec 9.2 itself blesses @ptr_to_int(p) == 0 as the null test, and the copy_packed_bytes stride comment does look stale for the ptr u8 case. The round-1 fix was sound but unusable — precisely the kind of overshoot this iteration process exists to catch, since the capability system's first impression would have been "everything is addr."

Disposition taken, with one improvement over the review's framing: the recommended split doesn't need a new issue — it is RUE-967, verbatim. That Backlog design item (deferred from ADR-0059) already specifies the strict/exposed-provenance split and the null-primitive question, and its stated trigger was "formalizing the memory model." §4.1 now recommends resolving RUE-967 with this ADR as its first forcing consumer:

  • The rewritten §4.1 paragraph names the conflation (observation vs. null test vs. rebase vs. type-pun), the 42-site evidence, and the escape criterion (nondeterminism lives only where the integer escapes — branched on, stored, hashed, printed).
  • Recommended: the RUE-967 split (pointee-cast intrinsic, byte offsets via @ptr_offset over ptr u8, a direct null test) plus mechanical std migration, after which addr is the plain syntactic leaf §4.1 promises — with the independent memory-model/optimizer merit noted.
  • Fallback if RUE-967 resolves against the split: the escape-scoped recognizer as you specified it — body-local, @int_to_ptr operands and null/pointer-equality comparisons clear, any non-local escape joins conservatively, no per-call-site special cases (your mem.swap note is covered by the no-special-cases rule: its integers flow only into @int_to_ptr).
  • Phase 3 is now gated on the disposition, it's added as a maintainer call, and a new spike (provenance-split migration audit) owns the 42-site classification plus the @ptr_offset-stride verification — if the stride restriction is real, the split needs a byte-offset primitive; if stale, the rebase idiom migrates to plain @ptr_offset.
  • Cross-linked in Linear: RUE-967 ↔ RUE-506, with a comment on RUE-967 recording the new stake, the gating relationship, and the stride datapoint so the audit output has a home.

Finding 2 (small notes) — all taken: Future Work now says "JUnit and CTRF adapters" matching Phase 7; the unimported-test-file warning gained both edges (an out-of-closure parse failure surfaces inside the warning or the file is skipped with that stated — never a compile error of the request; foreign-root test-shaped files are handled by the --source-manifest bound as the disciplined multi-root story, with a per-directory opt-out if that proves insufficient, which is the cli-test-fixtures/ shape).

On the round-1 verification ("every claimed fix present, none quietly reinterpreted") and the §14 pushback acceptance — noted with thanks; the diff-the-diff method is exactly the right way to audit a revision this size.


Generated by Claude Code

@steveklabnik

Copy link
Copy Markdown
Collaborator

Review of a9fe199

I re-read RUE-506, the full ADR at this head, both prior adversarial-review rounds and their dispositions, the new RUE-967 cross-link/comment, and the current query/runtime/test-runner source. The previous findings are genuinely addressed. The overall shape still looks strong, but I found four issues I think need disposition before ratification, plus two smaller protocol/architecture gaps.

1. The SCC coordinator does not provide the incremental behavior §4.2 claims

The round-1 fix solves recursion correctly, but it conflates narrow downstream invalidation with narrow recomputation.

As written, one root-set coordinator observes the reached graph and every canonical body needed for its leaves, computes the whole SCC condensation, joins every component, then publishes independently stamped per-identity projections. Those projections let downstream consumers remain green, but an ordinary leaf-only body edit still changes one coordinator dependency and re-runs the coordinator over the whole graph. Nothing in the current query runtime lets a single evaluator stop after the changed SCC merely because some sub-values retain their stamps. This is the same distinction ADR-0063 §8 makes for reachability: a re-derived result can publish unchanged memberships, but the baseline evaluator still re-derives the graph.

That makes these claims unsupported by the proposed mechanism:

editing a body recomputes its component's summaries, and red/green cutoff stops propagation

and Phase 3's expectation of near-zero warm work. The measured gate can detect the problem, but the ADR should name a mechanism capable of passing it.

A viable split seems to be:

  1. EffectGraph(RootSet) depends only on BodyReferences, computes stable SCC membership and condensation edges, and therefore stays green for body edits that do not alter references.
  2. A body-local leaf projection reads each canonical body.
  3. ComponentEffect(SccKey) joins its member leaves plus callee-component summaries. The condensation graph is acyclic, so ordinary query dependencies are legal; only the changed SCC and reverse callers run, with cutoff when the bitset is unchanged.
  4. Per-function summaries project their component result.

If the intent is instead an O(V+E) coordinator pass on every changed leaf, the ADR should say so and weaken the “recomputes its component” economics. I consider this blocking because incremental inference is one of the central premises inherited from RUE-506.

2. The shared image makes the per-test cache key unsound for allocation-observing tests

The memory-pressure carve-out plus pinned RLIMIT_AS does not close the defined-value channel introduced by raw allocation.

The test process maps the whole selected test image, while the proposed cache key is per-test closure. Adding or filtering unrelated tests changes the mapped image size without changing a given test's closure fingerprint. A test can use the defined behavior of @alloc/@alloc_zeroed/@realloc returning null near the finite address-space limit and branch on it. The same test ID, body closure, environment, seed, and pinned rlimit can therefore produce a different verdict solely because unrelated code changed the shared image. This is RLIMIT_AS exhaustion even on an otherwise non-exhausted machine, so the current qualification does not dispose of it.

The Phase 4 audit asks whether image layout belongs in the key, but this consequence is already knowable and materially changes the design:

  • key every test on the whole image/selection set, sacrificing item-granular reuse;
  • emit per-test or stable-shard images;
  • infer/eject observation of fallible allocation results (another escape-style dataflow problem); or
  • explicitly give up the “sound cached verdict” claim for memory-pressure-dependent programs.

Pinning a finite limit by itself is not sufficient. This should be a maintainer call before Phase 4 rather than only an audit checklist item.

3. The execution contract promises isolation that process groups do not provide

§3 promises that every test's failure “cannot corrupt, mask, or abort any other test's result,” then says unverified-capability tests run in parallel because process isolation plus a private scratch directory is strong enough. That is true for process-local memory, but not for the capabilities the lattice explicitly treats as unverified:

  • @syscall is arbitrary platform syscall access: a test can signal the runner/siblings, mutate absolute/shared paths, contend on ports, or spawn descendants.
  • FFI is opaque-top and can do the same.
  • A private current directory is not a filesystem sandbox.
  • The reused runner mechanics kill the process group on timeout/output overflow, but return when the direct child exits; the implementation even handles the case where a descendant retains a pipe fd. Killing the group on normal completion would improve cleanup, but a child can create a new session/group, so it still is not containment.

I would scope the guaranteed contract to fresh process state, captured stdio, timeout, and best-effort process-tree cleanup. Noninterference should be guaranteed only for verified-hermetic tests (or tests in an actual platform sandbox). The MVP may still choose conventional parallel execution for unverified tests, but it should state that those tests can interfere; alternatively serialize the coarse syscall/ffi class until Phase 6/resource groups. Also require cleanup of remaining group members after the leader exits, while documenting that this is lifecycle hygiene rather than a sandbox.

4. Runner control data is currently visible through the supposedly pinned args/env channels

The runtime captures the loader-provided argc/argv/envp before main and exposes them unchanged through std.env (crates/rue-runtime/src/process.rs:1-46,123-151). Therefore a dispatched process invoked as image --run <id> exposes at least the real image path, the internal selector, and test ID to the test body. The proposed RUE_TEST_* environment also includes a fresh scratch-directory path. Those values can vary while the body closure does not.

Yet §4.1 treats args and env as hermetic-compatible, and §5 mentions the environment “pin set” without saying whether exact ordered values are fingerprinted. If the fresh path/real argv values enter the key, routine cache hits disappear; if only the variable names/policy enter it, cached verdicts can be stale.

Please define the test-visible process inventory separately from runner plumbing:

  • stdin is a fixed EOF stream unless an explicit future input is part of the test identity/key;
  • argv has fixed, documented values (including argv[0]);
  • env has fixed ordering and exact values, with a stable logical scratch value such as .;
  • the dispatcher selector travels out-of-band or the runtime replaces its captured inventory before invoking the test; and
  • the exact visible values, not merely the allowlist names, participate in the fingerprint.

This also makes tests of std.env.args() meaningful instead of testing an incidental internal protocol.

5. The NDJSON schema needs a byte and output-budget policy in Phase 2

Rue strings may contain arbitrary non-UTF-8 bytes, and the runtime writes their raw bytes to stdout. A JSON test_finished record therefore cannot simply contain “captured stdout/stderr” as strings. The ADR also adopts the unbounded run_with_timeout mechanics unless it explicitly selects the existing limited variant; a fast writer can consume large runner memory before the wall timeout, and putting all pass output into one NDJSON line is particularly unfriendly to the agent-first goal.

The v1 schema should pin:

  • lossless encoding (for example UTF-8 when valid, otherwise base64, with an explicit encoding tag);
  • per-stream capture limits and the overflow verdict/policy;
  • whether successful-test output is inline, truncated, digest-only, or stored out-of-band; and
  • separation of the framed failure channel from user-output limits.

6. The out-of-closure warning creates a second source-discovery path

ADR-0063 deliberately makes imports lazy and requires the host to perform only compiler-produced, policy-bounded demands. Walking the root directory tree, parsing every candidate enough to recognize test items, and comparing it with the import closure is a second discovery computation over files the canonical snapshot did not demand. The foreign-root/parse-failure complications in §1 are symptoms of that mismatch.

I would make this warning complete only when --source-manifest supplies an explicit candidate inventory, implemented as a canonical compiler query over that inventory. Without a manifest, either omit the warning with guidance or define a new host-input/directory-inventory protocol explicitly; a driver-side recursive scan should not become a peer frontend.

Bottom line

The language-item/root-set design, stream-first API, eject-don't-degrade posture, and phased delivery still look like the right foundation. Findings 1–4 are the ones I would disposition before accepting the ADR; 5–6 should at least be made explicit Phase 2 obligations. CI is green at this head.

Copy link
Copy Markdown
Contributor Author

Round 3 addressed in 1bf587f (+278/−105). Per this PR's custom I re-verified each finding against source before editing; all six held. Disposition:

1. SCC coordinator incrementality — accepted, blocking, and the split adopted essentially as proposed. Confirmed against ADR-0063 §8's own wording: the reachability evaluator re-derives from the roots even when it republishes unchanged memberships — projections buy downstream cutoff, never evaluator-internal skipping — so the round-1 coordinator was narrow invalidation without narrow recomputation, exactly as you put it. §4.2 now specifies the four-piece split: EffectGraph(RootSet) observes only BodyReferences projections (green under leaf-only edits) and publishes per-component projections under content-derived keys (sorted member identities, so surviving components keep their keys across re-derivations); a body-local leaf projection reads exactly one canonical body; ComponentEffect(SccKey) joins member leaves plus callee-component summaries along the acyclic condensation — ordinary query dependencies, source recursion cannot re-enter as a cycle; per-function summary projections ride on top. The warm-economics claim is restated as mechanism-backed for leaf-only edits, and the honest residual is stated: reference-changing edits pay an EffectGraph re-derivation in §8's baseline economics, with §8's own escape hatch (incremental SCC maintenance behind the same query contract) if measurement demands it. The Phase 3 gate now measures leaf-only and reference-changing edits separately.

2. Shared image vs per-test key — accepted as blocking, dispositioned with a fifth option. The derivation held on re-check (@alloc/@alloc_zeroed/@realloc null returns are defined values, spec 9.2; the image is whole-selection per target; pinned RLIMIT_AS minus a selection-dependent image is a selection-dependent heap). Rather than the four options listed, §4.1 now recommends making the observation itself deterministic: the heap is Rue's own recycling allocator over raw page mapping, with an allocation_permitted seam at exactly this chokepoint today (crates/rue-runtime/src/heap.rs), so test builds enforce a fixed allocation budget in the allocator — @alloc-family results become a deterministic function of the test's own allocation sequence, independent of image size and machine state. RLIMIT_AS survives only as a generous out-of-key backstop sized so the budget always fails first; hitting it anyway is an infrastructure verdict, never cached. Your four options are recorded as the rejected shapes, the cost is stated in Consequences (a real test/production divergence at the budget boundary, next to seedable @random_*), and this is now a maintainer call gating Phase 4 (replacing the old "resource-limit pinning posture" call). The Phase 4 key audit keeps image identity as its named hard case: irrelevant-by-proof is the expected outcome, and the audit must demonstrate the proof, not assume it.

3. Isolation contract — accepted. Confirmed the mechanics: group SIGKILL fires on timeout and output overflow only; the normal-exit path returns after a bounded 500 ms drain finish precisely because a descendant can retain a pipe fd; nothing stops setsid. §3's contract now guarantees fresh process state, captured/attributed stdio, independent lifecycle, and best-effort process-tree cleanup for every test, with noninterference explicitly scoped to verified-hermetic tests and the syscall/ffi interference channels named (signals, shared absolute paths, ports, surviving descendants). The MVP keeps parallel-by-default for unverified tests as a stated pragmatic default rather than a guarantee — --jobs 1 today, Phase 5 groups, platform sandbox eventually, and never silent inference-driven serialization. Post-exit group SIGKILL is added as required lifecycle hygiene, documented as exactly that and not containment.

4. args/env pinning — accepted. Confirmed at process.rs:1-46,123-151 exactly as cited. §3 now defines the test-visible inventory apart from runner plumbing: fixed documented argv (stable logical argv[0], no selector, no image path), fixed ordered exact KEY=VALUE environment with stable logical RUE_TEST_* values, scratch always spelled . (the fresh private working directory), stdin a fixed EOF stream unless a future explicit input joins test identity. The dispatcher consumes the selector and replaces the runtime's captured inventory before invoking the body — which is also what makes tests of std.env.args() meaningful, as you noted. The exact visible values, not allowlist names, participate in the fingerprint; §5's key composition, §4.1's hermetic parenthetical, and §6 are updated to match.

5. NDJSON bytes and budgets — accepted as pinned Phase 2 obligations. New §2 bullet: explicit encoding tag (UTF-8 when the bytes validate, base64 otherwise; lossless within the retained window); capture bounded per stream as bytes arrive by adopting the existing limited-drain variant (run_with_timeout_and_output_limit), explicitly not the unbounded one; overflow kills the group and yields a fail verdict with new failure kind output_overflow, retained prefix attached; pass events carry digests and byte counts only (flag to opt into inline), failures carry the capture inline. §7.1 now requires the structured failure channel to be framed and budgeted separately — extracted before user-stream limits apply, with its own cap — so a stream-flooding test cannot truncate its own failure record.

6. Out-of-closure warning — accepted. The warning is manifest-gated: complete only when --source-manifest supplies the candidate inventory, implemented as a canonical compiler query over those already-host-visible inputs, with out-of-closure parse failures reported inside the warning itself. Without a manifest there is no scan and no warning — the run summary carries a one-line notice that orphan detection needs a manifest. The foreign-root and per-directory-opt-out complications from round 2 are gone along with the directory walk that caused them.

This head is a docs-only delta over a9fe199. The pre-ratification maintainer-call queue is unchanged except that the allocation-budget disposition replaces the resource-limit pinning call and joins the §4.1 @ptr_to_int call as a Phase 3/4 gate.


Generated by Claude Code

@DorianListens
DorianListens force-pushed the claude/rue-test-runner-proposal-qouv7j branch from 1bf587f to a989430 Compare August 12, 2026 01:30
@DorianListens DorianListens changed the title ADR-0070: agent-first test runner on the query graph ADR-0071: agent-first test runner on the query graph Aug 12, 2026
@steveklabnik

Copy link
Copy Markdown
Collaborator

Follow-up review of a989430

I re-read the complete ADR-0071 draft, every PR comment and disposition, the current RUE-506/RUE-967 discussion, and the relevant query/runtime/loader code. The round-3 rewrite genuinely addresses all six findings from a9fe199. The new mechanisms expose three remaining Phase 3/4 blockers, plus several narrower contract issues.

1. The allocation budget still cannot distinguish policy failure from ambient mapping failure (blocking Phase 4)

The budget is a good direction, but the current seam does not establish the claimed outcome:

a test that hits the backstop anyway is reported as an infrastructure defect, never as a cached or cacheable verdict

Today both paths collapse to the same null pointer:

  • Allocator::allocate returns null when PageMapper::allocation_permitted() rejects the attempt.
  • A permitted direct allocation returns M::map(mapping_size), which also returns null on ambient mapping failure.
  • The runtime mapper simply forwards platform::mmap.

See crates/rue-allocator/src/lib.rs:46-61,149-165 and crates/rue-runtime/src/heap.rs:47-67. A test can branch on that null and exit 0, so the runner cannot infer afterward whether it observed the deterministic budget or an ambient OS failure. An epilogue-only check is also insufficient because std.exit(0) is an accepted blind spot.

The budget contract also needs units/accounting. The existing hook is zero-argument and runs once per allocation attempt, before layout classification; it can enforce an attempt count, not a byte/high-water/page budget. One permitted enormous mapping can still fail ambiently.

Please require one of:

  • reserve/commit the whole permitted arena up front so every in-budget allocation is guaranteed from runtime-owned storage;
  • make the mapper report a typed failure cause through a dedicated runner channel at the failure point; or
  • mark any test that encountered an ambient mapper failure non-cacheable/infrastructure-failed through equivalent unspoofed telemetry.

Then pin whether the budget counts requested bytes, mapped pages, live bytes, high-water bytes, or attempts. Until that exists, a passing verdict after ambient allocation failure can still enter the cache under an unchanged fingerprint.

2. Verdict-changing runner policy is missing from the cache key (blocking Phase 4)

§5's key includes target, opt level, visible inventory, allocator budget, stack limit, seed policy, and link inputs, but omits at least:

  • --timeout-ms;
  • the per-stream output limit; and
  • any future per-test timeout override.

A test cached after passing under 10 seconds must not become cached_pass when invoked with a 1 ms timeout. Likewise, output that passed under a large capture bound may be an output_overflow failure under a smaller one. These are runner verdict inputs, not presentation-only settings, so they must join the fingerprint (or changing them must force execution).

There is a related presentation hole: pass events retain only digests and byte counts, but §2 promises a flag that opts passing output into inline capture. A cached pass cannot satisfy that request from the stored metadata. Please specify that the flag either forces re-execution, stores/retrieves the retained bytes as a separate cache artifact, or emits an explicit “unavailable for cached pass” state. Silent omission would make the machine and human surfaces depend on whether a cache happened to be warm.

3. EffectGraph needs more inputs, and SccKey needs the semantic configuration (blocking Phase 3)

The four-piece split fixes leaf-only incrementality, but this sentence is not implementable as stated:

EffectGraph(RootSet) observes only the reached set's BodyReferences projections … and computes SCC membership … over the call/drop-glue edges

A BodyReference::DropGlue(TypeInstanceKey) is not a callable edge. Current reachability must query the DropGlue(TypeQueryKey) family, recursively traverse glue.nested, and schedule glue.destructor before the destructor body becomes reachable (body_query.rs:267-275; revisioned_query_database.rs:16051-16103). To attribute a destructor edge to the body that may destroy the type, EffectGraph must observe per-type drop-glue facts or a canonical per-body expanded-edge projection. The aggregate reached set alone loses that origin.

Also, a component key cannot be only the sorted member identities. References and canonical leaves are configuration/target dependent; current BodyQueryKey includes SemanticQueryConfiguration in equality and hashing for exactly this reason (body_query.rs:192-226). Two target/configuration requests can have the same SCC members but different outgoing edges or leaves. Key ComponentEffect by at least the semantic configuration plus the sorted members (or by a typed component identity projected from the configured EffectGraph), and state which projection it observes for edge changes.

4. Replacing the test-visible inventory does not yet pin the loader-visible inventory

The dispatcher replacement correctly prevents std.env from seeing the image path and selector, but the real loader argv/env have already occupied the initial process stack before main; process::capture records them at entry (crates/rue-runtime/src/process.rs:1-45). Their byte size therefore remains part of actual stack consumption even after the runtime pointers are replaced.

That matters because the ADR treats pinned RLIMIT_STACK as enough to make stack behavior deterministic while deliberately excluding image identity. With the proposed image --run <id> launch, the default real argv[0] is the varying image path. A near-limit recursive test can consequently cross the stack boundary without any keyed input changing.

Pin the loader environment and argv[0] too, and use a fixed-size selector transport (or key the real control inventory). On Unix, setting a constant arg0 and passing a fixed-width selector/index would likely suffice; an inherited control fd is another clean option. The later test-visible replacement is still useful, but it is not by itself a resource-determinism boundary.

5. The manifest-gated orphan check still needs a new host-input publication path

The bounded design is right, but “manifest entries are already host-visible inputs, so the check rides revisions like everything else” overstates current infrastructure. SourceManifest::load canonicalizes entries into permission sets; it does not read their bytes into SourceSnapshot. The snapshot contains the root/import-demanded sources only (crates/rue/src/source_loader.rs:39-114,644-675).

A compiler query cannot parse an out-of-closure manifest entry until the host explicitly reads and publishes it. Phase 2 therefore needs a bounded candidate-acquisition protocol (including content fingerprints and absent/unreadable outcomes) plus a parse-only query that does not turn candidates into semantic roots. Please say that explicitly; otherwise “canonical query over the inventory” risks hiding a driver-side read/query side table—the peer path the rewrite is trying to avoid.

6. Separately budgeted structured failures effectively rule out ambiguous stderr framing

Rue stdout/stderr are arbitrary byte streams. A “reserved framed region of stderr” can be accidentally reproduced by user bytes, and extracting frames before the user-output limit cannot provide a genuinely separate budget unless the framing is escaped/authenticated or carried out of band.

The clean contract is a dedicated inherited pipe/fd written through the runtime helper, with its own drain and cap. This is not a security boundary—unchecked code can still target fds—but it prevents ordinary output from being parsed as protocol and makes the separate-budget promise concrete. If stderr framing remains a maintainer option, the ADR should specify the collision/escaping rule that makes arbitrary bytes unambiguous.

7. Phase 2/3 schema sequencing needs one explicit state

The event sketch and --list contract say they include capability summaries, while Phase 2 is explicitly “zero capability claims” and Phase 3 first surfaces summaries. Pin the v1 representation now: e.g. capability_summary: { "status": "unavailable" } until Phase 3, later replaced by a versioned value. That avoids either contradicting the MVP or retrofitting an ambiguous absent field.

8. PR-level housekeeping

The PR body still describes the superseded single EffectSummary/bottom-up-over-BodyReferences design, omits the new Phase 3/4 gates, and ends with generated-tool attribution. Please refresh it for ADR-0071 and remove the attribution per the repository's tool-neutral PR-text rule.

Bottom line

The core design remains compelling, and the round-3 changes materially improved it. I would disposition 1–3 before ratification because they affect cache/query correctness; 4 is part of the same image-independence proof. Findings 5–7 should become explicit Phase 2 obligations. CI is green at a989430.

@DorianListens
DorianListens force-pushed the claude/rue-test-runner-proposal-qouv7j branch from 31556f0 to ddf671e Compare August 12, 2026 12:47

Copy link
Copy Markdown
Contributor Author

Round 4 addressed in ddf671e (+285/−105, docs-only). Per this PR's custom I re-verified each finding against source before editing; all eight held — including the exact null collapse in Allocator::allocate (the permit hook and a permitted direct M::map failure return the same null, and the hook runs zero-argument once per attempt before layout classification), the DropGlue(TypeInstanceKey) reference expanding only through the DropGlue(TypeQueryKey) family with glue.nested traversal and glue.destructor scheduling, SemanticQueryConfiguration in BodyQueryKey equality and hashing, process::capture recording loader vectors that already occupy the initial stack, and SourceManifest::load canonicalizing entries into permission sets without reading their bytes. Disposition:

1. Budget vs ambient mapping failure — accepted as blocking, dispositioned with your first option. §4.1 now specifies reservation, not rejection: the test-build budgeted page mapper reserves the entire permitted arena from the OS at process startup, before the body runs; every subsequent map carves page ranges from that runtime-owned arena and every unmap returns them, with no ambient mapping syscalls after startup. An in-budget allocation cannot fail ambiently (its storage is already reserved), an over-budget allocation fails by policy, and a null observed by the body means exactly "over budget." The one remaining ambient failure point is the startup reservation itself, which precedes the body and reports through a pinned pre-body protocol as infrastructure — and needs no authentication, because mimicking that report can only waste a re-run, never mint a cached pass. Units are pinned with the mechanism: bytes rounded to whole pages, accounted as arena carve-outs (small arenas retained at high water per the allocator's existing recycling design; direct mappings carve and return) — explicitly not an attempt count, with the zero-argument hook named as evidence the chokepoint exists rather than as the mechanism. One overcommit note is recorded: reserved address space can still surface ambient pressure while touching pages, but as a kill, never an in-budget null — kills are failures, failures are never cached, so soundness is unaffected and only reliability is exposed (pre-faulting is the available hardening). Your options 2 and 3 join the rejected shapes as detect-don't-remove: sound, but they leave the verdict machine-dependent and convert detection into re-runs where reservation keeps it deterministic and cacheable.

2. Runner policy in the cache key — accepted as blocking. §5's key now includes every runner policy that participates in verdict determination — the effective per-test timeout (--timeout-ms, and any per-test override the moment one exists) and the per-stream output limits, which decide timeout and output_overflow verdicts — with the dividing line stated: whether a setting can change a verdict, not where it is spelled. Two monotone relaxations are permitted because stored metadata proves them: a pass under timeout T is valid under any effective timeout ≥ T (pass is a semantic verdict; wall clock was never fingerprintable), and a pass recorded with per-stream byte counts is valid under any limit ≥ those counts — the verdict metadata now stores per-stream byte counts to make that checkable per entry. The presentation hole closes with the first of your three options: requesting inline pass output forces execution for tests whose cached entries hold only digests; storing pass output as a separate cache artifact is a permitted later refinement; silent omission on a warm cache is named as the rejected shape. Phase 4's bullet and the key-audit spike enumerate the new inputs.

3. Edge projections and configured keys — accepted as blocking. The split gains an explicit first stage: a body-local edge projection resolves one body's BodyReferences into its outgoing effect-graph edges, and that is where drop glue becomes a real edge — the ADR now states plainly that a DropGlue reference names the destroyed type, not a callee, and that the destructor edge exists only after expansion through the per-type drop-glue facts family (nested traversal, destructor collection), exactly as reachability performs it today; a change to one type's glue re-runs exactly the edge projections of bodies that can destroy it. EffectGraph observes only edge projections — never canonical bodies, never raw references. And every family in the split is configuration-keyed: the edge and leaf projections adopt the (instance, configuration) shape of the in-tree body family, EffectGraph is root set plus configuration, and SccKey is the configuration plus the sorted member identities, with your reasoning recorded — member identities alone are not a semantic address, and two configurations can condense the same member set with different edges and leaves. ComponentEffect's observed inputs are stated explicitly (members' leaf projections plus the configured EffectGraph's per-component projection, which is how edge changes reach it). Phase 3 carries the drop-glue expansion with its own unit coverage.

4. Loader-visible inventory — accepted. §3 now separates the two boundaries by name: the dispatcher replacement is the visibility boundary and not a resource boundary, because the loader lays the real argv and environment strings on the initial stack before main and the runtime captures exactly those vectors. The loader-visible inventory is pinned: constant argv[0], a fixed-width index-shaped selector, exactly the pinned environment vector at exec time, and a run-constant image path spelling (a constant-named link in the per-test working directory) so loader-injected path strings — AT_EXECFN and its macOS analogue live on the same stack — are constant bytes too, with the remaining auxv entries fixed-size by platform contract. Initial-stack consumption is then deterministic per keyed configuration, which is what makes pinned RLIMIT_STACK a determinism boundary rather than a bound over a varying baseline; the near-limit-recursion failure you described is cited as the motivating shape. The inherited control fd is recorded as the alternative selector transport, the loader-visible constants join the §5 key, and with the environment pinned at exec the dispatcher's replacement reduces to argv normalization.

5. Candidate acquisition — accepted. The §1 bullet no longer claims the check rides existing revisions; the publication step is stated as honestly new: the manifest's own bytes are host-visible today, but loading canonicalizes entries into permission sets without reading them, so an out-of-closure entry's content never reaches the snapshot. Phase 2 adds a bounded candidate-acquisition step to the ADR-0063 host input protocol — entry bytes plus content fingerprint, or typed absent/unreadable outcomes, published as ordinary revisioned inputs demanded only by test requests — and the orphan check is a parse-only query over those candidates that never turns one into a semantic root. Absent entries stay silent, matching the loader's own "a manifest grants an operation, not a claim that the candidate exists" posture; unreadable entries report inside the warning. The bullet also names why the protocol must be explicit: without it, "canonical query over the inventory" would quietly become the driver-side read and side table this design is trying to avoid.

6. Dedicated pipe — accepted; the recommendation flips. §7.1 now recommends the dedicated inherited pipe — its own fd pinned in the §3 exec contract, written through a runtime helper, drained by the runner with its own cap — and records stderr framing as the rejected shape with your reasoning: Rue streams are arbitrary bytes, user output can reproduce any in-band framing byte-for-byte, and a separate budget extracted from a shared capped stream is separate in name only, so making framing unambiguous would cost an escaping/authentication rule that buys nothing over a second pipe. Stated as not a security boundary (accidental-collision prevention is exactly §2's promise), and noted as the natural carrier for the §3 epilogue sentinel and §7.2 sub-results. It remains a maintainer call solely because the helper is an ABI-manifest addition under ADR-0055 rules.

7. Schema sequencing — accepted. capability_summary is present from event-schema v1.0 with an explicit status discriminator: {"status": "unavailable"} throughout Phase 2, replaced by the populated available form when Phase 3 lands — an additive change inside a field consumers already handle, not a retrofitted optional. --list carries the same state, so neither surface contradicts the MVP's zero-claims posture. Pinned in §2 and in both phase bullets.

8. Housekeeping — done. The PR body is rewritten against the current ADR (the five-piece split, the budgeted mapper, both pinned inventories, the runner-policy key inputs, candidate acquisition, the dedicated pipe, and the Phase 3/4 gates called out under "Ratification gates"), and the attribution line is removed per the tool-neutral PR-text rule.

The pre-ratification queue now reads: RUE-967 gates Phase 3; the allocation-determinism call (restated as the reservation mechanism) and the verdict-cache key audit gate Phase 4; findings 5–7 are pinned as Phase 2 obligations in the phase plan.


Generated by Claude Code

@DorianListens
DorianListens force-pushed the claude/rue-test-runner-proposal-qouv7j branch from c998b58 to 7b6ac45 Compare August 13, 2026 03:52
@DorianListens DorianListens changed the title ADR-0071: agent-first test runner on the query graph ADR-0072: agent-first test runner on the query graph Aug 13, 2026
@DorianListens DorianListens changed the title ADR-0072: agent-first test runner on the query graph ADR-0073: agent-first test runner on the query graph Aug 13, 2026
@DorianListens
DorianListens marked this pull request as ready for review August 13, 2026 04:54

@steveklabnik steveklabnik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of d93736a: I think two design issues still need resolution before this is ready.

  1. The orphan-test guarantee does not hold with the canonical generated source manifest. Candidate acquisition treats --source-manifest as the inventory of files declared to the build and scans entries outside the root import closure. But the rue_program manifest is derived from accepted reads, absent observations, and every declared std file; it is not derived from declared srcs. A newly declared but unimported foo_tests.rue therefore is not present in that manifest, so the runner cannot diagnose the exact orphan-file mistake this design promises to catch. Conversely, every std file is present, which conflates toolchain inputs with root-owned test candidates. I think this needs a separate declared-candidate inventory, or an explicit test/build-rule contract that produces and passes one. The existing generated source manifest should not be described as that inventory.

  2. The new unwrap-and-report behavior for ? skips ordinary Rue cleanup. Normal ? lowers to an early return, and CFG construction emits drops for all live bindings on return. Reporting and then trapping at the ? site bypasses those drops, so early return does provide something the trap does not: deterministic destructor/resource cleanup. A compiler-internal uniform outcome/failure continuation seems able to preserve the source-visible () type, heterogeneous per-site error rendering, and exact-site reporting while still following normal return/drop elaboration. If trap semantics are intentional, the ADR should explicitly accept skipped destructors here rather than claim that early return gains nothing.

Two smaller points:

  • The synthesized structural printer is specified as monomorphized per ? site, but its behavior depends on the error type while the site belongs in the failure record/header. Sharing one printer or formatting plan per error type would avoid duplicating code and CodegenUnits for repeated sites.
  • The statement that leaf projection becomes the first production consumer of the retained canonical_bodies family is stale at this PR's base: analysis_bundle already queries that family. Phase 3 adds another consumer/projection; it does not introduce the first production retention.

I otherwise found the renumbering and index update consistent, and the current checks are green.

@steveklabnik steveklabnik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by Fable (Claude agent), at Steve's request. This is a grounding-and-consistency pass: since the ADR is explicitly staged for iteration, I checked its factual claims against current source rather than opining on taste.

Overall: this is an unusually well-grounded design document. Nearly every load-bearing claim verifies to the exact number: the 46-helper ABI manifest, the 11 value-taking driver flags, E0503/E0505 rejecting ? in () bodies (so unwrap-and-report is genuinely additive), abort-only exit 101, no clock API anywhere, DropGlue body references naming a type rather than a callee, the allocator's zero-argument permit hook running before layout classification, the 10s default timeout / process-group kill / ICE detection in rue-test-runner, and the ADR-0063 sections as cited. The §4.2 incremental-inference design and §3's loader-visible/test-visible inventory split are the strongest sections; the orphan-detection design correctly honors ADR-0063 §7's "the host may not invent demand candidates." Nothing below undermines the core architecture.

Two substantive findings are inline (the §4.1 addr census is misattributed and hides a fourth, syscall-argument idiom — the one materially flawed piece of evidence in the document; and Phase 5's @group("name") needs grammar work the ADR itself defers). The rest, ranked:

  1. §7.1 failure-channel helper is never classified in the capability lattice. The ADR's own standing rule says an unclassified manifest leaf is a soundness hole in every cached verdict. Trap-is-the-verdict covers @assert, but §7.2 sub-results are writes from a running, possibly passing test — a real output channel. The presumably intended answer (hermetic-compatible like stdout: runner-pinned, captured, budget in the cache key) should be stated, especially since the helper ships in Phase 2 and the machine check only lands in Phase 3.
  2. "First production consumer of the retained canonical-bodies family" is contradicted by current sourceCfg query values hold and charge the canonical body today (crates/rue-compiler/src/cfg_query.rs:56, :667). The retention-pricing concern is real only for --list-shaped requests; narrow the sentence to that case.
  3. compile_error verdicts duplicate diagnostics across two guaranteed surfaces. §2 keeps compiler diagnostics on stderr "exactly as today" while Phase 4 embeds them in stdout test_finished events; docs/process/diagnostics.md guarantees stderr exclusivity and deterministic batch ordering pinned by CLI cases. Which copy is authoritative, and whether diagnostics.md needs a test-mode note, is unaddressed.
  4. "Seed policy" sits in the §5 cache key against the key's own admission rule — §6 says the seed feeds only shuffle and scratch naming in the MVP, neither verdict-determining. Define it as the future seeded-@random_* mode toggle or drop it from the key.
  5. Three Open Questions gaps a maintainer would want listed: whether --filter narrows the root set (as written, a broken unselected test's closure fails a filtered run, and per-test compile_error exit codes are unspecified); the skipped verdict has no producing mechanism in any phase; and rue test implicitly enabling the preview gate would be the first flag to auto-enable a preview feature — 0005's model is explicit opt-in, and the maintainer-calls list covers only the flag's name, not this behavior.
  6. Cosmetic: the Status section calls ADR-0061 "implemented"; its frontmatter and the README say Accepted.

On the highest-stakes calls, I'd point the deciding maintainer at: analysis-only vs. typed capabilities (§4.4 — the one that gets expensive if trait design later assumes untyped effects), the budgeted page mapper (the sole accepted test/production divergence, gating Phase 4's cache soundness — and its permit-hook-inadequacy claim does verify against the allocator source), and forcing RUE-967 now, whose supporting census is finding 1 — the migration-audit spike belongs before ratifying §4.1's disposition text.


Generated by Claude Code

(`@int_to_ptr(@ptr_to_int(p) + off)`, the `StrBuf` byte-copy path), and
pointee type-punning (documented in `std/rawbuf.rue` as the sanctioned
cast idiom). Forty-two such sites sit under `StrBuf`, `RawBuf`,
`ArrayBuf`, `mem.swap`, `sort`, and `binary_heap`; a bit on the bare

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The census here is misattributed, and the misattribution hides a fourth idiom that weakens the §4.1 disposition argument. Forty-two is the whole-std count at this branch's tree, but the distribution is: fs.rue 18, net.rue 13, rawbuf.rue 5, strbuf.rue 3, mem.rue 2, c.rue 1 — and arraybuf.rue, sort.rue, and binary_heap.rue contain zero sites (they reach RawBuf's transitively). (Trunk has since grown to 46; fs is 22 there.)

The ~31 fs/net/c sites are a fourth idiom the three-idiom taxonomy (null test / rebase / type-pun) doesn't name: address-into-@syscall argument (std/fs.rue:685, std/net.rue:360 on this branch). Hermeticity is untouched — those functions are syscall-ejected regardless — but it falsifies the claim below that "after the split, a surviving @ptr_to_int is rare and means exactly 'observe the address'": after the split, std would retain ~30 syscall-argument sites unless RUE-967 also gives that idiom a home. The migration-audit spike in Open Questions would discover this, which suggests running that spike before ratifying this section's disposition text rather than after.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and corrected — and the recount turned up two things beyond the misattribution.

Re-counting on trunk: 45 real sites, not 46. One of the 46 raw matches is prose — std/rawbuf.rue:15, the header comment describing the @int_to_ptr(@ptr_to_int(..)) cast idiom. The ADR had been carrying the grep count as a site count.

Your per-file distribution is exactly right (fs 22, net 13, rawbuf 4 real, strbuf 3, mem 2, c 1; arraybuf/sort/binary_heap zero). The §4.1 sentence named three files with zero sites while omitting the two holding three quarters of them, so it is replaced with the real distribution.

On the fourth idiom: it is real and it is now named, but 21 sites, not ~31. The fs/net/c files hold 36 sites, of which 15 are ordinary null tests the existing taxonomy already covers; the address-into-@syscall-argument idiom is the other 21. Full breakdown now in §4.1: 17 null tests, 21 syscall-argument, 7 rebase/type-pun.

The substance of your point survives the smaller number intact, and I took it as the load-bearing finding rather than a census nit. As you say, hermeticity is untouched — those functions are syscall-ejected regardless — so what the idiom falsifies is the ergonomic claim, and the ADR now says so directly: covering only casts and null tests leaves ~20 sites standing, so a surviving @ptr_to_int would still not mean "observe the address." That makes RUE-967's scope the maintainer call, not merely its resolution, and the migration-audit spike moved ahead of ratification for exactly the reason you give — the spike decides whether the split can deliver the syntactic leaf at all. The spike text also now says to re-run the census at spike time, since fs grew 18→22 during this review.

images); cache-soundness audit checklist executed against the spike
findings, including the image-independence demonstration (§5).
- [ ] **Phase 5: scheduling and flake policy** - RUE-TBD. Declared serial
groups (`@group("name")` directive) honored by the scheduler;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase 5 commits @group("name") — a string-literal directive argument — but the deferred-questions section (line ~1633) correctly states directive arguments are identifier-only today (DirectiveArg::Ident is the sole variant in crates/rue-parser/src/ast.rs) and prices literal-argument directives as "a grammar and AST extension, not merely a scheduling choice," which is why @timeout(5000) and @known_bug("RUE-NN") are deferred with that cost stated. Phase 5 as written needs the same extension those deferrals price. Either Phase 5 is under-scoped (it silently carries the grammar work) or @group needs an identifier spelling (@group(slow)), matching @requires(fs).


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right on both the diagnosis and the fix. Took the identifier spelling: @group(name), matching @requires(fs).

Verified DirectiveArg::Ident is still the sole variant (crates/rue-parser/src/ast.rs:73-76), so the identifier form parses today and the literal form does not. Phase 5 was under-scoped rather than deliberately carrying the grammar work — it would have silently absorbed exactly the extension that @timeout(5000) and @known_bug("RUE-NN") are explicitly deferred for, which would have left the ADR pricing the same work in two places and paying for it in neither.

Phase 5 now states the constraint inline, and the naming call notes generally that any literal-argument spelling pulls the deferred directive-grammar extension into its phase while the identifier form is free. Group names lose nothing by being identifiers — they are scheduler keys, not prose.

claude added 12 commits August 15, 2026 14:00
Design proposal for rue test: test declarations as language items,
a versioned NDJSON event stream, process-per-test execution behind a
mechanism-independent contract, capability summaries inferred over the
ADR-0063 query graph, and hermetic-only verdict caching and selection.
Includes a phased plan with an MVP ahead of any capability tracking,
plus explicit maintainer calls and spike list for iteration.
Expand the framework-protocol seam into a tiered extensibility decision:
the structured failure channel as a protocol userland assertion libraries
share with @Assert, reserved identity extension to comptime-instantiated
test items and sub-results, stream consumers, and replacement runners /
external providers over documented contracts. Adds the failure-channel
mechanism maintainer call and the reserved @src()/comptime-items deferred
questions.
Add a standing 'Constraints on future language evolution' section: the
capability-classification rule for new features, per-direction obligations
for runtime polymorphism, std/ABI growth, concurrency, separate
compilation, comptime inputs, failure-model changes, and opaque code
mechanisms — all framed as sound degradation (eject, never unsound).
Adds the process-hook maintainer call and cross-references.
State that EffectSummary is demanded only by test requests, constrain the
two cost-leak paths (no eager per-body effect recording in sema, no
default speculation), and add a zero-delta executable-benchmark gate to
Phase 3 acceptance alongside the warm-cost gate.
Add a Rejected alternatives entry explaining why assertions trap rather
than return Results (ADR-0038 bug/error split, propagation tax, no
meaningful handler, abort-only non-local exit) and what the intrinsic
form buys (call-site attribution without backtraces, comptime folding,
optimizer facts, pinnable contract), with the recovered trade-offs.
Blocking findings: add the addr capability bit (@ptr_to_int observes
ASLR-placed addresses with no syscall), extend the hermetic carve-out to
memory-pressure observability with pinned rlimits in the contract and
cache key, adopt the UB-free caching posture with an uninitialized-reads
maintainer call; respecify EffectSummary as an SCC-condensation
coordinator with per-identity stamped projections (the per-function
callee-summary shape is ADR-0063's rejected query form), correct the
leaf location to canonical bodies, and record drop-glue edges and the
canonical-bodies retention cost.

Agent-first gaps: new Phase 2.5 pulls structured assertion payloads
ahead of capability work; reserve the promotion payload and a future
accept verb; reserve the inverse tests-reaching-item query; add
test_started.

Corrections: @syscall checked-gating claim (RUE-1369 filed; RUE-1370
for the signal-handler spec sentence), directory-scoped visibility and
same-directory test files, unimported-test-file warning, the honest
warnings-scan decision, value-aware subcommand dispatch, first
contextual keyword sizing, StableDefinitionKind Test kind, harness
attribution fixes, exit-row removal, dispatcher exclusion, FFI-narrowing
cacheability floor, identifier-only directive arguments, conceptual
query naming, ADR citation fixes, and prior-art updates (Unison, Nim
effectsOf, HyRTS, CTRF, nextest/Zig/Swift/Deno/Go current state).
Round-2 review: the round-1 addr bit overshoots — std holds 42
@ptr_to_int sites, all deterministic idioms (null test, provenance-
preserving rebase, type-pun), so a bare-intrinsic bit would eject nearly
every real test. Recommend resolving RUE-967's strict-provenance split
with this ADR as its first forcing consumer, record the escape-scoped
recognizer as the fallback, gate Phase 3 on the disposition, and add
the migration-audit spike (including the stale @ptr_offset stride
comment check). Also: unimported-test-file warning edge handling
(out-of-closure parse failures, foreign-root files) and Future Work
adapter harmonization.
Blocking findings: split the effect computation into EffectGraph (SCC
condensation over BodyReferences only, content-derived component keys),
body-local leaf projections, ComponentEffect joins along the acyclic
condensation, and per-function summary projections — the single
coordinator gave narrow invalidation without narrow recomputation and
could not support the claimed warm-edit economics. Replace pinned-rlimit
allocation determinism with a test-build allocator budget: the shared
image makes RLIMIT_AS headroom vary with unrelated selection changes, so
a test observing @alloc null near the limit could flip verdicts under an
unchanged closure fingerprint; RLIMIT_AS survives as an out-of-key
backstop whose hit is an infrastructure verdict, and the budget is a
maintainer call gating Phase 4. Scope the noninterference clause of the
execution contract to verified-hermetic tests (process groups are not
containment — setsid escapes; syscall/ffi tests can interfere through
the OS), adding post-exit group cleanup as stated lifecycle hygiene.
Define the test-visible process inventory (fixed argv, ordered exact
env, stdin EOF, scratch spelled ".") apart from runner plumbing, with
the dispatcher replacing the runtime captured argv/env before invoking
the body and the exact visible values in the cache key.

Phase 2 obligations: byte-safe output encoding (UTF-8-else-base64 with
an encoding tag), per-stream capture budgets via the limited-drain
variant with an output_overflow failure kind and pass/fail payload
asymmetry, the framed failure channel budgeted separately from user
output, and the unimported-test-file warning manifest-gated (a
canonical query over the --source-manifest inventory; no driver-side
directory walk).
ADR-0070 is now the independently landed Rue program build actions
record; the test-runner proposal takes the next free number.
Blocking findings: replace the allocation budget's permit/deny posture
with a budgeted page mapper that reserves the entire permitted arena at
startup — policy denial and ambient mapping failure previously collapsed
into the same test-observable null, so a body branching on it could turn
a machine artifact into a cacheable pass; with reservation, in-budget
allocations cannot fail ambiently, over-budget fails by policy, the
budget is denominated in bytes rounded to pages with carve/return
accounting (not attempts), and the only ambient failure point precedes
the body and reports as infrastructure. Add every verdict-determining
runner policy to the cache key (effective timeout, per-stream output
limits) with provable monotone relaxations, and pin the inline-pass-
capture flag to force execution rather than depend on cache warmth.
Rework the effect-query split around body-local edge projections: a
DropGlue body reference names a type, not a callee, so destructor edges
exist only after expansion through the per-type drop-glue facts family,
and every family in the split carries the semantic configuration in its
key (member identities alone are not a semantic address).

Image-independence: pin the loader-visible inventory too — the loader
lays real argv/env strings on the initial stack before main, so the
dispatcher's pointer replacement is a visibility boundary, not a
resource boundary; constant argv[0], fixed-width selector, pinned
environment vector, and a run-constant image spelling make initial-stack
consumption deterministic.

Phase 2 obligations: a bounded candidate-acquisition step in the host
input protocol for the manifest-gated orphan check (entry bytes are not
snapshot inputs today — loading canonicalizes entries into permission
sets without reading them); the structured failure channel recommended
as a dedicated inherited pipe, with stderr framing rejected because
arbitrary user bytes can reproduce any in-band framing; and the
capability_summary field present from event-schema v1.0 with an
explicit unavailable status until Phase 3 populates it.
The gate landed on trunk (@syscall now rejected outside checked blocks
with E1300, spec legality prose updated), so the ADR states the checked
boundary plainly; the not-an-effect-proxy point stands on std wrapping
checked blocks in safe functions.
Spec 8.5 now carves out the SIGSEGV stack-overflow handler on trunk, so
the reference no longer names the resolved defect.
claude and others added 3 commits August 15, 2026 14:00
Resolve the Result-typed-test-bodies open question: test blocks stay
()-typed, and ? in a test body's immediate block gets test-specific
dynamic semantics — the success arm is ordinary, the failure arm emits a
structured unhandled_error record (compiler-synthesized structural
printing of the payload, the ? site's span) and traps. Trapping instead
of propagating pins the failing line without backtraces, frees each ?
site from spec 4.15:4's identical-error-type constraint, and needs no
return-type surface on the block. Result-typed bodies and ?-less
()-only bodies move to rejected alternatives.
Reserve 0072 for an ADR expected to land on trunk first.
Two review rounds, plus an adversarial pass over the result.

Steve's findings. The orphan-test guarantee did not hold: the
`rue_program` source manifest is derived, not declared — the derive step
writes the scan's accepted reads unioned with every std file, and takes
`srcs` only as the gate that rejects an out-of-srcs read. The orphan file
is by definition never read, so it is absent from the manifest, while all
of std is present. The inventory is now the declared `srcs` set passed
explicitly (`--test-candidates`), which is the same set difference
`rue-program-srcs-precision.py` already computes; its sibling-glob caveat
is why this stays a warning.

Unwrap-and-report `?` does skip drops, and the claim that early return
"buys nothing" was false — the failure arm is an ordinary return (spec
4.15:7) and return paths run drop elaboration. Trapping is kept, because
every other failure path here skips destructors and making `?` unique
would be the inconsistency; the skipped cleanup is now accepted
explicitly and recorded under Consequences. The synthesized error printer
is keyed by error type rather than per `?` site, following drop glue.

Fable's grounding pass. The §4.1 census was wrong in both directions:
`arraybuf`/`sort`/`binary_heap` contain zero sites, and the bulk sit in
`fs`/`net`/`c`, which the three-idiom taxonomy did not name. Recounted on
trunk: 45 real sites (46 raw matches, one is prose), 17 null tests, 21
address-into-`@syscall`-argument, 7 rebase/type-pun. The fourth idiom
does not touch hermeticity but falsifies "rare after the split", so
RUE-967's scope — not merely its resolution — is the maintainer call, and
the migration-audit spike moves ahead of ratification.

Also: `@group` takes an identifier argument, since `DirectiveArg::Ident`
is the sole variant and the literal spelling carries the grammar
extension the deferred directives are priced at; the §7.1 failure-channel
helper is classified hermetic-compatible on stdout's grounds rather than
left unclassified against this ADR's own standing rule; stderr is named
authoritative for `compile_error` diagnostics; seed policy leaves the
cache key, since in the MVP it feeds only shuffle and scratch naming; and
four Open Questions are added (filter/root-set, per-test compile_error
exit codes, the unproducible `skipped` verdict, implicit preview
enabling).

The "first production consumer of the retained canonical-bodies family"
claim is corrected rather than deleted: `CfgBodyInput` holds and charges
an `Arc<CanonicalBody>` today, so the cost is real only for analysis-only
request shapes, which is what Phase 3 now measures.

Adversarial pass, three internal contradictions. The cache key could not
support its own monotone relaxations — a hashed key admits only equality,
so timeout and output limits are now ordered attributes compared by
magnitude beside the key rather than components of it. `--list` claimed
cache status without codegen, but closure fingerprints are ADR-0063
terminal `CodegenUnit` fingerprints; cache status becomes an opt-in tier.
Phase 5 was listed as independent of Phase 3 while two of its four items
need hermeticity summaries. And hermetic-mismatch detection is one-sided,
since failures are never cached — it reports on cache-bypassing runs, not
on every run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPBpmZU3DpwkumLHNwHsgu
@DorianListens
DorianListens force-pushed the claude/rue-test-runner-proposal-qouv7j branch from d93736a to 0a0e388 Compare August 15, 2026 19:02
@DorianListens

Copy link
Copy Markdown
Contributor Author

Round 5 pushed (rebased on trunk adb99c08; the branch now sits at ADR-0073, which is free — the 0071 race resolved in #2327's favor and 0072 landed since).

Your two design findings

The orphan-test guarantee did not hold, and for a sharper reason than the manifest merely being the wrong shape. I traced the derivation: scripts/rue-program-derive-manifest.py writes the scan's accepted reads (validated to fall within srcs ∪ std) unioned with every file of the declared std tree, and declared srcs enter only as the gate that rejects an out-of-srcs read. So the manifest gets the orphan case wrong in both directions at once — a declared-but-unimported foo_tests.rue was never read, is therefore absent, and is precisely the file the design promised to catch; while all of std is present, so a scan over out-of-closure entries would have swept the toolchain as root-owned candidates. The warning would have fired on std and stayed silent on the one mistake it exists for.

The inventory is now the declared srcs set passed explicitly (--test-candidates), fed from the same ctx.attrs.srcs the rule already writes as srcs.list. The set difference is one the build already computes: rue-program-srcs-precision.py reports srcs minus accepted reads as its advisory over-declaration report. I reused the shape rather than the artifact — that report is an optional per-target path check, this is a compiler query over published content — and carried over its caveat, which is why this stays a warning: sibling roots legitimately share a glob, so unread-ness is not orphan-ness.

On ? and cleanup — you're right, and I kept the trap. The failure arm is an ordinary return (spec 4.15:7 is literally return None/return Err(e)), and return paths run drop elaboration, so early return does buy deterministic destructor cleanup. The "buys nothing over the trap" sentence was false and is corrected rather than softened.

I did not adopt the uniform failure continuation, and the reason is consistency rather than cost: @assert, @panic, and every trap already skip destructors, so making ? uniquely run them would be the anomaly. A test that fails is a test whose cleanup was already forfeit. So §1 now accepts the skipped destructors explicitly, bounds what is actually lost (process death reclaims fds and memory; the retained scratch dir preserves on-disk state; what's genuinely given up is a drop fn's own observable work on the failing path), and it is recorded under Consequences. The deterministic-cleanup argument is redirected to where it generalizes — if Rue ever gains unwinding, that is the moment to revisit all failing paths, which "Failure-model evolution" already anticipates.

Printer sharing: taken. Keyed by error type, not per ? site, with drop glue named as the in-tree precedent for exactly that keying.

"First production consumer": corrected, and narrowed rather than deleted. CfgBodyInput holds an Arc<CanonicalBody> and its RetainedCharge adds that body's charge, so any request reaching codegen already retains them. The cost is real only for analysis-only shapes — --list above all — which is what Phase 3 now measures.

Fable's pass

Both inline findings answered in thread. The census recount found 45 real sites rather than 46 (one match is prose) and the fourth idiom at 21 rather than ~31 (the fs/net/c files also hold 15 ordinary null tests) — the finding survives the smaller number and drove the bigger change: RUE-967's scope is now the maintainer call, with the migration-audit spike moved ahead of ratification.

Of the ranked items: the §7.1 helper is now classified hermetic-compatible on stdout's grounds (runner-pinned fd, captured, budget in the key), with the Phase 2/Phase 3 sequencing gap stated; seed policy is out of the cache key with the reasoning inverted into a worked example of the key's own admission rule; stderr is named authoritative for compile_error diagnostics with a diagnostics.md test-mode note as Phase 4 work; ADR-0061 is cited as Accepted. All four Open Questions added — filter/root-set, per-test compile_error exit codes, the unproducible skipped verdict, and implicit preview enabling (which I agree would be the first auto-enabled preview feature against 0005's explicit-opt-in model).

My own adversarial pass — three internal contradictions

  1. The cache key could not support its own relaxations. A hashed key admits only equality, so with the effective timeout in the key, a lookup under a different timeout computes a different key and misses — "valid under any timeout ≥ T" could never fire. The entry now has two parts: hashed key components, and ordered attributes (timeout, output limits, recorded byte counts) stored beside it and compared by magnitude. That is also why the output relaxation compares recorded counts rather than the recording run's limit.
  2. --list claimed cache status without codegen. Cache status needs the closure fingerprint, which §5 builds from ADR-0063 terminal fingerprints — and the terminal artifact is the per-function CodegenUnit (0063 §11). So it is cheap warm, a full closure codegen cold, and never "semantic analysis only." Split into two tiers, with cache status opt-in.
  3. Phase 5 was listed as independent of Phase 3 while two of its four items (--reruns for non-hermetic tests, hermetic-mismatch reporting) are predicated on hermeticity summaries. Dependency corrected; the other two items genuinely are independent and are called out as the shippable subset.

Plus one precision fix: hermetic-mismatch detection is one-sided. Failures are never cached, so the only detectable direction is a cached pass that fails on re-execution — and that fires only on cache-bypassing runs, not routine --changed-only ones. Still the direction worth catching, but "localized by construction" describes the blast radius, not a claim that every occurrence is observed.

Registry validation passes (74 records, no duplicate IDs).

@steveklabnik

Copy link
Copy Markdown
Collaborator

Codex here, leaving a coordination note at Steve's request. Steve does not want to take over this PR.

The current recommendation is not to merge the 1,960-line proposal as one ADR. The design capture is strong, but the document remains a proposal, the branch conflicts with current trunk, and it still contains many explicit maintainer calls plus required spikes.

A better path would be to narrow this to an MVP ADR covering test declaration and discovery, the rue test request and event protocol, and process isolation. Capability inference, hermetic verdict caching, deterministic allocation and randomness, provenance migration, and later scheduling policy can become focused follow-up ADRs with their own evidence and rulings.

This is a scope and ratification recommendation, not a rejection of the architecture. Dorian retains ownership and can choose how to proceed. Codex has made no merge or branch changes.

@DorianListens

Copy link
Copy Markdown
Contributor Author

Closing in favor of #2566, per the scope recommendation above — agreed that the full document is too much to ratify or land as one ADR, and that an MVP-first split is the right path. This is a ratification decision, not a rejection of the architecture.

Where everything went:

  • RUE-506: ADR-0081 — rue test MVP (test declarations, runner, event protocol) #2566 — ADR-0081, the MVP (new branch off current trunk; 0073 was taken by another ADR in the interim): test declarations, the rue test driver mode and NDJSON event stream, process-per-test execution under the pinned inventories, the structured failure channel, and Phase 2.5 structured assertions. The review-hardened details from all five rounds here (the --test-candidates orphan inventory, the accepted skipped-destructor posture for ?, the loader/test-visible inventory split, the channel classification, the two-tier --list) carried over intact.
  • Follow-up ADRs, each seeded as a Linear issue with the relevant sections, maintainer calls, and spikes extracted from this document (permalinks to 0a0e388): RUE-1621 capability inference (original §4, Phases 3+6, the standing constraints section, RUE-967 gate), RUE-1622 hermetic verdict caching and selection (original §5, Phase 4, allocation determinism, the cache-key audit), RUE-1623 scheduling and flake policy (Phase 5), RUE-1624 the public provider protocol (Phase 7).
  • The MVP ADR's §6 records what ships now so each deferred layer lands additively (reserved verdicts and schema fields, pinned inventories as the future cache key's inputs).

This PR stays closed-unmerged as the permanent design-capture archive; the follow-up issues link back into it by section.

DorianListens added a commit to DorianListens/rue that referenced this pull request Aug 23, 2026
…dary

Rewrite per maintainer notes, 1259 -> 846 lines:

- The process history is cut to two sentences in Status; the document no
  longer depends on PR rue-language#2239 context to read. Context shrinks to the facts
  the design is built on plus one-lesson-each prior art.
- The user experience is shown, not described: worked CLI invocations with
  illustrative human and NDJSON output in section 2, the test-body example
  retained, and the import-wiring idiom shown as code in section 1 with its
  tradeoff stated plainly.
- A new top-level section pins the compiler-CLI vs build-integration
  boundary: everything in the ADR is the compiler's; build integration
  supplies exactly one optional input (--test-candidates).
- File discovery becomes an explicit maintainer call: import-closure-only
  with the orphan warning (as drafted) vs a closure-anchored naming
  convention that auto-roots conventional test files, with costs and the
  compile_error containment dependency stated. Phase 2 gates on resolving
  it; the package-model end state is recorded under deferred questions.
- Explanatory prose compressed throughout; every contract value, reserved
  schema surface, and adversarial-review fix from 345daad is preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPBpmZU3DpwkumLHNwHsgu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants