Repository navigation
The .dag acceptance harness: staged front end, typed per-stage evidence, one named acceptance contract - #8821
Conversation
…named acceptance contract Given a candidate .dag source string, return typed evidence of how far it got and why it stopped. Stages mint evidence; a named contract mints acceptance. - v2.compiler.staged_front_end: one authority for the front-end order, minting per-stage evidence; ingested_resolved_module_from_source is now a projection of it (fold to the resolved node or the first refusal, reproducing bind_outcome's diagnostic threading), not a second copy of the order. - v2.workflow.dag_acceptance: obligations with derived prerequisites, four-arm StageExecution (the fourth is undecided, for observed-but-unreadable), a derived verdict with no stored field, and one receipt shape with a policy parameter. - extdeps.rustc + v2.workflow.dag_acceptance_rustc: the fifth stage. v2's TranslateTo is emit() and never invokes a toolchain, so target compilation is a separate obligation with a real rustc binding at the periphery. A gunbc serve route was considered and refused against the frozen seam. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rrier The probes answered which side of a determinate row the compiler currently takes: infer passes and TranslateTo(Rust) rejects for the add candidate. That is a fact about the compiler, so it lands as a note rather than as a test — pinning it would make this file a defect pin that reds the moment v2 gains the derivation it lacks. The assertions keep the property worth protecting: the harness always locates the answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eptance distinctly The floor caught the first cut minting a second TargetCompileAccepted beside v2.std.leaf_model_verification's — the nicknaming violation, and it broke every existing consumer of the real one. The observation now defers to that authority for the candidate-facing decision and adds only the three states a live invocation can be that a modeled expectation cannot. Stage obligations renamed off ResolveObligation, which collided with gunbc.ci_materialization. An accepted stage carrying diagnostics is now its own constructor, so a consumer matching StagePassed cannot absorb the FrontierAccepted case. Measured consequence, recorded on the carrier: an unresolved callee passes resolve with no diagnostic at all and passes infer carrying infer_grounding_not_derived. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Eval branch is not exercised from a source string because no candidate-relative entry-point resolution reaches EvalClaim.runtime yet. That is a gap in what is observed, and it is named on the carrier with its closing trigger rather than left as an implied pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured: an unresolved callee passes resolve silently and passes infer with infer_grounding_not_derived, which is the declared FrontierAccepted state — and that diagnostic is about grounding, a different property than the one that failed. The vocabulary for the failed property is not missing: resolve_reason_unbound_symbol exists and fires for the same name in value position. So the floor status of the callee position is UNDETERMINED, stated as such rather than closed in either direction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review 54506: the _ms scalars were a second representation of duration beside std/measure.dag. StageCostRow, the ledger and the budget refusal now carry Millisecond. The ledger counts SPENT rather than REMAINING, which the same move forces: Nat is a CommutativeSemiring, so it has addition and no subtraction, and a decrementing ledger would have needed an operation its carrier does not provide. Admission is spent + required against the declared total, and the refusal reports required, total and already-spent — strictly more of the fact than a derived remainder. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed in the head commit — review 54506 was right, and fixing it forced a second correction I would not have found otherwise. Adopted What the adoption forced: the ledger now counts SPENT, not REMAINING. Re-verified by execution, not by typecheck: all 11 witnesses in — sent from fierce-newt-478 |
…transport is shell Review 54507. TargetCompileInvocationRefused had no producer and mapped to the same row as TargetCompilerUnavailable — a distinction asserted on the input side and erased on the output side, and a constructor nothing emits. The surviving arm still refuses rather than fabricating a verdict; the trigger for bringing it back names a producer. The rustc transport keeps its fixed-literal sh -c with positional parameters, and the carrier now records what was measured rather than assumed: -o /dev/null and --emit=metadata=/dev/null both fail because rustc writes temporaries beside its output, and they fail by printing error: lines an observer would read as the candidate being rejected. A fixed /tmp path races concurrent scoring. The dissolution is a modeled pipe and scoped temp dir, which do not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both findings from review 54507 are fixed in the head commit. Taking them in the order of how much they changed.
The
So the per-invocation directory is not decoration — it is what makes the observation attributable to the candidate. The script stays a fixed literal with positional parameters ( I did not add a 🟡 marker. A dissolution trigger is a lifecycle fact, not permission, and self-authorizing one here would be the move DESIGN calls out. What would dissolve this is recorded as an observation on the carrier: a modeled pipe and scoped temporary directory expressible as nodes. No such primitive exists today, which makes this the honest bottom transport rather than a routed-around obstacle. Re-verified by execution after both changes: 11/11 acceptance witnesses, and the wet pair still green — real — sent from fierce-newt-478 |
…surement Review 54517: run_front_end_to was used without being listed, and run_front_end was listed without being used. Audited the whole import surface of the three new modules rather than the one line the review saw — three more unlisted uses and eight unused entries across the consumers. The undefined-callee question is now settled per path and recorded as a measurement. v1 refuses at resolve, located, naming the function, with a control proving the same shape resolves, infers and evaluates when the callee is defined. v2 refuses an undeclared name in VALUE position with resolve_reason_unbound_symbol and accepts it in CALLEE position silently, then accepts at infer with infer_grounding_not_derived. The class rung is the minimum across paths, which is v2's. Only the direction that should hold is pinned as a test: the value-position refusal, with its reason. Asserting the callee silence would defend the gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed in the head commit — and the finding was worth more than the one line it named, because I audited the whole import surface rather than just that call site. What was actually wrong: One correction to the finding, offered as a fact rather than a defence. Re-verified by execution after the change: 11/11 acceptance witnesses and 7/7 staged-front-end witnesses. Also in this commit, unrelated to the review: the undefined-callee question raised earlier is now settled per path and recorded as a measurement rather than a defect. v1 refuses at resolve, located, naming the missing function — with a control proving the same file shape resolves, infers and evaluates when the callee is defined, so the refusal is the treatment and not the setup. On the v2 path the same property is answered by position: an undeclared name in value position refuses at resolve with — sent from fierce-newt-478 |
|
CI green on head Green is not the same as selected, so I checked enrollment rather than assuming it. The floor log prints only held rows (206 known-red + 101 route-gap = the 307 distinct witness rows in it), so my witnesses passing means they do not appear there — absence proves nothing either way. The positive check is the roster delta: main's most recent successful run planned 10340 / passed 10033; this run plans 10358 / passes 10051, exactly +18 on both, which is exactly the number of hermetic That same arithmetic says what is not enrolled: the two wet — sent from fierce-newt-478 |
What this is
The automated acceptance oracle for
.dag: given a candidate source string, return typed evidence of how far it got and why it stopped. Stages mint evidence; a named acceptance contract mints acceptance. It is a pure function of aStringwith no transport of its own.A serve route was considered and REFUSED
The work item this landed under was titled "expose the .dag compile chain over
gunbc serve". That is not what this PR does, and the refusal is recorded here rather than left as a path silently not taken.gunbc serveis a declared frozen quarry route.gunbc.roadmap_serveroadmap_serve_seam_restoration_note, mirrored verbatim at theCommands::Servearm insrc/v1/stage0/src/main.rs: "FROZEN MEANS: no new options, no new verbs, no new routes into the interpreter, no completion work, no flag or environment gate re-opening it" — and "if this route begins attracting completion work ... the freeze has been repealed by drift." Its declared successor isroadmap_serve_interpreted_scaffold, dissolving to emit-on-demand, and the active ROADMAP row "Serve the roadmap page from emitted native code, never the tree-walking interpreter" records that this trigger already fired in production (the 2026-08-08 srv1 two-hour page wedge). A route running a front end, infer, emit and arustcsubprocess per request is the heaviest possible new workload on the seam that wedged.The requirement behind the title was amortizing the corpus load (~39s load against ~55ms of candidate work), and that is a batching fact, not an HTTP one:
claim_batch/claim_executoralready hold a module index built once at startup, so many candidates score in one process with no new interpreter surface. The dispositions were re-verified against the tree, not recalled.The stage graph
EvalandTranslateToare siblings over one inferred tree; neither is later.TargetCompileis a fifth stage the compiler does not perform:v2.compiler.compilecompile_inferred_translateis exactlyemit(tree, target)and never invokes a target toolchain (verified in the tree, not assumed), soTranslateToproves emission and nothing about whether the emitted source compiles.Edges live in exactly one place —
obligation_prerequisites— and every consumer reads them rather than re-spelling the order.Two structural moves (§4b), not runtime checks
TargetCompileClaimcarries theTranslationClaimit consumes. "Target-compile an emission nobody requested" has no spelling; the prerequisite is derived from the claim the obligation already holds.DagAcceptanceReceiptcarries contract identity, candidate identity (content hash of the source), and the executed rows. There is no verdict field to transcribe, so rung inflation on this harness's own output is unrepresentable rather than lens-caught.The floor caught this PR nicknaming, and that is in the diff
The first cut minted its own
TargetCompileAccepted— andv2.std.leaf_model_verificationalready declaresTargetCompileVerdict = TargetCompileAccepted | TargetCompileRejected { diagnostic_code }. Whole-pool resolution meant my duplicate broke every existing consumer of the real one, and--required-floorrefused with three of them named. The observation now defers to that authority for the candidate-facing decision (TargetCompileDecided { verdict }) and adds only the three states a live invocation can be that a modeled expectation cannot.ResolveObligationlikewise collided withgunbc.ci_materialization, so the stage obligations areTokenizeStage … TargetCompileStage.Accepted-clean and accepted-with-diagnostics are different constructors
A stage can return
Acceptedwhile carrying typed located diagnostics — DESIGN names thatFrontierAccepted. If the receipt folded it into an ordinary pass, a frontier candidate would score as clean and every metric would inherit the blindness.StagePassedWithFrontieris a separate constructor, not an optional field, so a consumer matchingStagePassedcannot absorb the advisory case;AcceptanceContractSatisfiedcarries the frontier stage labels.Measured with it, 2026-08-21:
fn add(x: Int, y: Int) -> Int { undefined_callee(a: x) }— a call to a name no declaration provides — passes resolve with no diagnostic of any severity, then passes infer carrying one, reasoninfer_grounding_not_derived. So it is the declared frontier state rather than a silent below-floor accept, and the harness reports it as such. Not claimed: that anything reports the missing declaration itself — the frontier diagnostic is about grounding.Durations consume
std.measure, and that forced a better ledgerReview 54506 caught
_ms: Intscalars — a second representation of duration besidestd/measure.dag. They are nowMillisecond(Measure<Time, Milli, Nat>). The adoption forced a real change rather than a rename:Natis aCommutativeSemiring, so it has addition and no subtraction, and a decrementing ledger would have needed an operation its own carrier does not provide. The ledger therefore counts spent, admission isspent + requiredagainst the declared total, and the refusal carriesrequired,budget_totalandalready_spentinstead of one derived remainder.Facts kept apart (§5)
BlockedByPrerequisite(impossible) vsSkippedAfterDecisiveRejection(a fail-fast policy chose not to pay).VerifierBudgetUnavailable(a fact about us). A budget exhaustion isIncomplete, never a rejection — otherwise a slow machine reads as a bad model.StageUndecidedarm rather than being rounded toward rejection.StageExecutiontherefore has four arms, not three; the fourth is argued on its carrier.The front end was DECOMPOSED, not copied (§3)
ingested_resolved_module_from_sourcewas a singleOutcome<Node>for four stages, so it could not say which stage a candidate died at. Re-inlining the four calls beside it would have been a second copy of the order. Instead the order moved tov2.compiler.staged_front_endand that arrow is now a projection of it — a fold to the resolved node or the first refusal, reproducingbind_outcome's diagnostic threading (accumulate on accept,rejected_with_pendingon refusal) and re-running nothing.A bounded run (the harness asks for exactly the depth its contract needs) is a third completion, not an absent node — rendering it as "no resolved module" would be the empty-observation narrow.
Evidence — green by execution, with discriminating REDs
All runs on a locally built
claim_batch(arm64, this container), against real source strings through the real chain.Staged front end (6/6 PASS): including the discriminating pair — a candidate dying at parse lands at stage 2, a candidate dying at resolve lands at stage 4. If the decomposition carried nothing they would land on the same row.
Acceptance harness (11/11 PASS): contract satisfied; resolve death located at resolve; infer
BlockedByPrerequisitebehind a failed resolve with the verdict stillRejected; a stage the contract never asked for accounted asNotRequiredByContract; verifier-budget exhaustion asserted two-sidedly — the rows sayVerifierBudgetUnavailableAND the verdict isIncomplete, notRejected; the target-compile prerequisite derived from its own translation claim (compared by identity, not discriminant); the four observation arms mapping to passed/rejected/undecided/not-run; fail-fast skip present underInteractiveFailFastand absent underFullLedger; a translate row through the real compiler asserted determinate; and an accepted-with-diagnostics row landing as a frontier pass rather than a clean one.The fifth stage, proven wet with real
rustc:claim_batch --wetoverdag_acceptance_rustc_wet_test, both green in one run on one host:Valid Rust is ACCEPTED and invalid Rust (
x + truewherei32is required) is DIAGNOSED, by realrustcreading the source from stdin. Either half alone is satisfiable by an observer that always answers one way; the pair is not.An earlier version of that pair failed honestly and is worth recording:
-o /dev/nullmaderustcfail to create its temp dir, so valid Rust was refused and invalid Rust "passed" for the wrong reason. The pair caught it; a single-sided assertion would not have.Measured while building this, reported rather than buried
fn f() -> Int { undefined_name }refuses at resolve, butfn f(x: Int) -> Int { undefined_callee(a: x) }does not — an unresolved callee passes resolve while an unresolved bare value name does not. Not touched here; it is a compiler fact this harness now makes visible per candidate.TargetCompilePolicywas not minted. The acceptance rule has exactly one point today, and an axis with one inhabitant is a second name for a fixed rule. The next-rung trigger is on the carrier.Rung honesty about this PR's own claims
dag_acceptance_rustc_wet_testand not enrolled in the hermetic floor, because the hermetic envelope refuses host effects (correctly — mocking it would score candidates against a fabricated exit status).EvalandTranslateToexecution from a source candidate is unexercised end-to-end, and the reason is a real compiler fact rather than an omission: see the infer row note below. The fail-fast/full-ledger split is therefore proven at the decision function, not end to end, and that is stated on the test.Measured on this tree, 2026-08-21, and recorded on the carrier rather than pinned as a test: for
fn add(x: Int, y: Int) -> Int { x + y }ingested from source, tokenize/parse/normalize/resolve/infer all pass andTranslateTo(Rust)rejects. The tests assert DETERMINACY (the row is passed or rejected, never missing) rather than today's side — pinning the side would make this file a defect pin that reds the moment v2 gains the derivation it lacks, and what is worth protecting is that the harness always locates the answer.