Pre-phase design — Provenance Propagation
Status: DECISIONS RECORDED — revised 2026-05-12 after slice-2
recon (see “Design correction” below). Slice 0 of the Provenance
Propagation phase. CTO delegated the scope call; this doc is the
contract the multi-tier work executes against. Same role
runtime-split-design.md played for 33J7b.
Filed: 2026-05-12. Revised: 2026-05-12 (slice-2 recon corrected a load-bearing premise — D1, D5, R6, and the slice plan changed; the original framing is preserved struck-through where it matters so the correction is auditable).
Origin: the corvid-differential-verify test
corpus_scan_includes_deliberate_failure is red. Investigation
traced it to a real tier-soundness divergence and
tests/corpus/combined_all.cor has been byte-identical since Phase
20g, where it was committed as a program that should agree across
all four tiers. The CTO call: don’t paper over it — close it as an
invention, not a fix.
Design correction — the typechecker is grounded-blind (recon, 2026-05-12)
The slice-2 recon pass falsified this doc’s original premise before
any code was written. What the original draft assumed: the
typechecker sees Grounded<String> for a data: grounded prompt’s
return and accepts String + Grounded<String> via the “legacy
compatibility” assignability rule at types.rs:153-156.
What the recon found: Type::Grounded is only ever constructed
from an explicit Grounded<T> type annotation a user writes by
hand (checker/expr.rs:536, checker/types.rs:261). There is no
path from a data: grounded effect to Type::Grounded in the
checker. So:
audit() -> String uses audit_effect(effect carriesdata: grounded) returns plainType::Stringto the typechecker. The grounding lives only in the effect row, never in the type.first + secondincombined_all.corisString + Stringto the checker → it typechecks clean (confirmed: differential-verify’s error is “interpreter run failed”, not “typecheck failed” — sotypecheckpassed).- At runtime the interpreter wraps
audit()’s result inValue::Groundedbecause of the effect →eval_arithmeticseesValue::String + Value::Grounded→TypeMismatch.
The divergence is not “checker permissive, interpreter strict.” It
is the checker and the runtime disagree about whether the value is
grounded at all. The type system is unsound here — it says
String where the runtime produces Grounded<String>.
Consequence for the moat. @grounded_pure (D6) needs the
compiler to reason about grounded-ness. A grounded-blind checker
can never prove “no laundering.” So the real fix is not “teach the
interpreter to tolerate Value::Grounded” (that would be Design Y —
the rejected shortcut, since it leaves the moat impossible). The fix
is Design X: make data: grounded a type-system property — a
prompt/tool/agent whose effect row carries data: grounded returns
Type::Grounded<T>. CTO call (delegated 2026-05-12): Design X, it
is the only design that fixes the soundness hole and enables the
moat. D1, D5, R6 and the slice plan below are revised accordingly.
The problem, precisely (revised)
Grounded<T> is Corvid’s provenance-carrying type — a value plus a
ProvenanceChain proving where it came from. Today:
- Typechecker is grounded-blind for effect-induced grounding.
data: groundednever producesType::Grounded; only an explicitGrounded<T>annotation does. Thetypes.rs:153-156“legacy compatibility: Groundedassignable to T” rule exists but only fires for hand-annotated grounded values — it is not what lets combined_all.corpass (that passes because the checker never sees grounding at all). - IR lowering (
corvid-ir/src/lower.rs:1172-1187) only emitsUnwrapGroundedfor an explicit.unwrap_discarding_sources()call. ImplicitGrounded<T> → Tcoercions produce no IR node. - Interpreter (
corvid-vm/src/interp/expr.rs) —eval_arithmeticpattern-matches concreteValue::Int/Float/String/List. AValue::Groundedfalls through toTypeMismatch. - Native codegen / replay — the
UnwrapGroundedIR node is handled, but since lowering never emits one for implicit coercions, those tiers face the same raw-grounded-operand gap.
The gap: data: grounded is a runtime-value fact the type system
cannot see, so the four tiers cannot be made to agree on it — and
the moat cannot be built on a fact the compiler is blind to.
The reframe: invention, not fix
A fix makes the tiers agree. An invention makes grounded values do something no other language or framework can. The phase ships:
Provenance Propagation — Grounded<T> is contagious through
every operator, preserved end-to-end across all four tiers, and
never silently dropped. The compiler can prove a boundary launders
nothing.
The principled core: Grounded<T> is an applicative functor and
provenance-merge is its monoid. State that law once and every
pure operation lifts for free — it is one algebra, not a pile of
per-operator special cases.
The pitch: in Corvid, “did this value come from an AI, how was it computed, and how confident is it” is a compile-time-decidable property that survives every operation. You cannot accidentally launder a model output into trusted data.
Scope — what ships, what defers
CTO call (delegated 2026-05-12). Ship the moat with discipline; do not bundle three inventions into one phase on an already-slipped launch track.
Ships in this phase:
- Base contagion —
Grounded<T>contagious through all operators (arithmetic, concat, comparison), all four tiers. @grounded_pure— the compiler-enforced no-laundering boundary. The moat.- The
Derivedprovenance representation — base contagion uses it from day one so how-provenance’s data model ships with the base and no migration is needed later. - The invention-shipping contract (README / tour / inventions.md / spec / tests).
Defers to follow-up phases (sequencing, not shortcutting — each ships fully later):
- Pillar 1 polish — how-provenance DAG rendering. The
Deriveddata ships here; the richcorvid trace dagrendering of the operation tree is a follow-up. - Pillar 2 — compile-time confidence budgets. Runtime confidence
composition ships here (it has to —
GroundedValuecarries a confidence field). Compile-time@confidence(>= X)enforcement through arithmetic, and the question of whether*deserves a composition rule other thanMin, is its own focused phase.
Decisions
D1 — Grounding is a type-system property + the contagion law (revised 2026-05-12)
Decision, part A — data: grounded produces Type::Grounded<T>
(Design X). A prompt / tool / agent whose effect row carries
data: grounded has its declared return type T wrapped to
Type::Grounded<T> by the checker. The type system stops being
blind to effect-induced grounding; the checker, interpreter, native
codegen, and replay finally agree on which values are grounded.
This is the soundness fix the recon surfaced as mandatory — without
it the contagion law has nothing to operate on and @grounded_pure
(D6) is uncheckable.
Decision, part B — the contagion law (uniform, all operators).
Any operator application with at least one Grounded operand
produces a Grounded result. No exceptions:
Grounded<T> ⊕ T→Grounded<T>T ⊕ Grounded<T>→Grounded<T>Grounded<T> ⊕ Grounded<T>→Grounded<T>- comparisons:
Grounded<T> ⊗ T→Grounded<Bool>(and the two other operand shapes)
⊕ ranges over arithmetic (+ - * / %) and string/list concat;
⊗ over comparisons (== != < <= > >=). && / || are
short-circuit and out of scope (they already route specially).
Ordering note. Parts A and B are coupled: shipping A without B
would break combined_all.cor at the checker — check_binop
matches concrete Type::Int/Type::String arms, so a
Type::Grounded operand would fall through to its error arm. B
(contagion in check_binop / check_unop) must land first (dormant
— nothing produces Type::Grounded from effects yet), then A
activates it. Both are corvid-types changes; the slice plan lands
them as one slice for that reason.
Why uniform. A per-operator carve-out is the pile-of-special- cases anti-pattern. The applicative-functor framing demands one law. A stated law that the implementation only partially honors is exactly the spec/behavior drift the project forbids — so the phase implements the complete law, comparisons included.
D2 — Grounded<Bool> in control-flow conditions
Decision. if / while / match conditions accept
Grounded<Bool>. The branch decision implicitly unwraps the bool —
this implicit unwrap is recorded (the trace shows “branch taken
under grounded condition”) and is IR-visible (see D5). It does
NOT violate @grounded_pure: branching consumes the bool to pick a
path, it does not emit a laundered value; the returned value’s
provenance still tracks through whichever branch produced it.
Why. Contagion that stops at control flow is not contagion. “This code path was taken because of an AI output” is precisely the kind of fact provenance should capture. “Powerful, not powerless” — grounded values stay usable in control flow.
D3 — The Derived provenance representation
Decision. Add a fifth ProvenanceKind variant:
ProvenanceKind::Derived { op: String, // "add", "concat", "lt", ... inputs: Vec<ProvenanceChain>, // recursive — a real tree}When an operator produces a grounded result, the result’s
ProvenanceChain is a single ProvenanceEntry whose kind is
Derived { op, inputs }. inputs holds the operand chains in
operand order; an ungrounded operand contributes an empty
ProvenanceChain (self-describing as “(ungrounded)” to renderers —
no new Literal variant needed).
Mechanical consequence — shipped in slice 1. ProvenanceKind
derived Eq; ProvenanceChain / ProvenanceEntry did not. The
recursive Vec<ProvenanceChain> payload needs the whole tree Eq.
Slice 1 added Eq to the chain types — sound because they carry no
floats (confidence lives on the VM’s GroundedValue, not core’s
ProvenanceChain). Also shipped: a ProvenanceChain::derived(op, inputs, timestamp_ms) constructor as the single mint point all four
tiers call. ✅ Done — see slice 1 in the plan.
Why a tree, not a flat merge. ProvenanceChain already has a
flat merge() — it stays, for cases that legitimately accumulate
sources (agent handoffs). But operator results want how-provenance:
the operation tree, not just the leaf set. Flat-merging at operator
sites would lose which sources fed which operand — and recovering
that later would be a representation migration. Shipping Derived
now means how-provenance’s data model is permanent from day one;
only the rich DAG rendering defers.
D4 — Runtime confidence composition: Min, uniform
Decision. A grounded operator result’s confidence =
Min over the operands’ confidences; an ungrounded operand
contributes 1.0. Uniform across all operators in this phase.
Why. GroundedValue already documents confidence as “composed
via Min through the call graph” — this extends the existing,
shipped rule to operators rather than inventing a new one. Whether
specific operators (notably *, where independent confidences might
multiply) deserve a different rule is the deferred Pillar 2 design
question. Using the shipped rule is not a shortcut; refining it is
the deferred pillar’s job, and refining it later is non-breaking
(it only tightens runtime confidence values, never types).
D5 — The legacy Grounded<T> → T rule: load-bearing now, keep it, make it visible (revised 2026-05-12)
Decision. Keep the types.rs:153-156 legacy assignability rule.
Under Design X (D1 part A) it is no longer “legacy cruft to retire”
— it is load-bearing: when data: grounded starts producing
Type::Grounded<T>, every existing consumer of a grounded
prompt/tool result suddenly sees Grounded<T> where it saw T. The
legacy rule is what keeps that from breaking every program — it lets
Grounded<T> keep flowing into T positions (return, args,
bindings, struct fields). Removing it would turn Design X into a
workspace-wide breaking change. It stays.
But it must not stay invisible. Make every implicit Grounded<T> → T coercion IR-visible: when the legacy coercion fires, IR
lowering inserts an explicit discard node (reuse UnwrapGrounded,
or a dedicated CoerceGroundedDiscard — slice 3 decides which is
cleaner). Operator operands do not need this — D1’s contagion law
covers them (the result stays grounded; nothing is dropped).
Consequence:
- Normal agents: implicit coercion still compiles and runs. Near-zero blast radius on existing code — the coercion still works, it is just no longer invisible. (Exact blast radius is measured in slice 2 — see R6.)
@grounded_pureagents: the checker sees the discard node and refuses to compile (D6).- The provenance drop becomes greppable in the IR and visible in traces — closing the “silent” part of the gap without a breaking removal.
Why. This is the powerful-and-safe resolution. Removing the rule
outright (the option the chat rejected as “powerless”) would now —
post-Design-X — be a workspace-wide break, not just “ceremony on
existing code.” Keeping it invisible (the rejected “shortcut”)
leaves silent laundering. Keeping it visible gives normal code its
ergonomics back AND gives @grounded_pure something concrete to
forbid AND makes every drop auditable. Best of three.
D6 — @grounded_pure — the moat attribute
Decision. A new agent attribute, sibling of @deterministic /
@replayable. @grounded_pure agent foo(...) compiles iff the
checker proves foo’s body contains no provenance-stripping site:
no UnwrapGrounded node and no implicit-coercion discard node (D5)
is reachable. It is a reachability check over the IR — the same
proof shape @deterministic already uses for “no nondeterministic
source reachable.”
Composition with sibling attributes is orthogonal: an agent may
be any subset of @deterministic / @replayable / @grounded_pure;
each is an independent proof obligation. @grounded_pure says
nothing about determinism and vice versa.
Why this is the moat. Base contagion alone is “provenance
propagates” — nice. @grounded_pure makes it load-bearing: mark a
boundary and the compiler guarantees no AI output is laundered into
trusted data across it. That is the approve-moat shape — a
compile-time refusal — applied to data flow. It is the most on-thesis
thing in the whole set, and it is cheap once contagion and D5’s
visible-discard node exist.
D7 — combined_all.cor becomes idiomatic
Decision. Change tests/corpus/combined_all.cor’s main()
signature from -> String to -> Grounded<String>. The program
genuinely produces a grounded value (audit() is grounded, +
propagates per D1); the signature should say so. The corpus shows
idiomatic Corvid. The legacy-coercion-discard path (D5) gets its
own dedicated fixture rather than being demonstrated ambiguously by
combined_all.cor.
D8 — Effect rows unchanged
Decision. Contagion is a type-and-value-level property. It
does not alter effect rows. data: grounded on an effect is what
originates a Grounded<T> value; operator contagion propagates
it downstream. The agent’s effect profile — what the differential-
verify tool compares across tiers — is unchanged by this phase. The
tiers must agree on types and values, which is what D1–D6 deliver.
Risk mitigations
- R1 — native codegen is the hardest tier. Grounded values
through the C ABI. Mitigation: the
corvid-differential-verify4-tier check IS the mitigation — if native diverges from interpreter, the tool catches it. Slice 5 is gated by re-running the differential verifier; native ships only when it agrees with interpreter byte-for-byte. - R2 — recursive
Derivedchains grow with expression depth. Mitigation: depth is bounded by source expression nesting — not unbounded. Note it; do not optimize during the feature. If it ever becomes a perf problem, that is a separate, measured slice. - R3 — the
Grounded<Bool>control-flow cascade may ripple wider than expected. Mitigation: sequence it (slice 6) behind the load-bearing arithmetic/concat slices. If it ripples past its slice, it splits — it does not block the rest of the phase. - R4 — D5’s visible-discard change could surface existing silent
coercions. Mitigation: the slice-7 discard node is additive
— the coercion still compiles and runs, it just gains an IR node.
Only
@grounded_pureagents are affected, and there are none until slice 8. Confirmed by the full workspace test run + corpus baseline staying green through slices 3–7. - R5 —
@grounded_pureinteraction with@deterministic/@replayable. Mitigation: D6 specifies orthogonal composition up front; slice 9 includes a test matrix over the attribute subsets. - R6 — Design X blast radius (added 2026-05-12). Making
data: groundedproduceType::Grounded<T>means every consumer of a grounded prompt/tool result now seesGrounded<T>. The legacy rule (D5) absorbs most positions (return / args / bindings / fields — anything governed byis_assignable_to). The positions it does NOT absorb are operator operands (check_binop/check_unopmatch concrete arms) — which D1 part B covers — and any position with a hard concrete-type match elsewhere in the checker. Mitigation: slice 2 includes an explicit blast-radius measurement — after wiring Design X, run the full workspace test suite +verify --corpus+ everyexamples/program, and enumerate every new failure. The expectation is that legacy-rule- D1-contagion cover essentially all of it and the residue is a
countable handful of sites needing explicit
Grounded<T>annotations or.unwrap_discarding_sources(). If the residue is large or structural, slice 2 stops and the phase re-scopes — that is the recon gate, not a thing to patch around.
- D1-contagion cover essentially all of it and the residue is a
countable handful of sites needing explicit
Slice plan
One commit per slice. Validation gate between every commit (workspace
check + targeted tests + corvid-runtime-core wasm32 build + corpus
baseline). Push every slice. Wait for acknowledgement at slice
boundaries.
-
Slice 0 — design doc. This document. Commit:
docs(grounded-prop-0): record provenance-propagation design. -
Slice 1 —
Derivedrepresentation. ✅ shipped.ProvenanceKind::Derived { op, inputs: Vec<ProvenanceChain> }landed incorvid-runtime-core/src/provenance.rs.Eqadded toProvenanceChain+ProvenanceEntry(the recursive tree needs it; no floats in core’s chain types so total equality is sound).ProvenanceChain::derived(op, inputs, timestamp_ms)constructor — the single mint point all four tiers will use. 5 round-trip tests (incl. recursive-tree survival + empty ungrounded-operand chain +Eq). Gate green: wasm32 build clean, core 20/20, runtime 257/257, workspace check clean, corpus baseline unchanged. -
Slice 2 — typechecker: contagion law + Design X (revised 2026-05-12).
corvid-types, landed in two coupled steps within one slice per D1’s ordering note:- 2a — contagion law, dormant. ✅ shipped.
check_binop/check_unopstripGrounded<>from operands, run the normal rule on the inner types, re-wrap if any operand was grounded (ungrounded()helper). Mid-slice finding: the contagion law has a second enforcement site in the checker — the provenance-reachability analysiseffects/grounded.rs:: expr_is_grounded, which proves a-> Grounded<T>agent’s return actually traces to adata: groundedsource. Its_ => falsearm swallowedBinOp/UnOp, so a grounded operand’s grounding did not reach the return. 2a fixes both halves: the type rule and the reachability walk.&&/||scoped out of both (D1). 8 checker tests (every operand shape + a conditional-not-blanket guard + an ordinary-arithmetic regression guard); each positive test fails without 2a. Dormant confirmed: full workspace test sweep stays green except the one pre-existingcorpus_scan_includes_deliberate_failure(the failure this phase exists to fix — unchanged). - 2b — Design X. ✅ shipped. The
EffectRegistryis now built before the main check pass and threaded intoChecker(it was built only for the post-passes before).check_tool_call/check_prompt_call/check_agent_callcallground_if_effect_grounded: if the callee’s effect row carriesdata: grounded, the call-site return type is wrapped toType::Grounded<T>(already-grounded returns are not double-wrapped). A sharedeffect_row_is_groundedhelper was extracted ineffects/grounded.rs(one source of truth, also used by the reachability analysis). 2 Design-X tests added (effect-induced grounding via a non-retrievaluser effect + a conditional guard). - 2c — blast-radius measurement (R6). ✅ ZERO residue.
cargo check --workspace --tests: 0 errors.cargo test --workspace --lib: only the pre-existingcorpus_scan_includes_deliberate_failurered (unchanged — goes green at slice 10).corvid-types: 222/222.verify --corpus: baseline unchanged. Everyexamples/*.corclean exceptrefund_bot.cor, which fails identically on HEAD (pre-existingpython importeffect-constraint issue, unrelated to grounding). The legacy rule (D5) + 2a’s contagion law absorbed the entire blast radius — not “a countable handful,” nil. R6’s stop-and-rescope gate does NOT trigger.
- 2a — contagion law, dormant. ✅ shipped.
-
Slice plan reshaped (2026-05-12, after slice-3 recon). The recon found two things: (1) “operator nodes carry grounded-result info” is already done —
IrExprcarriesty: Typeandlower_exprcopies it from the typechecker’sself.types, which slice 2 made contagion-aware; nothing to build. (2) The D5 discard node is real work (~4 coercion-site categories — return / call-args / typed-bindings / struct-fields — and lowering has no central coercion point today), but it is a@grounded_pureprerequisite, not an execution prerequisite: slices 4/5/6 (interpreter / native / control-flow) do not need it (combined_all.corruns onceeval_arithmetichandlesValue::Groundedoperands — the runtime does not type-check returns). So D5 moves to its own dedicated slice adjacent to its only consumer. Decisions D1–D8 untouched; execution ordering now tracks the real dependency graph. Phase grows 11 → 12 slices. -
Slice 3 — IR carries grounded-result info (verification). ✅ shipped.
corvid-ir: 2 lowering tests confirming slice 2’s contagion law flows end-to-end intoIrExpr.ty— a contagious operator’s IRBinOpnode hasty: Grounded<Int>, an ordinary one keeps the plain inner type. The IR-tier checkpoint. No production code:lower_expralready copies the typechecker’s per-span type intoIrExpr.ty, so slice 2’s contagion result flows in for free; this slice proves it and guards against regression. Gate green: corvid-ir 34/34, workspace check clean, wasm build clean, corpus baseline unchanged. -
Slice 4 — interpreter. ✅ shipped.
corvid-vm: a single contagion lift at the top ofeval_binop— if either operand isValue::Grounded, strip the wrappers (unwrap_for_op, fully recursive), run the normal operator on the inner values, re-wrap with theDerivedchain (D3) andMinconfidence (D4). Applicative-functor framing: arithmetic, equality, and ordering all lift through the one site. 4 unit tests + full workspace sweep green.Finding —
corpus_scan_includes_deliberate_failurewent green here, not at slice 10 as the plan predicted. Why: that test was red becauseverify_corpusaborted the whole scan when the interpreter run ofcombined_all.corcrashed (the originaleval_arithmeticTypeMismatch). The test only ever needed the interpreter crash unblocked — not all four tiers. The differential verifier compares effect profiles, and per D8 contagion does not touch effect rows, so once the interpreter run completes, all four tiers agree oncombined_all.cor’s profile. This does not mean the phase is done: the test going green is a weaker property than “the contagion law executes correctly on all four tiers.” Native codegen producing correct grounded values (slice 5), control-flow (6), the@grounded_puremoat (7–9), the D7 fixture (10), and the invention contract (11) are all still required deliverables — the profile-level differential verifier simply does not exercise them. -
Slice 5 — native codegen: value-correctness (reshaped 2026-05-12 after recon; CTO decision). Recon found the native grounded model is structurally degenerate: a
Grounded<T>is a bare machine value (grounded_attest_*returnsvalue_tyunchanged, registering provenance into a single global “last scalar attestation” slot). It has no per-value provenance tracking, so it cannot build the byte-identicalDerivedtree the contagion law (D3) requires — that needs grounded values to carry runtime identity (handles), a native-ABI rework rippling through native codegen + the C runtime + the attestation store + possibly the Phase 22 FFI surface. That is phase-sized, and per CLAUDE.md needs its own pre-phase design chat.CTO decision: honest sequencing, not a shortcut. Slice 5 ships native grounded-scalar value-correctness. ✅ shipped.
Recon during execution sharpened the scope. The initial plan was “strip
Grounded<>before all native operand-routing decisions.” Two of those landed cleanly; one did not:- ✅
is_wrapping_int_binop/is_wrapping_int_unop(corvid-ir) were grounded-blind — under@wrapping, a grounded-Intoperator lowered to the overflow-trappingBinOpinstead ofWrappingBinOp. Fixed with the sharedType::ungrounded()method. Grounded scalars (Int/Bool/Float) are now value-correct at native — and they were the only cleanly-tractable case, because scalars are not refcounted, so there is no ownership entanglement. - ❌ The
BinOpstring-routing fix (matches!(left.ty, Type::String)→ see throughGrounded) produces correct values but leaks: the dataflow / dup-drop passes treatGrounded<...>as opaque (ownership::is_refcountedreturnsfalsefor it — deliberately; making it see throughGroundeddouble-frees, proven by a broken parity test). So grounded-refcounted-type (String / List / Struct) operator value-correctness is entangled with the native grounded ownership model — the same model the follow-up phase reworks. Reverted; the string-routing site keeps aNOTEcomment recording why it is deliberately grounded-blind.
Net slice-5 scope: grounded-scalar value-correctness (the
@wrappingIR fix) + the sharedType::ungrounded()method (used bychecker/ops.rs,corvid-ir, and available tocorvid-codegen-cl). Grounded-refcounted-type operator value-correctness joins the native grounded-handle rework in thenative-grounded-handlesfollow-up phase — an#[ignore]dgrounded_string_concatparity test is filed as that phase’s executable spec. Verified bycorvid-irunit tests + the pre-existing grounded parity tests staying green (the profile-level differential verifier does not catch grounded-value correctness — see the slice-4 finding).The native grounded-handle rework — per-value provenance, the
DerivedDAG at native — is a dedicated follow-up phase, with a pre-phase stub atdocs/meta/native-grounded-handles-design.md. The “all four tiers” acceptance criterion is amended accordingly: native is value-contagious in this phase; native’s provenance-DAG-through-operators is the follow-up phase’s deliverable. - ✅
-
Slice 6 — control-flow conditions. ✅ shipped. Recon found Corvid has only
Stmt::If(andStmt::For, butforiterates, not Bool-conditions);whileandmatch/whendon’t exist as stmts — D2’s “if/while/match” was partially aspirational. Slice 6 covers every Bool-condition site that exists today:- Typechecker
Stmt::If(checker/stmt.rs) and eval-declassert <expr>(checker/decl_eval.rs) now usecond_ty.ungrounded()soGrounded<Bool>flows through. - Interpreter
require_boolstripsValue::Groundedrecursively (the centralised Bool-extraction helper); the if-stmt evaluator refactored to callrequire_boolinstead of its inline grounded-blind match (DRY). - Native: no change needed.
Grounded<Bool>is machine-identical toBool(a barei8),Boolis not refcounted (no ownership entanglement like slice 5’s refcounted-type case), andbrifon a bare bool already works. - 1 corvid-types test (
if_condition_accepts_grounded_bool) + 1 corvid-vm test (require_bool_strips_grounded_for_control_flow). Workspace sweep stays 0 failures.
D2’s “recorded + IR-visible” condition-unwrap defers to slice 7: the trace event + the IR-visible discard node ride on D5’s general discard-node mechanism, which slice 7 ships across all implicit-coercion sites (return / args / bindings / fields / conditions). Slice 6 makes the condition path work; slice 7 makes its drop visible for
@grounded_pure(slice 9).Out-of-scope follow-up:
&&/||operators still reject aGrounded<Bool>operand (the design doc’s existing non-scope for&&/||contagion);if grounded_bool && othertherefore still errors at the operator, not the condition. - Typechecker
-
Slice 7 — D5 discard node.
corvid-ir: lowering inserts the visible discard node (reuseUnwrapGroundedor a dedicatedCoerceGroundedDiscard) at every implicitGrounded<T> → Tcoercion site — return, call-args, typed bindings, struct fields. This is the@grounded_pureprerequisite: it makes every silent provenance drop greppable in the IR. Must be complete across all coercion sites or@grounded_pure(slice 9) is unsound.Shipped 2026-05-15/16 as a sub-split (commits
6bad408,942c7e7):-
7a — typechecker side table.
Checked.grounded_coercion_sites: HashSet<Span>populated by arecord_if_grounded_coercionhelper called at every value-flowis_assignable_tosite: return / let-with-annotation / yield / call-arg / struct-field-init (free + imported) /Some(...)against anOption<T>expectation / list-element flowing into an inferred-bare element type / replay-arm +else-arm bodies /if-condition (D2 grounded-tolerant but still IR-visible). Operator unification and signature compatibility sites are intentionally NOT recorded — those aren’t value-flow sites. Test coverage pins exact-span equality at the four most visible sites so any new site added without a recorder call breaks the test, not the moat. -
7b — IR insertion + runtime alignment.
corvid-ir’slower_exprwraps the producedIrExprinIrExprKind::UnwrapGroundedat every recorded span. The inner expression keeps itsGrounded<T>type; the wrapper carries the stripped inner type. Inserting the node surfaced a pre-existing soundness gap: the runtime only wrapped tool results inValue::Groundedwhen the literal effect name was"retrieval", but Design X (slice 2b) had promoted ANYdata: groundedeffect’s return toType::Grounded. Closing the gap inside 7b (required by the validation gate):IrTool.produces_grounded/IrPrompt.produces_groundedcomputed once at lower time via the now-publiccorvid_types::effects::effect_row_is_groundedhelper.maybe_ground_tool_resultreads the field instead of name-matching"retrieval"; idempotent on already-Grounded values so adversarial double-wrap paths stay safe.- New
maybe_ground_prompt_resultmirrors the tool path for non-stream prompt finalisation incorvid-vm::interp::prompt::finalize_prompt_result. Stream prompts are explicit non-scope here — chunk-by-chunk grounding is a larger contract. UnwrapGroundedruntime semantics preserve confidence while discarding provenance:Value::Grounded(v, chain, conf)strips to barevwhenconf == 1.0, otherwise re-wraps asValue::Grounded(v, EMPTY_CHAIN, conf). The split honours slice 7’s intent — “where it came from” is what’s being discarded; “how trusty it is” is a separate concern downstream confidence-gate checks still need. Defensive no-op on already-bare values keeps an out-of-sync tier from panicking the interpreter.
-
-
Slice 8 —
@grounded_purefront end.corvid-syntax/corvid-ast/corvid-resolve: parse + represent + resolve the attribute. -
Slice 9 —
@grounded_pureproof.corvid-types/corvid-ir: the reachability proof obligation (D6) — noUnwrapGroundedand no slice-7 discard node reachable in a@grounded_pureagent’s body — plus the attribute-composition test matrix (R5). -
Slice 10 — corpus fixtures (D7).
combined_all.cor’smain()→-> Grounded<String>so the corpus shows idiomatic Corvid (the program genuinely produces a grounded value); new dedicated legacy-coercion fixture exercising the slice-7 discard node. Note:corpus_scan_includes_deliberate_failurealready went green at slice 4 (see that slice’s finding) — this slice is now just the fixture work, not the test-revival. -
Slice 11 — invention-shipping contract. README catalog entry,
corvid tour --topic provenance-propagationdemo,inventions.mdproof-matrix row, spec section,learnings.mdcloseout, ROADMAP tick.
Estimated ~3 weeks. The phase is large because it is honest: a language capability that is not end-to-end across all four tiers is not shipped.
Invention-shipping contract (CLAUDE.md)
Provenance Propagation is a nameable Corvid capability. Before the phase closes, slice 11 must deliver:
- README catalog entry — “Provenance Propagation” with the
contagion law + the
@grounded_pureguarantee. corvid tour --topic provenance-propagation— a demo whose source compiles through the normal driver pipeline, showingGrounded<T>flowing through operators and a@grounded_pureboundary refusing a laundering attempt.docs/reference/inventions.mdrow — shipped status, runnable command, test coverage, spec link, explicit non-scope (Pillars 1 & 2 deferred).- Spec section — defines the contagion law, the
Derivedrepresentation,@grounded_pure’s proof obligation. - Tests — validate every claim the catalog entry makes.
Acceptance criteria for closing the phase
- All 12 slices (0–11) on
main. -
Grounded<T>contagious through arithmetic, concat, and comparison — verified across checker, interpreter, and native (native = value-correctness; native’s byte-identical provenance-DAG is thenative-grounded-handlesfollow-up phase per the slice-5 CTO decision). Replay consistency verified for the tiers in scope. -
@grounded_purecompiles clean agents and refuses laundering agents; orthogonal composition with@deterministic/@replayabletested. -
tests/corpus/combined_all.corcompiles + runs consistently across all four tiers;corpus_scan_includes_deliberate_failuregreen. -
cargo test --workspacegreen; corpus baseline otherwise unchanged. -
corvid-runtime-corestill builds towasm32-unknown-unknown(theDerivedaddition must stay wasm-clean). - Invention-shipping contract delivered (slice 11).
-
learnings.md+ ROADMAP updated.
What’s out of scope
- Pillar 1 polish — rich how-provenance DAG rendering in
corvid trace dag. Data model ships here; rendering is a follow-up. - Pillar 2 — compile-time
@confidencebudget enforcement through arithmetic, and per-operator confidence-composition rules. RuntimeMincomposition ships here; the compile-time pillar is its own phase. &&/||contagion — short-circuit booleans route specially today; folding them into the contagion law is a small follow-up if it proves worth it.- Performance — the
Derivedchain’s memory footprint is measured-not-optimized; a separate slice if it becomes real.