Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Argon RFDs

Design decision records for the Argon language, runtime, and toolchain.

An RFD records why a choice was made. It is not the specification. The Lean 4 mechanization at spec/lean/ is canonical for the substrate; the reference manual (Part I of this book) describes the surface in prose. RFDs explain how those landed where they did.

Format

  • Filename: NNNN-kebab-case-name.md. Four-digit, no reuse. Numbers are not recycled even when an allocation is abandoned, so the sequence may carry gaps: 0012 was never authored — the number was skipped during a period of concurrent branch allocation (the same churn that renumbered the refinement RFD 0016 → 0017 once numeric-tower took 0016 on main) and the slot was left empty rather than reused.
  • Structure: Question · Context · Decision · Rationale · Alternatives · Consequences · Open questions.
  • States:
    • discussion — open; not committed.
    • committed — decided and binding. Implementation may or may not have landed; the decision is fixed.
    • accepted — partially implemented — decided and binding, with implementation landed in part; the State: line annotates what has shipped and what remains.
    • superseded — replaced by a later RFD. Cite the successor.
    • archived — the record outgrew this directory and moved intact (with history) to archive/ at the repository root. The number stays allocated; the index row points at the new location.

Authoring an RFD

RFDs live at spec/rfd/ in the repository. The published book reflects whatever is on main. To open a new RFD, allocate the next number, write the file, and submit it via PR — discussion happens on the PR or in linked threads, not by gating merge on consensus.

The format mirrors Oxide Computer Company’s RFD process (Yegge / Dijkstra-via-Bryan-Cantrill lineage) but lighter: no separate authoring tool, no review-state tracker, no required pre-commit lint. A repository PR is the discussion vehicle; the RFD’s State: field carries the decision status.

Index

#TitleState
0001ID architecturediscussion
0002#[comptime] attributediscussion
0003Reasoner backend dispatchdiscussion
0004pub fact declarationsdiscussion
0005Relation subsumptioncommitted
0006Field mutability via mutdiscussion
0007Missing-value semantics under OWAdiscussion
0008Standpoint-sheaf equivalence proof roadmapcommitted (Path A landed)
0009std::mlt library scopediscussion
0010Negative facts / strong negationdiscussion
0011Aggregate semantics under OWAdiscussion
0013Toolchain distributionaccepted — partially implemented
0014Runtime serving surfacediscussion
0015mutate body surface: EdgeQL-shaped, set-semanticcommitted
0016Numeric tower: exact by defaultcommitted
0017Refinement classification: where (primitive) vs iff (defined)committed
0018Production reasoner: the incremental DBSP engineaccepted — partially implemented
0019Mutation write-path correctness: construction, identity, read-your-writes, exact valuesaccepted — partially implemented
0020The runtime data engine: a composable query + reasoning pipelineaccepted — partially implemented
0021The reasoner execution engine (as built): joins, optimizer, factorization, BYODS, incrementality disciplinecommitted
0022Package-path addressing (pkg, not crate) and the build evaluability gatecommitted
0023Reflective TypeRef: type-as-value in the meta-calculus (RP-003 GAP-1)committed
0024Allen interval algebra as a library (std::allen), not substrate operatorscommitted
0025check discharge: vocabulary-staged compile-time and runtime constraint checkingaccepted
0026Trait rule members: clause-union dispatch, conformance, and the implements intrinsicaccepted — implementation planned
0027The meta-property plane: axis bindings, catalog tiers, value-position resolution, and substrate-neutral modifiersaccepted — implementation planned
0028Defeasibility redesign: honest heads, the defeat-directive plane, and strategy as a compilation schemeaccepted — implementation planned
0029Derived values and aggregate terms: body-level binding, aggregate sources, roundingcommitted
0030Package dependencies ([dependencies], path deps v1)committed
0031The relation-constraint plane + meta-property completioncommitted
0032oxup manages editor-extension installationcommitted
0033The ad-hoc query and mutation surfaceaccepted — implemented
0034Source text encoding and the Unicode lexical policy (UAX #31 identifiers, NFC, module reachability)committed
0035The composable operator-tree execution pipeline: shared LogicalPlan lowering, tree optimizer, physical mapper, generalized executor, table operatorsdiscussion
0036Heterogeneous and specialized data stores: foreign federation, external-valued attributes, persistence swap; the connector SPI, placement, world-assumption tieringdiscussion
0037The macro atom: a phase-separated, hygienic, declarative-first expander over surface syntaxdiscussion
0038The prelude, ambient scope, and symbol-precise stdlib loadingdiscussion
0039Composable mutations: nested invocation and derived readsaccepted — implemented
0040The procedural macro layer: a total, structurally-recursive meta-language over reflected syntaxdiscussion
0042The re-checkable emission boundary: a self-validating .oxbin and sound direct artifact emissiondiscussion
0043Theory packages and the neutrality boundary: where ontologies and higher-order theories livediscussion
0044Package registry, workspaces, and distribution: a static content-addressed registry over the CDNdiscussion
0045The world-assumption write-side: refuse-on-K3-not and the #[world] per-concept opt-indiscussion
0046Derivation serving surfaces: query, delta, explain, trace (kill the per-rule derive loop)discussion
0047The temporal value library (TC39-Temporal-modeled std) and the value/ontology boundarydiscussion
0048The test atom: in-language unit tests, and why a test is substrateaccepted — partially implemented
0049Error-tolerant diagnostics: recovery, source-faithful expansion, What/Where/Why/Fixdiscussion
0050Documentation architecture: three books, correctness by construction, and a verified authoring pipelinediscussion
0051oxfmt: a canonical, idempotent source formatter over the shared CSTdiscussion
0052Deployment topologies, the connection abstraction, and host-language parity: embedded vs. standalone, Rust/TS parity, access-vs-durabilitydiscussion
0053The standalone concurrent engine: transactor, MVCC reads, and IVM-in-lockstepdiscussion
0054Set-valued, characterization-relative composition over standpointsdiscussion
0055The conformance corpus: a standing golden differential-correctness gateaccepted
0056Enactment-time-bitemporal rules: the law as it stoodaccepted
0057Argon as a served platform: the operational host (loopback engine + auth gateway, OTel, lifecycle, config, packaging)discussion
0058The Tableau Model Engine (modal escape hatch)proposed
0059The drift gate as a typed interface manifestaccepted
0060The Lean substrate mechanization architectureaccepted
0061A single VERSION file is the source of truth for the toolchain version (git-independent, read at build time; the tag is cut from it, the atomic advance is the only bump)accepted
0062CI tiers + affected-scoping: main green by construction — explicit PR/queue/post-merge tiers (named setup outputs), cargo xtask affected scoping, a curated merge-critical release-gate, and a documented post-merge accepted-risk setaccepted
0063The bitemporal read-point: the snapshot at now for derived reads (valid-time + transaction-time; clock frozen at the boundary; cache validity-epoch guard)accepted
0064Compiler source decomposition: add-only hotspots, a ~3,000-LOC per-file ceiling (with the irreducible runtime-spine exception), grammar.d//lower//commands//per-concern impl Store siblings, and content-free merge=regenerate codegen mergesaccepted — implemented
0065The runtime⟷engine seam: oxc-value (the shared value vocabulary as a leaf crate) + the QueryEngine facade trait, making engine and runtime disjointly ownable without splitting the unified RFD 0020 design; records the FORK-2 resolution (the LogicalPlan operator tree is live on the production query path)accepted
0066Agent integration: the Argon MCP server (oxc-mcp / ox mcp) — a runtime-provenance-led third frontend over the transport-agnostic dispatch core (sibling to oxc-serve), backed by RuntimeService for coherence (build → reload → query → explain), with a coherence invariant, ephemeral/promotable forks, and an honest why-not boundarydiscussion
0067The temporal literal surface, carrier reach, and the four-valued comparison frontier: one #…# delimiter with inner-shape kind determination (resolving #1285 — #…Z# is the Instant literal), nanosecond valid-time reach (agents/robots + legal on one substrate), out-of-range valid-time must refuse not saturate, astronomical time as a modeled domain not a civil-carrier stretch, and the native-four-valued-comparison decidability obligationdiscussion
0068Native four-valued temporal comparison: decidability of a paraconsistent metric reasoner over ℤ — funds RFD 0067’s sole open research obligation and records the mechanized result (kernel-clean, #print axioms-verified) that the designated non-explosive Both does not break the ultimate-periodicity that buys decidability (the twist coordinate-wise reduction), the binding constraint that metric operators must propagate the full Belnap pair, and the shift-invariance restriction the decidable fragment requiresdiscussion
0069The shared-live-KB write model: concurrent authoring, coexistence, and resolution — many writers on one durable store with durable disagreement. Reasoning-native coexistence (append-Both fold, never the retraction group-wall); two operators not one (monotone LUB merge + a read-time defeasible resolver — a coexistence/resolution split corroborated across six independent fields; the composition soundness is deferred to Lean); per-source-extent Single-Writer write unit; the KB as a verb (lazy cross-standpoint fold); a new tertiary cross-standpoint-designation layer (legal-pluralism forum-priority); the CALM monotone/non-monotone coordination boundary; external decorrelated verification for cross-writer trust; and the retirement of promote()-as-last-writer-winsdiscussion
0070Derive-backed classification: the shape clause — a shape-defined concept is an ordinary IDB atom of the reasoner’s WFS/AFT fixpoint, with a stated + mechanized realization biconditional; the guardrail floor; the construct count; the Can-membership dispatch policycommitted
0071Agent-tooling distribution: the top-level agents/ tree (per-host peers, single-source discipline), the ~/.argon/.agents/ oxup-owned home, and oxup agents register/unregister/list registrationcommitted
0072Persisted derived read-model: populate .oxbin section 6 (projection-cache) with the build-time serialized RelationCatalog — deterministic CBOR (amending the never-implemented Cap’n Proto sketch; a named artifact-tier deviation from 0035 D7’s persisted-columnar, reconciled via payload_version), validity keyed on a length-framed derivation-input commitment (artifact_hash ∥ enumerated GlobalControl semantics incl. tzdb ∥ mapping ∥ axes ∥ DERIVATION_SEMANTIC_EPOCH) + an unforgeable pristine-seed marker (generation tuple as cross-check only), consumed fail-closed by the strict read path (world/metric restamped; v1 fragment = undated + clock-free + non-temporal, re-derived consumer-side; registries-empty + tzdb-pin + preflight-bounded-decode preconditions; provenance-scoped defaults enforcing the trust boundary), gated by a cache-seeded ≡ recomputed differential arm driven by the production routing predicate (doc-hidden audit API) + golden conformance entries (incl. poisoned-cache + epoch-mismatch fixture); section 7 stays reserved and its OE1205 refusal gets wireddiscussion
0076Mutability of relation ends: apply the field-mutability rule (mut? Ident : TypeExpr, immutable default) to relation endpoints — mut opts an end into value variation through tuple retract/assert history, the immutable default binds the end for its dependent context’s lifetime; effective mutability is conjunctive over relation subsumption; the write path gains retraction, assertion-freeze, dependent-context cascade, and subsumed/derived-extent gates plus a RetractIndividuals kernel effect, and the reflection plane exposes the effective per-end is_mut bit as armMutability; rides a MAJOR core-IR bumpdiscussion
0079Main rewind and contract-driven reimplementation: rewind main to 59a49045 (old tip preserved as dev), re-land the 512-commit performance-and-features range through a phased, contract-reviewed PR sequence; companions in archive/rfd/0079/ carry the per-subsystem architecture record, the behavioral-contract catalog, the test triage, and the baseline-latent bug-fix inventory. Numbers 0073–0078 are allocated on dev and appear here when their content re-landsarchived
0081Check-pass rule registry: one auditable rule table per enumeration axis (BODY, ITEM), each entry data (codes, mask, fn, driver-vs-inline wiring); adopts pull request #1727’s body-walk driver as the BODY axis, adds a for_each_item + ItemKind-mask driver for the ITEM axis, and inventories inference-interleaved and cross-layer gates as inline rows rather than migrating them; exactly-once-per-axis and byte-identical-per-wave invariants; motivated by the pull request #1701 / #1727 seam-hole historydiscussion
0082Cross-module defeat: import-scoped #[defeats] resolution over the importing module’s visible names (defeat edges stay in-package), the combined-artifact defeat pass at workspace assembly, connected-component strategy identity, cross-package refusal (in-workspace only), and #[default] as the cross-module override-consent token — successor amendment to RFD 0028, discharging the deferred workspace-scoped-resolution workdiscussion
0083Defeat-plane compilation: realize RFD 0028’s Governatori strategy by compiling the mechanized transform (Transform.lean) at elaboration into ordinary strict WFS rules in the .oxbin — support / blocking / team-defeat-fold / F2-reinstatement as $-qualified internal relations — deleting the interpretive runtime resolver (oxc-runtime/src/defeasible.rs) whose per-head memo + reentrancy guard + fail-open fallback produced the order-dependent memo-poisoning / under-suppression / recursive-default-fail-open / short-name-collision bug family; recursive #[default] dependencies admitted (WFS-native), defeat-graph cycles still refused (OE0718); executable defeat metadata stripped and re-homed to an inert DefeatCompilationProvenance carrier so no old runtime double-applies; artifact grows (format minor, no Core-IR bump); elaboration-time switch (OXC_DEFEAT_ELABORATE selects a compiled vs a legacy artifact — the artifact hash is the cache discriminator, so no epoch bump); oracle-clean acceptance (PR #1793) + Lean golden transformed-program vectors via EmitVectors; the recursive-fixpoint theorems are the scoped Lean prerequisitecommitted
0084Mapped relation subsumption: the explicit argument-mapped subsumption form, keyed by parent end name, becomes the ONLY surface — a child names, for each parent end, what fills it (Home(p, c) <: Loc(p = p, c = c)), may pin a parent end to a constant (WagesUSA(p, amount) <: IncomeItem(p = p, amount = amount, c = USD)), and may cast a child end to a parent supersort; both the bare form (RFD 0005) and the positional filler list are removed and refused (positional fillers re-pair silently under a same-sort parent-end reorder — verified empirically — so only parent-name keying is mechanically reorder-safe); no same-name abbreviation (p = p, never bare p); a breaking source change with a mechanical in-repo rewrite (identity filler lists read off each child/parent declaration; semantics-preservation proved by mappedChecks_identity_eq_bareChecks, which survives as the internal migration-soundness witness); subsumption generalizes from tuple-inclusion to mapped-tuple-inclusion (the image of the child under the declared mapping φ); the canonical family-query surface is the trailing Parent(args) via k (own-extent witness — each row once under its declaring relation), whose meaning is the generalized specializes(k(memberArgs), Parent(parentArgs)) atom (its one-desugar-point target and spec-level semantics; correspondence is the declared mapping, never visual position) — the parent frame is information-complete because D1 forbids dropped child ends; threads the mapping through end-mutability inheritance (RFD 0076 OE0267/OE0268), the retract/amend cascade, relation-value application dispatch (pull requests #1805/#1806, merged), reflection ($specializes carries the mapping — the wire/reflection field is additive optional-on-decode), coverage/OE1404, and the incremental circuits (project/map/union); maintainability by construction — one canonical resolved-mapping structure read by every plane, one surface spelling (no sugar to drift), exhaustive wildcard-free matches over the mapping-entry enum, refusals over silent adaptation, and dispatch/cascade as the same φ with the shared-translation-function obligation on the dispatch slicediscussion
0085Incremental rule circuit compiler: compile accepted rules into signed persistent delta circuits with exact Z-set arithmetic, operator-local state, atomic frontiers, recursion, checkpoint recovery, and full-evaluation differential gatesaccepted — partially implemented
0086Admission-point checks: an undecided membership stops a blocking check’s write, and only reports under a reporting one — a check whose violation body reads membership in a computed (iff/shape-defined) concept gets a first-class inconclusive verdict, disposed of per check from the severity the modeler already wrote, and is judged at the admission point of the data it governs (the write’s valid-time instant, or the all-times view) rather than over the whole store at commit time. Mechanism: membership the well-founded fixpoint leaves undetermined (three-valued Can — a negation/defeat standoff or open-world absence) is neither collapsed to a pass nor fired as a false accusation, replacing the measured status-quo silent-pass (probed on the default branch: a Severity::Error check over undetermined membership fires only an [undefined]-grade observe note and the write commits); disposition of a new inconclusive tuple requires no new surface — blocking (Severity::Error) refuses the write (OE1430), reporting severities and #[observe] report at undefined grade, and an => emit check simply does not emit; a per-check firing closure means a write outside every closure runs no discharge; a point-free judgment of point-variable membership is refused (unimplemented, no diagnostic code allocated; three variability sources: dated evidence, dated rule window, clock-reading classifier) on the write / ox derive / on-demand surfaces; an aggregated standoff re-registers OE1332 as OE1430; the check-rule registry (RFD 0081) stays the dispatch substrate, taking the runtime gate as a single inline inventory row and no driver-wired row; a hard resolution-pass boundary reads resolved identities and never rewrites resolution or introduces a second scope-identity carrier; the Lean obligation extends Reasoning/Checks.lean with the inconclusive set as wfPossible ∖ wfTrue over membership atoms and a no-collapse partition theoremcommitted
0087Fixpoint typed views: a typed navigation view (f: [T] from Rel.endpoint) whose element type T is standoff-capable (a shape-clause concept whose membership is a rule head the well-founded fixpoint can leave undefined; a braced iff { P } refinement cannot stand off and is out of scope in both directions) is evaluated IN that fixpoint rather than materialized store-locally — a single pin-resolution ladder keys every concept-pinned field read, a raw (extent-blind EDB) + gate (fixpoint rule) split keeps a fixpoint-decided extent live and delta-maintainable, and the K3 standoff at the view boundary is surface-honest (definite extent served, undefined region rides $undefined:: and surfaces loudly — never silently included or dropped, the consumer adjudicates); the ladder CONSUMES the per-scope resolution pass (no rewrite, one scope model), a view over a subsumed-parent relation walks the φ-images of RFD 0084, filtered views over a standoff-capable element type refuse fail-closed, and it shares ONE principle with the admission-point-checks RFD (undetermined never silently coerces; each construct declares its own policy) with independent policies and no shared dispatch structure; rides a derivation-semantic epoch bump. The standoff policy’s aggregate arm is the one piece still undecideddiscussion
0090In-body determinedness tests — the ADOPTED surface for per-check disposition of an undetermined membership verdict (owner decision 2026-07-24), chosen over a declaration-level #[undetermined(...)] disposition attribute that this record carries as its rejected alternative (the two surfaces are mutually exclusive): a check-body atom undetermined(A) / determined(A) reading the converged three-valued model (holds iff the membership atom’s verdict is Can), so cascading policies inexpressible under the single-disposition attribute — statutory tie-breaker ladders, definite-blocks-while-undetermined-reports in one check — become body branching; check-plane-only by a stated soundness argument (a determinedness read in a derive body is truth-value reflection into the fixpoint, circular under WFS, refused OE1431; anchored to wfPossible ∖ wfTrue in Checks.lean); a conservative three-valued exhaustiveness warning (OW0744) over unhandled undetermined paths, falling through to RFD 0086 slice 1’s severity-derived default; shared base is the slice-1 honesty gate (pull request #1826); comparison table (expressiveness vs. near-zero compiler cost + grep-able governance) and an honest maintainability account (new expression form, the standing plane boundary, the rot-prone coverage pass); implementation open in pull request #1848, stacked on the slice-1 honesty-gate base (pull request #1826); the rejected attribute’s own implementation was drafted in pull request #1840 and closed unmergedcommitted
0093iof denotes membership, whole: iof(x, C) means an individual’s FULL membership — asserted, <:-closed, predicate-refinement-derived, and rule-derived alike — with extent and meta following it, so that C(x), x : C, and iof(x, C) are three exact spellings of one question, equal under substitution of a bound type variable; the seed-time $iof materialization, which answers empty for a shape-defined concept (RFD 0070) whose members the reasoner computes, is an incomplete implementation of an already-ratified meaning (RFD 0017, RFD 0023, RFD 0070 L5, Realization.lean) rather than a narrower definition, and the same seed scan breaks meta(x) == C as a third silently-empty spelling; the remedy makes the reflection relation carry derived membership — the semantic decision asked for; the implementation mechanism stays open between generated bridge rules ($iof(x, C) :- C(x) plus <: closure over the catalog-closed $specializes), the form the record is written against, and rewriting constant type positions with a single materialization above every read, which is semantically indistinguishable — deleting the individual-tier seed scan while the declaration-derived catalog tier stays extensional, turning extent(t) into a projection and $meta’s individual tier into an antichain over $iof (measured equivalent row-for-row, and deliberately NOT extended to the catalog tier, whose meta is the exact declared introducer); the bridge is positive so concept↔relation recursion is monotone, in-component negation is evaluated by the well-founded semantics exactly as a shape classifier in a negation cycle already is, undefined membership stays the third truth value and must not collapse to false, and recursion through aggregation stays refused (OE1317); requires NO change to the Lean mechanization, where membership is an opaque catalog field and extentOf is defined from it; REJECTS this record’s own first remedy (rewriting only the statically-known iof(x, C) and refusing meta(x) == C) because rewriting only the constant form makes iof(x, Voter) and iof(x, t), t == Voter extensionally unequal, breaking substitution for a first-class type-value-polymorphic predicate; DECLINES a second asserted-plane intrinsic as not load-bearing, unmotivated by any use case, already covered on the write path by OE0211, and — being identifier-lexed unlike the keyword-lexed iof — a needless expansion of the reserved-name gates; records that the qualified type test x : pkg::C is already fail-closed (OE0223) and needs a better message rather than a new diagnostic, and that x is T is not a membership spelling at all; six slices from an executable oracle pinning today’s wrong answers through derived $iof (the first behaviour-changing slice, epoch 12 or later) to optimization last; RFD 0092 remains the alternative on the whole surfacediscussion
0094Rule-body reads of a completed undefined companioncommitted

RFD 0001 — ID architecture

  • State: discussion
  • Opened: 2026-05-27
  • Decides: identifier types used across oxc-protocol, oxc-oxbin, oxc-instantiate, oxc-runtime, oxc-storage-mem, oxc-storage-pg, oxc-reasoning.

Question

Argon currently identifies every declared symbol, every event, every partition key, and every runtime tuple element with a 16-byte uuid::Uuid. Across ~286 call sites and ~21 distinct identifier roles, this is one type doing many jobs. The reasoner — which IS the data system — sees UUIDs in every Z-set tuple and every storage index entry. Is uuid::Uuid the right identifier type for Argon, and if not, what is?

Context

How identifiers flow through Argon

A modeler writes Argon source. The compiler lowers it to a sequence of typed axiom events stored in a .oxbin artifact. The runtime loads the artifact into a Module, opens a Store against a StorageBackend, executes mutations that emit more events, and answers queries by reasoning over the resulting fact set.

Identifiers appear at every stage:

StageIdentifier shapes
Sourcequalified_path: String (“demo::Person”)
ASTUUIDs minted from Uuid::new_v5(WORKSPACE_NS, facet ++ qualified_path)
Wire (.oxbin)UUIDs in 70+ body fields and 7 mandatory event-header fields
Storage indexesUUIDs in BTreeMap keys
Reasoner Z-setsUUIDs encoded as Value::Id(Uuid) in tuple bytes
Hot pathUUIDs everywhere; 16 bytes per identifier, 17 bytes per identifier with CBOR framing

Why the choice is load-bearing

In a system whose hot path involves billions of tuple comparisons in Z-set joins, the identifier IS the data structure. Concrete costs at scale:

  • AxiomEvent header: 128 bytes of identifiers per event (7 mandatory UUIDs + 1 SHA-256). At a billion events: ~104 GB of just IDs in the header.
  • Storage indexes: every (LiveKey, HistKey) entry carries two UUIDs (tenant + fork) plus a kind tag. ~40 bytes per index entry.
  • Z-set tuples: every Value::Id is 17 CBOR-encoded bytes. A 3-arg relation tuple is ~55 bytes for IDs alone.

Survey: what comparable systems do

We studied three prior art systems carefully (Kora, Nous, the generic UUIDv8 graph-DB proposal). Each made specific design choices that don’t map cleanly to Argon:

  • Kora (/Users/ivanleon/Code/wt/eidos/main/): Iri(NonZeroU32) backed by a process-wide LazyLock<ThreadedRodeo> interner. Per-engine ConceptIndex with TOP=0, BOTTOM=1 sentinels — owl:Thing / owl:Nothing baked in. Global static state. Tightly coupled to OWL semantics.
  • Nous (/Users/ivanleon/Code/wt/orca-mvp/main/crates/nous/): define_id! macro generating newtyped u64 ids derived from FNV-1a 48-bit hashes of qualified IRIs. Sparse → dense IdBridge rebuilt per schema; DERIVED_ID_BIT = 1<<63 overload on IndividualId. 48-bit hash collides at ~2^24 entries.
  • Generic graph-DB recommendation (UUIDv8 outer + 64-bit InternalId inner, with HLC timestamp inside the UUIDv8): the HLC timestamp inside the wire ID breaks byte-deterministic builds, which is a non-negotiable Argon property.

None of these are right for Argon directly. The substrate-neutrality requirement (no OWL Thing/Nothing), the per-build determinism requirement (no HLC inside wire IDs), and the multi-axis partition model (tenant × fork × standpoint × module — none of which are subordinate to the others) all push toward a custom design.

Decision

Argon defines ten identifier types, each tuned to its identity-source and role. No uuid::Uuid anywhere. The uuid crate is dropped from the workspace.

The types

TypeBytesIdentity sourceScopeRole
Iri(Arc<str>)Modeler-authoredI/O boundaryQualified path: "demo::Person". Surface contract; never in hot paths.
NameRef(NonZeroU32)4Symbol-table positionPer-workspaceWire-format identifier for every declared symbol (concept, relation, module, standpoint, metatype, metarel, metaxis, trait, impl, struct, enum, rule, query, mutation, compute, sink, macro, test).
TenantId(NonZeroU32)4Provisioning tablePer-deploymentTenant partition key.
ForkId(NonZeroU32)4Fork tablePer-tenantFork partition key (per-tenant scope).
IndividualId(NonZeroU64)8System-allocated counterPer-(tenant, fork)Dynamic individual identity. Replaces caller-provided UUIDs; external identifiers are data (a hasExternalId property), not identity.
EventId(NonZeroU64)8Build-deterministic counter (compile-time); HLC-derived Snowflake (runtime)Per-(tenant, fork)Per-event identity. Layout: `[tx_seconds: 32
AxiomKey([u8; 16])16BLAKE3-128 of canonical bodyPer-(tenant, fork)Logical proposition identity. Same proposition asserted twice has the same AxiomKey.
InternalId(NonZeroU64)8Built at Module::load; thrown away at unloadPer-Module-loadRuntime-only hot-path id. Layout: `[kind: 8
ContentId([u8; 32])32BLAKE3-256 of bodyContent-derivedCryptographic content hash. Replaces SHA-256.
CompositionSignature([u8; 32])32BLAKE3-256 of composition inputContent-derivedWorkspace composition signature. Replaces SHA-256.

Amendment (2026-06-11, issue #270 / PR #285). The NonZeroU32 → INT4 mapping above carries an invariant the original RFD left implicit in the Postgres encoder. Recorded here so call-site comments can cite it: a NameRef’s Postgres wire mapping is INT4 with the high bit reserved — the valid band is [1, 2^31-1]. This is enforced by the sqlx::Encode impl (oxc-protocol/src/ids.rs), which signed-converts through i32::try_from and refuses any value with the high bit set rather than letting it wrap negative on the wire. The 0 slot stays the NonZero* niche sentinel; the top bit is held in reserve. Two consequences follow. First, any hash-derived NameRef (the property_id_for_field / reflective_sort_name_ref / individual_id_from_name stand-ins, which fold a BLAKE3 prefix into an id pending the symbol-table lift) must mask into the band — & 0x7FFF_FFFF, zero-folded to 1 — or a coin-flip of field names would set the high bit and abort the mutation (PR #285). Second, folding a hash into 31 bits is not injective; PR #285 adds the load-time collision gate (OE0231) so two distinct Type::field pairs that alias one id refuse loudly instead of silently sharing a storage column. The hash stand-ins are a bridge: the sequential interning table that derives NameRefs from canonical symbol-table position (Phase 3 below) is the production follow-up tracked at #270, and it retires both the mask and the gate.

Two ways identity is derived

The 10 types split cleanly on identity-source:

Content-addressed (deterministic from source content; same source → same byte):

  • NameRef — derived from symbol-table position, which is derived from canonical-sorted qualified paths.
  • AxiomKey — BLAKE3-128 of canonical body bytes.
  • ContentId — BLAKE3-256 of body bytes.
  • CompositionSignature — BLAKE3-256 of composition input.

Allocation-addressed (system-allocated counters, deterministic per-build):

  • TenantId, ForkId — provisioning tables (counters under operator control).
  • EventId — per-event counter (deterministic in build mode; Snowflake in runtime).
  • IndividualId — per-mutation counter (deterministic within a build pass).

Iri is a surface artifact; InternalId is a runtime-only artifact. Neither participates in wire identity.

BLAKE3 unification

AxiomKey (128 bits) and ContentId (256 bits) come from a single BLAKE3-256 invocation of the canonical body bytes. AxiomKey is the first 16 bytes; ContentId is the full 32 bytes. One hash invocation per event.

BLAKE3 replaces SHA-256 throughout because: ~3× faster, parallel-friendly, same cryptographic strength, smaller code size. The replacement is a one-time wire-format change tied to this RFD.

Per-lattice sentinels (engine-local, never on wire)

Within a single Module load, the reasoner builds InternalId-space per lattice (per metatype’s subsumption lattice, per standpoint lattice, per refinement lattice). Within each lattice, the reasoner reserves:

  • The lattice’s (universal) at the lowest available InternalId for that lattice’s kind+partition.
  • The lattice’s (inconsistent) at the next one.

These are engine-local artifacts, recomputed at every Module::load, never written to the wire. The lattice’s actual top and bottom are declared concepts (e.g., the universal in a metatype’s subsumption lattice is a concept declared in stdlib like std::mlt::Class); the engine’s reservation is purely an evaluation-time optimization for short-circuit operations.

This differs from Kora/Nous, which reserve TOP=0 / BOTTOM=1 globally across all concept IDs — that’s an OWL-ism (a single owl:Thing for the whole ontology). Argon doesn’t have a single universal; each lattice has its own bounds, and they’re declared entities, not primitive IDs.

Type-distinct newtypes via the define_id! macro

Each ID type is a newtype with its own Display, Ord, Hash, Serialize, Deserialize, big-endian wire encoding, and NonZero* niche for Option<_>. Generated via a single define_id! macro (inspired by Nous’s pattern). Crossing roles requires explicit conversion — no accidental TenantIdForkId confusion at the type level.

Rationale

Why NameRef instead of UUIDv5(path)

The .oxbin format already mandates a symbol-table section (§D.5) with HDT-PFC-compressed canonical-sorted qualified paths. We’ve been routing around it by minting UUIDv5(path) instead of using the symbol-table position directly.

UUIDv5(path) gives:

  • 16 bytes per reference
  • Determinism (same path → same UUID)
  • Cryptographic collision resistance

NameRef = symbol-table position gives:

  • 4 bytes per reference
  • Determinism (canonical sort order)
  • Zero collision risk (by construction — positions are unique)
  • Native: the dictionary the format already requires

The savings are 12 bytes per declarative reference. Across hundreds of bodies in a real workspace with hundreds of references each, this is substantial. And the property — same source → same NameRef — is preserved.

Why content-addressed AxiomKey at 128 bits, not 64

A 64-bit AxiomKey saves 8 bytes per event header — ~5% of the header. The cost: birthday-collision probability ~37% at 2^32 entries; necessitates collision-handling machinery (either deterministic re-salting or content_id fallback verification on every lookup).

128 bits is birthday-safe past 2^64 entries (effectively unbounded). Collision handling unnecessary. BLAKE3-128 is fast (free, given we compute BLAKE3-256 for ContentId anyway). The 8 bytes saved on the event header aren’t where the storage wins live — those are in declarative _id fields (16 → 4 = 12 bytes saved each, multiplied across every body) and Z-set tuples (17 → 9 bytes per ID, multiplied across millions of tuples).

The architectural rule: don’t compromise the wire format for bytes that aren’t on the hot path.

Why IndividualId is system-allocated, not caller-provided

Pattern: every database treats internal entity identity as surrogate, and external (caller-provided) identity as data. Postgres uses BIGSERIAL PRIMARY KEY + external_id TEXT UNIQUE. Datomic uses partition-encoded entids + :db/ident for natural keys.

Argon mutations like register(external_id: Text) should:

  1. Allocate a fresh IndividualId(NonZeroU64) internally.
  2. Emit iof_assertion(IndividualId, Person).
  3. Emit hasExternalId(IndividualId, external_id) for the caller’s identifier.

The caller can query “find the Person where hasExternalId = ‘user_12345’” later. External identity is a property; internal identity is a surrogate. This is the pattern that scales and stays clean — and it lets IndividualId be 8 bytes (system-allocated) instead of 16 bytes (caller-provided UUID).

Why InternalId is runtime-only

InternalId layout [kind: 8 | partition: 16 | sequence: 40] is optimized for:

  • Cache-friendly Vec<u64>-indexed bitmaps (per-kind, per-partition).
  • Fibonacci-hashed U32-keyed sets in hot loops.
  • Zero-cost type discrimination via the kind byte.

But this layout is engine-policy, not modeler-visible. We reserve the right to renumber on compaction, change partition functions, etc. Making InternalId part of the wire format would couple wire to engine — wrong direction. It stays runtime-only; Module::load builds a NameRef ↔ InternalId dictionary; the reasoner operates entirely in InternalId space.

Why no UUIDs

Five reasons:

  1. We control allocation. UUIDs solve the “globally unique without coordination” problem. Argon’s identifiers are either declared (NameRef from canonical symbol position), system-allocated (EventId, IndividualId), or content-derived (AxiomKey, ContentId). No coordination problem exists.

  2. Type safety. All 21 identifier roles collapsing into one Uuid type is a regression. Newtypes per role give compile-time discrimination.

  3. Storage efficiency. Replacing UUIDs with the right-sized type per role saves 60-80% of identifier bytes across the system.

  4. Wire format determinism. UUIDv4 is random; UUIDv7 has wall-clock time; UUIDv5 is one hash family. Custom types let us pick the determinism story per role (content-hash for AxiomKey, sort-position for NameRef, deterministic-counter for EventId in build mode).

  5. No external compatibility need. Argon doesn’t need to interoperate with systems-that-mint-UUIDs at the identifier level. Federation happens at the Iri level (qualified paths), not at the binary-ID level.

Alternatives considered

Alt 1: Keep UUIDs everywhere

The status quo. Universal, well-understood. Costs: 145 bytes of identifiers per event header; 17-byte Z-set tuple elements; ~286 call sites with one type doing many jobs; full 16-byte width for partition keys with low-cardinality.

Rejected. The hot-path costs and lack of type discrimination outweigh the familiarity benefit.

Alt 2: UUIDv8 outer + 64-bit InternalId inner (the generic doc recommendation)

A two-tier system with UUIDv8 (RFC 9562, custom layout) as the durable external identifier and a packed 64-bit InternalId for hot paths.

Rejected. UUIDv8 in any recommended form embeds a timestamp (HLC), which breaks Argon’s byte-deterministic-build invariant. And the “external identifier” tier isn’t needed for Argon today — we don’t federate at the binary-ID level.

Alt 3: Snowflake-style 64-bit time-ordered IDs throughout

A single 64-bit ID type [time: 42 | shard: 10 | seq: 12] for everything.

Rejected. Conflates allocation-addressed (events) with content-addressed (declarative symbols). Concepts shouldn’t have time in their identity; events should. Single-type-for-everything is what we’re moving away from.

Alt 4: Nous’s FNV-1a 48-bit ConceptId

Hash the qualified path with FNV-1a, truncate to 48 bits, that’s the ID.

Rejected. Collides at 2^24 entries (~16M). Nous trusted IRIs not to collide; Argon shouldn’t. And it conflates declarative identity (paths) with run-of-the-mill IDs.

Alt 5: Kora’s per-engine ConceptIndex with HashMap<Iri, u32>

Each engine maintains its own Iri → u32 index, rebuilt per session.

Rejected as primary scheme (kept as inspiration for InternalId at Module::load time). Per-engine indexes don’t address the wire-format problem; they’re an in-memory representation.

Consequences

Wire format changes (major)

  • Bump oxbin_format_version major (Phase 2 of rollout — see below).
  • All _id: uuid::Uuid fields in oxc-protocol::storage::*Body types become typed: NameRef, EventId, AxiomKey, etc. per their role.
  • AxiomEvent header shrinks from 145 bytes of identifiers to 81 bytes (44% reduction).
  • .oxbin files produced under the old format are not readable under the new format. We have no externally-deployed .oxbin files; this is acceptable.

Code changes

  • uuid crate removed from workspace dependencies.
  • blake3 crate added (replaces sha2).
  • New oxc-ids crate (or oxc-protocol::ids module) carries the 10 types + define_id! macro.
  • oxc-instantiate::identity becomes the Iri ↔ NameRef ↔ symbol-table builder.
  • oxc-runtime::Module::load builds the NameRef ↔ InternalId dictionary.
  • oxc-reasoning::compile::Value becomes Value::Internal(InternalId) plus inline variants.
  • oxc-storage-mem indexes re-keyed on (TenantId, ForkId, kind) instead of (Uuid, Uuid, &'static str).

Performance

Expected wins:

  • Reasoner memory: ~30-50% reduction in Z-set tuple key storage (17 → 9 bytes per ID).
  • Event headers: 44% smaller (145 → 81 bytes).
  • Storage indexes: ~50% smaller (UUID → u32/u64 partition keys).
  • Hash operations: BLAKE3 ~3× faster than SHA-256.

Costs:

  • Module::load builds a dictionary (one pass over declared symbols; negligible).
  • Dropping UUID crate removes a well-tested dependency; replaced with custom types that need testing.

Determinism preserved

Every wire-format identifier is content-derived (NameRef from sort position; AxiomKey from BLAKE3 of body; ContentId from BLAKE3 of body; EventId from build-deterministic counter in build mode). Same source → byte-identical .oxbin. Property preserved.

Phased rollout

The full design can be landed in four independently-shippable phases:

Phase 1 — runtime InternalId, no wire change (~3 days). oxc-reasoning::compile::Value::Internal(InternalId) replaces Value::Id(Uuid). Module::load builds a Uuid ↔ InternalId dictionary. Wire format unchanged. Win: ~30% reduction in reasoner Z-set memory.

Phase 2 — wire format break (~1 week). Add oxc-protocol::ids with all 10 types. Replace every _id: uuid::Uuid in body types. Drop uuid crate, add blake3. Bump oxbin_format_version major. Win: 44% reduction in event header size; type-safe identifiers throughout the wire.

Phase 3 — full Iri interner + symbol-table lift (~3 days). Iri(Arc<str>) with per-workspace arena. The .oxbin symbol-table section becomes the load-bearing dictionary it was designed to be. Win: cleanup of qualified_path: String duplication.

Phase 4 — dense engine structures (~1 week). Fibonacci-hashed U32Set, KindBitmap (Vec<u64> indexed by InternalId), RoleEdges-style packed adjacency. Per-lattice sentinel discipline. Win: foundation for future reasoner backends (SLG, DBSP, Kripke) sharing dense set primitives.

Open questions

  • Cross-workspace federation: when two workspaces’ .oxbin artifacts need to interoperate, what’s the bridge? Open. Likely: an explicit GlobalRef type at the federation boundary only, derived from (workspace_uuid, NameRef) or from full Iri. Out of scope for this RFD.

  • Distributed minting of EventId: the shard: 12 field is reserved but currently always 0. A future RFD addresses the distributed-minting protocol (Stateless Snowflake from container IP, range pre-allocation, or CRDT-style — see “Stateless Snowflake” Chinthareddy 2025).

  • External identifier indexing: when IndividualId is system-allocated and the caller’s identifier is data (a hasExternalId property), querying by external identifier requires a property-indexed lookup. The storage layer’s per-relation indexes (book §20.3.1) cover this, but specific query ergonomics for “find by external id” want a small SDK helper.

  • AxiomKey for non-data axioms: mutation_decl, query_decl, compute_decl are declarative axioms (have stable NameRef-based identity). Should their AxiomKey be BLAKE3(NameRef) or a special discriminator? Likely the former for uniformity; settle when wire format is finalized.

RFD 0002 — #[comptime] attribute

  • State: discussion
  • Opened: 2026-05-27
  • Decides: surface syntax and semantics of compile-time evaluation in Argon. Affects oxc-parser (attribute recognition), oxc-instantiate (compile-time lifting), oxc-reasoning (the engine, run at build time on a subset of rules), oxc-oxbin (materialized fact storage), and diagnostic codes OE1307/OE1308.

Question

Argon’s reasoner can evaluate rules at runtime (the keystone path) or at compile time (when all inputs are statically known). The compile-time path is strictly faster: facts get materialized into the .oxbin at build time and shipped as data, eliminating runtime evaluation for that rule entirely. When and how should the modeler opt in to compile-time evaluation? What does it mean per rule mode? What’s the auto-vs-explicit story?

Context

Why compile-time evaluation matters

Argon’s central thesis is the reasoner IS the data system. The same engine that derives adult(p) at query time can derive adult(p) at build time — if its inputs (Person(p), hasAge(p, n)) are statically known. Materializing the derivation at build time gives:

  1. Runtime queries see literal facts, not derivations. Faster, simpler query path.
  2. The .oxbin ships the closed-world extent. Downstream consumers (other compilation units, runtime, federation peers) don’t re-derive.
  3. Compile-time errors for impossible derivations. A check rule that fails at build time is a build error, not a runtime exception.
  4. A natural staging surface. Modelers can mark which derivations are “decided at build” vs “decided at runtime” — explicit control over the deployment-time computational tier.

What “compile-time” means here

The reasoner has two distinct invocation points:

  • Runtime modeEngine instantiated inside oxc-runtime::Store; rules evaluate against live event state; result is queried via Store::query_derive.
  • Compile-time mode — same Engine, instantiated inside oxc-instantiate during .oxbin lowering; rules evaluate against the source’s declarative facts (iof_assertion events from insert operations in mutations declared inside the workspace); result is emitted into the .oxbin’s events section as fresh axiom events (with derivation provenance pointing to the comptime rule).

The “engine is the engine” property holds: same code, same semantics, different invocation context.

What’s a “static input”?

A predicate is statically known at build time if its extent is fully determined by:

  • Axioms declared in source code (pub kind, pub rel, pub mutate invocations baked into module initialization).
  • Outputs of other comptime-eligible rules (the comptime fixpoint propagates).

A predicate is runtime-bound if any contributing fact comes from:

  • Caller-invoked mutations (user runs register(...) against a running Store).
  • External federation peers.
  • Network/IO sources.

Today’s MVP only has declarative axioms — everything is statically known. But the distinction is structural, not implementation-momentary.

Decision

Attribute surface

#[comptime]
pub derive adult(p) :- Person(p), hasAge(p, n), n >= 18;

The #[comptime] attribute applies to a single declaration. Binary form in v1 — no arguments. Argument form is parser-reserved for v2 (see Open Questions).

Per-mode semantics

Mode#[comptime] semantics
deriveMaterialize at build time. The derived extent is computed by the engine during .oxbin lowering and emitted as fresh iof_assertion / relation_tuple axiom events (with derivation field pointing to the rule’s AxiomKey). Runtime queries against the rule’s predicate return the materialized facts directly; no runtime evaluation.
queryEmbed the query’s result as a literal value in the .oxbin. The query body is evaluated at build time; the result (Vec<Tuple>) is stored. Callers receive the literal. Re-running the query at runtime is a no-op — the answer is baked.
checkGate the build on the check passing. The check rule is evaluated at build time; if it produces any tuple satisfying its body, the build fails with the check’s diagnostic. (This is the strictest mode — comptime checks are essentially compile-time assertions.)
fnConst-fn-like. The function is evaluated at build time when called from a context where all its arguments are themselves comptime-known. Otherwise it falls back to runtime evaluation. Same semantics as Rust’s const fn.
mutateForbidden. Mutations are runtime operations by definition — they extend the event log at invocation time. #[comptime] on a mutation emits OE1308 ComptimeForbiddenOnMutate at build time.

Auto-lifting

The compiler automatically lifts any rule (regardless of #[comptime]) whose body predicates are all statically known and whose result fits within the declared comptime budget. The lift is transparent — produces the same axiom events as if the rule had been declared #[comptime].

#[comptime] is therefore a promise (“this rule MUST be evaluable at build time”), not just an enable. If the modeler declares #[comptime] on a rule whose body references a runtime-only predicate, the build fails with OE1307 ComptimeNotStatic.

This makes #[comptime] checkable: it’s a verification that a property holds, not just a performance hint.

Diagnostic codes

  • OE1307 ComptimeNotStatic — rule declared #[comptime] depends on runtime state (a mutation-bound predicate, a federation-bound predicate, or an indirect dependency through another runtime-only rule).
  • OE1308 ComptimeForbiddenOnMutate#[comptime] applied to a mutate declaration.
  • OE0227 AttributeArgsNotYetImplemented (existing) — #[comptime(...)] with any arguments emits this; arg-form is parser-reserved for v2.

Materialization output

A #[comptime] derive p(x) :- A(x), B(x) rule produces, at build time:

  1. The derivation: every tuple in p’s extent emitted as an iof_assertion or relation_tuple axiom event.
  2. The provenance: each emitted event carries derivation set to the rule’s AxiomKey (per RFD 0001) so retraction propagates correctly.
  3. A comptime_lifted: bool field in RuleDeclBody indicating “this rule’s facts are pre-materialized; runtime evaluators may skip it.” (Adds 1 byte to RuleDeclBody.)

Reasoning about freshness

If the source code changes such that a comptime-lifted rule’s inputs change, the .oxbin is rebuilt and the materialized facts are recomputed. The Salsa-style incremental compilation layer (book §18.2) treats comptime_lifted rules as memoization targets — their inputs are tracked, and only changed-input rules re-materialize.

Rationale

Why per-mode semantics, not uniform

The five rule modes (fn / derive / query / mutate / check) have genuinely different meanings, and “compile-time” means something different for each:

  • derive produces facts → comptime means baking those facts into the artifact.
  • query produces a result value → comptime means embedding that value.
  • check produces a verdict → comptime means asserting at build time.
  • fn is a function call → comptime means const-evaluation in the Rust/C++ tradition.
  • mutate mutates state → comptime makes no sense; mutations are by definition runtime.

A single uniform “evaluate at build time” semantics would erase these distinctions. The mode-aware semantics match the modeler’s mental model.

Why auto-lift + explicit attribute (not just one)

Auto-lift alone risks silent performance cliffs: the modeler doesn’t know whether a rule is being materialized or runtime-evaluated. Adding a runtime-only predicate elsewhere could silently degrade performance.

Explicit-only would force the modeler to annotate every rule, even the obvious cases.

Auto-lift plus #[comptime] as a checked promise gives both: most rules auto-lift transparently; the modeler can opt-in to “this MUST be comptime” for ones where the property matters. Same pattern as Rust’s const fn — the compiler can evaluate any expression at compile time when possible, but const fn is the promise that this function is callable in const context.

Why argument-form is reserved, not implemented in v1

Four argument variants we expect to want:

  • #[comptime(strict)] — fail build if auto-lift can’t reduce all inputs (stronger than the default OE1307 — even runtime-derivable-but-not-yet-evaluated counts as failure).
  • #[comptime(profile)] — emit a build-time report of cost/cardinality for this rule.
  • #[comptime(embed)] — for query mode: embed the result inline in source as a literal, not just in the .oxbin (useful for let X: extent = query foo inside a fn).
  • #[comptime(lazy)] — comptime-eligible but defer materialization until first runtime access (useful for very-large extents).

None of these are necessary in v1. We commit the parser-level syntax (#[comptime(...)] parses without error) but emit OE0227 on any non-empty arg list. This lets us add the arg-form in v2 without changing parser surface.

Why OE1307 distinguishes “comptime-eligible but not yet evaluated” from “comptime-impossible”

A comptime rule depending on another rule that’s itself comptime-eligible but not yet processed should propagate up the comptime fixpoint, not fail. The build’s job is to find the comptime fixpoint over the rule graph; only rules that hit a runtime-only predicate or a cycle (that can’t be broken by other comptime rules) fail with OE1307.

Why mutate is forbidden, not silently no-op

Mutations modify state. State at compile time is the static fact base; “running” a mutation against it would change what’s baked into the artifact, which violates the closed-world property (the .oxbin’s facts are what the modeler wrote, plus what derivations they declared). Allowing #[comptime] mutate ... would either:

  • Silently no-op (confusing — modeler thinks something happened).
  • Modify the build’s fact base (dangerous — implicit data mutation hidden in code).

Better to forbid loudly via OE1308.

Alternatives considered

Alt 1: No comptime at all — everything is runtime

The status quo. All rules evaluate at runtime; the .oxbin carries only declared facts plus rule definitions.

Rejected: gives up the major performance win of build-time materialization. Loses the natural “compile-time assertion” surface for check rules. Forces every cross-cutting query to pay runtime evaluation cost.

Alt 2: Implicit only — no attribute, just auto-lift

Every rule the compiler can prove statically-decidable is auto-materialized. No modeler annotation.

Rejected: silent performance cliffs as discussed. Loses the “this MUST hold” checkable property. Loses the natural place to thread future args (strict/profile/lazy).

Alt 3: Explicit only — every comptime rule must be annotated

No auto-lift; only #[comptime] rules are materialized at build.

Rejected: forces annotation noise on the obvious cases. Modelers will either over-annotate (defensive #[comptime] everywhere) or under-annotate (missing perf wins). Loses the simplicity of “the compiler does the right thing by default.”

Alt 4: #[const] instead of #[comptime]

Borrow Rust’s terminology for symmetry.

Rejected: “const” connotes value-level immutability in Rust, which is a different concept from “evaluated at build time.” Argon’s facts are already immutable once asserted; the relevant axis is when they’re asserted (build vs runtime), not whether they’re mutable. comptime is clearer.

Alt 5: Zig-style comptime (full compile-time computation)

Zig’s comptime does general compile-time evaluation including type-level computation. Argon could expose the same generality.

Rejected as the v1 design. Zig’s comptime is a much larger surface — it includes generic instantiation, type erasure decisions, and runtime/compile-time polymorphism. Argon’s #[comptime] is scoped to the substrate’s reasoning capability: which rules get materialized. Generalizing to Zig-shape comptime is interesting future work, but it’s a different feature.

Consequences

Wire format

RuleDeclBody gains a comptime_lifted: bool field. 1 byte added per rule decl. Negligible.

Build performance

Comptime-eligible rules add to build time (the engine runs at build). Trade-off: build is slower; runtime is faster + simpler. For workloads where the .oxbin is built once and queried many times, this is strictly positive. For dev-loop scenarios (rebuild frequently), Salsa-style incremental compilation memoizes most of the work.

Diagnostic surface

Two new emission sites (OE1307, OE1308 — already reserved in grammar.toml from the earlier diagnostic landing). The parser keeps the existing OE0227 for non-empty arg form.

Code volume

Modest:

  • Parser change: ~10 lines to recognize #[comptime] attribute on declarations.
  • oxc-instantiate change: build-time engine invocation for comptime-eligible rules, comptime fixpoint analysis, OE1307 emission. ~200-300 LOC.
  • oxc-oxbin change: emit derivation events from comptime rules into the events section. ~50 LOC.
  • RuleDeclBody schema: comptime_lifted field. ~5 LOC.

Determinism preserved

Comptime evaluation produces the same axiom events from the same source — the engine is deterministic, and the inputs are entirely from source. Byte-identical .oxbin guaranteed (per RFD 0001).

Open questions

  • The argument form in v2#[comptime(strict|profile|embed|lazy)]. Specific semantics deferred to a follow-up RFD when we have benchmarking to validate the tradeoffs. The parser-level syntax stays reserved.

  • Comptime budget — should there be a soft limit on how much work a comptime rule can do (e.g., “evaluate up to N facts; if exceeded, emit a warning and fall back to runtime”)? Useful for guarding against accidentally exponential comptime work. Likely yes, with a default of 10M facts and a CLI/config override.

  • Comptime + standpoint federation — when standpoint A imports from standpoint B and B has comptime-lifted rules, A sees B’s materialized facts. But if A re-evaluates a derived predicate against its own facts plus B’s, does the comptime materialization compose correctly under the FDE info-join? Likely yes (it’s just facts), but worth a separate verification when standpoint federation is mechanized in the Lean.

  • Interaction with retraction — a comptime-materialized rule’s derived facts have a derivation lineage. If a contributing axiom is retracted at runtime (via delete operation), the derived facts must also be retracted. The retraction propagation needs to traverse the derivation graph. Architectural: yes; implementation: needs the Phase-4-style dense provenance index from RFD 0001 to be efficient.

  • Comptime + temporal — for rules with explicit temporal qualifiers (at(t), during(...)), does comptime still make sense? The rule’s truth depends on t. Probably comptime should be allowed only when temporal qualifiers are themselves static (e.g., literal timestamps), and emit a diagnostic when they’re symbolic. Defer to the temporal-substrate work track.

RFD 0003 — Reasoner backend dispatch

  • State: discussion
  • Opened: 2026-05-27
  • Decides: how multiple reasoning backends (semi-naive, SLG, DBSP, SMT, Kripke, Kora-extension) compose under one Engine interface; the dispatch policy for per-stratum routing; the registration discipline; the modeler-facing diagnostic surface for unsupported-tier rules.

Question

Argon’s tier ladder admits seven distinct reasoning tiers (§10.1: structural, closure, expressive, recursive, fol, modal, metaorder). No single algorithm handles all seven — semi-naive Datalog handles stratified recursive; SLG tabling handles expressive; DBSP handles recursive under IVM; SMT handles fol; Kripke tableau handles modal; Kora-EL handles a slice of OWL profile semantics. How do these backends compose under one engine interface? What does the dispatcher do? When does the modeler see backend choices vs not?

Context

What’s already in place

Phase 1 of the reasoner is shipped (oxc-reasoning crate, ~530 LOC of evaluator + classifier + scaffolding). The keystone test passes: derive adult(p) :- Person(p), hasAge(p, n), n >= 18 derives the right facts end-to-end.

The current shape:

Engine
  ├── executors: Vec<Box<dyn TierExecutor>>   (registration-ordered)
  └── run_rules(...) — dispatches each rule to the first executor
                       whose supported_tiers() contains the rule's
                       classified tier.

TierExecutor trait
  ├── name(&self) -> &'static str
  ├── supported_tiers(&self) -> TierSet
  └── execute(stratum, input) -> ReasoningResult<RelationCatalog>

The single MVP implementation SemiNaiveExecutor advertises {Structural, Closure, Recursive}. The dispatcher is one-deep: pick the first executor that supports the tier. Per-rule tier comes from RuleDeclBody.main_tier, populated by oxc-instantiate::tier_classify::classify_body.

Where this falls short

The “first executor that supports the tier” policy works for the trivial case (one executor) but doesn’t say what happens when:

  1. Multiple executors claim the same tier (e.g., SemiNaive and DBSP both handle recursive). Which wins? Order-of-registration is the current answer; that’s an implicit policy with no surface.

  2. A rule’s body spans multiple tiers (e.g., a recursive rule that joins against an expressive qualified-cardinality predicate). Today the rule’s main_tier is the max of its atoms’ tiers — so the whole rule goes to whatever handles the max. That’s correct semantically but ignores stratification: the lower-tier sub-rule could run on the cheaper engine, results then fed into the higher-tier engine.

  3. Cross-backend data exchange — every backend produces RelationCatalog (Z-set BTreeMaps). Good for composition; the next stratum reads from the prior stratum’s output. But what if a backend uses internally-different data (DBSP’s arrangements vs SLG’s tabled tables)? The exchange happens at stratum boundaries via RelationCatalog; backend internals stay private.

  4. The modeler’s diagnostic surface — OE1305 (TierNotYetImplemented) is reserved but emission sites aren’t wired. When does the modeler see it? At build time (the rule’s tier exceeds any registered backend) or at query time (the rule’s classified tier is unsupportable)?

  5. Optional backends (Kora-extension)std::owl is a stdlib package that brings in the Kora-EL / Kora-DL backend. How does that get registered? Is it an explicit Engine::with_executor call, or implicit on import std::owl?

Decision

The dispatch model: per-stratum, registration-ordered, with explicit OE1305 emission

  1. Strata are the unit of dispatch. The physical plan (PhysicalPlan) carries a Vec<Stratum>. Each stratum has a tier (the max of its rules’ tiers). The dispatcher picks one executor per stratum and runs it.

  2. Registration order = preference order. Executors are pushed onto Engine::executors in registration order. The dispatcher picks the first executor whose supported_tiers() contains the stratum’s tier. This makes the policy explicit and modeler-controllable via build configuration.

  3. OE1305 emits at build time, not query time. When oxc-instantiate classifies a rule and the workspace’s configured backends don’t claim its tier, the build fails with OE1305. The modeler sees the error at compile time, before any runtime invocation.

  4. Backend registration is workspace configuration. Backends are declared in ox.toml (or the equivalent), not in source code. Default workspaces register SemiNaiveExecutor only. Adding SLG, SMT, etc. is an explicit opt-in via:

    [reasoning]
    executors = ["semi-naive", "slg-tabled"]
    

    Kora-extension is enabled by import std::owl in source, which adds kora-el (and kora-dl) to the executor list automatically.

  5. Cross-backend exchange is the shared RelationCatalog. All backends produce and consume RelationCatalog (Z-set BTreeMap). Internal representations (DBSP arrangements, SLG tabled solutions) are private. Composition happens at stratum boundaries: stratum N’s output catalog is stratum N+1’s input catalog.

The seven backends, mapped

BackendTiers coveredAlgorithmStatusEstimated effort
SemiNaiveExecutorstructural, closure, recursiveNaive/semi-naive bottom-up Datalog over Z-sets; stratified NAFPhase 1 shippeddone
DBSPExecutorrecursive (preferred over SemiNaive when IVM matters)True IVM via differential dataflowPost-Phase-4 of RFD 0001~1 month
SLGExecutorexpressiveTop-down tabled WFS (chalk-engine-shaped, NOT XSB port)Post-MVP~6-10 person-months
SMTExecutorfolExternal SMT solver (Z3 or cvc5) via unsafe logic { } blocks onlyPost-MVP~2 months
KripkeExecutormodalTableau over Kripke framesPost-MVP~3 months
KoraExtensionExecutorDL profiles when std::owl importedKora-EL (vendored)Post-MVP “compiler extension”~2 weeks integration
(Reserved) MetaorderExecutormetaorderBounded order-arithmetic procedure; semi-decidable beyond boundsSpeculativeunknown

Composition rules:

  • DBSP wins over SemiNaive when both are registered: register DBSP first so the dispatcher picks it. DBSP is a strictly more powerful evaluator (handles IVM, deltas, retractions natively); SemiNaive becomes a fallback for environments where DBSP can’t be deployed.
  • Kora-extension only handles its declared tier slice. It doesn’t claim recursive even though some EL rules look Datalog-shaped — Kora’s optimizations are OWL-specific and don’t generalize. The dispatcher uses Kora only for rules tagged as DL-shaped (a future RuleDeclBody field).

What the modeler sees

The modeler does not see backend names in source code. Rules are tier-classified by the compiler; backend selection is a deployment concern.

The modeler does see OE1305 when a rule’s tier exceeds the workspace’s configured backends. The diagnostic suggests which backend would handle it:

error[OE1305]: rule `complex_query` classified at tier `expressive` —
              no registered executor handles this tier
  --> demo.ar:42:1
  |
  | pub derive complex_query(p) :- ...
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: enable the `slg-tabled` executor in ox.toml:
        [reasoning]
        executors = ["semi-naive", "slg-tabled"]

The TierExecutor trait — refined

#![allow(unused)]
fn main() {
pub trait TierExecutor: Send + Sync {
    fn name(&self) -> &'static str;
    fn supported_tiers(&self) -> TierSet;

    /// Execute one stratum. Reads from `input` (current state +
    /// prior strata's outputs); returns the stratum's catalog delta.
    fn execute(
        &self,
        stratum: &Stratum,
        input: &RelationCatalog,
    ) -> ReasoningResult<RelationCatalog>;

    /// Optional: backend-specific cost estimate for a stratum.
    /// Returned values are comparable across backends (smaller = preferred).
    /// Default impl returns `f64::INFINITY` (never prefer).
    fn estimate_cost(&self, _stratum: &Stratum, _input: &RelationCatalog) -> f64 {
        f64::INFINITY
    }
}
}

The estimate_cost method is the hook for cost-based dispatch (v2). v1 uses pure registration-order dispatch; v2 may evolve to “pick the registered executor whose cost estimate is lowest.” Deferred to a follow-up RFD.

Stratum boundary policy

The optimizer is responsible for stratification: walk the rule dependency graph, partition into SCCs, topologically sort the SCCs into strata. Each stratum is single-tier (max of its rules). NAF is stratified across stratum boundaries (sound).

Inter-stratum NAF (a rule in stratum N+1 uses not p(...) where p is populated in stratum N) is the standard stratified-WFS case. Intra-stratum NAF is a cycle and either:

  • Handled by the backend if the backend supports WFS (SLG does; SemiNaive doesn’t beyond stratification).
  • Rejected with OE1309 (StratificationNafCycle) at build time.

Rationale

Why per-stratum, not per-rule

A real-world workload has many rules at different tiers. Dispatching per-rule means launching the backend’s per-call overhead for each. Per-stratum amortizes: the backend sees all its rules, can build its own internal arrangements once, run them all to fixpoint within its scope.

This matches DataFusion’s pattern (which inspired the architecture): each ExecutionPlan node runs to completion in one backend context; data flows between nodes via the shared columnar format. Argon’s analog: each stratum runs in one backend context; catalogs flow between strata via the shared Z-set format.

Why registration order, not declared priority

A priority: i32 field on TierExecutor would let backends declare their own preference order. Rejected because:

  • It distributes the dispatch logic — each backend needs to know its place in the ordering vs other backends.
  • It introduces a coordination problem when two backends claim the same priority.
  • Registration order achieves the same expressivity (push backends in the desired order) without per-backend coordination.

Workspace configuration (ox.toml’s [reasoning].executors = [...]) makes the order explicit and modeler-visible. That’s the right surface for the policy decision.

Why OE1305 at build time, not query time

The alternative is lazy: register backends; at query time, if no backend handles the tier, fail. Rejected because:

  • A modeler shouldn’t ship an .oxbin that they don’t know is unrunnable. Build-time emission catches it early.
  • The set of registered backends is a deployment property; the modeler knows it at build configuration time.
  • “Build it, ship it, then it fails when the user runs it” is a bad UX for a system whose virtue is “deterministic builds give you runtime guarantees.”

Why workspace-config, not in-source backend selection

Source code shouldn’t say “use SLG for this rule.” Two reasons:

  • The modeler should describe what they want derived, not how to derive it. That’s the entire point of declarative semantics.
  • Backend choice is deployment policy. A test workspace uses SemiNaive; a production workspace uses DBSP + SLG. Same source code, different deployment.

The exception: std::owl is a stdlib package that brings in OWL semantics. Importing it implicitly enables Kora-extension because the modeler is asking for OWL-shape reasoning. This isn’t “backend selection”; it’s “load this semantics extension.”

Why DBSP wins over SemiNaive when both registered

DBSP handles every workload SemiNaive handles, plus retractions and IVM. It’s strictly more powerful. Once DBSP is mature (Phase-4-of-RFD-0001 or later), the natural default is “use DBSP if available, fall back to SemiNaive otherwise.” Registration order encodes this: push DBSP first.

SemiNaive stays in the stack because:

  • It has no external dependencies (DBSP needs the timely dataflow runtime).
  • It’s simpler to reason about correctness in.
  • It’s the reference implementation that other backends can be tested against.

Why Kora is “extension” not “default”

Argon’s substrate is OWL-neutral. Baking OWL-specific optimizations into the default executor stack would couple Argon to OWL semantics — which RFD 0001 and the broader architecture explicitly avoid.

Kora becomes available when a modeler imports std::owl. That import is a deliberate statement: “I want OWL semantics for this part of my model.” The Kora backend then handles those rules; non-OWL rules go through the default backends.

Alternatives considered

Alt 1: Single mega-engine

One backend handles all tiers via dispatched algorithms internally. Examples in prior art: SWRL-DL engines that switch between Datalog and tableau internally.

Rejected: kills modularity. Adding a new backend (SLG, DBSP) requires modifying the mega-engine. Cross-engine testing becomes impossible.

Alt 2: Runtime backend resolution

Backends register themselves via plugin discovery at runtime; the engine probes for capability.

Rejected: introduces dynamic-loading complexity Rust doesn’t natively support and Argon doesn’t need. All known backends are statically-known at build time.

Alt 3: Backend per rule mode

derive rules go to one backend, query rules to another, etc.

Rejected: backend choice is about tier (what features the rule uses), not mode (what kind of declaration it is). A derive rule and a query rule with the same body should run on the same backend.

Alt 4: First-class cost-based dispatch from v1

estimate_cost is mandatory; dispatcher picks the lowest-cost backend per stratum.

Rejected for v1: backends don’t yet have meaningful cost models. SemiNaive’s cost estimate is fact-count; DBSP’s would involve arrangement-sharing analysis. Without a calibrated cost surface, “lowest cost” is noise. Registration-order is honest about the v1 state. Cost-based dispatch becomes v2 once enough backends ship to make calibration meaningful.

Consequences

Code structure

The Engine and TierExecutor trait already exist (oxc-reasoning/src/executor/mod.rs). This RFD codifies the existing API; minor refinements needed:

  • Add estimate_cost default method (deferred-use API).
  • Add workspace configuration support (ox.toml parsing for [reasoning]).
  • Wire OE1305 emission from oxc-instantiate::tier_classify when the configured backends don’t cover the classified tier.

Diagnostic surface

OE1305 emission gains a structured “suggest enabling X backend” hint. Requires the diagnostic system to know which backends could handle which tiers — a static table in oxc-diagnostics or oxc-reasoning::backend_registry.

Each future backend’s commit

Each new backend lands as:

  1. A new module in oxc-reasoning::executor::{name}.rs implementing TierExecutor.
  2. Registration plumbing in workspace config.
  3. Backend-specific tests showing the keystone-equivalent for its tier (e.g., SLG’s keystone is a recursive-aggregate rule like count over a stratified-NAF derived predicate).
  4. An interop test showing the backend composes cleanly with SemiNaive at stratum boundaries.

Kora vendoring decision

Per the earlier session decision: when we bring in Kora, we vendor the code into oxc-reasoning::executor::kora and own the implementation. We don’t depend on the upstream kora-* crates. This isolates Argon from Kora’s design choices and lets us evolve the embedded reasoner independently.

The vendoring is an explicit one-time copy; subsequent changes happen in Argon’s tree. If upstream Kora improves substantially, we re-vendor with a documented migration. This matches the pattern other projects use for embedded engines (e.g., rust-analyzer’s vendored libraries).

Open questions

  • Stratum boundary policy with retractions: when a fact is retracted in stratum N, the retraction must propagate to stratum N+1’s derived facts. The current RelationCatalog exchange is a snapshot, not a delta stream. For DBSP this is solved natively (it IS the delta stream); for SemiNaive we recompute. Architecturally clean; performance-cost matters for large stores. Defer to the DBSP integration RFD.

  • Workspace config schema: ox.toml’s [reasoning] section needs a concrete schema. Reserve the section now; nail down the schema when the second backend (DBSP or SLG, whichever lands first) makes it necessary.

  • Per-rule backend override: should the modeler ever be able to say “use SemiNaive for this rule even though DBSP is registered”? Possible use cases: debugging (compare backends on the same rule), workload-specific tuning. Defer; revisit when a real use case emerges.

  • Backend feature negotiation: a backend may support a tier but not a specific feature (e.g., SemiNaive supports recursive but not recursive with aggregates). How does the dispatcher know? Either (a) backends advertise feature support more granularly than tier, or (b) the classifier bumps tiers when specific features are used (e.g., aggregates always classify as expressive). The current classifier does (b); the trade-off is granularity vs simplicity. Probably keep (b) for v1.

  • Dynamic backend selection by data characteristics: e.g., “use DBSP if the input cardinality is over 10M; use SemiNaive otherwise.” Cost-based dispatch (v2) addresses this. Out of scope for v1.

  • Modal + temporal interaction: when temporal qualifiers nest inside modal operators (or vice versa), the relevant tier is modal per §10.1. Which backend handles? KripkeExecutor in the design; verify when the temporal-substrate work track lands the temporal evaluator.

RFD 0004 — pub fact declarations

  • State: discussion
  • Opened: 2026-05-28
  • Decides: surface syntax for declaring ground-truth facts (axioms) in source code; per-mode semantics relative to pub mutate; the relationship between source-declared facts and runtime-asserted facts in storage; how compile-time materialization (#[comptime] per RFD 0002) feeds on these facts.

Question

Today, ground-truth facts in an Argon system can only arrive at runtime via mutation invocation. The keystone test demonstrates this: there’s no way to write Person(alice) in source code; the only path is to call register(alice). Should Argon admit a source-level fact declaration syntax? If so, what’s the right surface, what’s the relationship to mutations and storage, and how does compile-time materialization fit?

Context

What the gap looks like today

pub kind Person; declares a concept. pub rel hasAge(p: Person, n: Nat); declares a relation. pub derive adult(p) :- Person(p), hasAge(p, n), n >= 18; declares a rule. Nothing in the current surface lets a modeler write Person(alice) or hasAge(alice, 25) as a fact. The only way to get those tuples into the store is to invoke a mutation at runtime.

This creates several problems:

  1. #[comptime] is starved. RFD 0002 specifies compile-time materialization for derive rules. But the build-time engine has no facts to materialize against — every fact requires a runtime mutation call. The #[comptime] flag stamps on the rule body but the lifter has nothing to evaluate.

  2. Test fixtures are awkward. The keystone test had to inject hasAge(p, 25) via a test-only helper because the only fact path was through mutation execution (which until this session didn’t bind integer args). Even after Value-typed mutation args landed, declarative tests want to express “given these facts” without runtime invocation gymnastics.

  3. Bootstrap data is awkward. Real ontologies have built-in facts: standard units, well-known individuals, vocabulary mappings (e.g., the OWL stdlib’s rdf:type predicate; the temporal stdlib’s Second, Minute instances). Today these would need runtime mutation calls at startup. Source-level facts let them ship as part of the .oxbin.

  4. The substrate’s “five atoms” model is incomplete without it. The five atoms (meta-calculus, construct, rule, trait, macro) describe declarations. But declarations alone don’t carry data — they describe shape. Facts ARE data; they should have a declarative surface.

What “fact” means semantically

A fact in Argon is an axiom event of kind iof_assertion, relation_tuple, meta_property, subsumption_axiom, or partition_axiom. Each is a positive ground proposition. Modelers writing source today can declare structure (concepts, relations) and behavior (mutations, derive rules) but cannot declare contents (ground propositions).

The minimal addition: surface syntax that emits these axiom events at build time. The .oxbin carries them just like mutation-emitted events. The reasoner sees them as live state. Compile-time materialization can finally run with input.

Decision

Surface syntax

pub fact Person(alice);
pub fact Person(bob);
pub fact hasAge(alice, 25);
pub fact hasAge(bob, 12);

The pub fact keyword sequence introduces a single ground proposition. Its body is exactly the same syntactic shape as a mutation’s insert operations — a predicate name followed by a parenthesized argument list. Each argument is either an identifier (interpreted as an Individual reference) or a literal (Int, Bool, Text).

Multiple facts can share a single declaration via a brace-delimited block:

pub fact {
    Person(alice);
    Person(bob);
    hasAge(alice, 25);
    hasAge(bob, 12);
}

This is purely a syntactic convenience — semantically identical to N independent pub fact declarations.

Identity allocation for fact arguments

When a fact references an identifier (alice, bob), the elaborator allocates a fresh IndividualId for it (per RFD 0001’s “individuals are system-allocated” rule). The mapping identifier → IndividualId is interned per workspace build, so:

  • The same identifier mentioned in two pub fact declarations resolves to the same IndividualId.
  • Different builds of the same source produce the same IndividualId (deterministic).
  • Different identifiers (alice vs bob) get distinct IndividualIds.

The identifier itself becomes available for runtime lookup via the workspace’s symbol table — callers can pass Value::Individual(alice_id) to mutations or queries. (Today the runtime hashes the variable name for unbound parameters; this convention extends naturally.)

Per-axiom-kind semantics

Fact shapeResulting axiom event
pub fact Concept(individual)iof_assertion { concept_id = NameRef(Concept), individual_id = IndividualId(individual) }
pub fact relation(arg1, arg2, ...)relation_tuple { relation_id = NameRef(relation), args = [encode(arg1), encode(arg2), ...] }
pub fact axis(target) = valuemeta_property { axis_id = NameRef(axis), target_id = NameRef(target), value } (sugar for the meta-property family per §13)
pub fact A <: Bsubsumption_axiom { sub_id = NameRef(A), super_id = NameRef(B) } (an alternative spelling of the <: in concept declarations)

The fourth row’s surface (pub fact A <: B) is shorthand for declaring subsumption outside a concept declaration’s <: clause — useful for late-binding or library-extension idioms. Initial implementation focuses on the first two rows; meta-property and subsumption shapes land when the broader §13 / §11 substrate matures.

Relationship to mutations

A pub fact declaration is equivalent to a mutation that runs at module load with no parameters. The compile-time-equivalent expansion:

// User writes:
pub fact Person(alice);
pub fact hasAge(alice, 25);

// Semantically equivalent to (but the compiler does NOT actually
// emit this; it emits the axiom events directly):
pub mutate __bootstrap_module() {
    insert iof(alice, Person);
    insert hasAge(alice, 25);
}
// + invocation of __bootstrap_module at module-load time

The advantages of NOT going through a synthetic mutation:

  1. Directly content-addressed. The fact’s AxiomKey is BLAKE3-128(canonical_body), deterministic per RFD 0001. Mutations get fresh EventIds at invocation; facts get content-stable identity.
  2. No runtime invocation needed. The .oxbin carries the events directly; the runtime sees them in Store::seed_from(&Module).
  3. Compile-time materialization works. RFD 0002’s #[comptime] lifter finally has inputs.

Interaction with #[comptime]

After pub fact lands, the comptime lifter has a non-trivial path:

pub fact Person(alice);
pub fact Person(bob);
pub fact hasAge(alice, 25);
pub fact hasAge(bob, 12);

#[comptime]
pub derive adult(p) :- Person(p), hasAge(p, n), n >= 18;

At build time, the lifter:

  1. Collects facts from pub fact declarations → catalog seed.
  2. Runs the comptime rule against the seed.
  3. Emits the derived tuples (adult(alice) in this case) as fresh iof_assertion / relation_tuple events with derivation provenance.
  4. The runtime sees the derived facts directly; no re-evaluation needed.

Mutability + lifecycle

pub fact declares an immutable ground fact. The fact exists from module-load onwards. Retraction must go through:

  • A mutation: pub mutate forget_alice() { delete iof(alice, Person); }.
  • A retraction event at runtime (when the storage layer supports it).

The fact’s AxiomKey is content-addressed, so even after retraction the event log records both the assertion and the retraction. Replaying the log reproduces the lineage.

Diagnostic surface

  • OE0210 FactReferencesUnknownConceptpub fact Person(alice) where Person is not a declared concept.
  • OE0211 FactArgArityMismatchpub fact hasAge(alice) for a binary relation.
  • OE0212 FactArgTypeMismatchpub fact hasAge(alice, "twenty-five") where the relation expects Nat.
  • OE0213 FactInsideFnpub fact is a module-level declaration; not admitted inside function bodies, traits, etc.

Rationale

Why a new keyword fact, not overloaded syntax

Alternatives we ruled out:

  • pub Person(alice); — overloads the visibility keyword. Reads ambiguously next to pub kind Person.
  • pub iof alice : Person; — too low-level; exposes the wire-format axiom kind to the surface.
  • #[fact] Person(alice); — attribute-driven, but attributes describe modifiers, not the kind of declaration. Confusing.

pub fact reads cleanly, parallels pub kind / pub rel, and is unambiguous about meaning.

Why allow block form

The block form (pub fact { ... }) is shorthand for repeated declarations. Real modules have lots of bootstrap facts (a stdlib of units, well-known individuals); writing 50 pub fact declarations vs. one block of 50 is just keyboard noise.

The block form’s semantic is identical to N independent declarations. No grouping invariants implied.

Why content-addressed AxiomKey, not synthetic mutation EventId

Two reasons:

  • Determinism: same source → same .oxbin bytes. The fact’s identity comes from its content, not from a mutation’s invocation order.
  • Lineage clarity: a pub fact declaration is a statement about reality, not an operation that “happens.” Treating it as an event with content-derived identity matches the modeler’s mental model.

Why immutable

pub fact describes ground truth as the modeler authored it. Allowing inline mutation (pub fact Person(alice); pub fact !Person(alice); — withdraw the prior?) would conflate declaration with operation. Cleaner: declarations are immutable; runtime mutations are the path for state change.

The .oxbin-resident facts can still be retracted by a mutation at runtime; the assertion + retraction live in the event log together.

Why not full datalog-style “extensional database” syntax

Some Datalog systems allow large facts via a separate file format (e.g., .csv of relation tuples, loaded by relation name). Argon could borrow this.

Rejected for the v1 surface: introduces a second source format with different rules. The same module would split between .ar (rules) and .csv (facts) — a coordination burden. pub fact keeps everything in one file format.

For very large fact sets (e.g., million-row knowledge bases), a tooling layer can pre-process external data into pub fact declarations. The language surface stays uniform.

Alternatives considered

Alt 1: No pub fact; require synthetic mutations

The modeler writes pub mutate bootstrap() { insert iof(alice, Person); } and a runtime layer calls it on module load.

Rejected: confuses operations (mutations) with declarations (facts). Every fact-heavy module would have boilerplate. The #[comptime] lifter would still need to recognize “this mutation is actually fact bootstrapping” — same problem at one layer of indirection.

Alt 2: data keyword (a la SQL)

pub data hasAge { alice = 25; bob = 12; }

Rejected: “data” overemphasizes table-shaped thinking. Argon facts can be heterogeneous (subsumption axioms, meta-properties); data reads like “tabular data only.”

Alt 3: Module-level expression syntax

pub Person(alice); at module scope (no leading keyword).

Rejected: parser ambiguity with function calls. pub would have to mean two different things depending on what follows; messy.

Alt 4: Inline in concept declarations

pub kind Person {
    instances: alice, bob, carol;
}

Rejected: couples instance enumeration to concept declaration. The whole point of separating concept from instance is that instances can be added without modifying the concept. Argon’s metatype model assumes this separation.

Consequences

Wire format

No new axiom kinds — pub fact emits existing iof_assertion, relation_tuple, etc. events. The wire format is unchanged.

Parser changes

New keyword FACT_KW. New FactDecl AST node parallel to ConceptDecl / RelDecl. ~50 LOC in grammar.toml + argon.ungrammar + oxc-parser/src/grammar.rs.

Elaboration changes

oxc-instantiate gains a lower_fact_decl similar to lower_concept_decl. ~100-150 LOC. The fact’s args resolve through the same SymbolTableBuilder as other declarations.

Identity allocation for fact-mentioned individuals

The SymbolTableBuilder gains an intern_individual(name) -> IndividualId method. The first mention of alice allocates a fresh IndividualId; subsequent mentions return the same one. Per-build deterministic.

Comptime lifter

Per RFD 0002, the comptime lifter materializes derive rules into fresh axiom events at build time. After pub fact lands, the lifter has facts to evaluate against. The lifter implementation can finally land.

Test ergonomics

Tests like the keystone become much cleaner:

#![allow(unused)]
fn main() {
let source = r#"
mod demo;
pub kind Person;
pub rel hasAge(p: Person, n: Nat);

pub fact Person(alice);
pub fact hasAge(alice, 25);

pub derive adult(p) :- Person(p), hasAge(p, n), n >= 18;
"#;
// ...load, query, assert. No mutation calls. No test helpers.
}

Open questions

  • How are pub fact declarations sorted in the events section? Per the canonical-event-key sort, events sort by (tenant, fork, standpoint, kind, vt_start, tx_from). Facts get tx_from = 0 (declared at the workspace’s origin time)? Or do they get a deterministic tx_from derived from source position? The latter preserves source order; defer to implementation.

  • Can pub fact reference future-declared symbols? pub fact Person(alice); followed by pub kind Person; — does this work? Symmetric to how derive can reference forward-declared concepts. Default: yes; the elaborator does a single AST walk and resolves all references. Concrete error case: pub fact UnknownConcept(alice); → OE0210.

  • pub fact inside mod blockspub mod sub { pub fact ... } admits per-submodule fact scoping. Symbol resolution follows module visibility rules (per §3.1). No special handling beyond what’s already established.

  • Bulk-load tooling — for million-row fact sets, a CLI/tooling story is needed. Out of scope for the language surface; addressable post-MVP.

  • Interaction with standpoint scoping — when standpoints are surfaced, a fact in standpoint X is only visible in X (and its supertypes per FDE info-join). Defer to the standpoint substrate work track.

  • Negative facts / classical negationpub not_fact Person(alice) to assert that alice is not a Person. Useful in OWA / classical contexts; relates to §12.2’s Truth4 bilattice. Defer; orthogonal to this RFD’s positive-only scope.

RFD 0005 — Relation subsumption

  • State: committed
  • Opened: 2026-05-28
  • Decides: surface syntax for declaring that one relation subsumes another; the elaborator checks (endpoint covariance, cardinality refinement, metarel compatibility); the substrate impact; whether Argon adopts UML’s three-mechanism (subsetting / redefinition / specialization) story or collapses it into a single mechanism.

Question

Today, <: is a substrate-level subsumption operator admitted only on concept declarations (pub kind Adult <: Person). The substrate’s SubsumptionAxiomBody is generic over sub_id/super_id and would accept relation IDs without modification, but the surface grammar (§5.3 rel-decl) has no <: clause and the elaborator has no relation-side covariance machinery. Should Argon extend <: to relations? If so, what does the surface look like, what does the elaborator check, and how does this interact with UML’s three property-specialization mechanisms?

Context

Concrete modeler demand

The driving case (Luiz Almeida, 2026-05-28 design thread): a temporal-substrate model needs a sub-relation whose endpoints narrow and whose tuples flow into the parent.

pub type TimePoint <: TimeInterval;
pub type AbsoluteTimePoint <: TimePoint;

pub type TimeScale {
    timePoints: [TimePoint] from timeScaleHasTimePoint.range,
}

pub type IndefiniteTimeScale <: TimeScale {
    timePoints: [AbsoluteTimePoint]
        from indefiniteTimeScaleHasAbsoluteTimePoint.range,
}

pub rel timeScaleHasTimePoint(domain: TimeScale, range: TimePoint) [1] [1..*];
pub rel indefiniteTimeScaleHasAbsoluteTimePoint(
    domain: IndefiniteTimeScale,
    range:  AbsoluteTimePoint,
) [1] [1..*];

The intent: every tuple of indefiniteTimeScaleHasAbsoluteTimePoint is also a tuple of timeScaleHasTimePoint. Today there’s no way to express this; the modeler has to write a derive rule by hand, losing the structural property and the elaborator’s covariance check.

Substrate readiness

oxc-protocol::storage::SubsumptionAxiomBody:

#![allow(unused)]
fn main() {
pub struct SubsumptionAxiomBody {
    pub sub_id:   uuid::Uuid,    // generic — accepts concept or relation IDs
    pub super_id: uuid::Uuid,
}
}

The reasoner’s subsumption-closure logic (oxc-reasoning) parameterizes over symbol kind by construction. The substrate is ready; the surface and elaborator aren’t.

Parser asymmetry (the load-bearing gap)

oxc-parser/src/grammar.rs::concept_or_rel_decl already calls supertype_clause on the concept branch (lines 608–609), parsing <: via the existing LT_COLON / SPECIALIZES_KW machinery. The rel branch (lines 599–605) does not. The grammar’s SupertypeClause node is shared. Extending <: to relations reuses existing parser infrastructure; it does not introduce new syntax.

Argon’s existing position on shadowing and override

Three spec rules constrain the design space:

  1. §3.4 name resolution does not walk <: chains. Resolution order is local → imports → auto-prelude → primordials. Verified in oxc-resolver/src/resolve.rs:207–255resolve_path walks module prefixes only.

  2. §5.4 impl Type namespaces members as Type::name. Relations declared inside impl Person are reachable only as Person::ParentOf, not at module level. Verified in oxc-resolver/src/symbols.rs:57Item::ImplBlock(_) => return None. Impl-block contents don’t register top-level symbols.

  3. §5.2 explicitly forbids implicit override. OE0206 InstantiationFieldShadows rejects iof-axis shadowing; OE0208 AmbiguousFieldFromMultipleParents rejects diamond ambiguity. Verbatim: “The substrate offers no automatic merge, override-by-position, or last-wins behavior — every collision is resolved explicitly.”

These three together eliminate the namespace problem UML’s {redefines} exists to solve: in Argon, Manager::subordinate and Employee::subordinate are already distinct qualified paths from the start.

UML’s three mechanisms and how they map

UML mechanismWhat it expressesArgon’s resolution
{subsets}R1’s tuples ⊆ R’s tuples; both relations live<: on rel-decl (this RFD)
{redefines}Subclass shadows parent’s same-named propertyNot a problemimpl Type namespacing yields distinct paths a priori
Implicit specializationCovariant refinement of inherited propertySubstrate-level subsumption — handled by subsumption_axiom; no surface mechanism needed

UML needs three mechanisms because UML conflates extent-level subsumption with namespace-level shadowing AND assumes properties propagate through the class hierarchy by name. Argon decouples all three concerns. One surface mechanism (<: on rel-decl) covers all three UML cases.

Decision

Surface

Extend rel-decl to admit a <: (or specializes) clause after the cardinality list:

rel-decl ::= attribute* 'pub'? <metarel-name> Ident generic-params?
              rel-param-list cardinality-list?
              supertype-clause?           // NEW
              rel-body? ';'?
supertype-clause ::= '<:' TypeExpr (',' TypeExpr)*
                  |  'specializes' TypeExpr (',' TypeExpr)*

The clause applies uniformly across every metarel-introducing keyword (rel, material, mediation, formal, …) — the production is shared with concept-decl’s supertype clause, identical to how SupertypeClause is currently defined in the ungrammar.

pub rel indefiniteTimeScaleHasAbsoluteTimePoint(
    domain: IndefiniteTimeScale,
    range:  AbsoluteTimePoint,
) [1] [1..*]
    <: timeScaleHasTimePoint;

Semantics

R1 <: R declares: every tuple of R1 is also a tuple of R. The reasoner’s subsumption-closure auto-derives

R(a₁, …, aₙ) :- R1(a₁, …, aₙ).

without an explicit derive rule. Queries against R see R1’s tuples; queries against R1 see only R1’s tuples.

Elaborator checks at the <: clause

  1. Arity equality. R1 and R must have the same parameter count. Mismatch → OE0150 RelationSubsumptionArityMismatch.

  2. Endpoint covariance. For each position i, R1.param[i].type <: R.param[i].type. Mismatch → OE0151 RelationSubsumptionEndpointVariance.

  3. Cardinality refinement. For each slot i, R1.cardinality[i] must be no looser than R.cardinality[i]. Formal rule: [c..d] refines [a..b] iff c ≥ a and (d ≤ b or b = *). Mismatch → OE0152 RelationSubsumptionCardinalityViolation.

  4. Metarel compatibility (MVP rule). meta(R1) == meta(R). Same metarel on both sides. Mismatch → OE0153 RelationSubsumptionMetarelMismatch. Cross-metarel subsumption (e.g., material <: formal if a vocabulary declares a metarel lattice) is deferred — see Open Questions.

  5. Acyclicity. The relation subsumption graph must be acyclic. R1 <: R1 (direct or transitive) → OE0154 RelationSubsumptionCycle. Enforced by the same acyclic-DAG check used for concept subsumption.

Substrate impact

Zero new axiom kinds. The subsumption_axiom event with sub_id = R1’s NameRef and super_id = R’s NameRef carries the fact. The reasoner’s existing concept-subsumption closure generalizes by parameterizing over the symbol kind — relations are looked up via the same RelationCatalog machinery already used for storage.

Reasoner impact

oxc-reasoning::SemiNaiveExecutor extends its subsumption-closure pass to relation IDs. The existing concept-closure is parameterized on a SymbolKind enum; the implementation lifts the same Floyd-Warshall-style transitive closure over the subsumption DAG. Cost: O(|relations| · |subsumption_edges|) at module load.

Field-view propagation

A field declared via field: [T] from Rel.endpoint projects from Rel. When R1 <: R, a subtype concept may declare a field projecting from R1:

pub type TimeScale {
    timePoints: [TimePoint] from timeScaleHasTimePoint.range,
}
pub type IndefiniteTimeScale <: TimeScale {
    timePoints: [AbsoluteTimePoint]
        from indefiniteTimeScaleHasAbsoluteTimePoint.range,
}

The two fields are distinct projections per-concept; they share a name but live in different qualified namespaces (TimeScale::timePoints vs IndefiniteTimeScale::timePoints). Access from a TimeScale-typed binding projects the broader field; access from an IndefiniteTimeScale-typed binding projects the narrower one. Subsumption-closure ensures the narrower field’s tuples appear in the broader field’s view via the underlying relation <:.

Rationale

Why one mechanism, not three

UML’s {subsets} + {redefines} + implicit specialization is the right modeling vocabulary for a language whose properties propagate by name through the class hierarchy. Argon does not have that propagation:

  • Name resolution doesn’t walk <:.
  • impl Type namespacing creates distinct qualified paths from the start.
  • §5.2 forbids implicit override.

Given these three, UML’s {redefines} solves a problem Argon doesn’t have. A subclass that wants to “redefine” a parent’s property declares its own relation in its own qualified namespace; the relation-level <: carries the extent containment; the modeler accesses through whichever qualified path is appropriate. No additional mechanism is required.

Why extend <: (not introduce a new operator)

The substrate’s subsumption relation is uniform across symbol kinds (concepts, relations, standpoints) — SubsumptionAxiomBody is already generic. Using <: consistently preserves that uniformity at the surface. Introducing a new operator (e.g., subsets, >:, #[subsets(...)]) would imply the substrate distinguishes mechanisms it does not in fact distinguish.

Why same-metarel for MVP

A vocabulary may eventually declare metarel-level subsumption (material <: formal); when that lands, the elaborator’s metarel-compatibility check follows the metarel lattice. For MVP, conservative same-metarel-required keeps the elaborator simple and matches the most common use cases. The conservative rule generalizes monotonically — relaxing it later does not break any existing models.

Alternatives considered

A. Three-mechanism mirror of UML

Add <: for subsetting AND a separate #[redefines(Parent::rel)] attribute (or redefines keyword) for namespace shadowing. Rejected. Argon’s name resolution + impl Type scoping eliminate the shadowing problem; a redefines mechanism would address a non-problem. Adds language surface for no semantic gain.

B. Implicit subsumption from endpoint typing

When a pub rel R1 has endpoints <: R’s endpoints, automatically infer R1 <: R. Rejected. Conflicts with Argon’s “no implicit override” policy (§5.2). Modeler intent must be explicit; covariant endpoint types alone do not signal a desire for tuple flow into the parent.

C. Derive-rule expansion

Tell modelers to write pub derive R(a, b) :- R1(a, b); by hand. Rejected. Loses the structural property (no covariance check), bloats the module with boilerplate, and gives the optimizer no opportunity to specialize storage layout for subsumption-closed relations.

D. Defer entirely to a follow-on

Don’t extend <: to relations now; revisit when more pressure builds. Rejected. The substrate already supports it. The grammar gap is forcing modelers (Luiz, OntoUML imports) to work around. The cost of landing is small (~1–2 days of focused work). Deferring accumulates technical debt and modeler confusion.

Consequences

What lands

PieceWhereApproximate size
<: clause in rel-decl prosespec/reference/src/05-constructs.md §5.3~5 line edit
SupertypeClause on RelDecl in ungrammarcompiler/crates/oxc-syntax/argon.ungrammar1 line
Parser: invoke supertype_clause in rel branchoxc-parser/src/grammar.rs~5 LoC
Elaborator covariance + cardinality + metarel + acyclicity checksoxc-instantiate~120 LoC + 5 diagnostic codes
Subsumption-closure for relationsoxc-reasoning~30 LoC — extend existing concept closure
Diagnostic codes OE0150–OE0154oxc-syntax/grammar.toml + appendix-c-diagnostic-codes.md5 entries
Lean: extend Argon.Substrate.Construct.Relation with subsumption witnessspec/lean/Argon/Substrate/Construct.lean~40 lines (lift the concept-side machinery generically)

What modelers gain

  • Direct expression of UML {subsets} and OntoUML’s relation specialization patterns.
  • Property narrowing in subtypes via paired relation+field declarations.
  • Cleaner OntoUML import path (relation subsumption is a first-class OntoUML construct).
  • Elaborator-verified endpoint covariance and cardinality refinement at declaration sites.

What modelers do NOT gain

  • A separate redefines mechanism — Argon doesn’t need one (see Decision).
  • Implicit override semantics — same name without <: remains unsupported; explicit qualification is the modeler’s tool.
  • Cross-metarel subsumption (MVP) — initially restricted to same-metarel; lifted when metarel-lattice support lands.

Compatibility

Pure addition. No existing code changes behavior; the new clause is optional and absent in all today’s source. The subsumption_axiom wire format is unchanged.

Open questions

OQ1 — Field-level same-name across <:

When B <: A, both declaring a same-named field whose projection comes from <:-related relations (Luiz’s case), §5.2 has no explicit rule. Three readings:

  • Permissive: allow whenever the underlying relations are in a subsumption relationship; the field views inherit the connection transitively.
  • Strict: flag as shadowing per §5.2’s no-implicit-override policy; require explicit Parent::field qualification.
  • Inherit the relation’s signal: treat the relation-level <: as the modeler’s explicit acknowledgment; no field-level signal required.

Recommend the third reading as the cleanest extension of §5.2 — the relation’s <: carries the intent through to the derived field views, no new field-level mechanism needed. Confirm before implementation.

OQ2 — Metarel lattice

Should metarels themselves admit <: (e.g., metarel material <: metarel formal in a vocabulary)? UFO’s relation taxonomy is layered; a vocabulary might want to express that material relations are a kind of formal relation. Deferring to a follow-on RFD; the conservative same-metarel rule in this RFD is forward-compatible.

OQ3 — N-ary relation subsumption

The covariance check (R1.param[i].type <: R.param[i].type for each i) generalizes to n-ary relations trivially. Verify the elaborator’s covariance pass handles ternary and higher relations without special-casing. Unlikely to be a problem — the per-position check is uniform — but the test suite should cover ternary cases explicitly.

OQ4 — Interaction with from Rel.endpoint field-view cardinality

When R1 <: R and a subtype declares a field f: [T] from R1.endpoint, what cardinality bound applies to f? The narrower relation’s slot bounds, or the broader’s? The narrower’s, almost certainly — the field is projecting from R1, not R — but verify the existing field-view elaboration respects this.

References

  • §3.4 (name resolution), §5.2 (concept supertype clauses, OE0206/OE0208), §5.3 (relations), §5.4 (impl Type) — spec/reference/src/
  • oxc-protocol/src/storage.rs:574–578SubsumptionAxiomBody
  • oxc-resolver/src/{resolve.rs, symbols.rs} — name-resolution implementation
  • oxc-parser/src/grammar.rs:599–672 — concept/rel decl parser and supertype_clause
  • oxc-syntax/argon.ungrammar — typed AST shapes
  • UML 2.5.1 §9.5 (Properties; subsetting and redefinition)
  • Carvalho, V.A., Almeida, J.P.A., Guizzardi, G. (2017). Multi-level ontology-based conceptual modeling. Data & Knowledge Engineering 109, 3–24 — for the OntoUML relation-specialization patterns this RFD enables on import.

RFD 0006 — Field mutability via mut

  • State: discussion
  • Opened: 2026-05-28
  • Decides: surface syntax for opting a field into post-construction updatability; the implicit default for non-annotated fields; the interaction with #[intrinsic], from-clauses, refinement, and metatype rigidity; how update-stmt becomes the only legal write path; the lowering to append-only event pairs; the elaborator checks and diagnostic codes.

Question

Argon’s spec today has no field-level mutability discipline. Every non-derived field is implicitly mutable via the update-stmt grammar (§7.5:306). This contradicts the rest of the architecture — the substrate is value-semantic with no borrows (§7:26), .oxbin is content-addressed and immutable (§19), the event log is append-only (§20.1), and the Lean State.lean + Fixpoint.lean prove information-monotonicity. Should Argon admit a field-level mutability marker (mut), what’s the default, and how does it interact with the existing modifiers?

Context

Today’s effective semantics

  • field-decl ::= attribute* Ident ':' TypeExpr ('=' expr)? ('from' relation-ref)? (§5.1:18). No mutability slot.
  • update-stmt ::= 'update' (Ident | pattern) 'set' '{' field-assign (',' …)* '}' ('where' expr)? ';' (§7.5:306). Any field may appear in field-assign.
  • Worked example (§7.5:334): update c: Company set { name = new }name is implicitly updatable.
  • #[intrinsic] is an attribute (not a keyword) declaring that a field must be set at kind level by every iof-instance (§5.1:135, §10.2:57). It governs construction-time required-ness, not post-construction mutability. A field can be both #[intrinsic] and (today) implicitly updatable.

What’s opt-out from mutability today

  • Refinement-determined classification (§7.5:357) — the substrate derives membership; explicit insert iof rejected with OE0211 IofInsertOnRefinedType.
  • Rigid metatype classification (§7.5:356) — kind / subkind / category individuals can’t be re-classified; insert iof / delete iof rejected with OE0210 IofInsertOnRigidType.
  • from-derived fields (§5.1:18) — value comes from a relation projection; not directly assignable.
  • Architectural.oxbin, Module, Engine (§19); axiom_events only grows via append (§20).

The genuine gap

There is no language for “field that is set at construction (or by #[intrinsic] binding, or from-derived) and may not be subsequently updated.” Modelers write pub kind Person { name: String, dob: Date, current_address: Text } with no way to say “name and dob are set-once; only current_address changes.” Today’s update-stmt admits writes to any of them.

The Argon vault’s open-questions doc (Efforts/On/Argon/scratch/open-questions response.md #18) names this verbatim: “Mutability annotations. No grep hit for mut/const/#[mut] on properties. Likely absent. Tied to #19 (change patterns). Genuine gap.”

mut is available

Per appendix-a:6-18, mut is not in the reserved keyword list. The mutation-related reserved words are mutate, insert, delete, update, upsert, detach, forget. Adding mut is a non-conflicting lexer change.

Decision

Surface

mut is a field-declaration modifier. Without it, fields are set at construction (or via #[intrinsic] kind-level binding, or by from-clause derivation) and immutable thereafter. With it, a field admits writes via update-stmt inside mutate bodies.

pub kind Person {
    name: String,                   // set at construction; not updatable
    dob: Date,                      // set at construction; not updatable
    #[intrinsic] ssn: Text,         // must be set at kind binding; not updatable
    mut current_address: Text,      // updatable post-construction
    mut current_employer: Company?, // updatable, optional
    #[intrinsic] mut current_role: EmploymentRole,  // both: required at construction, updatable later
}

pub mutate move(p: Person, addr: Text) {
    update p set { current_address = addr };       // OK
    // update p set { name = "Bob" };              // OE0820: name is not `mut`
}

Updated grammar

field-decl ::= attribute* 'mut'? Ident ':' TypeExpr ('=' expr)? ('from' relation-ref)?

Order: attribute* mut? Ident. Attributes precede mut; mut precedes the identifier. This places mut adjacent to the field name where its scope is most visible.

Defaults

  • A non-mut field is immutable post-construction.
  • A mut field is mutable post-construction.
  • A from-derived field is always derived (not directly assignable); mut on a from-derived field is rejected with OE0822 MutOnDerivedField.
  • A field with #[intrinsic] is required at construction; orthogonal to mut.

Lowering

Field mutations lower to append-only event pairs on the underlying axiom. A mutate body with update p set { current_address = "new" } emits:

  1. A retract event (Polarity::Retract) on the prior property_assertion row whose body is { entity_id = p, property_id = NameRef(current_address), value = "old" }. The retract event’s asserts_axiom points to the prior assert’s EventId.
  2. A new assert event (Polarity::Assert) on the new property_assertion row whose body is { entity_id = p, property_id = NameRef(current_address), value = "new" }.

Both share the same content-addressed AxiomKey for the proposition “p has current_address X” — the proposition’s identity is the property assertion’s logical content; the value is what changes. Per RFD 0001, AxiomKey is BLAKE3-128(canonical_body), so old and new have distinct AxiomKeys but the same (entity_id, property_id) pair identifies them as alternative assertions of the same property.

Elaborator checks

  • OE0820 UpdateImmutableFieldupdate e set { f = expr } where f is not mut. Caller is the update-stmt elaborator.
  • OE0821 MutOnRefinementDerivedField — reserved; not yet emitted (refinements are concept-level today, not field-level; this exists in case refinement gains field-level derivation later).
  • OE0822 MutOnDerivedFieldmut f: T from rel.range. The from-clause already determines the value; mut is contradictory.
  • OE0823 MutOnRelationTupleField — reserved for Form B/C relation tuple bodies whose intrinsic fields may want their own mutability discipline; nail down when relation-tuple updates land.

Interaction with the four orthogonal axes

The four existing constraint axes plus mut form a clean matrix:

AxisGovernsmut-relation
Metatype rigidity (kind vs role etc.)Whether x can stop being iof TIndependent. A rigid concept can have mut fields.
Refinement (concept-level where)Whether membership in T is derivedIndependent. Refinement determines classification, not field values. Fields of a refined concept follow normal mut rules.
from-clause (field-level)Whether the field’s value is a relation projectionmut is rejected on from-fields (OE0822).
#[intrinsic] (field-level attribute)Whether the field must be set at constructionIndependent. #[intrinsic] mut f: T is admitted — “must be specified at construction AND can change after.”

Same commit ships update-stmt

The update-stmt grammar (§7.5:306) is reserved but unimplemented in the parser/lowerer/runtime today. mut is a no-op without update. The implementation of this RFD ships:

  1. Parser: update statement parsing (§7.5:306 + 307).
  2. Lowerer: update lowers to a new Operation::Update core_ir variant which expands to retract+assert event pairs at runtime.
  3. Elaborator: rejects update p set { non_mut_field = ... } with OE0820.
  4. Runtime: execute_mutation’s operation interpreter handles Operation::Update.

Local let mut is OUT OF SCOPE for this RFD

The §7.5:293 'let' Ident (':' TypeExpr)? '=' expr ';' grammar has no rebind form. A let mut x = ...; x = ... local-rebind story is meaningful but deferred to a follow-up RFD when the mutate-body language grows expression-level computation needs. The current RFD only addresses field mutability on declarations.

Rationale

Why immutable by default

Three reasons:

  1. Aligns the surface with the substrate. Argon is content-addressed, append-only, value-semantic. Implicit field mutability is the one surface-level dissonance with the rest of the architecture. Making mutability opt-in restores coherence.

  2. Forces modelers to identify stable attributes. Real ontologies have a sharp distinction between identity-bearing attributes (birth date, SSN, kind classification) and contingent attributes (current address, status, balance). Today’s syntax doesn’t make this visible. mut-by-default would have been a research-friendly default but explicit-opt-in matches the Mercury / Rust / Haskell tradition: types tell you what’s mutable.

  3. Pairs with the rest of Argon’s modeling discipline. #[intrinsic] says “must be set”; from says “is derived”; <: says “is subsumed by”; refinement says “is constrained by.” Adding mut says “may change post-construction” — slots into the same pattern. No mut ⇒ no post-construction change.

Why keyword over attribute (mut vs #[mut])

Three considerations:

  • Visual prominence. Mutability affects how the field participates in mutations; it’s a first-class semantic property, not metadata. Keyword form (pub mut current_address: Text) signals this; attribute form (#[mut] current_address: Text) reads as decoration.

  • Consistency with rest of the surface. pub, from, where, :, = are all keyword-shaped in field declarations. #[intrinsic] is an attribute because it’s a kind-level constraint (governs the binding site), not a value-level property. mut is value-level. Keyword fits.

  • Frequency of use. Field declarations are common; mut will appear often. Keyword form is shorter (3 chars + space vs 8 chars + space).

Why ship update-stmt with mut

The update keyword is reserved but the parser doesn’t recognize it; the lowerer doesn’t emit Operation::Update. Without it, mut is a marker that nothing reads. Two reasons to ship together:

  • Coherent surface. Modelers learn mut and update together as a unit. Documentation lands as one chapter, not two halves separated by a release.
  • End-to-end testable. A pub_fact_keystone analog with pub mutate move(p: Person, a: Text) { update p set { current_address = a }; } followed by a query_derive proves the full path.

Why orthogonal mut × #[intrinsic]

The two govern different lifecycle points:

  • #[intrinsic] — “must be specified by every iof-instance at kind binding”
  • mut — “may be re-assigned post-construction by update-stmt”

A real ontology pattern: #[intrinsic] mut employment_role: Role — every employee has a role from day one, and the role can change as they’re promoted. Forcing them to choose between “intrinsic” and “mutable” is a false dichotomy.

Why no let mut in this RFD

The mutate-body statement language (§7.5:293-313) is currently minimal: let, match, insert/update/delete/upsert/detach/emit, for, if, expr;. Local rebinding is interesting but isolated — it doesn’t interact with the field-mutability story. Treating it separately lets the field-mutability discussion stay focused.

Alternatives considered

Alt 1: mut as documentation only (β from the design discussion)

Keep current “any field is implicitly updatable” semantics. Use mut as a documentation marker driving tooling (audit, indexes, change-notification).

Rejected: doesn’t close the gap the vault names. Adds noise without changing semantics. The whole virtue of mut is forcing modelers to identify which attributes change.

Alt 2: mut for non-field axes only (γ)

Skip field-level mutability; use mut for pub mut concept (concept-level opt-in to state change) or pub mut rel (relation-level opt-in to tuple update).

Rejected: pub mut concept is redundant with metatype-driven rigidity (role is already anti-rigid). pub mut rel is interesting (insert/delete vs update) but is a smaller question than field mutability and can be addressed via a future RFD once relation-tuple intrinsic-field semantics solidify.

Alt 3: Attribute form #[mut]

Use #[mut] instead of mut. Composes with the existing attribute machinery; parallel to #[intrinsic].

Rejected as primary surface. Considered seriously; the keyword form wins on visual prominence + consistency with pub / from (other field-decl keywords). Attribute form might be admitted as an alias if grep-friendly attribute scanning becomes valuable, but the canonical surface is the keyword.

Alt 4: Tri-state: mut / default / const

Add both mut (mutable) and const (set-at-construction-then-frozen) with default being “construction-time once, kind-level once, no other constraint.” (const is already reserved per appendix-a:8.)

Rejected: the three-state design is more expressive but adds modeler load. The current architectural decision is “default to immutable; opt-in to mutability via mut.” const stays reserved for compile-time-known-value semantics if/when that landing strip becomes necessary.

Alt 5: Per-field via update rule mode

Instead of marking the field, mark the mutate body: declare which fields the body may touch via a frame clause. Closer to TLA+ / Reiter SSA semantics.

Rejected: heavier syntax. Real-world ontology workflows have many small mutations touching one field each; per-mutate-body declaration would multiply boilerplate. Field-level marker is more direct.

Consequences

Wire format

property_assertion events already carry an EventId and the body. Adding mut doesn’t change the wire format — the runtime emits retract+assert event pairs as it would for any other mutation. The field-decl’s mut marker is part of the concept/struct/etc. declaration’s wire body and rides along.

core_ir

Operation enum (today: InsertIof, DeleteIof, InsertTuple, DeleteTuple, Forget) gains:

#![allow(unused)]
fn main() {
Operation::Update {
    entity: Term,
    assigns: Vec<FieldAssign>,
    where_clause: Option<Term>,
},
}

Plus the FieldAssign shape: { field_name: String, op: AssignOp, expr: Term } where AssignOp is Set | AddSet | SubSet (for =, +=, -=).

Parser

  • New mut keyword recognized at the lexer level (added to grammar.toml’s keyword list).
  • field-decl parser admits optional mut token between attributes and the ident.
  • update-stmt parser implements §7.5:306 including where clause.

Elaborator

  • lower_concept_decl / lower_struct_decl walk field declarations; carry mut flag through to wire body (ConceptDeclBody.fields[i].mut_flag or similar — TBD when the field body wire shape solidifies).
  • lower_mutate_body recognizes update-stmt, produces Operation::Update.
  • Elaborator checks (OE0820, OE0822) emitted during update-stmt lowering.

Runtime

execute_mutation’s operation interpreter gains a Operation::Update arm:

  1. For each FieldAssign, evaluate the expr against the current state.
  2. Look up the prior property assertion event for (entity, field_name).
  3. Emit a retract event for the prior + assert event for the new.

Diagnostics

Three new codes in grammar.toml, propagating to oxc-diagnostics::generated.rs + appendix-c-diagnostic-codes.md:

  • OE0820 UpdateImmutableFieldupdate e set { f = expr } where f lacks mut.
  • OE0821 MutOnRefinementDerivedField — reserved; not yet emitted.
  • OE0822 MutOnDerivedFieldmut f: T from rel.range is contradictory.

Spec migration

The spec’s worked examples need updating. Specifically:

  • §7.5:334 update c: Company set { name = new } — either annotate name as mut or rename the example to use a field that’s plausibly mut.
  • §21 walking example: review for any field updates and add mut annotations.

Lean

Argon/Syntax/Decl.lean (field-decl) gains a mut: Bool field. Argon/Storage/AxiomBody.lean doesn’t change (property assertions are already individual axiom events; the retract+assert lineage is already in Polarity + asserts_axiom). Argon/TypeSystem/FlowTyping.lean may need refinement of the immutability assumption to “non-mut fields stay immutable across mutation”; the proof obligation is small (per-field discipline rather than blanket immutability).

CI drift gate

field-decl’s Lean inductive carries mut: Bool; the matching Rust struct in oxc-protocol::storage::FieldDecl (or wherever fields are wire-typed) must include it. CI drift check covers this.

Open questions

  • Should mut admit += / -= only on numeric types? §7.5:307 field-assign allows =, +=, -=. For text fields current_address += "..." is ambiguous (string concatenation? error?). Lean toward: += / -= admitted only when the field’s type implements the arithmetic ops (Nat, Int, Real, Money, Duration). Resolve when the trait system covers numeric op overloading.

  • mut on collection fields: pub mut friends: List<Person> — does mut admit both update p set { friends = [...] } (replace) and insert p.friends(other) (push)? Probably both, but the semantics need pinning down when collection-field mutation lands.

  • mut on relation tuple bodies (Form B/C): a relation declared with intrinsic property body (§5.2 Form C) carries its own fields. Do those fields admit mut? Almost certainly yes, with the same defaults — but the syntactic surface (pub rel hasJob(p: Person, c: Company) [1] [1..*] { mut salary: Money }) needs validation. Reserved as OE0823.

  • Interaction with #[brave] / stable models (§7.8): mut writes happen at runtime; brave-derived facts at build time. The two don’t collide today but if a #[brave] rule references a mut field’s prior value, the rule’s stratification needs to account for the temporal axis. Defer to when brave rules see real use.

  • Update via partial assignment: update p set { current_address = ..., current_employer = ... } — atomic both-or-neither, or sequential? Today’s transactional semantics of mutate bodies suggests atomic; the elaborator emits the retract+assert event pairs in a single mutation transaction. Confirm when the runtime’s transaction semantics formalize.

  • Identity-preserving field mutation: when a mut field changes, the entity’s IndividualId stays the same (RFD 0001). The AxiomKey for the property changes (content-addressed). Confirm the MutationReceipt shape adequately captures the lineage for downstream consumers (provenance witnesses, audit logs).

  • Should pub fact (RFD 0004) be able to set a mut field at construction? pub fact Person(alice) { current_address: "..." } — yes, presumably, since this is construction-time assignment, not post-construction update. The fact-decl grammar in RFD 0004 doesn’t yet have a body form for setting fields; add when needed.

RFD 0007 — Missing-value semantics under OWA

  • State: discussion
  • Opened: 2026-05-28
  • Decides: what field: T (required), field: T? (optional), and field: Truth4Of<T> (epistemic) each mean under CWA and OWA; what happens when the value isn’t asserted; how Option<T> lifts (or doesn’t) inside rule-body comparison atoms; the diagnostic surface that forces the modeler to pick an intent rather than guess one.

Question

Argon today lets a modeler write pub kind Person { age: Nat? }. What does the ? mean? Two readings, both defensible, both produce different rule-evaluation outcomes:

  • Reference-ontology reading. Some persons genuinely have no age — the field’s absence is a positive fact about the world. None is on a par with Some(0) as a piece of information.
  • Implementation reading. Every person has an age, but the KB may not yet record it — the field’s absence is epistemic uncertainty. None means unknown, not no age.

The OWL/SHACL ecosystem keeps these apart by treating cardinality (owl:FunctionalProperty, sh:minCount) as TBox/SHACL-shape declarations and treating ABox-incompleteness as a property of the knowledge graph, not of the property. Argon, today, conflates them: T? is the only surface, the spec doesn’t say which intent it expresses, and §5.1 line 42 — “yield Option::None if the field’s declared type is T?, else error” — forces modelers to reach for T? whenever the KB might be incomplete, even when the modeler means the implementation reading. That’s a real loss of expressivity, and it forces the rule-evaluation semantics to invent an ad-hoc lift rule for Option<T> >= T that the spec never actually specifies.

This RFD picks the semantics, picks the surface, and documents the elaborator’s lifting discipline.

Context

The motivating thread (Almeida + Almeida, 2026-05-28)

Gustavo Ladeira (Sharpe) asked:

pub kind Person { age: Nat? }

pub derive Adult(p: Person) :- p: Person, p.age >= 18;
pub derive Minor(p: Person) :- p: Person, not Adult(p);

“In OWA, can a derive resolve to CAN? Does p.age >= 18 lift to Can if age isn’t present? And then is not Adult(p) for a person with no age Is(true) (so Minor fires erroneously) or Can (so Minor stays unknown)?”

João Paulo Almeida (UFES) sharpened the framing:

“There is a key issue here to flesh out. Optionality in this form (in an implementation) is usually ambiguous. If we are building a reference ontology (about the world), an optional age would mean there [are] people without age (so, this would be a bad modeling choice). But when this is about an implementation, we’d also like to know whether within the knowledge base this information (about age) is optional. In the RDFS/OWL world, this is partially addressed with age being a functional data property (thus every person has an age even though we might not know it), and then might be SHACL constraints to clarify whether the knowledge graph must have information about age.”

JPA’s distinction is the question this RFD answers.

What the substrate already mechanizes (and what it doesn’t)

Foundation/Truth4.lean + Foundation/Projection.lean mechanize the bilattice Truth4 = {Is, Not, Can, Both} and the Pietz–Rivieccio Exactly-True projection. Reasoning/Fixpoint.lean is K3-aware: rule-body conjunction (§6.10.5 truth table) and Kleene negation (¬Is = Not, ¬Not = Is, ¬Can = Can) are the operators the stratified fixpoint already uses.

§6.9 verbatim: “The substrate enforces this by lifting the derive/query evaluation into Truth4 under OWA and projecting to Boolean only at refinement membership / if / match boundaries.” §12.2 verbatim: “Boolean projections: Canfalse. Only Is(true) is designated.”

What the substrate does not specify:

  1. How field: T under OWA behaves when no value is asserted. §5.1 line 42 says “error.” But the OWL functional-property pattern — every Person has an age; the KB may not know it — requires this to lift to Can, not error.

  2. How Option<T> comp_op T evaluates in a rule-body atom. No rule. Today the elaborator type-errors, the parser accepts it, and the runtime would have to invent a coercion. The Gustavo trace above implicitly assumed None >= 18 lifts to Can; that assumption isn’t anywhere in the spec.

  3. Whether structural-optional and epistemic-optional have separate surfaces. §6.6 says T?Option<T>. That’s the structural reading. The implementation reading currently has no surface — and §5.1’s “else error” forecloses it.

Three real gaps. JPA’s question lands on all three.

OWL / SHACL precedent

The reference systems handle JPA’s distinction with separation of concerns:

  • TBox (OWL). Cardinality declarations (owl:FunctionalProperty, owl:minCardinality, owl:maxCardinality) state what is true of the world: every Person has an age. ABox-incompleteness does not contradict TBox cardinality under OWA — it means the missing fact is unknown.
  • SHACL shapes. Constraints over the knowledge graph (sh:minCount, sh:maxCount, sh:datatype) state what must hold of the KB. A SHACL violation means the KB is incomplete, not that the world is malformed.

Argon’s existing machinery already has the analogous parts:

  • TBox-cardinality analog. Field declarations on concepts. age: Nat says every Person has an age (OWL functional + min 1). age: Nat? says some Persons may genuinely lack an age.
  • SHACL-shape analog. where { … } refinement clauses (§6.3) and #[intrinsic] (§5.2). These constrain the KB, not the world.

The pieces are in place. What’s missing is the lift discipline that connects them under OWA.

Decision

Decision pending ratification (2026-06-12, PR #289). This RFD is in discussion state, and its OE1014 story (Decision table row 1 / the lift rule below) places the required-field-completeness diagnostic at field-access / evaluation time — a query reading an unasserted required field under CWA is the schema violation. PR #289 needed a completeness gate for the insert iof(x, T) classification side-door (audit ufo-mut-06), and shipping the evaluation-time emitter is a larger build (it lifts every required-field read into a CWA cardinality check). So #289 decides and implements a narrower, complementary site: an in-body-vs-staged discriminator at commit time. A mutate body that classifies x into T and writes fields of x in the same body is constructing x in-body; completeness is judged at body end (read-your-writes, RFD 0019 RC2) and OE1014 refuses atomically if a required field is left unset. A body that only classifies — no in-body field writes to x — is staged construction and is permitted to defer (this RFD’s “ABox-incompleteness is fine”); it currently produces no diagnostic. Rationale: a blanket write-time / construction-time refusal would break staged construction across mutations (the legal_norms_can_vote::register and keystone add_sat → set_cap patterns), which this RFD explicitly blesses; keying on whether the same body populates the individual is the discriminator that lets both shapes stay green. This is a fresh design decision owned by #289, not a clause this RFD already settled — the RFD specifies the opposite site (field-access). The field-access-time / evaluation-channel emitter (Decision table row 1 CWA column, the lift rule’s state lacks ... CWA → OE1014 line) remains the open half of this RFD and is unbuilt; it is tracked at #292, and this RFD still needs ratification to lock both halves.

Three surfaces, three distinct intents

SurfaceIntentConstruction timeQuery under OWA (value not asserted)Query under CWA (value not asserted)
field: TOntologically present. Every instance has this property.Required; absent → OE0207Access lifts to CanSchema violation; OE1014
field: T?Structurally optional. The property genuinely may not apply.May be omitted; defaults to NoneReturns None (a positive fact)Returns None (a positive fact)
field: Truth4Of<T>Epistemic-explicit. Modeler wants the four-valued shape exposed.May be Is(v) | Not | Can; defaults to CanReturns whatever is assertedReturns whatever is asserted

These three surfaces correspond exactly to JPA’s distinction (rows 1+3 are the implementation reading; row 2 is the reference-ontology reading), plus an escape hatch for modelers who want the bilattice shape directly. The default T row matches OWL functional-property + min-cardinality 1 under OWA; the T? row matches Rust-style structural Option.

The lift rule (the load-bearing piece)

In rule-body atom context, a field access p.field evaluates as follows:

For field: T (required, ontologically present):

state has hasField(p, v)        → Is(v) at the Truth4 level; surface value is v
state lacks hasField(p, _), OWA → Can at the Truth4 level
state lacks hasField(p, _), CWA → model-level error OE1014 (cardinality violation)

For field: T? (structurally optional):

state has hasField(p, v)         → Some(v)
state lacks hasField(p, _), any  → None  (a positive fact; no Truth4 lift)

For field: Truth4Of<T>:

state has hasField(p, v)         → Is(v)
state has not_hasField(p)        → Not        (explicit negative assertion)
state lacks both                 → Can

Surface comparison atoms use the resulting Truth4 / Option / value:

p.age >= 18          // T:    Is(true) | Not | Can per the trace above
                     // T?:   TYPE ERROR — see below
                     // T4Of: Is(true) | Not | Can — same as T, but explicit

Option<T> in comparison atoms is a type error

Option<T> comp_op T (and Option<T> comp_op Option<T>, and friends) is rejected by the elaborator: OE0612 OptionComparisonRequiresHandling with a diagnostic that suggests three explicit forms:

// 1. Pattern in the rule body — preferred when None should fail-closed
p.age is Some(a), a >= 18

// 2. Helper method on Option<T>
p.age.is_some_and(|a| a >= 18)

// 3. Match with explicit unknown handling
match p.age {
    Some(a)    => a >= 18,
    None       => false,           // pick: false / true / Truth4Of::Can
}

The first form (is Some(a)) compiles to a guarded match that fails the conjunction (binds nothing) on None — a positive fact about absence. The second is sugar for the same. The third lets the modeler explicitly say what None means in their model.

This is JPA’s point made structural: if you wrote T?, the language refuses to guess what None should mean in a comparison; you must say.

Required-field-under-OWA: the lifted access rule

§5.1 line 42 amends to:

No value present at field access — under OWA, lift to Can at the Truth4 level (the surface returns the field’s declared type via the K3 fail-closed projection: false for Bool, omitted for collections); under CWA, emit OE1014 RequiredFieldUnasserted (the schema declared this field present; the KB must record it). At construction time, the existing OE0207 IntrinsicPropertyMissing rule for #[intrinsic] fields is unchanged.

The asymmetry — construction strict, query OWA-lifted — is intentional and matches OWL: TBox cardinality requires presence, ABox completeness is a separate (SHACL) concern.

Diagnostic codes

CodeSeverityNameTrigger
OE0612ErrorOptionComparisonRequiresHandlingOption<T> comp_op T (or comp_op Option<T>) without explicit handling. Suggests is Some(a) / is_some_and / match.
OE0613ErrorTruth4ComparisonRequiresHandlingTruth4Of<T> comp_op T without is Is(a) || is Not || is Can handling. Suggests is outcome-suffix or match.
OE1014ErrorRequiredFieldUnassertedUnder CWA, a required field’s value is not derivable from any asserted axiom — the schema mandates presence; the KB violates it. Surfaces at evaluation, not at construction.
OW1015WarningOptionalFieldAmbiguousIntentpub kind X { f: T? } declared without a #[doc] comment or #[intent(structural)] / #[intent(epistemic)] attribute; the elaborator notes the ambiguity per JPA. Demoted to off by #[allow(optional_field_ambiguous_intent)].
OE1016ErrorTruth4OfOnStructTruth4Of<T> field type on a struct (no metatype classification). Restricted to ontologically-classified concepts.

Spec edits

SectionEdit
§5.1 line 42Add the OWA lift case (above).
§5.1 (new subsection §5.1.x)Document the three field-type intents with the table from “Three surfaces, three distinct intents.”
§6.6 line 40Footnote: T? is the structural-optional surface; for epistemic uncertainty, use T + OWA or Truth4Of<T>.
§6.9 (Interaction with NAF para)Cross-reference §5.1.x; clarify that the Truth4 lift applies only to required fields and Truth4Of<T> — structural-optional fields don’t lift, they return Option.
§7.3.1 (Rule-atom grammar)Add the lift discipline: when a comparison atom’s left side has type T, evaluation lifts to Truth4 per §6.9; when it has type Option<T>, the elaborator emits OE0612 unless the modeler handled None.
Appendix CAdd the five diagnostic codes above.

Lean mechanization

ModuleAddition
Argon/Substrate/Construct.leanExtend FieldDecl carrier with the three intent kinds (Required, StructurallyOptional, EpistemicExplicit).
Argon/Reasoning/State.leanLift accessField to return Truth4 × Value, branching on the field’s declared intent + the governing WA.
Argon/Reasoning/Fixpoint.leanThe existing K3 conjunction + negation already handles the Truth4 carrier; no change needed past the access function.
Argon/TypeSystem/Soundness/FieldAccess.lean (new)Theorem: under OWA, required-field access lifts to Can when the asserting axiom is absent; under CWA, the absence is contradictory (proof via Reasoning/Stratification.lean’s well-founded fixpoint).

The theorem statement is roughly:

theorem field_access_owa_lift
    {P : Program} {p : Individual} {f : RequiredField}
    (h_no_assertion : ¬ ∃ v, P.state.has (hasField p f v))
    (h_owa : P.worldAssumption f.declaringConcept = .open) :
    P.accessField p f = (Truth4.can, default)

with the CWA branch as a separate (sharper) statement: under CWA, h_no_assertion is provably false for any iof(p, concept) ∈ P.state (the schema mandates a value), so the case is unreachable and the elaborator’s OE1014 is sound.

The Gustavo trace, after this RFD lands

With Gustavo’s code as written (age: Nat?):

pub kind Person { age: Nat? }
pub derive Adult(p: Person) :- p: Person, p.age >= 18;
//                                         ^^^^^^^^^^^ OE0612

The elaborator rejects the comparison at OE0612, asking the modeler to pick. Gustavo then makes the modeling choice JPA was asking him to make — either:

// Reference-ontology reading: people with no age aren't adults; Minor fires
pub derive Adult(p: Person) :- p: Person, p.age is Some(a), a >= 18;
pub derive Minor(p: Person) :- p: Person, not Adult(p);
// carol (no age) → Adult = Not (the `is Some(a)` fails) → Minor = Is

or:

// Implementation reading: every person has an age; KB may not know it
pub kind Person { age: Nat }     // required, OWA-aware lift handles the gap
pub derive Adult(p: Person) :- p: Person, p.age >= 18;
pub derive Minor(p: Person) :- p: Person, not Adult(p);
// carol (no age asserted, OWA) → p.age lifts to Can
//   → Adult body = Is ⊓ Can = Can
//   → not Adult(carol) = ¬Can = Can
//   → Minor(carol) = Is ⊓ Can = Can
// Minor's extent at Bool projection: carol omitted. At Truth4Of<Person>: shown as Can.

JPA’s two intents are no longer conflated; the elaborator has forced the choice; the Truth4 lift handles whichever choice the modeler made; the diagnostic carries the explanation.

Rationale

Why three surfaces instead of two

A naive design would keep just T and T? and let OWA do all the work. That collapses JPA’s two intents into one surface (T?), reproducing today’s ambiguity. Truth4Of<T> adds a third surface for the specific case where the modeler wants the bilattice value exposed in storage — relatively rare, but exactly what one needs when modeling, e.g., a clinical-trial endpoint where “patient response = unknown” is a first-class data point separate from “patient response = no.”

Three surfaces is the minimum that lets every modeler intent be expressed cleanly:

  • T: TBox cardinality 1 + OWA-tolerant ABox.
  • T?: structural optionality (no TBox cardinality assertion).
  • Truth4Of<T>: explicit four-valued storage.

Why type-error on Option<T> comp_op T

JPA’s “ambiguous” is the load-bearing observation. If the elaborator silently coerces None to false (Reading C, SQL-style), the modeler never confronts the ambiguity and the model’s semantics drifts from intent. If the elaborator silently lifts None to Can (Reading A, Truth4-style), the modeler is again not consulted. The type error is the modeler-respecting move: ask the modeler what they mean.

The cost is one extra ceremony at every Option<T> comparison site (is Some(a), a >= 18 instead of a >= 18). The benefit is no silent ambiguity, no quiet semantic drift, and a diagnostic that doubles as documentation.

Why required-field-under-OWA lifts to Can (not errors)

Without this lift, the spec is internally inconsistent. §6.9 promises OWA semantics (knowledge is open; absence is uncertainty); §5.1 promises construction-time strict checks (required fields must be set). Today these collide: under OWA, an iof(carol, Person) axiom can be asserted with no hasAge tuple, but accessing carol.age errors out — which contradicts §6.9’s premise that absence is uncertainty.

The fix preserves both intents by separating their layers. Construction (the axiom event being inserted) stays strict — you can’t write Person { name: "carol" } without age because the construction is a single axiom and missing required fields are malformed at the event level. Query (subsequent field access on a possibly-incomplete KB) lifts to Can under OWA, matching OWL functional-property semantics exactly: every Person has an age (TBox), but we may not know it (ABox-incompleteness is fine under OWA).

CWA does not tolerate this: under CWA, an iof(carol, Person) with no hasAge(carol, _) is a schema violation, and OE1014 is correct.

Why OW1015 OptionalFieldAmbiguousIntent

The warning catches the JPA case at declaration time: a modeler writes f: T? without saying which intent they mean. The warning links to the table in §5.1.x and suggests either #[doc] documentation, an explicit #[intent(structural)] attribute, or migration to T + OWA. It can be silenced per-field. The warning is off by default in the prelude (std::* legitimately uses T? for plumbing types like Option<Person> returned from one { ... } queries) and on by default in user code with #[intent(...)] available as the disambiguator.

Alternatives considered

A. Implicit lift of Option<T> to Truth4 in rule bodies (Reading A)

Auto-translate None → Can, Some(v) → standard comparison verdict. Rejected. Silently picks the implementation reading; loses the reference-ontology reading entirely. Modelers who wrote T? to mean “some persons have no age” would find their rules treating those persons as unknown instead of positively-not-an-adult — a semantic regression that’s invisible at the source. Fails JPA’s framing.

B. Option<T> comp_op T returns Option<Bool> via functor lift (Reading C)

Auto-translate None comp_op v → None, Some(a) comp_op v → Some(a comp_op v). Force the modeler to write match on the result. Rejected. Type system gets noisier without solving the underlying ambiguity; modelers will reflexively write match { Some(b) => b, None => false } and the SQL-NULL silent collapse returns. The type-error path (Decision) forces the choice earlier where the modeler still remembers the intent.

C. Only two surfaces: T + OWA and T?

Drop Truth4Of<T> as a field type. Rejected. Loses the explicit-epistemic case (clinical-trial endpoints, audit fields marking “unknown” as a first-class value). The marginal cost of admitting Truth4Of<T> as a field type is low — it’s an existing stdlib type — and the expressivity is meaningful for medical / legal / scientific modeling.

D. Defer the entire question; leave §5.1’s “else error” rule as-is

Wait for more modelers to hit the wall. Rejected. Two of the language’s core consultants (Gustavo, JPA) hit it on day one of working through a temporal model. The substrate is Truth4-aware; the spec gap is at the surface; the cost of resolving is small (~5 elaborator checks + the diagnostic codes). Deferring would let modelers internalize ad-hoc workarounds and would force oxc-instantiate to ship without crisp Option-handling semantics.

E. Adopt OWL/SHACL syntax wholesale (#[functional], #[min_count], #[max_count])

Map TBox-cardinality + SHACL-shape vocabulary to attributes on field declarations. Rejected as the primary surface. Argon’s T / T? distinction is more ergonomic than OWL’s flat-property + cardinality-restriction model; adopting OWL vocabulary as the primary surface would be a Rust-aesthetic regression (per the user-memory directive defaulting to Rust/Cargo aesthetic). The OWL/SHACL pattern is reachable as a target via the field-type decision matrix above, without forcing modelers to write #[functional, min_count = 1] at every field site.

Consequences

Source-level

  • A pub kind Person { age: Nat } model becomes feasible under OWA where today the spec forces Nat?.
  • Option<T> comp_op T rule-body atoms must be rewritten with is Some(a) / is_some_and / match. The Cargo-style ecosystem migration: one-shot cargo ox fix rewrite per repo.
  • Truth4Of<T> becomes available as a field type. Modelers who want OWL-style four-valued storage have a first-class surface.

Substrate-level

  • Two new variants on FieldDecl (intent kind: required / optional / epistemic).
  • One new branch in accessField: returns (Truth4, Value) instead of Value. Hot-path impact: one extra tag byte in the tuple-encoding, negligible.
  • relation_tuple axiom kind unchanged; the lift happens at the accessField level, not in storage.

Runtime-level

  • The K3 truth tables (§6.10.5) are already mechanized and used; this RFD adds no new operators.
  • Query results returning collections now omit Can-valued field rows under K3 fail-closed projection — already the spec, but newly exercised at scale.

Diagnostic-level

  • Five new codes (OE0612, OE0613, OE1014, OW1015, OE1016).
  • One existing code (OE0207 IntrinsicPropertyMissing) keeps its current semantics; the new OE1014 covers the disjoint case of required-non-intrinsic absence under CWA.

Compatibility

T? retains its current Option<T> semantics — modelers who already use it for structural optionality see no behavior change. The new OE0612 may flag existing rule bodies; the suggested fix is purely mechanical (is Some(a), insertion). T under OWA gains new expressivity that didn’t exist before. No silent semantic changes to any existing well-typed program.

Open questions

OQ1 — Truth4Of<T> as field type interactions with mutation

A field declared f: Truth4Of<T> can be set to Is(v), Not, Can, or Both(v1, v2) via insert. What is the storage representation? Likely two relations: hasField_pos(p, f, v) and hasField_neg(p, f). Both is encoded as both relations holding simultaneously. The mapping is mechanical but worth specifying before implementation.

OQ2 — Interaction with refinement clauses

A refinement pub subkind Adult <: Person where { self.age >= 18 } over a required age: Nat field, under OWA, evaluates to Is | Not | Can per §6.9. Does the refinement-classification machinery in TypeSystem/Soundness/FlowTyping.lean handle the Can case? Spot-check needed: I expect yes (the existing OWA branch already covers it), but the new lifting rule introduces a Can source the type system didn’t previously consider.

OQ3 — Aggregate semantics over Can-valued cells (resolved — see RFD 0011)

What does sum { p.age | p: Person } evaluate to when some p.age lift to Can? Resolved by RFD 0011: monotone aggregators (sum non-negative, count, set_collect) evaluate to Truth4Of<T> with interval bounds [lower, upper] where lower = aggregate over filter-Is-true cells and upper = aggregate over filter-Is-true-or-Can cells; non-monotone aggregators (min, max, avg, string_join, percentile) propagate Can (any Can-cell in the filter set → whole result is Can). The Truth4 result is projected at the typed boundary per §12.2’s K3 fail-closed rule; the projection is observable via diagnostic OW0613 (info in query/fn, warning in derive, error in check) and an always-present aggregation-metadata envelope on query results carrying the interval bounds and Can-cell counts. The motivating Gustavo Ladeira / J.P. Almeida thread (2026-05-28 → 2026-05-29) is the worked example; the full design and rationale lives in RFD 0011.

OQ4 — Path traversal under Can-valued intermediate steps

For alice.parent.spouse.age, if alice.parent lifts to Can, does the full chain short-circuit to Can or does it propagate through the K3 conjunction of step verdicts? Existing field-path semantics in Reasoning/Rule.lean treats path steps as conjunctions; this would give natural propagation. Confirm.

OQ5 — #[intent(structural)] / #[intent(epistemic)] as the disambiguator

The proposed attribute makes the modeler’s intent explicit at declaration time. Should it be required for T? declarations in non-prelude modules? Two readings: (a) required (zero-ambiguity policy; matches Argon’s “no implicit override” philosophy from §5.2); (b) optional with OW1015 warning (gentler migration). Default to (b); revisit if modelers report confusion.

OQ6 — Migration path for existing UFO vocabulary

The UFO stdlib package declares several optional fields in its concept catalog (Person, Organization, …). Are these structural-optional or epistemic-optional? A scan of ufo/src/*.ar is needed to assign intents per-field before this RFD lands; this is a UFO-package PR, not an argon PR.

References

  • §5.1 (struct/concept field declarations, OE0207), §5.2 (concept supertype clauses), §6.3 (refinement, three-valued membership), §6.6 (T?Option<T>), §6.9 (CWA/OWA), §6.10.5 (strong-Kleene truth tables), §7.3.1 (rule-atom grammar, is unknown outcome-suffix), §12 (Truth4 + Pietz–Rivieccio projection) — spec/reference/src/
  • Foundation/Truth4.lean, Foundation/Projection.lean, Reasoning/Fixpoint.lean, Reasoning/State.lean — substrate mechanization
  • oxc-instantiate (elaborator), oxc-reasoning::compile::Value (runtime carrier) — implementation sites
  • W3C OWL 2 Web Ontology Language Direct Semantics, §2.3.3 (functional property axioms)
  • W3C SHACL §3 (Shape constraints)
  • Pietz, A. & Rivieccio, U. (2013). Nothing but the truth. Journal of Philosophical Logic — the Exactly-True semantics underlying §12.2’s K3 fail-closed projection.
  • Belnap, N. (1977). A useful four-valued logic. — the underlying bilattice.
  • Almeida, J.P.A. (UFES) and Ladeira, G. (Sharpe), Slack thread, 2026-05-28 — the motivating discussion.

RFD 0008 — Standpoint-Sheaf Equivalence Proof Roadmap

  • State: committed (Path A landed)
  • Opened: 2026-05-28
  • Path A landed: 2026-05-29 — Argon/Standpoint/AFTEquivalence.lean mechanizes the discrete T3 obstruction equivalence (aft_discharges_T3_obstruction proven).
  • Decides: the proof obligation, viable proof paths, and arc structure for elevating standpoint-sheaf equivalence from “axiomatized / open research” to mechanically proven in Lean.

Question

Standpoint logic (Gómez Álvarez & Rudolph 2021) and sheaf cohomology (Abramsky & Brandenburger 2011) independently formalize multi-perspective knowledge. The conjectured equivalence between them — “a set of standpoint axioms has a consistent global merger iff the associated sheaf has trivial H¹” — is currently axiomatized in Argon’s Lean mechanization, with AGENTS.md noting it as “open research.”

Should Argon push to prove this equivalence, and what’s the right proof path?

Context

Why this matters strategically

The competitive audit identified federation across standpoints as Argon’s most distinctive feature — no other production language has it as a first-class primitive. The current Lean mechanization proves a Finset-based version (Argon/Locality/SheafEquivalence.lean: grounded MCS equilibrium = minimal global section over module DAGs). This is operationally useful but does not connect to the broader logical-topological equivalence the literature poses.

Without the broader proof, Argon’s federation claim is “the runtime computes equivalent results to a sheaf model under our axiomatic embedding.” With the proof, the claim becomes “the runtime computes a sheaf model — and we know precisely when and how the model breaks.”

The user has designated this a worldclass objective. The vault’s “Standpoint-Sheaf Dictionary” note carries the conjectured translation table. This RFD makes the proof obligation concrete.

What’s already proven

In Argon/Locality/SheafEquivalence.lean (~180 lines, lake-green):

  • Theorem 1 (bottom-up computation → equilibrium): definitional.
  • Theorem 2 (equilibrium → global section): from local_fp inclusion.
  • Theorem 3 (equilibrium is minimal global section): from local_fp being a least fixpoint.

These cover a BeliefAssignment (function ModId → Finset Atom) with generic bridge and local_fp operators. The acyclicity of the module DAG is required.

Adjacent mechanization (Argon/Standpoint/Federation.lean) proves the FDE bilattice info-join (federate_eq_both_iff, strictFold_preserves_inK3) — the AFT-side analogue of “cross-source disagreement detection.”

What’s NOT proven

The three sub-theorems forming the conjecture:

Sub-theoremStatementStatus
T1S5 Kripke frame (W,R) over standpoint set with induces a canonical Grothendieck topology J on the standpoint category S.Unproved
T2Bridge rules form sheaf restriction maps satisfying the gluing axiom (local sections agreeing on overlaps extend uniquely to a global section).Unproved
T3Grounded equilibrium ≅ H⁰(F); irreducible disagreement ≅ H¹(F) ≠ 0 (Abramsky-Brandenburger 2011).Unproved

Decision

Pursue the proof in three escalating proof paths, each independently shippable.

Path A — AFT-only restatement (lowest risk, ~600 LOC) — LANDED 2026-05-29

The existing Foundation/Federation.lean’s federate_eq_both_iff is already the bilattice-algebra-side analogue of “H¹ ≠ 0 detects contextuality.” T3 is restated in AFT terms:

federate contribs = .both ↔ no global K3-section exists

This makes T3 a corollary of existing mechanized work. T1 and T2 are not discharged in this path — we lose the cohomological diagnostics (cocycle witnesses, spectral solver) but keep the soundness story.

Cost (actual): 1 new file Argon/Standpoint/AFTEquivalence.lean (~225 LOC including docstrings). One iteration through Lean’s cases/split_ifs tactics.

What landed:

  • SheafClassification inductive: consistentTrue / consistentFalse / undetermined / obstructed.
  • sheafClassify : List Truth4 → SheafClassification — direct semantic over federate.
  • aft_discharges_T3_obstruction (proven): sheaf-obstructed ↔ federate contribs = .both. The load-bearing theorem; the AFT-side analogue of “H¹(F) ≠ 0 detects contextuality.”
  • sheafClassify_consistentTrue_iff, sheafClassify_consistentFalse_iff, sheafClassify_undetermined_iff — three companion characterizations.
  • sheafObstructed_iff_disagreement_or_explicit_both — composes the above with federate_eq_both_iff for the diagnostic-surface-shaped form.
  • sheafClassify_singleton, sheafClassify_empty — base cases.

What this ships: a named theorem in Lean asserting “Argon federation = AFT-bilattice-based cross-source consistency.” Bridges the existing operational federation runtime to a categorical-flavored claim.

Path B — Frame-theoretic (medium, ~1200 LOC)

Replace the Grothendieck topology with the simpler structure of a complete Heyting algebra (frame) of standpoint-downsets. Mathlib has Order.Heyting.Basic and Topology.Sheaves.Sheaf over locales. Bridge rules become frame homomorphisms.

This discharges T1 routinely (frames induce topologies; standpoint-downsets form a frame canonically) and T2 (frame homomorphisms preserve gluing). T3 still requires work but on more familiar ground than full Grothendieck descent.

Estimated cost: ~1200 LOC. ~6 weeks.

What it ships: a partial proof of the equivalence — the topology induction and the bridge-rule-as-restriction-map sides, with T3 stated as a theorem schema parameterized over the localic sheaf machinery.

Net gain over Path A: the equivalence is now bidirectional (Argon → sheaf AND sheaf → Argon round-trip), not just embedding.

Path C — Full Grothendieck (highest, ~2000 LOC)

Mathlib has CategoryTheory.Sites and Grothendieck topologies. Construct the topology directly; prove T1, T2, T3 via descent.

The genuinely novel obstacle: connecting Hansen-Ghrist’s sheaf Laplacian diffusion (proved for vector-space stalks) to Argon’s lattice-valued knowledge requires either a box-embedding (Garcez-Lamb) relaxation or a discrete cohomology theorem. This is open research; no published work bridges it.

For the equivalence itself, this gap is not blocking — the equivalence can be discrete on the lattice side. But the computational payoff (spectral solver, cohomological diagnostics) depends on it.

Estimated cost: ~2000 LOC. ~12 weeks for the equivalence; the spectral bridge is a separate research effort.

What it ships: a research paper. Independent value beyond Argon’s runtime needs.

  1. Arc N+1 (this RFD): Path A. ✅ Landed 2026-05-29. Discharges the immediate Argon claim (“federation is sound under AFT info-join, which is the bilattice-algebra-side analogue of the sheaf claim”). Closes the wire-clean version of the worldclass objective.

  2. Arc N+2 (follow-on): Path B. Lifts to frame-theoretic, discharging T1 + T2 + a parameterized T3. The equivalence becomes bidirectional.

  3. Arc N+3 (research-paper track): Path C. Full Grothendieck. Separately publishable; not on the Argon roadmap critical path. Bridge to continuous diffusion is a deferred research obligation.

Paths A and B compose: Path A’s theorems are corollaries of Path B’s. Path B’s theorems are corollaries of Path C’s. Each arc strictly strengthens the previous.

Rationale

Why three paths rather than one

Path C is what the literature poses. Path A is what we can ship next week. Path B is the sweet spot. Allowing all three to live in the roadmap acknowledges that:

  • Argon’s runtime needs the operational claim (Path A) now.
  • The mathematical claim worth defending publicly (Path C) is research-scale.
  • The intermediate (Path B) delivers most of the value with mathlib4’s existing infrastructure.

Why not just ship Path C and skip the intermediates

Path C’s continuous-discrete bridge is a genuine open problem. If we commit to Path C exclusively, the roadmap is hostage to one unproven research result. Splitting into three paths means each is independently shippable; if Path C stalls on the continuous-discrete bridge, Paths A and B are unaffected.

Why the AFT-only path counts as discharging the worldclass objective

The vault’s “Standpoint-Sheaf Dictionary” note maps:

  • federate contribs = .both ↔ “irreducible disagreement”
  • K3-fragment containment ↔ “global section exists”

federate_eq_both_iff already proves this in Argon/Standpoint/Federation.lean. Restating it as the canonical sheaf-equivalence theorem under the AFT lens IS the worldclass deliverable for Argon’s runtime semantics, operationally. The Grothendieck topology proof is the categorical deliverable, with mathematical content beyond the runtime claim. Both are worth pursuing; Path A satisfies the worldclass objective for Argon-as-a-language; Path C satisfies the worldclass objective for Argon-as-a-publishable-research-contribution.

Alternatives considered

Alt 1: leave the equivalence axiomatized

Mark the three sub-theorems as axiom declarations with citations to Gómez Álvarez & Rudolph 2021 and Abramsky & Brandenburger 2011. AGENTS.md permits cited axioms. Cost: zero. Risk: the runtime claim depends on the citations being correct.

Rejected per user strategic clarification: “we should absolutely push for a proper proof and make sure that it’s world-class.”

Alt 2: ship only the AFT-only path

Land Path A; declare the categorical claim out of scope. Cost: ~600 LOC. Risk: ceiling-bound; the federation story remains “operational only,” with no path to spectral / cohomological diagnostics.

Rejected as too narrow. The user designated this worldclass; a worldclass deliverable includes the mathematical claim, not just the operational one.

Alt 3: ship Path C exclusively

Commit to the full Grothendieck construction; treat A and B as intermediate steps not worth landing independently.

Rejected: the continuous-discrete bridge is a research gap with no published solution. Risking the roadmap on it is unwise. Splitting into three paths lets each ship independently.

Consequences

Lean changes (per arc)

  • Arc N+1 (Path A): ~600 LOC. New file Argon/Standpoint/AFTEquivalence.lean. Extends Federation.lean with the canonical theorem statement.

  • Arc N+2 (Path B): ~1200 LOC. New file Argon/Standpoint/FrameEquivalence.lean. Depends on mathlib4 Order.Heyting.Basic + Topology.Sheaves.Sheaf.

  • Arc N+3 (Path C): ~2000 LOC. New file Argon/Standpoint/SheafEquivalence.lean (NOT the existing Locality/SheafEquivalence.lean, which proves the discrete Finset form). Depends on mathlib4 CategoryTheory.Sites. Spectral bridge is a separate research effort.

Reference book changes

§11 (Standpoints and federation) gains a new subsection citing this RFD’s three theorems as the soundness foundation. Currently §11.x reads as informal motivation; after Path A lands, it can cite the mechanized theorem directly.

Runtime impact

Path A: none. Federation runtime continues to use AFT info-join; the equivalence theorem is a soundness claim about it.

Path B: enables across[] queries to use frame-homomorphism preservation as a static guarantee. Diagnostic surface gains an “irreducible disagreement: cocycle on edges {…}” message under H¹ obstruction.

Path C: enables sheaf Laplacian diffusion as an alternative federation evaluator (spectral solver for acyclic module DAGs). Spec gain λ₁-spectral-gap as a federation cost metric.

Citation registry

Each path adds named theorems whose proof obligations cite specific prior work. The citation list (book §22 references) gains:

  • Gómez Álvarez, S. & Rudolph, S. (2021). Standpoint Logic: Multi-Perspective Knowledge Representation. (Path A, B, C)
  • Abramsky, S. & Brandenburger, A. (2011). The Sheaf-Theoretic Structure of Non-locality and Contextuality. (Path B, C)
  • Hansen, J. & Ghrist, R. (2020). Opinion Dynamics on Discourse Sheaves. (Path C, spectral bridge)
  • Mac Lane, S. & Moerdijk, I. (1992). Sheaves in Geometry and Logic. (Path C foundations)
  • Denecker, M., Marek, V., Truszczyński, M. (2000). Approximation Fixpoint Theory. (Path A AFT side)

Open questions

  • Continuous-discrete bridge for stalks. Hansen-Ghrist’s diffusion convergence is proved for vector-space stalks. Argon’s modules use lattice-valued knowledge. No published work bridges them. Path C’s spectral payoff depends on resolution; the equivalence itself does not. Is the spectral-bridge worth a dedicated research effort, or accept the discrete cohomology theorem?

  • Cocycle diagnostic surface. Under H¹ obstruction, what does the modeler see? “Irreducible disagreement on bridges {b1, b2, b3}” with a cycle witness? Diagnostic ergonomics design.

  • Functorial canonicity of T1. Classical topology literature has Grothendieck constructions for modal Kripke frames, but no published canonical induction for standpoint orders. Path C may require an originality contribution here.

  • MLT-style multi-level stalks. Standpoints can carry metatypes (a standpoint’s knowledge includes higher-order classifications). Whether sheafification respects MLT order arithmetic is unaddressed in the literature. Defer to MLT std library RFD.

  • Defeasible bridges. Bridge rules can themselves be defeasible (per Argon’s defeasibility substrate). Does the sheaf framework absorb Governatori-Rotolo +Δ/-Δ proof tags as graded restriction maps? Open.

RFD 0009 — std::mlt library scope

  • State: discussion
  • Opened: 2026-05-28
  • Last revised: 2026-05-29 (added #[order(N)] assertion decorator + completed decorator set per discussion with Tiago Sales; fixed @[…]#[…] sigil throughout to align with §14.2)
  • Decides: scope of the std::mlt library that provides Multi-Level Theory (Carvalho-Almeida 2018) as a parallel std::* package on Argon’s neutral substrate; surface syntax for decorators (relational AND assertion forms); the Datalog rules CL-1..CL-7 enforcing MLT well-formedness; per-rule diagnostic emission for OE1903–OE1907.

Question

Argon’s substrate is committed to MLT-as-library (RFD-relevant memory: “Higher-order theories as stdlib libraries”). The substrate provides the higher-order type primitives required (universe polymorphism via metaxis, level-indexed quantification via metatype) and now carries the five MLT primitive metarel kinds (MLTMetarelKind per the just-landed Argon/MetaCalculus/MLTKinds.lean). What’s needed is the library that operationalizes MLT — decorators, Datalog enforcement rules, diagnostic emission.

What should std::mlt v0.1 ship, and how should it be wired into the build pipeline?

Context

Why a library, not substrate

Per user strategic commitment: “MLT is a library, not substrate. The atoms of the language allow for MLT to be implemented, as we have higher-order type primitives, but MLT must be a library.” This preserves Argon’s neutrality. UFO, BFO, DOLCE, ML2 all ship as parallel std::* packages; the language doesn’t favor one foundational ontology over another. The substrate’s responsibility ends at exposing sufficient primitives.

This commitment is verified for MLT specifically by the just-landed substrate sufficiency scaffold:

  • instanceOf, specializes admitted via the inheritance lattice + IsCanNot machinery.
  • categorizes, partitions, subordinates admitted via MLTMetarelKind carriers.
  • Order arithmetic semantics live in Argon/MetaCalculus/Wellformed.lean as parametric Props over an abstract T.

What’s missing: the library that instantiates these primitives against modeler-written declarations and emits enforcement rules.

What MLT requires from a library

Per Carvalho-Almeida 2018 (Theorem-based MLT axiomatic theory) and Vault’s “MLT as a Library, Not Substrate” deep-dive:

  1. Decorators on metatype declarations that express MLT primitives at the surface. Two distinct kinds — relational (declare a cross-level relation between types) and assertion (declare a claim the compiler verifies against the derived fixpoint). Sigil is #[…] per Procedural macros; both kinds appear at declaration position.

    Relational decorators — desugar to canonical metarel-instance events; the relation participates in CL-1..CL-7 enforcement:

    • #[categorizes(T)] — the metatype’s instances are proper specializations of T; implies order(M) = order(T) + 1. (Carvalho-Almeida §3.2.)
    • #[partitions(T)]#[categorizes(T)] + members of the metatype pairwise disjoint and jointly exhaustive of T’s extent. (Carvalho-Almeida §3.3.)
    • #[subordinate_to(M)] — same-order subordination per Carvalho-Almeida §3.4. (Spelled subordinate_to per Higher-order modeling, not subordinates — the §3.4 relation reads “A is subordinate to B”.)
    • #[power_type_of(T)] — the metatype is the powertype of T per Cardelli 1988; every instance of T is also an instance of M. (Carvalho-Almeida §3.5.)

    Assertion decorators — desugar to a check rule verifying a claim against the derived fixpoint; fail-loud at runtime if the assertion conflicts with the iof chain, vacuous when the chain is incomplete:

    • #[order(N)] — modeler asserts has_order(Self, N); the elaborator emits an implicit check rule that fires OE1905 OrderInconsistency if the derived order disagrees. See § Decorator kinds — assertion vs. relational below for the full design.

    Tier decorators — module-level annotations that narrow the decidability tier:

    • #[order_bound(N)] — module declaration; caps every concept’s order at N. Lowers the module from tier:metaorder to a polynomial tier under the bound. (See Higher-order modeling, Tier ladder.)
  2. Datalog enforcement rules (kernel-native, fired automatically), per the Vault’s “Hybrid A+B” design pattern:

    • CL-1 Categorization: violation_categorization(X, T₂, T₁) :- categorizes(T₂, T₁), iof(X, T₂), ¬subclass(X, T₁). Emits OE1903.
    • CL-2 Partition disjointness: violation_partition_disjoint(Ind, T₃ₐ, T₃ᵦ) :- partitions(T₂, T₁), iof(T₃ₐ, T₂), iof(T₃ᵦ, T₂), T₃ₐ ≠ T₃ᵦ, iof(Ind, T₃ₐ), iof(Ind, T₃ᵦ). Emits OE1904.
    • CL-3 Order consistency: violation_order(X, T) :- concept_order(T, N), iof(X, T), concept_order(X, M), M ≥ N. Emits OE1905.
    • CL-4 Categorization inference: subclass(X, T₁) :- categorizes(T₂, T₁), iof(X, T₂). (Positive rule, no diagnostic.)
    • CL-5 Subordination requirement: violation_subordination(A, B) :- subordinates(A, B), (order(A) ≠ order(B) ∨ order(A) < 2). Emits OE1906.
    • CL-6 Powertype completeness: violation_powertype(T) :- power_type_of(M, T), iof(X, T), ¬iof(X, M). Emits OE1907.
    • CL-7 MLT compositional consistency (Carvalho-Almeida Theorem 5): a closure rule that propagates categorizes/partitions/subordinates interactions per the §3.4 composition table.
  3. Diagnostic codes OE1903–OE1907 wired into oxc-diagnostics and emitted from oxc-instantiate (or a follow-on pass) when CL-1..CL-7 detect violations.

  4. Library API surface modelers reach for:

    • use std::mlt::* brings the decorator set #[categorizes] / #[partitions] / #[subordinate_to] / #[power_type_of] / #[order] / #[order_bound] into scope.
    • std::mlt::well_formed!() build-time check macro that runs CL-1..CL-7 (plus the user’s #[order] assertions, if any) and fails the build on violation. Modelers serious about MLT correctness add this to their root module.
    • std::mlt::order(e: Entity) -> Option<Nat> library function — query a concept’s derived order at runtime; None when the iof chain is incomplete enough that order isn’t yet determined. (Operationally lowered to a one-shot query over the has_order/2 predicate per RP-003 §4.3.)

What std::mlt does NOT do

Per the strategic commitment, MLT-as-library means:

  • The library does NOT extend the substrate. No new atoms.
  • The library does NOT enforce MLT globally — only modules that use std::mlt opt in. Other modules see iof/specializes as usual without categorization arithmetic.
  • The library does NOT subsume UFO, BFO, DOLCE — those are parallel std::* libraries with independent enforcement.

Decision

Scope of v0.1

Three landings, each independent:

Phase 1 — Decorator parser + lowering (~1-2 weeks)

  • New oxc-parser recognition for the v0.1 MLT decorator set on type/metatype declarations:
    • Relational (lower to metarel_decl axiom events): #[categorizes(T)], #[partitions(T)], #[subordinate_to(M)], #[power_type_of(T)].
    • Assertion (lower to implicit check rules): #[order(N)].
    • Tier (module-level, narrows decidability tier): #[order_bound(N)].
  • oxc-instantiate lowers each decorator according to its kind:
    • Relational → canonical metarel_decl axiom event via Argon.MetaCalculus.MLT.declOfKind.
    • Assertion → synthesized check declaration; for #[order(N)] on concept X, the check is check OrderMatches { has_order(X, N) } keyed to OE1905 OrderInconsistency.
    • Tier → module-attribute on the elaborated program; the classifier reads it and narrows the tier.
  • Diagnostic emission for surface mis-use (decorator on a non-applicable position, bad argument shape, conflicting decorators on the same declaration). Reuses OE0708 ReservedAttributeName only when collision is genuine; the MLT-specific positional checks ride on a new fingerprint.
  • Sigil is fixed at #[…] (Procedural macros); the prior draft of this RFD used @[…] which was a typo against the rest of the spec.

Phase 2 — CL-1..CL-7 Datalog rules + OE1903–OE1907 (~3-4 weeks)

  • oxc-reasoning admits the seven CL-* rules as built-in rules (compiled at runtime, not modeler-written).
  • evaluate_to_fixpoint (or its semi-naive successor — depends on RFD 0003 backend dispatch) computes the violation predicates.
  • oxc-runtime lifts violation tuples to OE1903–OE1907 diagnostics at query-time-of-affected-rule.
  • Lean-side: extend Argon.MetaCalculus.Wellformed’s parametric predicates to a runtime-evaluable form; prove correctness against Carvalho-Almeida 2018’s axioms.

Phase 3 — std::mlt package + integration tests (~1-2 weeks)

  • Ship std::mlt as a workspace package with use std::mlt::* bringing in decorators.
  • Integration test exercising the Vault’s “biological taxonomy” canonical example (Animal → AnimalSpecies → DogBreed, order 0/1/2).
  • Documentation: short tutorial showing modeler-facing usage.

Total v0.1 effort: ~5-8 weeks across Phase 1+2+3.

Implementation pattern — “Hybrid A+B”

Per Vault’s MLT design synthesis: enforcement is two-layered.

  • A — Compile-time decorator expansion: #[categorizes(T)] on metatype M desugars to a canonical metarel categorizes(M, T) declaration that lands in the events list. Pure source transformation; no runtime cost. Assertion decorators (#[order(N)]) similarly desugar at compile time, but to a synthesized check rule rather than a metarel-instance event.

  • B — Runtime Datalog evaluation: CL-1..CL-7 rules fire continuously during query evaluation, detecting violations as they arise. Lazy by construction (a violation only matters when a query touches the affected predicate). Diagnostics emit at query time, not at build time.

This hybrid lets the build complete even if MLT violations exist (graceful degradation for in-progress modeling), while ensuring runtime queries fail loud if a violation is reachable.

Decorator kinds — assertion vs. relational

The two decorator kinds carry different semantic weight and warrant different mechanical treatment.

Relational decorators (#[categorizes], #[partitions], #[subordinate_to], #[power_type_of]) introduce a fact into the model: “this metatype is in this cross-level relation with that type.” They desugar to canonical metarel_decl events that participate in the CL-1..CL-7 enforcement closures. The diagnostic surface for these is the closure rules — a violation is detected when the modeler’s facts are mutually inconsistent under MLT’s axioms (Carvalho-Almeida §3.2–§3.5).

Assertion decorators (#[order(N)]) introduce a claim into the model: “this concept has this property.” They desugar to implicit check rules that fire at runtime against the derived fixpoint. The diagnostic surface is the check rule itself — a violation is detected when the modeler’s claim conflicts with what the iof chain actually derives.

#[order(N)] — design

use std::mlt::*;

#[order(2)]
pub type AnimalSpecies : TaxonomicRank <: Species  // claimed 2; derived order(AnimalSpecies) = 2 ✓

#[order(1)]
pub kind Dog : AnimalSpecies <: Animal             // claimed 1; derived order(Dog) = 1 ✓

let Lassie : Dog                                    // individuals don't need #[order]; order = 0 always

Semantics. On a concept declaration X with #[order(N)], the elaborator synthesizes a check rule equivalent to:

check OrderMatches { has_order(X, N) }   // mlt::E0007 in the library namespace; routed to OE1905 in v0.1

The has_order(_, _) predicate is std::mlt’s stratified-aggregate derivation over the iof DAG (RP-003 §4.3, lines 226–240):

  • has_order(x, 0) for any individual x;
  • has_order(t, n) for type t when n = 1 + max{m | iof(t’, t) ∧ has_order(t’, m)} (well-founded by iof acyclicity).

Three possible outcomes at check time:

StateWhat the check seesOutcome
Chain complete, has_order(X, N) derivableAssertion matches derived order✓ check passes
Chain complete, has_order(X, M) derivable for M ≠ NAssertion conflicts with derived orderOE1905 OrderInconsistency with message "#[order({N})] asserted on {X}, but iof chain derives order({M})"
Chain incomplete; has_order(X, _) undefined under the current stateAssertion neither confirmed nor refutedVacuous pass (three-valued: Unknown maps to OK under open-world)

The vacuous-pass case is the load-bearing one for incomplete-model checking — a modeler partway through wiring up the iof chain can assert #[order(2)] and the build won’t fail just because the chain isn’t done; it only fails when the chain explicitly contradicts the assertion. As the model grows toward completeness, more #[order] assertions become checkable, and any divergence shows up immediately.

Why this matters (ergonomics). Three concrete wins motivate the assertion decorator over inference-only:

  1. Incomplete-model verification. Modelers iterate. A partial iof chain doesn’t yet derive order, but the modeler has a belief about what the final order should be. #[order(N)] records that belief and turns it into a checkable invariant the moment the chain reaches completeness.
  2. Self-documenting models. A reader can see a concept’s intended tower position at a glance without traversing iof predecessors. This compounds in large models where the iof chain crosses module boundaries.
  3. Agent guidance. LLM-driven modeling is a primary v0 use case (per Argon’s stdlib-libraries design memo and design notes). Explicit assertions give the agent something to be checked against; pure inference gives the agent nothing to falsify. Agents reason better when their claims are observably refutable.

Why this design, not a procmacro. RP-003 GAP-3 calls for eventually re-homing all MLT decorators as pub macro declarations in std::mlt once the procmacro system is implemented (§14.2). For v0.1, parser-recognized attributes are simpler and ship sooner; the surface (#[order(N)]) is identical either way. Migration is internal.

Applicability. #[order(N)] is valid on:

  • Concept declarations (pub type, pub kind, and any user metatype-introduced declaration). Asserts the concept’s tower position.
  • Individual declarations (let X : T). Trivially asserts N == 0; mostly redundant since individuals always have order 0, but admitted for symmetry.

It is not valid on:

  • struct/enum declarations (no metatype, category error parallel to meta()).
  • Relation declarations (relations don’t carry order in MLT; the relation’s metarel does).

Mis-application emits OE1908 OrderAssertionMisplaced (a new code reserved by Phase 1).

Order ceiling at 2 for v1

Per Vault’s “Orca domain census” finding: real-world MLT patterns max out at order 2 (Animal → AnimalSpecies → DogBreed; no order-3 patterns required). v0.1 caps support at order 2:

  • concept_order predicate ranges over {0, 1, 2}.
  • OE1905 emits for order ≥ 3 declarations.
  • MLT* orderless types (universe polymorphism per Sozeau-Tabareau 2014) defer to v2.

This bound makes polynomial decidability concrete: D1Pred over a bounded type graph is polynomial; OE1905 enforcement is O(n²) in the type graph’s edge count.

Rationale

Why ship std::mlt rather than fold it into the language

Three reasons:

  1. Neutrality preservation. UFO and BFO have ontological commitments MLT doesn’t (UFO commits to a rigidity/sortality taxonomy; BFO commits to continuant/occurrent). If MLT were substrate, every Argon program would inherit MLT’s claims even when modeling a non-MLT ontology. Library-form means opt-in.

  2. Substrate parsimony. Adding MLT to the substrate would grow the five atoms to six. The substrate’s clean five-atom architecture (memory: “Argon substrate atoms fixed: five atoms; mutate not mutation; no event atom; standpoints first-class”) is load-bearing for the meta-calculus story; growing it for one foundational ontology breaks the symmetry.

  3. Future foundations. ML2, MLT*, DeepTelos all extend MLT in different directions. Library form lets each ship as a parallel package (std::ml2, std::mlt_star, std::deeptelos); substrate form would force one to be canonical.

Why decorators rather than a new keyword

Carvalho-Almeida’s notation uses categorizes as a relation name. The decorator form #[categorizes(T)] mirrors this exactly: “the metatype is categorized by T.” A keyword form (pub categorization Foo of T) would invent surface syntax that doesn’t appear in the literature.

Decorators also compose naturally with other attributes (#[categorizes(T)] #[disjoint] pub metatype M). A keyword form forces a single grammar position.

Why Datalog enforcement rather than compile-time

Several MLT constraints (CL-1, CL-3 in particular) require closing over the full extent of iof — instances asserted across the module. Compile-time evaluation can’t see runtime-asserted facts (added via mutations). Datalog evaluation fires lazily at query time, catching violations including runtime-added ones.

The downside: violations don’t surface until queried. A modeler can build a broken MLT module that passes the build. The catch is the build-time std::mlt::well_formed!() macro: it pre-runs CL-1..CL-7 against the build-time fact catalog and fails the build if violations exist. Modelers serious about MLT enforcement add this macro to their root module.

Why the OE1903–OE1907 codes are reserved but not yet emitted

The codes were reserved in grammar.toml when RFD 0006 landed (field mutability). The reservation predates this RFD because the diagnostic codes are part of the broader Argon diagnostic registry, not specific to std::mlt. Their EMIT SITES land with Phase 2; reservation is already in place.

Alternatives considered

Alt 1: ship MLT as part of substrate

Add categorizes/partitions/subordinates as substrate atoms; bake CL-1..CL-7 into the elaborator.

Rejected per user strategic commitment. See “Why ship std::mlt rather than fold it into the language” above.

Alt 2: ship std::mlt v0.1 with only the surface (decorators), defer Datalog

A “syntax-only” v0.1: parser admits decorators, but enforcement is documentation-only (“violations are the modeler’s responsibility”).

Rejected as too weak. Decorator-only would let broken MLT modules pass the build silently — the worst-of-both world (visible syntax claiming MLT compliance with no actual checking). Phase 2’s Datalog enforcement is what makes the library credible.

Alt 3: ship Phase 1 + 2, defer the std::mlt package wrapper (Phase 3)

Decorators + Datalog rules + diagnostics ship as part of the language; the std::mlt package is a thin re-export layer added later.

Rejected because it leaks MLT-specific names (categorizes, subordinates, etc.) into the language’s prelude. Keeping them library-prefixed (std::mlt::categorizes) maintains the neutrality claim.

Alt 4: full Carvalho-Almeida axiomatization (Theorems 1-8) in Lean before shipping

Mechanize every Carvalho-Almeida theorem in Argon/Locality/MLTAxioms.lean before allowing the Rust library to claim MLT compliance.

Rejected as too ambitious for v0.1. The substrate-sufficiency scaffold (MLTMetarelKind + classify_declOfKind theorem) discharges the expressibility claim; full axiomatization is a separate research effort (~24-38 person-months per Vault’s D-132 estimate). Defer to v0.2 or beyond.

Consequences

Wire format

No new axiom kinds. #[categorizes(T)] and its relational siblings desugar to canonical metarel_decl events; #[order(N)] desugars to a synthesized check declaration that emits a CheckDecl event keyed to OE1905. No new body types.

Lean changes

Phase 2 lands a Argon/Locality/MLTRules.lean (~400-600 LOC) that:

  • Defines the seven CL-* rule shapes against Argon.MetaCalculus.MLTKinds.
  • States the soundness theorem (CL-rules detect exactly the violations of Carvalho-Almeida’s axioms).
  • Proof handle: parametric predicates in Wellformed.lean already capture the violation semantics; this module instantiates them against runtime tuples.

Rust changes

Phase 1: oxc-parser decorator recognition + oxc-instantiate decorator-to-metarel lowering. ~300-500 LOC.

Phase 2: oxc-reasoning admits built-in rules; oxc-runtime lifts violation tuples to diagnostics. ~800-1200 LOC.

Phase 3: std::mlt package declaration in packages/std-mlt/ with re-exports. ~100-200 LOC.

Diagnostic surface

CodeNameSurfaceEmit sitePhase
OE1903CategorizationViolation#[categorizes(T)] violated at runtimeCL-1 closurePhase 2
OE1904PartitionDisjointnessViolation#[partitions(T)] overlapping instancesCL-2 closurePhase 2
OE1905OrderInconsistencyorder arithmetic violated; also the failure mode of #[order(N)] when assertion disagrees with derived orderCL-3 closure + synthesized check OrderMatches from Phase 1Phase 1 (assertion check) + Phase 2 (CL-3 closure)
OE1906SubordinationViolation#[subordinate_to(M)] mis-orderedCL-5 closurePhase 2
OE1907PowertypeIncomplete#[power_type_of(T)] extent gapCL-6 closurePhase 2
OE1908OrderAssertionMisplaced#[order(N)] on a struct/enum/relation declarationPhase 1 parser/elaboratorPhase 1

The #[order(N)] assertion is the first MLT diagnostic emitted from Phase 1 (the synthesized check rule fires under runtime evaluation but the check declaration itself lands in Phase 1’s parser/elaborator output). All other MLT diagnostics land with Phase 2’s CL-rule evaluator.

Per RP-003 GAP-4, the OE19xx codes are scheduled to migrate to the library namespace (mlt::E0001mlt::E0008); v0.1 retains OE19xx for continuity with the rest of the diagnostic registry and tracks the migration as an open question below.

Documentation

Reference book §13 (or §5.x — placement TBD) gains a subsection on MLT-as-library, citing Carvalho-Almeida 2018. The §17 ARS substrate description references MLT as one of the foundational ontologies the substrate admits via library composition.

Open questions

  • Phase 2 evaluation dispatch: CL-1..CL-7 are Datalog rules. They should run on the same executor as user-written derives. But CL-1..CL-7 are built-in, not modeler-written — should they be loaded as part of the runtime’s bootstrap, or shipped in a special “kernel” rule set? Affects how RFD 0003 (per-stratum backend dispatch) handles them.

  • Cross-library MLT consistency: if a module uses both std::mlt and std::ufo, do their iof/specializes semantics align? UFO inherits MLT semantics by reference (per Vault’s “UFO Design Limitations”); BFO does not. The interaction needs RFD-level treatment.

  • Order arithmetic via Lean universe inference vs explicit predicate: Vault’s Phase B1 + B2 work proposes mechanizing T2 substrate (Tarski-cumulative universes) so Lean’s elaborator infers order(T) automatically. v0.1 sticks with the explicit concept_order predicate; T2 mechanization is a separate research effort.

  • MLT orderless types*: universe-polymorphic types per Sozeau-Tabareau 2014. v0.1 caps at order 2; MLT* unlocks unbounded order. Defer to v0.2.

  • Powertype keyword deprecation: per Vault, pub powertype Name (categorizes|partitions) Target { instances } is currently hard-reserved in kind.rs:657 but marked for removal. The pattern system + decorator approach in this RFD obsoletes that surface. Removal unblocks pub metatype powertype = { ... } from user code. Coordinated removal with this RFD’s Phase 1.

  • MLT decidability under unbounded order: at order ≥ 3, decidability becomes uncertain (Carvalho-Almeida 2018 is silent past order 2). If v0.2 admits MLT*, the decidability classifier needs an MLT-specific tier or a refinement to the existing metaorder tier.

  • Backward compatibility of iof semantics: in MLT, iof(x, T) includes order arithmetic. In other foundational ontologies (UFO continuant, BFO universals), iof is order-agnostic. Module-local use std::mlt::* should NOT retroactively change iof semantics for non-std::mlt-using modules — but the runtime evaluator runs CL-1..CL-7 globally. Resolution: scope CL-1..CL-7 to predicates declared with MLT decorators (a metarel_decl.is_mlt_primitive flag added by Phase 1 decorator lowering).

  • OE19xx → mlt::E* namespace migration (RP-003 GAP-4): the diagnostic codes in this RFD use the language-level OE19xx range, inherited from when MLT was substrate-embedded. Per the MLT-as-library commitment, library-namespaced codes (mlt::E0001mlt::E0008, with the mlt:: prefix matching 09-higher-order.md:20) are the consistent endpoint. v0.1 keeps OE19xx for continuity with the existing registry; the migration is a coordinated rename across oxc-diagnostics, the Lean wellformedness module, and appendix-c-diagnostic-codes.md. Open question: do we migrate before or after Phase 2 emit sites land?

  • Assertion-decorator pattern as a reusable mechanism: #[order(N)] is the first assertion-style decorator. The same pattern is the natural shape for forthcoming ontological libraries — #[potency(N)] in std::potency (deep-instantiation level), #[stratum(N)] in std::ml2, etc. Should std::core expose a generic assertion-decorator builder (a procmacro helper) so each library doesn’t re-implement the lowering? Or do we accept boilerplate per library and re-evaluate when a third library wants this pattern? v0.1 hard-codes #[order]’s lowering; revisit after std::potency is sketched.

  • Vacuous-pass behavior under incomplete iof chains: #[order(N)] passes vacuously when has_order(X, _) is undefined at check time. This is the right default for incomplete-model development — but it means a modeler can ship a build with unverified assertions. Mitigation: the well_formed!() macro could escalate vacuous passes to warnings (OE1909 OrderAssertionUnverified) so modelers see what’s not yet covered. Decide whether to land that escalation in Phase 1 or defer.

  • #[order(N)] on individuals: let Lassie : Dog #[order(0)] is admitted but redundant (individuals always have order 0). Should the elaborator reject as a useless annotation (W0001-style warning) or silently accept? Argument for accepting: agent-generated models may always emit it for symmetry; the redundancy is harmless. Argument for warning: signals to the modeler that they may be confused about what #[order] means. Lean toward silent accept for v0.1; revisit if it becomes a source of confusion.

Discussion log

2026-05-29 — #[order(N)] added, decorator set completed

Tiago Sales asked in chat whether the syntax for declaring a type’s MLT order should be a refinement on the meta-calculus (pub type Species <: Taxon where { meta(self).order == 2 }) or an attribute (@[order(2)]). Neither matched the substrate’s actual design — meta(x) returns a Metatype value, not an arithmetic carrier, and @[…] is the wrong sigil. The substrate’s design is that order is computed, not declared: it falls out of the iof chain as a fixpoint over the well-founded has_order/2 predicate.

The follow-up question was the load-bearing one: “is implicit ordering sufficient for the modeler?” Tiago’s argument — that explicit assertions help with incomplete-model checking, documentation, and agent guidance — settled the question. #[order(N)] becomes a std::mlt assertion decorator that verifies the modeler’s belief against the derived fixpoint, fail-loud on conflict, vacuous-pass on incomplete chain.

Changes made to this RFD in the 2026-05-29 revision:

  1. Added #[order(N)] to the v0.1 decorator set as the canonical example of an assertion decorator (a new decorator kind alongside the existing relational decorators).
  2. Completed the relational decorator set — added #[power_type_of(T)] (was missing) and renamed #[subordinates(M)]#[subordinate_to(M)] (the §9 spelling, matching Carvalho-Almeida §3.4’s “A is subordinate to B” reading).
  3. Added #[order_bound(N)] as the module-level tier decorator (was referenced in §9 / RP-003 but not enumerated in this RFD).
  4. Fixed @[…]#[…] throughout. The @[…] form was a typo against Procedural macros and every example in the reference book.
  5. New diagnostic code OE1908 OrderAssertionMisplaced for #[order(N)] on inapplicable declarations (struct/enum/relation).
  6. New subsection “Decorator kinds — assertion vs. relational” with the #[order(N)] design in full, including the three-state check outcome table (pass / fail / vacuous) and the ergonomic rationale.
  7. New open questions: OE19xx → mlt::E* migration, assertion-decorator pattern as a reusable mechanism for std::potency / std::ml2, vacuous-pass behavior under incomplete chains, and the individual-redundancy question.

RFD 0010 — Negative facts / strong negation

  • State: discussion
  • Opened: 2026-05-29
  • Decides: surface syntax for asserting a fact’s negation as ground truth (distinct from absence-as-unknown under OWA, and distinct from default negation-as-failure in rule bodies); the wire-format mirror; how this composes with the standpoint federation runtime to make Truth4::Both operationally reachable.

Question

Today a pub standpoint X { pub fact Person(alice); } lets standpoint X assert alice ∈ Person as positive ground truth. The federation runtime’s query_dispatch then evaluates this row as Truth4::Is from X and Truth4::Can (implicit absence) from any other contributing standpoint. The AFT info-join Is ⊕ Can = Is, so the row surfaces tagged Is.

There is no current way for a standpoint to assert alice ∉ Person as positive ground truth. This means:

  • The bilattice value Truth4::Not (Belnap-Dunn’s F, AFT’s (F, F) pair) is never produced from a fact-derived per-source classification.
  • The bilattice value Truth4::Both (Belnap’s B, AFT’s inconsistent (T, F)) is consequently never produced by federate, because Is ⊕ Can ⊕ Can ⊕ ... = Is regardless of contributor count.
  • The federation runtime is operationally category-(b) bilattice-traced in the vault’s Bilattice-Native Query Evaluation taxonomy, not the category-(c) bilattice-native outcome the substrate’s proven theorems (federate_eq_both_iff, aft_discharges_T3_obstruction) advertise.

The decision: introduce a surface form for strong negation of facts, lower it to a new wire-format IofRefutation axiom kind, and have the per-standpoint materializer compute Truth4 values per (tuple, source) pair so that the runtime is operationally bilattice-native.

Context

What’s deferred from RFD 0004

RFD 0004 §Future Work explicitly anticipated this work:

Negative facts / classical negationpub not_fact Person(alice) to assert that alice is not a Person. Useful in OWA / classical contexts; relates to §12.2’s Truth4 bilattice. Defer; orthogonal to this RFD’s positive-only scope.

That deferral is now load-bearing: it gates the operational reachability of the bilattice’s most distinctive value.

The two negations distinction (Gelfond-Lifschitz 1991)

Gelfond and Lifschitz (1991) draw the canonical line between two negations every paraconsistent logic-programming surface must distinguish:

NegationMeaningSurfaceWire form
default / NAF (not)“not derivable in this scope” — closed-world inferencerule-body atom: not P(x)rule body, no wire trace
classical / strong (¬)“positively asserted to NOT hold” — open-world ground truthdeclaration: pub not_fact P(x);new IofRefutation axiom event

Argon already uses not in rule bodies (NAF, stratified). What it lacks is the declaration-level strong negation. The two are independent: NAF is a reasoning-time operator inferring absence from the current extent; strong negation is a build-time ground-truth assertion of refutation.

What the substrate already supplies

The Lean substrate is fully ready for this:

  • Argon.Foundation.Truth4 has the four-valued carrier with all algebraic laws.
  • Argon.Foundation.Bilattice.infoJoin is proven associative, commutative, idempotent.
  • Argon.Standpoint.Federation.federate is the AFT info-join over List Truth4.
  • federate_eq_classify characterizes federate outputs structurally.
  • federate_eq_both_iff: federate xs = .both ↔ hasBoth xs ∨ (hasIs xs ∧ hasNot xs) — the precise theorem stating when Both arises.
  • aft_discharges_T3_obstruction: sheafClassify contribs = .obstructed ↔ federate contribs = .both.

None of this is reachable by the runtime today because no source ever contributes .not. The Lean theorems are sound but the runtime cannot exhibit the case they characterize.

What the wire format already supplies

AxiomEvent.standpoint_id (set by the standpoint-block elaborator in commit 2776be9) tags each event with its asserting source. The materializer in commit 9aa8c73 (materialize_predicates_for_standpoint) filters per source. The dispatcher in the same commit feeds per-source classifications to query_derive_federated. The shape is right; only the negative half is missing.

The Polarity field is not a negation field

AxiomEvent.op: Polarity ∈ {Assert, Retract} is the time-evolution polarity (asserting vs withdrawing a previous claim under bitemporal extent). It is not the ontological polarity (P(x) vs ¬P(x)). Conflating them would break RP-004’s bitemporal contract. The right move is a new axiom kind for ontological refutation, orthogonal to op.

Decision

Surface (1) — declaration-level strong negation

pub not_fact Person(alice);
pub not_fact Adult(bob);
pub not_fact employed_by(alice, AcmeCorp);

Symmetric to pub fact P(x); syntactically. Lowers to a new wire event variant. Permitted at file level and inside pub standpoint X { ... } blocks; in the latter case the refutation is stamped with X.standpoint_id like positive facts (commit 2776be9’s mechanism applies uniformly).

Wire format (2) — new axiom kind IofRefutation

#![allow(unused)]
fn main() {
pub enum AxiomKind {
    ...,
    IofAssertion,
    IofRefutation,  // NEW
    ...
}
}

Body shape mirrors IofAssertionBody exactly (concept_id + individual_id). The semantic difference is carried in the variant tag, not in body fields. Encoding/decoding follow the same pattern as IofAssertion.

Symmetrically, a relation-tuple refutation lands as RelationTupleRefutation parallel to RelationTuple for the N-ary case. Same body shape; same variant-tag carries the polarity.

Lean drift (3)

Argon.Storage.AxiomKind gains iofRefutation and relationTupleRefutation constructors with @[language_interface] carried. The Lean side reads only the algebraic content; the bilattice/federation machinery is unchanged because it already handles Truth4.not symmetrically.

Materializer (4)

Store::materialize_predicates_for_standpoint is extended:

  • Maintain TWO per-source extents: positive: RelationCatalog and negative: RelationCatalog.
  • IofAssertion events stamp positive; IofRefutation events stamp negative.
  • Symmetric handling for RelationTuple / RelationTupleRefutation.

The output is PerSourceExtents { positive, negative } rather than a single RelationCatalog.

Dispatcher (5)

Store::query_dispatch for federated queries classifies each (relation, tuple) pair via the cross-product of per-source extents:

for each source S:
    in_pos = tuple ∈ positive[S].entry(rel)
    in_neg = tuple ∈ negative[S].entry(rel)
    truth4_S(tuple) =
        match (in_pos, in_neg) {
            (true,  false) => Is,
            (false, true ) => Not,
            (true,  true ) => Both,   // S already contradicts itself
            (false, false) => Can,    // S has no evidence
        }

The full row classification feeds query_derive_federated exactly as before; the AFT info-join over per-source Truth4 values yields the final row tag.

For derived (rule-output) relations, rule evaluation runs against the positive catalog per source as before; Truth4::Not contribution only arises when the rule’s head is one a refutation event positively names — which is unusual for rule heads but legal. Per-source Truth4::Both from a single source is the contradiction-with-self case (pub fact P(a); pub not_fact P(a); inside the same standpoint block) — useful as an internal consistency check; federation then surfaces it as Both.

Soundness

The implementation is the operational discharge of federate_eq_both_iff:

  • A federated row surfaces Truth4::Both ⟺ some source’s per-tuple Truth4 is Both, OR some source contributes Is and another contributes Not.
  • This is exactly the Lean theorem.

aft_discharges_T3_obstruction then lifts: sheafClassify of the federated row’s contribution list is obstructed ⟺ row tag is Both. The runtime now exhibits the contextuality-detection case the proven theorem characterizes.

Diagnostics

  • OE0640 NegativeFactArityMismatchpub not_fact P(x, y) where P has arity 1 (parallels positive-side checks in RFD 0004).
  • OE0641 NegativeFactUndeclaredPredicatepub not_fact P(x) where P is not a declared concept or relation.
  • Stratified-NAF / strong-negation interaction inside rule bodies is out of scope for this RFD: rule bodies admit only not (NAF). A future RFD may add ~ (strong negation) as a body atom, but that work composes cleanly only after IofRefutation exists at the wire level.

Rationale

Why this and not just NAF

NAF is reasoning-time and absent from the wire format. It already exists. The bilattice value Truth4::Not represents positive evidence of refutation — distinct from NAF’s “no positive evidence”. Without strong negation, a standpoint cannot DISTINGUISH “I don’t know whether P(alice) holds” from “I know P(alice) does NOT hold”. These are different epistemic states and the bilattice is designed to track exactly that distinction.

Why a new AxiomKind, not an extension of IofAssertion

Three reasons:

  1. Clean Lean drift. @[language_interface] checks variant alignment. Adding a body field forces every existing consumer to handle a new field; adding a sibling variant only forces consumers of the new variant to handle it. RP-004 chose the second pattern (IofAssertion, RelationTuple, PropertyAssertion are all sibling variants), maintaining symmetry.
  2. Storage efficiency. A standpoint with mostly positive facts wastes a byte per event encoding polarity: false. A separate kind allows the encoder to omit the discriminator in the common case.
  3. Diagnostic and tool surface. axiom-events queries that want all refutations for a standpoint scan by kind == IofRefutation directly. A polarity-field design forces every consumer to decode-and-test.

Why declaration-level, not rule-body

The conflation of strong negation and NAF in rule bodies is a known source of confusion in logic-programming languages (Gelfond-Lifschitz 1991 §1: “the two negations are often conflated in practice, leading to subtle bugs”). Argon’s design discipline is to surface them as distinct categories: NAF as a rule-body operator on the extent at evaluation time, strong negation as a declaration form that ships through the wire format. This RFD keeps to that discipline.

Why now, not as part of RFD 0004

RFD 0004 was scoped to positive ground-truth facts. The federation runtime didn’t exist then; the gap was theoretical. With federation operationally complete (commits 14–16 + standpoint scoping + dispatcher), the gap is now load-bearing: it gates the headline competitive claim (vault: bilattice-native runtime, category C, “production-scale systems do not”). RFD 0004’s deferral was correct then; closing it is correct now.

Why the standpoint federation is the right host for this

The bilattice’s most distinctive value (Both) is a cross-source notion: it requires two sources contributing opposite verdicts on the same proposition. Single-source Both arises only from a source contradicting itself (legal but unusual). Federation is the natural host because federation is where cross-source disagreement is computed. Implementing strong negation as a fact-declaration form makes it composable with the federation runtime already wired.

Alternatives

A1: pub fact !Person(alice);

Reuse pub fact with a ! prefix on the head atom. Closer to logic-programming notation. Rejected: harder to grep, conflates positive/negative in the same surface form, makes the elaborator’s per-form dispatch fuzzier, and reads worse in error messages.

A2: pub fact not Person(alice);

not keyword inline. Rejected: collides with the existing rule-body NAF not, the exact conflation Gelfond-Lifschitz warned against. Different surfaces for different semantics is a feature.

A3: Extend IofAssertion with a polarity: bool body field

Single AxiomKind, body carries polarity. Rejected per §Rationale above (Lean drift symmetry, storage efficiency, tool surface).

A4: Defer until rule-body strong negation is also designed

Bundle both surfaces into a single RFD. Rejected: declaration-level refutation is a complete, independently-useful feature — every modeler can write pub not_fact P(x); and benefit immediately; rule-body strong negation requires non-trivial stratification work (~ P(x) interacts with NAF and defeasible rules in subtle ways and probably needs an answer-set-style semantics). Shipping declaration-level first lets us learn from usage before designing the rule-body half.

Consequences

What modelers gain

pub standpoint internal_audit {
    pub fact employed_by(alice, AcmeCorp);
}

pub standpoint public_record {
    pub not_fact employed_by(alice, AcmeCorp);
}

pub query employment() -> employed_by
    across [internal_audit, public_record];

The query returns (alice, AcmeCorp) tagged Truth4::Both — explicit, runtime-visible, modeler-actionable evidence of cross-source disagreement. Under #[federate(strict)] this surfaces as OE1302 FederationDisagreement (other agent’s renumbering); under default paraconsistent the row surfaces with the Both tag intact.

This is the operational moat Argon claims and nobody else delivers (per the vault note).

What changes for existing code

Nothing breaks. Every existing source compiles unchanged because pub not_fact is a new declaration form parsed by a new branch; existing pub fact lowering is untouched; existing federation queries return identical results when no source uses not_fact (because no Truth4::Not enters the contribution list, so the info-join behaves identically).

What this opens up

  • Rule-body strong negation (future RFD): ~ P(x) in rule bodies, with answer-set-style or AFT-stable-pair semantics.
  • Refutation rules (future RFD): derive ~ Adult(p) :- Adolescent(p) — deriving negative extents from rules, not just facts.
  • Per-relation closure declarations (future RFD): a relation explicitly marked CWA could auto-emit IofRefutation for every tuple not asserted, making CWA reasoning composable with federation.

Open questions

  1. Wire-format symmetry with Retract. Should Retract of an IofAssertion event subsequently restate the proposition’s status as Can (no positive evidence; no negative evidence) or as Not (positively refuted)? Retract clearly leans toward Can; the semantic gap with IofRefutation is then preserved. The implementation should treat them distinctly.

  2. What happens when a single standpoint asserts both pub fact P(a); and pub not_fact P(a);? Per the dispatcher design above, the per-source Truth4 becomes Both directly; federation then surfaces Both. Should this also be a build-time error (single-source consistency check), or only a runtime tag? RFD position: lower without error; the bilattice tag IS the diagnostic. A separate #[strict_internal] per-standpoint attribute could opt into build-time rejection in a future RFD.

  3. Diagnostic emission scope. Should OE0640/OE0641 ALSO be emitted at federation-query time when sources disagree even under #[federate(strict)]? No — the strict-policy case is already covered by OE1302 FederationDisagreement (other agent’s renumbering). OE0640/OE0641 are build-time arity/declared-predicate checks; they don’t need a runtime counterpart.

References

  • RFD 0004 §Future Work — “Negative facts / classical negation”
  • RFD 0007 — Missing-value semantics (the related distinction between three field intents)
  • RFD 0008 — Standpoint-Sheaf Equivalence Proof Roadmap (Path A discharge)
  • Argon.Foundation.Truth4 — bilattice carrier with .not constructor
  • Argon.Standpoint.Federation.federate_eq_both_iff — proven theorem this RFD makes runtime-observable
  • Argon.Standpoint.AFTEquivalence.aft_discharges_T3_obstruction — sheaf-equivalence discharge
  • Gelfond, M. & Lifschitz, V. (1991). Classical Negation in Logic Programs and Disjunctive Databases. New Generation Computing.
  • Belnap, N. D. (1977). A useful four-valued logic.
  • Vault: Bilattice-Native Query Evaluation — operationality levels (a/b/c)
  • Vault: AFT-Grounded Truth Value Semantics for Argon — the AFT pair correspondence

RFD 0011 — Aggregate semantics under OWA

  • State: discussion
  • Opened: 2026-05-29
  • Decides: how aggregate atoms (sum, count, min, max, avg, set_collect, …) evaluate under OWA when filter or input cells carry Can verdicts; the per-mode default semantics (query / derive / check / fn); the diagnostic surface that makes the projection observable rather than silent; the aggregation-metadata envelope that surfaces interval bounds and excluded-cell counts at every data-system boundary; resolves RFD 0007 OQ3.

Question

Argon admits aggregate atoms in rule bodies and query bodies (§6.6, §7.3.1, §7.4): sum(i.value for i in income where i.taxable) > threshold and friends. Under OWA, the filter predicate i.taxable can evaluate to Can (per RFD 0007’s required-field-under-OWA lift). The substrate has not specified what aggregates do with Can-valued cells.

The spec at §12.2 has only one nearby sentence: “Set / list / record projections: Can-valued cells are omitted from the result set.” Applied literally to aggregates this is silent fail-closed at the filter — Can-cells get dropped, the sum is a definite number, the comparison yields a definite verdict, and the modeler never learns that the answer depended on uncertain data they did not see. This is the SQL-NULL bug at scale. For a language whose primary contract is “OWA semantics done right,” it is an unacceptable default.

But the formal-correctness response (the elaborator forces the modeler to declare an aggregate-Can handling at every site) is hostile to the 90% case: writing reports, dashboards, and analytics queries should feel like SQL, not like coursework on three-valued logic.

This RFD picks the semantics that gives Argon both: SQL-like ergonomics in the default case, OWA-sound interval semantics in the substrate, and a never-silent projection at the surface.

Context

The motivating Slack thread (Almeida + Almeida, 2026-05-28 → 2026-05-29)

Gustavo Ladeira (Sharpe) extended the discussion of RFD 0007 with a concrete query:

pub kind Income {
    taxable: Bool?,
    value:   Money,
}

pub query AllIncome() -> [Income] :- i: Income, select i

pub derive TotalTaxableIncomeExceedsThreshold(threshold: Money) :-
    sum(i.value for i in AllIncome() where i.taxable) > threshold

He asked: “does the derive resolve to Can if both lines below hold simultaneously?

sum(i.value for i in AllIncome() where i.taxable == true) <= threshold
sum(i.value for i in AllIncome() where (i.taxable == true) || (not exists i.taxable)) > threshold

He had derived the interval-bound semantics for monotone aggregates in his own message: lower bound = sum over confirmed-true filter; upper bound = sum over confirmed-true OR Can filter; verdict is Can exactly when the threshold falls in the open interval. The spec did not have a rule giving him that semantics.

João Paulo Almeida (UFES) had separately raised the structural-vs-epistemic ambiguity of Bool? (RFD 0007’s motivating point), which compounds: under structural-optional Bool?, Gustavo’s None-cells are not taxable (a positive fact of absence) and the aggregate is definite by structural exclusion; under epistemic Bool + OWA, missing taxable lifts to Can and the interval semantics is what he wants. RFD 0007 resolved the surface ambiguity. This RFD resolves what happens once the Can-cells reach an aggregator.

Substrate readiness

  • Aggregate grammar is in §6.6 / §7.3.1 (atom shape: aggregate (comp-op expr)?; expression shape: aggregate ::= ('sum'|'count'|'min'|'max'|'avg') '(' expr ('for' Ident 'in' expr ('where' expr)?)? ')').
  • Aggregate runtime is not yet implemented. oxc-reasoning/src/compile/rule.rs:78 returns RuleCompileError::AggregateNotYet. The semi-naive executor classifies aggregates at the expressive tier (§10.1) and rejects them; no execution path exists. This RFD is greenfield design, not a behavior change.
  • Lean mechanization has no Reasoning/Aggregate.lean. The Truth4 substrate (Foundation/Truth4.lean) + the K3 conjunction in Reasoning/Fixpoint.lean give the operators an aggregate semantics can build on; the aggregate-specific theorems do not yet exist.
  • Truth4 projection is documented at §12.2: K3 fail-closed projection (Pietz–Rivieccio Exactly-True) folds Can → false for Bool, omits Can-cells from set/list/record results. This RFD threads the aggregate question through that projection rule without contradicting it.

Why the obvious answers fail

Three semantics, evaluated independently:

OptionSemanticsProblem
O1 — Silent fail-closedCan-cells dropped from filter set; aggregate definiteUnsound under OWA. Reproduces SQL-NULL bug. Modeler is never told.
O2 — Blanket Can-propagationAny Can in filter set → whole aggregate is CanSound but over-conservative. Loses real conclusions (when sum-over-Is alone already exceeds threshold, verdict is still Can).
O3 — Interval boundsAggregate evaluates to lower/upper interval; comparison lifts to Truth4 by interval-vs-thresholdSound and informative for monotone aggregates; vacuous bounds for min/max/avg.

No single option covers all aggregator kinds. The decision is to combine them with type-driven projection, so the modeler picks the level of sophistication via the result type while the substrate always evaluates soundly.

Decision

Three rules, applied in order. Each rule is one click away from the next; the modeler picks the level at which they want to engage with Can.

Rule 1 — The substrate evaluates aggregates in Truth4

Under OWA, an aggregate agg(expr for x in S where φ(x)) evaluates over a Truth4-tagged set:

{ x ∈ S : φ(x) ∈ {Is, Can} }

— with each element marked by its filter verdict. The aggregator’s behavior on this tagged set depends on its monotonicity class:

Monotone aggregators (interval bounds)

For sum over non-negative values, count, set_collect:

lower = agg over { x : φ(x) is Is(true) }
upper = agg over { x : φ(x) is Is(true) or Can }

For sum over signed values, the bounds are sign-partitioned: lower = sum(Is) + sum(negative-valued Can cells); upper = sum(Is) + sum(positive-valued Can cells). This is monotone in each sign-class independently and computable in a single pass.

The aggregator returns the Truth4-lifted interval — internally a pair (lower, upper) carried as Truth4Of<T> with bounds metadata.

Non-monotone aggregators (propagating)

For min, max, avg, percentile, string_join, count distinct:

if { x : φ(x) is Can } is empty:
    result = agg over { x : φ(x) is Is(true) }    // definite verdict
else:
    result = Can                                   // any Can in filter set
                                                    // propagates to whole result

These aggregators have non-monotone bounds (min(A ∪ B) ≤ min(A) and the upper bound depends on Can-cell values that aren’t known), so interval semantics gives vacuous bounds in the worst case. Propagating is the only sound default that doesn’t over-claim; modelers who want sharper semantics narrow the filter explicitly to Is-only.

Aggregator-kind taxonomy

AggregatorClassBound rule
countMonotone[|Is|, |Is| + |Can|]
count distinctNon-monotone (membership-dependent)Propagating
sum (non-negative)Monotone[sum(Is), sum(Is ∪ Can)]
sum (signed)Monotone (per-sign)[sum(Is) + sum⁻(Can), sum(Is) + sum⁺(Can)]
set_collectMonotone[set(Is), set(Is ∪ Can)] (containment interval)
string_joinNon-monotone (order-dependent)Propagating
minNon-monotonePropagating
maxNon-monotonePropagating
avgNon-monotonePropagating
percentileNon-monotonePropagating

The classification is fixed at the aggregator-kind level, not per-call.

Rule 2 — The surface projection is type-driven

The Truth4-tagged aggregate result propagates until it hits a typed boundary (the declared return type of the enclosing query/derive/fn, the head of a derive, the predicate of an if/match, the membership-witness of a refinement clause). At the boundary, the §12.2 K3 fail-closed projection fires, governed by the boundary’s declared type:

Declared type at boundaryProjection rule
BoolIs(true) → true; Is(false) → false; Not → false; Can → false (Pietz–Rivieccio Exactly-True)
T (any concrete type)Is(v) → v; Not / Can → null-or-error per RFD 0007’s required-field rule
Option<T>Is(v) → Some(v); Not → None; Can → None (structural projection)
Truth4Of<T>identity — preserves the Truth4 verdict
Truth4Of<Bool>identity — Is(true) / Is(false) / Not / Can preserved

A modeler switches between ergonomic and sound semantics by changing one type annotation:

// Ergonomic: SQL-like; projects Can → false at Bool boundary.
pub query QuarterlyOK() -> Bool {
    sum(i.value for i in income where i.taxable) > threshold
}

// Sound: preserves three-way verdict for caller.
pub query QuarterlyOK() -> Truth4Of<Bool> {
    sum(i.value for i in income where i.taxable) > threshold
}

For derive rule heads, the same dial applies via the head type. A pub derive Foo(p: Person) is Truth4Of<Bool> rule populates a Truth4-aware IDB relation; consumers reading Foo(p) get Is | Not | Can directly.

Rule 3 — The projection is never silent

Two pieces ensure the modeler always sees the soundness picture, no matter which projection they chose.

3a. Per-projection diagnostic

Every fail-closed projection at an aggregate boundary emits a diagnostic with the interval bounds and excluded-cell count. The diagnostic level depends on the mode:

ModeLevelCodeRationale
query -> T (concrete T)InfoOW0613Ergonomic-first; modeler informed but not interrupted
query -> Truth4Of<T>(none)Already sound; nothing to flag
fn -> TInfoOW0613Same as query
derive (head over concrete T)WarningOW0613IDB pollution can propagate; warn more visibly
derive (head over Truth4Of<T>)(none)Sound; no need to flag
check (predicate body)ErrorOE0614Compliance — unsound checks defeat the purpose; modeler must explicitly opt out

The diagnostic carries:

14:5: info[OW0613]: aggregate filter projection fail-closed
  |
14 |   sum(i.value for i in income where i.taxable) > threshold
  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  | filter `i.taxable` may evaluate to Can; verdict projects to Bool with
  | Can → false (Pietz–Rivieccio Exactly-True, §12.2). Interval bounds for
  | this materialization: [$1,234,567.00, $1,256,789.00]; 5 Can-cells.
  |
help: change return type to Truth4Of<Bool> to preserve Can verdict:
  pub query QuarterlyOK() -> Truth4Of<Bool> { … }
help: or filter explicitly to suppress this diagnostic:
  ... where i.taxable is Is(true)              // fail-closed, explicit
  ... where i.taxable is Is(true) or Can       // include unknowns
help: silence per-rule with #[allow(aggregate_fail_closed)]

The diagnostic is suppressible per-site with #[allow(aggregate_fail_closed)]; in check mode, the diagnostic is OE0614 (error) and the only opt-out is #[check(fail_closed)] on the check declaration, which makes the soundness loss explicit at the check site.

3b. Per-result metadata envelope

Every aggregate query result, regardless of projection target, carries an aggregation_metadata envelope alongside the projected value. The envelope is always present when the aggregate’s filter set could have been Can-valued; it is absent when the aggregate is provably Can-free.

JSON shape (REST / wire format):

{
    "value": true,
    "aggregation_metadata": {
        "result_type": "Bool",
        "projected_from": "Truth4Of<Bool>",
        "projected_verdict": "Is(true)",
        "interval": { "lower": 1234567.00, "upper": 1256789.00 },
        "excluded_can_cells": 5,
        "excluded_can_value_bounds": { "lower": 22222.00, "upper": 22222.00 },
        "verdict_robust": true
    }
}

verdict_robust = true iff the projected verdict would be the same across every completion of the Can-cells. When false, the modeler / auditor / dashboard sees that the answer depended on the projection — exactly the SQL-NULL bug made visible.

CLI presentation (ox query):

$ ox query QuarterlyOK
true

(aggregate: 5 of 100 income rows had Can-valued `taxable`;
            bounds [$1,234,567, $1,256,789];
            verdict robust to all completions)

ox query --explain prints the full envelope; standard text mode prints a one-line summary when excluded_can_cells > 0. The envelope is computed during the substrate’s Truth4 evaluation (the bounds are already needed for monotone aggregators), so the runtime cost is one allocation per aggregate, not an extra pass.

Per-mode default summary

ModeDefault semanticsDiagnosticOpt-outOpt-in to sound
query -> TErgonomic; fail-closed projection at boundaryOW0613 Info#[allow(aggregate_fail_closed)]Return Truth4Of<T>
query -> Truth4Of<T>Sound; preserves Can(none)(n/a)(already sound)
derive (head T)Ergonomic; fail-closed at head projectionOW0613 Warning#[allow(aggregate_fail_closed)]Head type Truth4Of<T>
derive (head Truth4Of<T>)Sound; IDB carries Can verdict(none)(n/a)(already sound)
checkSound; fires alert on any non-Is(true) verdictOE0614 Error if fail-closed without opt-out#[check(fail_closed)](already sound)
fn -> TErgonomic; fail-closedOW0613 Info#[allow(aggregate_fail_closed)]Return Truth4Of<T>

Worked example — Gustavo’s case

After this RFD lands, Gustavo’s pattern, with the epistemic-Bool surface (per RFD 0007) and the sound-three-verdict return type:

pub kind Income {
    taxable: Bool,                // required + OWA — epistemic reading
    value:   Money,
}

pub query AllIncome() -> [Income] :- i: Income, select i

pub derive TotalTaxableIncomeExceedsThreshold(threshold: Money)
        is Truth4Of<Bool> :-
    sum(i.value for i in AllIncome() where i.taxable) > threshold

The substrate’s monotone-sum evaluates to Truth4Of<Money> with bounds [lower, upper] per Rule 1. The comparison > threshold lifts pointwise:

threshold <  lower            → Is(true)    // every completion exceeds
threshold >= upper            → Is(false)   // no completion exceeds
lower <= threshold < upper    → Can         // depends on which Can-cells are taxable

The head type Truth4Of<Bool> preserves the verdict (Rule 2). Consumers read TotalTaxableIncomeExceedsThreshold(threshold) and pattern-match on the three outcomes. No diagnostic fires (Rule 3) — the modeler chose the sound path explicitly.

If Gustavo had written is Bool on the head, the verdict projects fail-closed (Can → false), the diagnostic fires at warning level (derive mode), and the materialized IDB record carries false for the Can-case threshold. The metadata envelope on the query result still shows the bounds and Can-cell count.

Rationale

Why “substrate sound, surface projected” rather than “force the modeler”

The earlier draft of this design (A1+B1 from the Slack discussion) made the elaborator hard-error at every where-clause that could be Can, requiring the modeler to declare an aggregate-Can handling explicitly. This was formally correct but practically hostile: every aggregate over OWA data in a routine query would error until the modeler annotated. Argon’s contract as a practical data systems language requires that day-to-day analytics queries — reports, dashboards, ad-hoc SQL-like exploration — feel like SQL with one knob upgraded, not like a three-valued-logic seminar.

The substrate-Truth4 + surface-projected design separates the two concerns. The substrate always evaluates with Truth4 fidelity (the OWA-soundness contract is preserved regardless of what the modeler writes). The surface projection is type-driven: the modeler picks ergonomic Bool or sound Truth4Of via the return type, with the type-system change costing one character. The “never silent” rule ensures the modeler is informed about the projection at every fail-closed site without being blocked. This is the design SQL should have had.

Why the per-mode default split

query is the analytics surface — ergonomics-first. derive populates the IDB and propagates downstream; an unsound derive contaminates many subsequent rules. check is compliance — a check that silently fails-closed and misses a violation is the worst possible failure mode for the system. The diagnostic level escalates monotonically with the cost of silent-wrong: info → warning → error.

This is symmetric with how Rust handles Result<T, E>: ignoring the error is allowed but loud (#[must_use] warning by default), and the language never forces you to handle every case at every call site. The “you must handle this” boundary is at the typed-result boundary, not at every intermediate computation.

Why the metadata envelope is always-present (not opt-in)

The bounds and Can-cell counts are computed by the substrate regardless (they’re load-bearing for the monotone interval evaluation). Surfacing them at the result boundary is near-free. Making them opt-in (?metadata=true) would mean most data-system consumers — dashboards, CSV exports, REST clients — never see them, and the SQL-NULL bug returns silently. Always-present means the auditor / debugger / pipeline-engineer always has the evidence; the cost is a few extra bytes per result.

The CLI’s one-line summary (only printed when excluded_can_cells > 0) keeps the common-case output clean while still being honest. ox query --explain shows the full envelope.

Why interval semantics, not propagation, for monotone aggregators

Propagation (any Can → whole result Can) is sound but loses real conclusions. When sum-over-Is alone already exceeds threshold, the verdict is definitively Is(true) regardless of the Can-cells; propagation would still say Can and the modeler / system / auditor would get a less informative answer than the data supports. Interval semantics extracts the maximum signal from the partial data without over-claiming.

For non-monotone aggregators, interval semantics gives vacuous bounds (e.g., min with Can-cells could be anything in the Can-cells’ value range); propagation is the right default there because the alternative is bounds that don’t constrain. The taxonomy fixes the classification per-aggregator-kind so modelers don’t have to reason about which gets which.

Alternatives considered

A. Silent fail-closed (the SQL-NULL default, what §12.2 implies today)

Can-cells dropped from filter set; aggregate produces a definite verdict; modeler never told. Rejected. Reproduces the SQL-NULL bug at the OWA scale. Argon’s substrate guarantees OWA-soundness; a surface that silently violates the guarantee defeats the language’s central pitch.

B. Hard-error at every aggregate over Can-potential filter (the original A1+B1)

Elaborator refuses to compile until the modeler annotates the aggregate with one of is Is(true) / is Can / propagating. Rejected. Hostile to practical analytics — every query against OWA-required field must annotate; the migration cost from existing codebases is enormous; the friction undermines the data-system-as-product story.

C. Always-Truth4Of return type

Aggregators always return Truth4Of<T>; modelers must pattern-match to extract a value. Rejected. Most aggregator results are immediately compared or summed downstream where the ergonomic fail-closed projection is what the modeler wants; forcing Truth4Of<T> everywhere is the same hostile-friction problem as B.

D. Per-call annotation in the aggregate body

sum(i.value for i where i.taxable mode=interval) > threshold. Rejected. Annotation is at the wrong site — the modeler’s intent is about the projection, not the aggregator. The return type is the natural place to declare it.

E. Two-pass query model (run with one semantics, switch interactively)

ox query defaults to fail-closed; modeler runs ox query --reanalyze sound if they want to see the Can-case. Rejected. Doesn’t work for batch / programmatic consumers, and the modeler doesn’t know to ask the second time unless something cues them. The always-present metadata envelope (Rule 3b) gives the same effect without the second invocation.

F. Defer entirely

Wait until the aggregate executor lands (oxc-reasoning::AggregateNotYet) and decide then. Rejected. The executor needs this decision before it can be implemented; the design space has been actively explored; settling now means the implementation doesn’t ship with an ad-hoc choice. RFD 0007 OQ3 already promised follow-on; this is it.

Consequences

What lands

PieceWhereApproximate size
Spec §7.4 — aggregate semantics under OWA sectionspec/reference/src/07-rules.md~50 lines
Spec §12.2 — clarify “set projection” rule is about result-set membership, NOT aggregate inputspec/reference/src/12-truth4.md~10 lines
Spec §6.6 — note in expression-aggregate grammarspec/reference/src/06-types.md~5 lines
Diagnostic codes OW0613, OE0614, OW0615compiler/crates/oxc-syntax/grammar.toml + Appendix C~3 entries
Aggregator-kind taxonomy tableoxc-protocol::core_ir::AggregateKind (extend with MonotonicityClass)~20 LoC
Reasoning/Aggregate.lean — interval-bound theorem for monotone aggregatorsspec/lean/Argon/Reasoning/~120 lines
Runtime aggregate executor with Truth4 evaluationoxc-reasoning/src/compile/aggregate.rs (new)~300 LoC, replacing AggregateNotYet
Metadata envelope wire formatoxc-protocol::query_result::AggregationMetadata (new)~40 LoC
CLI ox query aggregate-summary renderingoxc-driver/src/commands/query.rs~30 LoC
ox query --explain extensionoxc-driver/src/commands/query.rs~50 LoC

Substrate impact

  • One new theorem in Reasoning/Aggregate.lean: for any monotone aggregator A over a filter predicate φ, the interval [A(Is), A(Is ∪ Can)] is a sound over-approximation of every completion of the KB. Proven by induction on the K3 fixpoint already established in Reasoning/Fixpoint.lean.
  • No change to subsumption_axiom, iof_assertion, relation_tuple wire formats. The metadata envelope is computed at query time, not stored.
  • No change to the Z-set Storage model. Aggregates are evaluated over materialized relation extents, which already carry the Truth4 tags from the underlying fixpoint.

Modeler impact

  • Routine analytics queries compile unchanged; behavior is SQL-NULL fail-closed but with bounded transparency via the metadata envelope and the info-level diagnostic.
  • Soundness opt-in is a one-character type change (BoolTruth4Of<Bool>).
  • check rules get stricter default — modelers writing compliance checks must engage with Can or explicitly opt out, which is the right default for compliance.
  • Existing rules that compiled under the unimplemented-aggregate path will compile under this RFD with the same surface; behavior change vs the unimplemented baseline is the addition of the diagnostic and the metadata envelope.

Tooling impact

  • ox query shows aggregate summaries by default when Can-cells were excluded.
  • ox query --explain shows full metadata envelopes.
  • LSP can surface the diagnostic inline; debuggers can read the envelope.
  • REST adapters return the envelope alongside results; clients can ignore it or render it.

Compatibility

The aggregate executor is a new implementation; there is no prior behavior to preserve. The grammar surface is unchanged. The metadata envelope is additive; consumers that don’t read it see the same JSON shape they would have seen.

Open questions

OQ1 — Aggregator-kind monotonicity classification under user-defined aggregators

When modelers register custom aggregators via pub aggregate (a future grammar surface for plugin aggregators), how do they declare the monotonicity class? Likely via attribute (#[monotone] / #[propagating]) on the aggregator declaration. Out of scope for this RFD; document when the custom-aggregator surface lands.

OQ2 — Aggregate-of-aggregate composition

What does sum(count(j.value for j in i.children) for i in roots()) look like under partial Can data? The inner aggregator produces a Truth4Of<Nat> interval per row; the outer aggregator over a sequence of intervals needs an interval composition rule. For monotone outer aggregators over interval-valued inputs, the composition is sum-of-bounds — pointwise interval arithmetic. Specify in the Reasoning/Aggregate.lean mechanization; document examples in §7.4.

OQ3 — Interaction with temporal aggregates (§6.10.5 + §7.3.2)

DatalogMTL operators since/until combined with aggregates. Under OWA + temporal Can, the interval bounds need to factor in the temporal extent. The temporal substrate’s three-valued evaluation (§6.10.5) composes pointwise; the aggregate’s interval semantics composes monotonically; the combined semantics needs verification. Likely lands in a follow-on RFD on temporal-aggregate semantics.

OQ4 — #[brave] mode interaction

§7.3 admits #[brave] for stable-model semantics. Under brave + OWA, aggregates need to evaluate across stable models. Each stable model produces a definite verdict; the aggregate result is the join across models. Out of scope; document when the brave-semantics RFD is written.

OQ5 — Metadata envelope schema versioning

The envelope is a wire-format addition; future fields (additional bounds, multiple Can-cell categorizations) require a schema version. Adopt the oxc-protocol versioning policy from RFD 0001; envelope carries a version: 1 field.

OQ6 — Persistent aggregate caching under metadata

Salsa-cached aggregate results need to track the metadata envelope; cache invalidation when Can-cells are resolved (via subsequent insert facts) must re-fire dependent aggregates. Likely a small extension to the Salsa cache key but worth verifying with the oxc-runtime Salsa wiring.

References

  • RFD 0007 — Missing-value semantics under OWA §OQ3 — the open question this RFD resolves
  • §6.6 (aggregate expression grammar), §6.9 (CWA/OWA), §6.10.5 (Kleene truth tables), §7.3.1 (rule-atom aggregates), §7.3 line 72 (“stratified aggregates”), §7.4 (query mode), §12.2 (K3 fail-closed projection)
  • oxc-reasoning/src/compile/rule.rs:78AggregateNotYet (the implementation site)
  • Foundation/Truth4.lean, Foundation/Projection.lean, Reasoning/Fixpoint.lean — substrate mechanization this RFD builds on
  • Faber, W., Pfeifer, G., Leone, N. (2011). Semantics and complexity of recursive aggregates in answer set programming. Artificial Intelligence — the stratification rule §7 cites
  • Pietz, A. & Rivieccio, U. (2013). Nothing but the truth. Journal of Philosophical Logic — the K3 fail-closed projection
  • Belnap, N. (1977). A useful four-valued logic — the underlying bilattice
  • Almeida, J.P.A. (UFES) & Ladeira, G. (Sharpe), Slack thread, 2026-05-28 → 2026-05-29 — the motivating discussion

RFD 0013 — Toolchain distribution + oxup toolchain manager

  • State: accepted — partially implemented (Stage 0 deployed, Stage 4 merged)
  • Opened: 2026-06-01
  • Last revised: 2026-06-02
  • Decides: how Argon ships to engineer laptops (Mac arm64) + ODE sandbox AMIs (Linux arm64 + x86_64); whether to adopt a rustup-style toolchain manager; the AWS-hosted distribution architecture; the ~/.argon/ legacy-install cleanup story.
  • Status: Implemented — the v0.2 toolchain shipped; this RFD is the design record.

Question

v0.1.0 ships a single ox binary from a homemade install.sh against git clone. For Sharpe-internal v0.2, what’s the install + distribution + toolchain-management story?

Decision

Adopt the rustup-style architecture with an oxup manager binary + argv[0] dispatch, splitting the compiler into four logical tools (oxc / ox / ox-lsp / oxfmt), distributed via AWS S3 + CloudFront at argon.sharpe-dev.com (managed via Pulumi in the shared infra account 285688017134). The user-facing reference is the book Toolchain chapter; the dist mechanics live in infra/ + the release pipeline.

Sharpe-internal scope means: no LICENSE / no Marketplace publish / no public installer; private bucket via Cloudflare-proxied CloudFront; CI’s ArgonReleaseRole is narrow-scoped (S3 PutObject + CloudFront invalidate only). Okta-gated downloads deferred to v0.3 (separate RFD).

Account note: the distribution lives in AWS account 285688017134, which is the default SSO profile (~/.aws/config) — the shared infra account where the ODE/orca-mvp Pulumi stacks, the s3://sharpe-pulumi-state backend, and the *.sharpe-dev.com wildcard ACM cert already live. It is not the shared-admin profile (account 548277374575, which is empty). Maintainers authenticate with assume default / AWS_PROFILE=default.

Implementation status (2026-06-02)

  • Stage 0 deployed to account 285688017134: S3 argon-dist-sharpe + CloudFront E16VOSAVQFX5Y8 (argon.sharpe-dev.com, proxied) + ArgonReleaseRole (OIDC). Edge verified (HTTP 403 = healthy empty bucket).
  • Stage 4 merged (PR #6): release.yml publish-dist (OIDC → S3 → CloudFront) + nightly cron, wired via repo secret AWS_ROLE_ARGON_RELEASE + var ARGON_CLOUDFRONT_DIST_ID. Graduated to the §5 toolchain (Stages 1–3 having landed): builds all four binaries, assembles argon-<v>-<plat>.tar.gz via scripts/assemble-toolchain.sh, and publishes the [artifacts.*] channel-<channel>.toml via scripts/make-channel-manifest.sh — the schema oxup install consumes (round-trip tested in oxup/src/fetch.rs; the format is pinned by scripts/make-channel-manifest.sh). Fixed two latent contract bugs (bare-ox tarball; [platforms.*] vs [artifacts.*] manifest).
  • Stays in the mgmt account (285). A move to a workload account (prod/623) was investigated and rejected: the workload-account org SCP p-pif76ezm denies s3:PutBucketPolicy / PutBucketPublicAccessBlock to interactive SSO admins, so the CloudFront-OAC bucket policy can only be set by an SCP-exempt CI role. For an internal static CDN that’s not worth a full CI-driven deploy apparatus; 285 (where SSO admins can set bucket policies) is the right home for maintainer-operated tooling. Maintainer-local pulumi up runs from nix develop .#infra.
  • Adjacent: Argon-aware PR review bots (claude-review.yml + claude.yml, PR #7) landed alongside this work.
  • Remaining: Stages 1–3, 5–7 (binary split → share/std/oxupoxfmt → hosted install.sh → smoke) → tag v0.2.0.

Scope of infra/

This RFD authorizes the new top-level infra/ directory holding the Pulumi project (TypeScript + Bun, matches the ODE pattern at ontology-tooling/infra/) that creates the distribution stack:

  • argon-dist-sharpe S3 bucket (us-east-2, private, OAC-only read)
  • CloudFront distribution (PriceClass_100, TLS 1.2+, two cache policies)
  • Reuse of the existing *.sharpe-dev.com wildcard ACM cert (us-east-1)
  • Cloudflare CNAME argon.sharpe-dev.com → CloudFront (proxied / orange cloud)
  • ArgonReleaseRole IAM role with GitHub OIDC trust for sharpe-dev/argon tag pushes

Maintainers run pulumi up locally with the default profile (assume default). CI never touches the infra stack — only the ArgonReleaseRole it outputs.

Scope of oxup/

This RFD also authorizes the new top-level oxup/ directory (Stage 3) — a standalone Rust crate (its own Cargo.toml/Cargo.lock, not a member of compiler/’s workspace) so it stays cheap to build for bootstrap. It is a single binary with argv[0] dispatch:

  • Invoked as ox/oxc/ox-lsp/oxfmt (via symlinks), it resolves the active toolchain (§4) and execs the real tool from ~/.argon/toolchains/<spec>/bin/<tool>.
  • Invoked as oxup, it’s the manager CLI (init/install/update/default/list/which/uninstall/self-update).

Decomposition (decided during Stage 2/3): Stage 3 lands in two PRs.

  • 3a — network-free foundation: the crate, argv[0] dispatch, toolchain resolution (§4 precedence), the ~/.argon layout, and the local manager commands (which/list/default/uninstall). Its own CI job in check.yml.
  • 3b — the network layer: install/update/self-update + auto-fetch-on-miss from argon.sharpe-dev.com, plus the release.yml §5 tarball (bin/ + share/std + manifest.toml) the fetch consumes. 3b also drops the Stage 2 stdlib embed once the tarball ships share/std. Gated on the release pipeline producing real toolchains.

Three-platform build matrix (v0.2)

  • macos-arm64 — all Sharpe dev machines (Apple Silicon)
  • linux-aarch64 — ODE sandbox AMIs + arm Linux CI runners
  • linux-x86_64 — generic Linux + non-arm CI runners

Intel Mac (macos-x86_64) deferred; Windows deferred.

Build order (eight stages, ~8 focused days)

StageOutput
0infra/ Pulumi project + apply via assume default (account 285688017134)
1Compiler binary split (oxc/ox/ox-lsp/oxfmt bin targets in compiler/)
2Stdlib migration (include_str!share/std/)
3oxup crate (argv[0] dispatch + channel resolver + auto-fetch)
4Release pipeline (GitHub Actions → S3 → CloudFront invalidate)
5oxfmt opinionated formatter (parse → CST walk → normalized emit)
6Hosted install.sh at argon.sharpe-dev.com/install.sh
7Smoke + docs + clean-VM verification

Out of scope for this RFD

  • Okta-gated downloads (Google Workspace SSO) — v0.3 RFD
  • Package registry + lockfile — v0.3+ when external deps land
  • Apple Developer signing / notarization — internal binaries
  • Telemetry payloads — opt-out reserved in settings.toml; no payload sent

See

  • The book Toolchain chapter (spec/reference/src/toolchain.md) — the user-facing install/channel/version reference
  • /infra/README.md — operator’s guide for running pulumi up
  • /infra/index.ts — top-level Pulumi wiring
  • ontology-tooling/infra/ — the ODE pattern this design adapts

RFD 0014 — Runtime Serving Surface

State: discussion

Question

Should Argon expose a first-party serving surface for executing a loaded .oxbin against tenant/fork scoped runtime state?

Context

The §19 runtime contract defines the in-process Engine / Module / Store semantics, but Ode and Tide workflows need an executable boundary: load the workspace .oxbin, keep Store state warm, dispatch generated SDK descriptors, and expose forks, snapshots, derive output, and debug traces.

The old kernel and devbox-kernel path mixed this runtime work with live source loading, generated SDK transport concerns, tenant hardcoding, workflow state, and Tide-specific globals. New Argon has a compiled .oxbin, so the runtime serving layer can be Argon-native and narrower.

Decision

Add ox runtime serve backed by an oxc-serve crate. The command serves a versioned /v1 API around a loaded .oxbin:

  • GET /v1/health
  • GET /v1/module
  • GET /v1/schema
  • POST /v1/dispatch/query
  • POST /v1/dispatch/mutation
  • POST /v1/dispatch/compute
  • fork-scoped dispatch aliases under /v1/forks/{fork}/dispatch/*
  • GET /v1/forks, POST /v1/forks, GET /v1/forks/{fork}
  • DELETE /v1/forks/{fork}, POST /v1/forks/{fork}/promote
  • GET /v1/forks/{fork}/diff/{other}
  • POST /v1/forks/{fork}/derive
  • POST /v1/forks/{fork}/derive/trace
  • GET /v1/snapshot
  • GET /v1/derived/individuals/{id}
  • GET /v1/derived/individuals/{id}/explain
  • GET /v1/derived/facts/{fact_id}/explain

The serving layer supports mem and pg storage. Both backends preserve the append-only ABox event-log model; derived state and snapshots are projections over .oxbin plus visible events. Postgres additionally owns durable fork records, generation counters, scoped/as-of scans, and projection-cache invalidation.

The generated SDK remains dependency-free and transport-agnostic. It emits types, validators, metadata, descriptors, and wire parse/serialize helpers. It does not import or generate a runtime client.

Rationale

This keeps the core runtime responsibilities inside Argon, where the .oxbin, storage, rule evaluation, and fork semantics live. It also keeps environment concerns outside Argon:

  • tenant/principal selection belongs to the caller or proxy,
  • Tide workflow state and run journals belong to Tide,
  • Ode UI state and visualization layout belong to Ode,
  • SDK transport adapters belong to the host application.

The /v1 API is descriptor-based so generated SDKs can call it through a small host transport without coupling generated code to HTTP, Tide, fetch, or a specific deployment.

Alternatives

One alternative was a separate ontology-tooling service that wrapped Argon. That would have duplicated runtime semantics and left forks, storage, and derive behavior outside the implementation that owns them.

Another alternative was generating a full SDK runtime client. That is more ergonomic for simple workflows, but it couples generated output to transport and deployment concerns. The chosen design leaves that as a hand-written host adapter.

Consequences

ox runtime serve is a long-running process and therefore introduces async HTTP dependencies in the compiler workspace. The serving layer must keep a strict boundary: no generic entity writes, no ad-hoc raw query/mutation execution, no Tide or Ode state, no tenant hardcoding, and no SDK transport generation.

Hot reload is allowed only after compatibility checks. Additive schema changes may load; declaration removal or field/relation type changes are rejected when live ABox data exists.

unsafe_logic and forget remain capability-gated. Where the underlying language/runtime substrate is not enabled, the serving API refuses the request instead of pretending the capability exists.

Open Questions

  • Whether the full PosBool DNF provenance witness tree should be exposed by the current explain endpoints or by a later provenance-specific API.
  • Whether projection caches should become durable response caches for selected read endpoints or stay as backend-maintained invalidation state until DBSP arrangements land.
  • The final production IAM mapping for fork, forget, and future unsafe_logic execution budgets.

RFD 0015 — mutate body surface: EdgeQL-shaped, set-semantic

  • State: committed
  • Opened: 2026-06-03
  • Decides: the surface and semantics of the imperative mutate body that §7.5 promises but the v0.1 implementation never shipped (issue #15) — require guards, let bindings, insert-as-expression with named-field entity construction, update … set { = / += / -= }, insert … into …, for iteration, and return; the collection model these operate over (first-class, stored, generic) and its determinism contract (adopts RP-006 Track A / D-133); the subset that ships first to unblock the overlay vs. the full surface; and the commitment to mechanize mutation execution semantics in Lean. Rejects the legacy orca do { } / retract { } / emit { } clause structure. Relates to RFD 0001 (identity), RFD 0006 (field mutability), RFD 0011 (aggregates in guards).

Question

§7.5 documents a full imperative mutate body — preconditions, local bindings, typed-literal entity construction, collection inserts, control flow, and return. §7.5.1 admits the v0.1 implementation ships only a datalog-style subset (insert iof, relation-tuple insert, simple-target update … set, delete, forget) and that everything else “is designed but not yet implemented in the parser, elaborator, or runtime.” Two production mutations in packages/overlay (Ayush & Aryan’s materializeExpectedSatisfaction / recognizeSatisfaction) cannot be expressed without the missing forms; the parser reports OE0001 unexpected token … at module level the moment it meets let/for/return after a require block.

What is the mutate body’s surface and semantics, what does it operate over, and what ships first?

Context

The blocker. The overlay needs, per mutation: a precondition (one with an aggregate — occurrence.value == sum(r.value for r in records)), construction of fresh entities with named fields whose handles are reused downstream (let record = insert ExpectedSatisfactionRecord { … } then timeInterval: period), appending to a collection at a nested path, per-element posting under a for, and a returned result.

Why it doesn’t parse. mutation_stmt (compiler/crates/oxc-parser/src/grammar.rs) recognizes only insert/delete/forget/update; any other leading token falls through recovery, the require { } block’s } is mistaken for the body’s, and the remaining statements are re-parsed at module level.

Three layers, only one of them easy.

  1. AST — the Lean already mechanizes the whole imperative body (MutateDecl { require, body : List Stmt }; Stmt = letStmt | insertStmt | updateStmt | forLoop | ifStmt | returnStmt | exprStmt | …; InsertForm = typedLiteral | namedTyped | intoCollection | relation), all @[language_interface]-tagged, so the Rust AST is generated. The grammar exists; the parser just doesn’t build it.
  2. Execution — today a body lowers to a flat Vec<Operation> (a datalog DML batch: InsertIof/InsertTuple/Update{set}/Delete/Forget). There are no bindings, no data-flow between statements, no entity minting, no control flow, no return. Individuals arrive as parameters and get classified/updated; nothing is created. This is the real gap.
  3. Substrate (Lean)mutate is deliberately not a substrate rule (lowerRule returns none; it is a “host-runtime entry point”), and its execution semantics are unspecified.

The legacy precedent we reject. The orca-era vault design (D-064) gave mutations a five-clause COBOL-like structure: require { } / do { } / retract { } / emit { } / return. The new Argon already dropped the divisions (the Lean has a flat body : List Stmt, not separate do/retract clauses). This RFD finishes that move in the opposite stylistic direction from imperative paragraphs: SQL/EdgeQL-shaped statements, not COBOL divisions.

The settled direction (this RFD’s design discussion, 2026-06-02/03). The mutate body is imperative-looking but set-semantic, rhyming with EdgeQL: named-field insert as a value, update … set with collection ops, for as set-mapped iteration, guards as preconditions. Collections are first-class and stored (not derived-from-edges) — Set/Map/List are already declared generics in §6 — and governed by a determinism contract (below). Rust-like return types and error handling (Option/Result/?) are explicitly out of scope here.

Decision

1. The body is a sequence of statements; insert is an expression

mutate-decl   ::= attribute* 'pub'? 'mutate' Ident generic-params? '(' param-list ')'
                  ('->' TypeExpr)? mutate-body
mutate-body   ::= '{' stmt* tail-expr? '}'
stmt          ::= require-stmt | let-stmt | update-stmt | insert-stmt
                | delete-stmt | for-stmt | expr-stmt | return-stmt
require-stmt  ::= 'require' (expr | '{' expr (',' expr)* ','? '}') ';'
let-stmt      ::= 'let' Ident (':' TypeExpr)? '=' expr ';'
update-stmt   ::= 'update' expr (':' TypeExpr)? 'set' '{' field-assign (',' …)* '}' ('where' expr)? ';'
insert-stmt   ::= 'insert' insert-form ('since'|'during'|'at' expr)? ';'
insert-form   ::= TypeExpr '{' field-init (',' …)* '}'        // typed-literal: construct + return entity
                | Ident ':' TypeExpr '{' field-init (',' …)* '}'
                | Ident 'into' expr                            // collection add (sugar — see §4)
                | Path '(' arg-list ')'                        // relation tuple / iof — existing
delete-stmt   ::= 'delete' delete-form ('at' expr)? ';'
for-stmt      ::= 'for' Ident 'in' expr '{' stmt* '}'
return-stmt   ::= 'return' expr ';'
field-assign  ::= Ident ('=' | '+=' | '-=') expr               // set / collection-add / collection-remove

insert TypeExpr { … } is an expression that evaluates to the freshly-constructed entity (EdgeQL / SQL RETURNING). This is forced by the binding case (let record = insert … { … } then referencing record), and it makes entity construction compose. Constructing a fresh entity mints a fresh identity per RFD 0001.

2. Return: tail expression, or explicit return (which stays first-class)

  • A mutate with -> T must produce a T. The body’s tail expression (Rust’s rule — no trailing ;) is that value; since insert is an expression, ending the body with an insert T { … } returns the new entity with no keyword.
  • A value bound with let is returned by a bare tail (sr) or by explicit return sr;.
  • return expr; is first-class, not merely an early-exit affordance — it is what makes returning collections, tuples, and (later) Option/Result readable.
  • No -> T ⇒ the mutate returns unit; the body is pure effects.
  • “Return everything inserted” is rejected as a default (fragile under reordering, ambiguous for multiple inserts). To return several things, the tail is an explicit tuple or collection.

3. Collections are first-class, stored, and governed by determinism-by-observability

Adopts RP-006 Track A / D-133:

  • The invariant: no language-observable result (return values, serialization, the mutation event-log order) may depend on hash-derived order. This is stronger than “the implementation is internally deterministic” and is the correct knob — a randomly-seeded hash table is admissible iff order never leaks; a fixed-seed one still couples program meaning to the hasher the moment order is observed, breaking reproducibility across Argon’s in-process / Postgres / DBSP backends.
  • Order-sensitivity is a type property. Vec/List are ordered (order is the contract). Set/Map are unordered: the contract forbids observing order; where an observable sequence is unavoidable the language yields a canonical key-sorted order, never hash order.
  • Default backing: BTreeSet/BTreeMap (canonical iteration for free; no rehash spikes). A SwissTable/hashbrown Map is the opt-in fast path for lookup-heavy, order-insensitive use. Elastic/funnel hashing (#[dense]) is deferred pending a measured high-density win (RP-006 §5, benchmark harness pending).

4. insert … into … is kept as sugar

insert x into <coll-path> is retained (users coming from SQL INSERT INTO expect it) and desugars to a collection add: insert x into a.b.recordsupdate a.b set { records += x }. The canonical/primitive form is update … set { coll += / -= … }; insert into is surface sugar over it. (Reverses this RFD’s draft decision to drop it.)

5. for is set-mapped, and the event-order leak is closed

for x in coll { … } applies its body per element. To keep the mutation event-log order deterministic (replay, provenance), iteration over an unordered collection proceeds in canonical key-sorted order by default (RP-006 §A.3). When the analyzer can prove the body’s per-iteration effects are commutative (independent updates to disjoint targets — e.g. the overlay’s for r in records { update r.account … } when the r.account targets are distinct), the canonical sort may be elided. DBSP’s Z-set deltas may discharge this automatically for IVM-backed mutations (open question).

6. Atomicity

A mutate body commits as one transaction: all of its effects, or none. A failed require aborts with no events emitted. (Consistent with the append-only event-log substrate.)

7. What ships first

The immediate subset — exactly what compiles residential_lease.ar:

FormIn first push
require expr; and require { e, … };, incl. sum / count aggregate (RFD 0011)
let x = insert Type { … }; (insert-as-expression + binding + identity minting)
update <nested-path> set { f = e, coll += e }
insert x into coll.path; (sugar → +=)
for x in coll { … } (effect; canonical iteration)
tail-expression / return expr;; atomic commit; [x] literals, indexing, field paths
delete/upsert set-patterns; if/match in bodies; for as a returning comprehension✗ later
full Rust-like collection stdlib API; #[dense]/elastic backing✗ later
emit / detach delete / forget integration; bitemporal qualifiers on the new forms✗ later
Rust-like return types + error handling (Option/Result/?)✗ later

Ayush & Aryan’s mutations in the shipped form:

pub mutate materializeExpectedSatisfaction(
    pair: CorrelativePositionPair, perPeriodValue: Real,
) -> MaterializedExpectedSatisfaction {
    require perPeriodValue > 0;
    let period = insert TimeInterval {
        start: pair.propositionalContent.recurrence.startsOn,
        end:   pair.propositionalContent.recurrence.endsOn,
    };
    let record = insert ExpectedSatisfactionRecord {
        account: pair.book.expectedSatisfactionAccount, value: perPeriodValue,
        timeInterval: period, allenRelator: AllenRelationType::Before,
    };
    insert record into pair.book.expectedSatisfactionAccount.records;   // sugar → update … set { records += record }
    insert MaterializedExpectedSatisfaction {                           // tail = return value
        records: [record], expectedAccount: pair.book.expectedSatisfactionAccount, postedCount: 1,
    }
}

pub mutate recognizeSatisfaction(
    occurrence: OccurrenceEvent, records: [SatisfactionRecord],
) -> SatisfactionRecognition {
    require occurrence.value == sum(r.value for r in records);
    for r in records { insert r into r.account.records; }              // per-element, canonical-ordered
    insert SatisfactionRecognition {
        recordedValue: occurrence.value, satisfactionAccount: records[0].account,
    }
}

8. Lean is a committed deliverable, not a vague “later”

To unblock the overlay we ship the parser + elaborator + runtime for the immediate subset ahead of the mechanized semantics. But the substrate work is a firm obligation, tracked, not optional: (a) refine the Lean AST (insert as expression; +=/-= field-assign ops; for set-semantics); (b) mechanize mutation execution semantics (entity construction & identity, statement sequencing with bindings, collection ops, atomic event emission); (c) the soundness obligation flow-typing already assumes (FlowTyping.lean states, without proof, that mutate writes respect the monotone fixpoint discipline). Per AGENTS.md this is Lean-first substrate work; the only concession is that code may land first to unblock, with the Lean following.

Rationale

  • EdgeQL/set-semantic over imperative-procedural. Argon’s working substrate is already relational (relation-tuple and iof edges); a set-at-a-time, expression-valued mutation surface is continuous with it and with the language’s declarative core, where a COBOL-style statement machine would not be. Crucially, the choice keeps ~90% of the syntax §7.5 already documents while changing its meaning — far less throwaway work than a ground-up redesign, and the Lean AST is already close.
  • insert as expression is not a stylistic preference; it is forced by let x = insert … + downstream reuse. Once forced, the return design (tail expression) falls out, and “what if multiple inserts” stops being ambiguous because only the tail is returned.
  • Determinism-by-observability (RP-006 Track A) lets the language be deterministic and free to use any map backend, by legislating observability rather than implementation. B-tree default because canonical iteration — on the hot path for a KR language — is free, while a hash table pays an O(n log n) sort to honor the same contract.
  • Keep insert into because the cost is one desugaring rule and the benefit is meeting SQL-trained expectations; the canonical += keeps the core small.

Alternatives

  • Build §7.5 imperative as literally documented (document/OOP model). Rejected: it bakes “entities own arrays you imperatively append to” into the language, away from the relational/EdgeQL direction, and still requires the entire new execution model — so it is not actually cheaper.
  • Pure-graph: make collections derived from edges (no stored collections). Rejected by decision: collections are first-class and stored (Set/Map/List), and the stdlib provides them. insert into/for are legitimate operations on stored collection fields, not smells.
  • Drop insert into, canonicalize only on +=. Considered and reversed — kept as sugar for ergonomics.
  • Auto-return the last/all inserts. Rejected: fragile and ambiguous.
  • EdgeQL with … select with no return keyword. Rejected: return must stay first-class for Option/collection/tuple returns and future error handling.
  • Elastic/funnel hashing as default backing. Deferred: real result (arXiv:2501.02305) but the win is at high load factors and no hardened implementation exists; gated on a measured Argon-workload win (RP-006).

Consequences

  • Parser: mutation_stmt grows from four verbs to the full statement set; insert becomes expression-valued; require/let/for/return/tail-expression parsing; struct-literal-in-tail disambiguation (the Rust if x { S {} } wrinkle).
  • AST/elaborator: consume the (already-generated) Stmt/InsertForm nodes; thread an environment for let bindings and insert handles; lower the new forms — the flat Vec<Operation> model grows into a statement/expression evaluation with data-flow.
  • Runtime: entity construction with identity minting (RFD 0001); collection field +=/-=; for evaluation with canonical ordering; atomic multi-effect commit; tail/return value.
  • Types: the ordered/unordered collection contract (§3) governs all collection use, not just mutations; it should also be reflected in §6 and the type-system Lean. sum/count in guards interacts with RFD 0011 under OWA.
  • Stdlib: Set/Map/List implementations + a Rust-like API (parallel workstream; the first push needs only += and list literals).
  • Reference: §7.5 / §7.5.1 rewritten from “designed, not implemented” to the shipped surface + the explicit later-list.
  • Lean: the obligations in §8 are now tracked work, including closing the flow-typing assumption.
  • Research: RP-006 (collection-backend benchmark; Bayesian; topology) proceeds in parallel; none gates this.

Open questions

  1. Identity minting details for insert Type { … } — resolved by RFD 0001; confirm the runtime hook and how a minted id appears in the event log / provenance.
  2. Are relation/extent tables insert-only-then-read-mostly? (The #[dense]/elastic precondition — RP-006 Q1.)
  3. Does iteration dominate the reasoning workload? Validate with a real op-trace before finalizing the B-tree-vs-hashbrown default (RP-006 Q2).
  4. Do DBSP Z-set commutative-monoid deltas discharge the for event-order mitigation automatically for IVM-backed mutations? (RP-006 Q3.)
  5. Postgres bridge order — can canonical iteration order be pushed into SQL ORDER BY? (RP-006 Q4 — the riskiest leak surface.)
  6. if/match in bodies and for-as-returning-comprehension — surface + value semantics, deferred past the first push. Resolved by Amendment 1.
  7. Error handlingOption/Result/? and Rust-like return types; their own RFD.

Amendment 1 (2026-06-05) — control flow is expression-valued; bodies and branches are blocks (resolves OQ #6)

OQ #6 left the surface and value semantics of if/match in mutate bodies open. While that gap stood, the two representations drifted: the Lean surface AST split the construct into a value form (Expr.ifExpr / Expr.matchExpr, with bare-Expr branches) and an effect form (Stmt.ifStmt / Stmt.matchStmt, with List Stmt branches), whereas the Rust grammar modelled a single IF_EXPR / MATCH_EXPR over BLOCK_EXPR branches. This amendment settles it.

Decision. if and match are expressions, and their branches/arms are blocks. A block { s₁; … ; sₙ tail? } is itself an expression: it executes its statements for effect and evaluates to its optional trailing expression, or to unit () when there is none (Rust’s block rule, continuous with §1–§2’s tail-expression return and insert-as-expression). The mutate body is a block. There is exactly one if and one match form — a value use (let x = if c { a } else { b }) and an effect use (if c { insert X } else { insert Y }) are the same construct, distinguished only by whether the branch blocks have a non-unit tail.

Why (not the split).

  • Rust/Cargo aesthetic — Rust has one if, an expression; Argon defaults to that idiom.
  • Consistency — §1/§2 already chose expression-orientation (tail-expression return, insert as an expression). A statement-only if contradicts it.
  • Expressiveness — the split cannot express let x = if c { let y = f(); g(y) } else { h() }: a bare-Expr branch admits no statements, and a statement-if yields no value. Block branches give both. The split is a strict expressiveness loss, for no gain.
  • No redundancy — one construct, not a value/effect pair that must be kept in sync.

AST / Lean consequence. Add Expr.block (stmts : List Stmt) (tail : Option Expr). Expr.ifExpr / Expr.matchExpr keep Expr-typed branches/arm-bodies (which now admit block), so their signatures are unchanged. Remove Stmt.ifStmt and Stmt.matchStmt — an effectful if/match is Stmt.exprStmt (ifExpr …) over block branches. (for remains a unit-valued effect statement for now; promoting it to an expression is uniform but not required, since it never produces a value — tracked, not blocking.) Because a block-expression runs statements, expression- and statement-evaluation become a single mutual clique; Argon.Runtime.MutationSemantics and its theorems (runMutation_error_noop atomicity, the fresh-monotonicity family) are re-mechanized against the merged induction. The @[language_interface] surface mirror and CoreIR lowering update accordingly.

Code consequence. The parser already yields IF_EXPR / MATCH_EXPR / BLOCK_EXPR. The elaborator routes a block to its body-op sequence and an effectful if/match to control-flow IR (Operation::If / Operation::Match over the branch blocks’ operations), while a pure-value if lowers to Term::IfExpr (evaluated by resolve_term_to_value). The _ => {} catch-all in mutate_lower::lower_body_stmt, which silently dropped unrecognized statements, is replaced by a loud diagnostic. Tracked in #74.

Amendment 2 (2026-06-09) — value-position match realized by the IfExpr desugar; constant-pattern subset; OE0203

Amendment 1 prescribed an Operation::Match control-flow IR alongside Operation::If. The implementation that landed realizes the value half of match differently — and the prescription is amended to match it.

Decision. A value-position match over constant patterns desugars at elaboration to the existing right-nested Term::IfExpr chainmatch s { P1 => v1, P2 => v2, _ => v3 }if s == P1 { v1 } else if s == P2 { v2 } else { v3 } — with no new IR (no protocol/drift change). For constant patterns the two semantics are identical: ordered first-match, the final arm’s value as the else-branch. The executable pattern subset is payloadless enum constant paths, Int/String/Bool/Date literals, or-patterns of those (consecutive conditions sharing a body), and the wildcard _; the richer §14 forms (binders, payload/record patterns, guards, type tests, is-outcomes) are refused loudly (OE1319) until they land. Exhaustiveness is required (OE0203 at ox check): a final _ arm, or full coverage of the scrutinee’s enum variants when statically known. The same desugar serves every value position — fn bodies, rule-body comparison operands (compiled to CompiledExpr::If in the reasoner), and mutate let RHS / update values / return values / require guards (evaluated by the existing Term::IfExpr arm of resolve_term_to_value); the Lean evalExpr gains the matching .matchExpr arm (evalMatchArms / patternMatchesConst).

Statement-position match (arms running effects) is now realized the same way — no Operation::Match: it lowers to a right-nested Operation::If chain over the arm blocks’ operation sequences (match s { P1 => { ops₁ }, P2 => { ops₂ }, _ => { ops₃ } }If(s == P1) { ops₁ } else { If(s == P2) { ops₂ } else { ops₃ } }), reusing the value desugar’s pattern classification and exhaustiveness (same constant subset, same OE0203 / OE1319 sinks; or-patterns expand to consecutive conditions sharing the arm’s operations; a wildcard-only match splices its arm unconditionally behind a hidden Operation::Let of the scrutinee, so an aborting scrutinee — a missing-field projection, say — aborts exactly as in the chained case, matching Lean’s evaluate-scrutinee-first order). Scrutinee Terms are pure, so per-link re-evaluation is effect-free. One scoping caveat: the Lean matchStmt/ifStmt semantics scope arm/branch-local bindings, while the Rust runtime threads one flat environment and currently leaks them past the match/if (pre-existing, recorded drift — #196). The enabling parser change threads an in-mutate-body flag so blocks at any nesting depth (match arms, if branches, for bodies) admit the full mutate statement set — which also fixed update not parsing inside if branches; blocks in value position (a let RHS block, a value-if branch, a value-match arm) refuse effectful statements loudly (OE1321) since value lowering reduces a block to its tail expression. On the Lean side, Stmt.matchStmt gains direct execution semantics (evalMatchStmtArms: ordered first-match, the selected arm’s block runs for effect, return propagates), intended-equivalent to the If-chain by construction. Stmt.matchStmt / Stmt.ifStmt are NOT yet removed as Amendment 1 ordered: the A1 encoding (exprStmt over Expr.block arms) needs block evaluation inside evalExpr, which merges the expression/statement interpreter cliques and requires evalExpr to carry control flow for return propagation — exactly the merged-clique re-mechanization A1 itself anticipated; doing it halfway would regress return-in-branch. The removal stays tied to that re-mechanization; the drift is documented at both constructors and in Argon.Runtime.MutationSemantics. OE1318 (Module::unsupported_mutation_forms) remains the gate for the genuinely unexecutable statement forms — emit now parses (EMIT_STMT) and lowers to Operation::Unsupported so it is refused there rather than as a generic parse error.

RFD 0016 — Numeric tower: exact by default

  • State: committed
  • Opened: 2026-06-05
  • Decides: the runtime representation and arithmetic semantics of the numeric tower — specifically that Real is exact (arbitrary-precision rational), Decimal/Money are exact, and machine floats (f32/f64) are the explicit opt-in inexact types; how the reasoner’s value domain represents numbers; and that aggregate folds (sum/avg/min/max) are exact. Resolves the “finer breakdown of the tower … deferred to a follow-up RFD” note in §17.1.

Question

§17.1 lists Real as “real number; runtime-chosen representation (defaults to IEEE 754 f64)” and defers the finer tower (rationals, fixed-point) to a follow-up RFD. But the substrate is already ahead of the book: the slice-4 mutation semantics (Argon/Runtime/MutationSemantics.lean:85) models Value.real : Rat — an exact rational — with exact parseDecimal, toRat, and evalRatBinary. Meanwhile the reasoner’s catalog value domain has no numeric beyond Int (a Real/Decimal/Money literal round-trips as opaque CBOR), so an aggregate like sum(r.value for r in …) where value: Real cannot fold at all.

What is Real’s representation, what does the reasoner store, and is avg exact? And which wins — the book’s “f64” or the Lean’s exact rational?

Context

  • Domain. Argon’s flagship workloads are finance and legal (the residential-lease / accounting overlay). For that domain IEEE-754 f64 is a bug generator: 0.1 + 0.2 ≠ 0.3, summed payments drift, and cumulative-satisfaction checks (e.value <= sum(r.value …)) become subtly wrong at the boundary. Exactness is a correctness requirement, not a nicety.
  • The Lean already chose exact. MutationSemantics.lean models Real as Rat deliberately (“the spec’s arithmetic is exact”). Per the project’s own rule — where the book disagrees with the Lean on something the Lean covers, the Lean wins; the book is a bug — §17.1’s “f64 default” is the bug.
  • avg wants a field. avg over a Real collection is Σ / n. Over rationals this stays exact (rationals are closed under division); over fixed-precision decimals or floats it rounds. Exact Real is what makes avg mathematically clean.
  • The reasoner gap. oxc-reasoning’s Value has Int(i64) but no exact Real/Decimal; the fold is Int-only. This blocks the overlay’s Met rule (the motivating case) and any Real/Money aggregate.

Decision

  1. Real = exact, arbitrary-precision rational. Numerator/denominator big integers; no precision ceiling; no implicit rounding. This is the canonical Real for the data/ontology substrate.
  2. Decimal and Money are exact. Decimal is arbitrary-precision base-10; Money is a currency-tagged Decimal. Both share the exact-rational carrier in the reasoner (a Decimal is a rational whose denominator is a power of ten); the currency tag and display scale are metadata on top. Money arithmetic (§17.1, D-069) is unchanged in typing; only its representation becomes exact.
  3. Floats are the explicit opt-in inexact tier. f32/f64 live in std::math::primitive (already the case) and are the only inexact numerics. Real never silently becomes a float. The implicit-widening chain keeps Int → Real (exact ⊆ exact) but the f32 → f64 → Real step is removed — a float reaches Real only via an explicit, lossy to_real-style conversion (a float is not exact, so widening it into the exact tier must be a visible choice).
  4. The reasoner value domain carries exact numerics. oxc-reasoning’s Value gains an exact-rational variant; Real/Decimal/Money literals materialize to it (replacing the opaque-CBOR/f64 path). Mirrors MutationSemantics.Value.real : Rat.
  5. Aggregate folds are exact. sum/min/max/avg fold over the exact domain: integer-exact when all operands are Int, else promoted to exact rational; avg stays exact (Σ/n as a rational). This mirrors MutationSemantics.foldAggregate.
  6. §17.1 is corrected to describe Real as exact arbitrary-precision rational (not f64), and the deferral note is resolved by this RFD.

Rationale

  • Correctness-first for the domain. A financial/legal substrate that silently rounds is unfit for purpose; exact-by-default makes “the numbers are right” the default, with floats available when a modeler explicitly wants approximate/scientific compute.
  • The Lean is the source of truth. Adopting exact rationals aligns the reference and the implementation to the already-mechanized MutationSemantics; it is reconciliation, not invention.
  • avg exactness is a concrete, checkable win unavailable under float or fixed decimal.
  • Implementation. The Rust reasoner uses num-rational::BigRational over num-bigint (MIT/Apache-2.0, on the workspace allow-list) — arbitrary precision, exact division, deterministic Ord/Hash over normalized components. Not rust_decimal (96-bit fixed; would diverge from the Lean Rat carrier and cap precision).

Alternatives considered

  • Real = f64 default (status quo §17.1). Rejected: float drift is a correctness bug for the domain; contradicts the Lean.
  • Real = fixed-precision Decimal (e.g. rust_decimal, 96-bit). Rejected: caps precision, rounds division (so avg is inexact), and diverges from the Lean’s Rat carrier.
  • Defer (keep folding Int-only). Rejected: blocks the motivating Met rule and every Real/Money aggregate; the deferral was already taken once in §17.1 and this RFD is its resolution.

Follow-ups

  • L5 — exact avg + a Reasoning/Aggregate.lean fold theorem (the interval/fold characterization; closes the Lean item of issue #52).
  • R4oxc-reasoning exact Value variant + exact encode_tuple + exact sum/min/max/avg fold mirroring MutationSemantics.foldAggregate; oxc-runtime literals → exact value (replace the opaque-CBOR path).
  • A future RFD may add custom-precision fixed-point / refinement-driven width selection (the remaining tail of §17.1’s deferral); not needed for exact-by-default.

RFD 0017 — Refinement classification: where (primitive) vs iff (defined)

  • State: committed
  • Opened: 2026-06-05
  • Decides: whether a concept’s refinement predicate (<: Parent where { P }, Refinement) is a necessary condition only (membership asserted; the predicate is an invariant) or a necessary-and-sufficient condition (membership derived; the substrate auto-classifies). Today every refinement is implicitly the latter. This RFD splits the surface so the modeler chooses explicitly: where { P } is primitive (description-logic ; necessary-only; membership asserted), iff { P } is defined (DL ; necessary-and-sufficient; membership derived). Confirms dyn remains reserved exclusively for runtime trait objects (Built-in type forms, Out of scope (v0)). Relates to RFD 0006 (refinement-determined classification was cited there as canonical), RFD 0007 (three-valued refinement membership under OWA), RP-007 (spec/research/RP-007-narrowing-under-mutation.md; value-dependent narrowing soundness).

Question

concept Adult <: Person where { self.age >= 18 } — is an arbitrary Person with age >= 18 automatically an Adult, or must Adult-membership be asserted, with the predicate merely constraining who may be one?

Today the answer is “automatically.” The runtime computes extent(Adult) = { x : iof(x, Person) ∧ predicate(x) } (compiler/crates/oxc-runtime/src/lib.rs:1986-2008, the “refinement-honesty pass”); the book states it outright — “the refinement is the substrate’s source of truth for membership… the substrate derives the iof classification automatically” (mutate, constraint 2 on insert iof) — and rejects explicit insert iof(x, Adult) with OE0211. That is the DL defined-class () reading, and it is the only reading the surface can express. There is no way to say “validate this predicate on every Adult, but membership is conferred, not inferred” — the DL primitive-class () reading, which is the common case in real ontologies.

Auto-classification by default, with no opt-out, is the wrong default for three reasons (Rationale). The question: what surface distinguishes the two, and what does each mean across the extent query, insert iof, construction, and mutation?

Context

The DL distinction. A primitive class C ⊑ D ⊓ P states necessary conditions: every C is a D satisfying P, but a D satisfying P is not thereby a C — membership is asserted (SubClassOf in OWL). A defined class C ≡ D ⊓ P states necessary and sufficient conditions: a D satisfies P iff it is a C — membership is inferred by the reasoner (EquivalentClasses; classification/realization). In real ontology engineering the overwhelming majority of named classes are primitive; defined classes are the deliberate minority written specifically to drive inference. Defaulting every where to defined inverts that.

What the substrate mechanizes vs. specifies. The Lean models the refinement predicate as a decidable fragment (spec/lean/Argon/Decidability/Fragment.lean) but leaves the instance-level value predicate D2Pred opaque (Fragment.lean:80-103: “we do not formalize the internal structure of QF-LIA formulas”). mutate emits only explicit assertIof/retractIof effects (spec/lean/Argon/Runtime/MutationSemantics.lean:104-115). The defined-class realization (iof(x,C) ↔ iof(x,parent) ∧ P(x)) exists as book prose + Rust runtime, not as a Lean theorem. So this is a clean point to fix the surface: the substrate has not committed to “all refinements are defined” — only the prose and one runtime pass have.

The corpus is entirely defined classes. Every where in a runnable example declares the refinement, never asserts iof to the refined type, and queries the derived extent: FullTime/Manager (examples/refinement_employment), ActiveAdult (examples/multi_field_refinement), Adult/Felon/SpecialClass feeding defeasible can_vote (examples/legal_norms_can_vote), FullTime over time (examples/temporal_promotion), Adult as-of (examples/temporal_as_of_surface). These are defined classes; migrating them whereiff is correctness, not churn. The sole where test that does not auto-classify hand-asserts alice iof Adult (compiler/crates/oxc-oxbin/tests/end_to_end_program.rs) — the natural primitive case.

No write-time enforcement exists today. The predicate is purely a query-time filter; construction, insert iof, and update perform no validation (oxc-runtime/src/lib.rs: Operation::Construct, Operation::InsertIof, Operation::Update). OE0210/OE0211 are declared in grammar.toml but never emitted. So a primitive where needs new enforcement machinery (its predicate must do something), and a defined iff needs OE0211 finally wired.

Decision

1. Two clause keywords; identical syntax, opposite membership semantics

where-clause ::= ('where' | 'iff') '{' refinement-pred (',' …)* '}'

The predicate grammar (refinement-pred ::= D1Pred | D2Pred, Refinement) is unchanged. The keyword alone selects the membership semantics:

  • where { P } — primitive (DL ). P is a necessary condition. Membership is asserted (by construction or insert iof). extent(C) is the set of entities asserted iof C (or iof a subtype) — identical to an unrefined subtype’s extent. P is enforced as an invariant at every membership-write point; it never widens the extent.
  • iff { P } — defined (DL ). P is a necessary-and-sufficient condition. Membership is derived: extent(C) = { x : iof(x, parentᵢ) ∧ P(x) } over C’s <:-ancestors-including-self — the current runtime behavior, unchanged. Manual assertion is forbidden.

2. insert iof / construction / mutation, per kind

operationwhere (primitive)iff (defined)
extent(C) queryasserted members (∪ subtypes); no predicate filteriof(ancestor) ∧ P, derived
insert iof(x, C)permitted; P(x) checked → OE0212 if violatedrejected OE0211 IofInsertOnRefinedType
insert C { fields }construct + assert iof C; P(fields) checked → OE0212construct + set fields; classification derived from P
update x set { f = v } where some where-C ∋ x constrains fre-check POE0212 if violatedn/a (membership re-derived at next query)

OE0211 (declared, previously unemitted) is narrowed to iff concepts — you cannot assert membership of a defined class because its membership is the predicate. For where, asserting membership is exactly how you become a member, so insert iof is permitted and the predicate gates it.

3. New diagnostic: OE0668 RefinementInvariantViolated

A membership-write that would place an entity in a primitive where-concept whose predicate it does not satisfy is rejected at runtime, surfaced as Result<_, Diagnostic> (the mutate-rejection channel mutate already promises). Three-valued, OWA-aligned (World assumptions (CWA / OWA), RFD 0007): reject only on positive evidence of violation (P evaluates to definite false); unknowninformation absence, i.e. a referenced field with no recorded value — permits the write. A primitive invariant blocks the demonstrably-bad, not the merely-unproven — symmetric with iff’s rule that unknown does not grant membership.

Amendment (2026-06-11, audit ts-01 / PR #272). The original clause folded unevaluable (a v0.1-unsupported form or a type-mismatched operation) into unknown-permits. That fold was the ts-01 critical: predicates over Real/Decimal/Money/Date were “unevaluable” to the v0.1 evaluator and therefore silently never enforced. The ratified semantics split the bucket: unknown = missing field only (OWA information-absence; permits where, withholds iff membership). A predicate that cannot be evaluated — unsupported form, malformed wire data, type-mismatched comparison — is a loud error (RefinementUnevaluable at runtime; OE0660 refuses unsupported forms at build time), never a silent permit or a silently-empty extent. Three-valuedness is for the world’s incompleteness, not the evaluator’s.

4. dyn is reserved exclusively for trait objects

Confirmed (your call (c)). dyn is and stays the keyword for deferred runtime trait objects (&dyn Trait / Box<dyn Trait>, Built-in type forms, Out of scope (v0)) — dynamic dispatch + type erasure, orthogonal to whether membership is derived. The classification axis uses where/iff; the dispatch axis uses dyn. They may co-occur on a future concept and must not share a keyword.

5. Substrate scope

The Lean surface AST (ConceptDecl) and storage body (Storage.AxiomBody.ConceptDeclBody) carry the primitive/defined discriminator now — a shared @[language_interface] inductive RefinementKind, drift-gated against the Rust RefinementKind mirror — and the two membership semantics are documented in the substrate docstrings. The realization theorem for iffiof(x,C) ↔ iof(x,parent) ∧ P(x), requiring the instance population + value-environment that State C A does not yet model — is not mechanized here; it is the value-dependent-membership case that RP-007 §1.2 hazard 2 / §4.5 scopes as open. It is left as a follow-up (RP-007-adjacent, #40), not claimed as done. This is honest staging, not a stub: the surface, elaboration, storage, and runtime are complete and proven-to-run (parser, elaborator, and five end-to-end runtime tests); the deep substrate theorem is research, exactly as RP-007 is.

Rationale

Why split at all (against auto-by-default). (1) Practice: most real classes are primitive; the common case should be the plain keyword. (2) No spooky inference: silent derivation of facts is un-Rust-like (the house aesthetic default); a modeler should opt into the reasoner minting memberships. (3) Soundness blast radius: value-dependent auto-classification is the source of RP-007’s hardest open hazard (a narrowing if x: VerifiedAdult invalidated by update x set { age = 10 }). Making it opt-in (iff) makes that hazard opt-in — the obligation narrows to exactly the concepts that asked for derivation.

Why iff. It is the membership biconditional: iof(x, Adult) ↔ iof(x, Person) ∧ age ≥ 18 reads directly as “Adult iff Person and age ≥ 18.” Necessary-and-sufficient is the literal meaning of “if and only if.” No other candidate (defined, :=, ) is as self-documenting at the use site, and it touches neither dyn nor = (taken by metaxis typed domains, Meta-calculus atom).

Why where for primitive. Rust’s where is a bound — a necessary constraint on a type, never a definition (fn f<T>() where T: Clone). Reusing it for “necessary condition on members” aligns with the Rust/Cargo aesthetic default and with DL . A where-refined concept reads as “a Person, further constrained to age ≥ 18” — a constraint, not a definition.

Why three independent principles agree (practice, no-spooky-inference, soundness-containment) plus two surface alignments (Rust where-bound, iff-as-biconditional) is why (a) — which keyword is primitive — has a correct answer rather than a coin-flip: where = primitive, iff = defined is over-determined.

Alternatives

  • Keep auto-by-default; add a keyword for primitive. Rejected: inverts ontology practice, keeps the spooky default, and leaves the RP-007 hazard pervasive. Also incompatible with choosing iff (which must be the defined form — it claims sufficiency).
  • Operator distinction <:+where vs =. (Adult = Person where {…}.) Mirrors DL / exactly but a one-character carrier of enormous semantic weight is dangerous, and = already introduces metaxis typed domains.
  • defined modifier on the concept. (pub defined kind Adult ….) Familiar to Protégé users but detached from the clause and heavier; loses the at-the-predicate self-documentation iff gives.
  • Reuse dyn (dyn where). Rejected per call (c): collides with trait objects, and “dynamic dispatch” ≠ “derived membership.”
  • Enforce primitive where purely via a separate check rule (status quo workaround: drop the predicate, add a constraint rule). Rejected as the only mechanism: it detaches the invariant from the concept’s identity and gives no surface to the necessary/sufficient choice. where-as-invariant and check-rules coexist; the former is a membership invariant local to the concept, the latter a global constraint.

Consequences

  • Migration. All 8 example where sites → iff; the corpus tests, CLI pipeline tests, and runtime unit test that assert derived extents update accordingly. Primitive-where behavior (no auto-classify, asserted membership, OE0668 on insert-iof / construct / update, OE0211 on iff insert-iof, atomic rejection) is covered by five new end-to-end runtime tests. The oxbin round-trip fixture stays where-compatible.
  • Wire format. ConceptDeclBody gains a classification discriminator; canonical-CBOR field order updated; decode_concept_decl back-compat: absent discriminator ⇒ iff is not assumed — pre-split artifacts are rebuilt (Argon is pre-1.0, in-process; no stored-artifact compatibility burden).
  • OE0211 semantics change. Previously declared-but-unemitted “refined type”; now emitted, and only for iff. Any future code keying on “has refinement predicate ⇒ reject insert iof” must key on “is iff.”
  • New runtime enforcement path (OE0668 RefinementInvariantViolated) for primitive where at construction / insert iof / dependent update.
  • Rigidity (OE0210) remains out of scope here: it gates insert iof on anti-rigidity (a UFO meta-property carried as meta_property events, not in the ontology-neutral ConceptDeclBody). Still declared-but-unenforced after this RFD; a separate rigidity-enforcement effort owns it. Noted so the two insert iof gates (rigidity, classification) are not conflated.

Open questions

  • iff realization soundness (follow-up, RP-007-adjacent #40): mechanize iof(x,C) ↔ iof(x,parent) ∧ P(x) in Lean, which requires extending the state model to carry the value environment — the RP-007 §4.5 sub-problem. Until then iff derivation is runtime-correct but not substrate-proven (parity with the pre-RFD status quo).
  • update-time invariant scope. v0.1 re-checks a primitive where predicate only against the directly mutated entity/fields. Transitive invalidation (a mutation to entity y that changes an aggregate a where-predicate on x reads) is not chased; the predicate fragment (OE0660) forbids the aggregate/subquery forms that could create such coupling, so this is currently vacuous — revisit if the fragment widens.
  • Should iff construction (insert C { … }) be sugar or an error? Decided: sugar (construct + set fields; classification derived). Revisit if it proves confusing that a constructed iff-C with predicate-violating fields silently is not in extent(C).

RFD 0018 — Production reasoner: the incremental DBSP engine

  • State: accepted — implemented with revisions; per-decision status below (2026-07-03)
  • Opened: 2026-06-05

Per-decision status (2026-07-03):

  • D3 — abandoned. No Feldera fork; the engine adopts the DBSP model on its own operators (RFD 0021 D6).
  • D11 WFS-over-cycles — shipped, without the SLG kernel. Batch Van Gelder alternating fixpoint atop semi-naive (executor/wfs.rs); supersedes the BUILD-SOON row (note under D11).
  • Persisted-IVM (D4/D6 arc) — shipped, with a ceiling. DRedc two-counter retraction (#642) + delta-seeded monotone insertion + the checkpoint-seeded read-model (RFD 0053 D6). Incremental insert remains monotone-only; WFS-cycle / rule-augmenting reads still full-recompute.
  • D1 — the unbuilt keystone in Rust. The substrate is a 2-valued signed-multiplicity Z-set; WFS is a hardcoded strategy, not a selectable fixpoint. The #640 AFT Phase-0/1 track is building the pair-encoding, differential-oracle-gated (note under D1).
  • D5 — still unformalized. The ℕ[X]→PosBool[X] retraction homomorphism (commuting with deletion) remains the open obligation gating incremental-retract-under-pair.
  • D8 — as-shipped oracle stack. FullRecomputeMaintainer + the Lean evalProgram executed oracle + the RFD-0055 conformance corpus; see the D8 note for a floor-scope correction.
  • Decides: the concrete realization of RFD 0003’s DBSPExecutor — the data model, evaluation engine, storage model, provenance, and invalidation that take Argon’s reasoner from a correct demo (cold, non-incremental, nested-loop semi-naive) to production-grade (“working and usable” at scale). Resolves RFD 0003’s deferred open question (“Stratum boundary policy with retractions → defer to the DBSP integration RFD”). Fixes the scope cut (RP-009 §4) and answers RP-009’s open questions (§9). Scopes the ARS substrate research record (vault ars-substrate R-3.1–R-3.8) down to what we build now vs design-for vs defer.

This RFD is the architecture decision record for the production reasoner. It is Lean-first where it touches semantics (the engine conforms to spec/lean/Argon/Reasoning/; divergence is a bug). It commits a plan, not code; its Phase 0 is a de-risking spike that gates the central engineering bet before any irreversible code lands.


Question

RP-009 mandates: bring the rule engine from correct MVP to production-grade, because it is the foundation the entire language sits on. RFD 0003 settled how multiple backends compose under one Engine/TierExecutor interface, named DBSPExecutor as the incremental recursive-tier backend, and explicitly deferred the engine itself — its data model, its IVM/retraction semantics, its storage and invalidation — to “the DBSP integration RFD.” This is that RFD.

Concretely: What is the production engine? — (1) the value/data model that lets a bilattice (Truth4) truth domain ride an incremental dataflow substrate that needs an abelian group; (2) the evaluation substrate (build vs fork vs wrap, and which); (3) how facts are stored and read at scale (the event log is the source of truth, but a cold per-query re-materialization is O(database) and dies at scale); (4) how derived state is incrementally maintained, persisted, and invalidated; (5) how provenance composes with incrementality; (6) how it conforms to the Lean; and (7) the scope cut + build order that gets us there correctly the first time, designed so the features we’ll need soon (defeasibility, metric-temporal, well-founded semantics over cycles) integrate neatly without a rewrite.


Context

Current state (verified against origin/main @ b7a24bc)

The engine is a sound, well-tested, semi-naive stratified-Datalog evaluator — correct on the Datalog + stratified-NAF + exact-rational-aggregate fragment, at small scale. It is not production-grade. The load-bearing gaps:

  • Cold + non-incremental. oxc-runtime::query_derive calls materialize_predicates(module) (a full scan of all IofAssertion/RelationTuple/IndividualPropertyAssertion events into a fresh RelationCatalog) then evaluate_to_fixpoint from scratch — every query. No derived state persists across queries; no incremental update on mutation. Every query is O(all events) + O(rules × facts × iterations), cold. This is the headline scaling failure.
  • Nested-loop joins, decode-per-tuple. extend_bindings/unify iterate every tuple of a relation and decode_tuple (CBOR) it inside the innermost join loop; Relation<Vec<u8>> = BTreeMap<CBOR-full-tuple, weight>. No join-key index, no arrangement (arrangement_body is reserved + inert). The dominant cost is O(iterations · rules · |prior| · |rel|) CBOR decodes.
  • Executor split. The live path is the free fn executor::eval::evaluate_to_fixpoint; SemiNaiveExecutor::execute is a stub returning an empty catalog; the RFD-0003 TierExecutor trait and the DataFusion-shaped logical/physical/optimizer layers are orphaned scaffolding.
  • Generation counter wired to the wrong cache. oxc-storage-pg has runtime_generations (bumped atomically with appends, invalidating a projection cache), and oxc-serve reads it — but only to memoize the hydrated Store (raw replayed events), not derived reasoning state. The §19.6 runtime-Salsa db (OxcRuntimeDatabase/EventLogInput) does not exist (oxc-runtime doesn’t depend on salsa). So there is no derived-state invalidation today.
  • Correctness items. Modal box/diamond erase to the inner atom — unsound for anti-rigid types (the Lean proves box(anti_rigid) → false in StaticDischarge.lean). The 1000-iteration cap is a hard Err (not silent), but is a literal at every call site, not a configured convergence policy.

What already works and must stay green: stratified Datalog + NAF; exact-rational sum/count/min/max/avg/count_distinct folds (RFD 0011/0016); the keystone end-to-end tests (oxc-runtime/tests/keystone.rs); Apt-Blair-Walker stratification (compile/stratify.rs, Tarjan SCC); and — partially — a Governatori three-stratum defeasible evaluator and per-standpoint federated Truth4 evaluation.

What the substrate requires (Lean-first conformance bar)

The only fully-mechanized reasoning semantics is strict-stratified perfect-model evaluation (Reasoning/Fixpoint.lean: Cat1-monotone to fixpoint, then Cat2-NAF once, per axis in topological order; Theorems terminate/unique/stable, zero sorry). Consequences for the engine:

  • Order-independence (stratified_fixpoint_unique): within-stratum rule/axis order is semantically irrelevant ⇒ any strategy converging to the same perfect model is conformant — DBSP, semi-naive, indexed, all sound. Engine choice is a performance decision, not a correctness one.
  • NAF order-sensitivity (cat2Apply_sublist): Cat2 extension is monotone only under List.Sublist; the engine must preserve relative Cat2 rule order across deltas.
  • Acyclicity is necessary (Necessity.lean): cyclic axis-dependency ⇒ non-unique. Cycle rejection (OE1309) is correct, not conservative.
  • Modal: box(anti_rigid) → false is proven and must be honored (or the case honestly refused).
  • WFS-by-default, #[brave] stable models, the defeasibility proof-tag engine, DatalogMTL operators, and aggregate OWA-intervals are book-promised but not Lean-backed — they are the “design-for / defer” set, not the conformance floor.

What the north-star application needs (RP-009 §4, the residential-lease + accounting overlay)

A shallow, narrow rule program (≈28 derive rules, one true positive recursion — BreachedAt over the propositional-content tree, with stratified not Met — one NAF pipeline). Its load-bearing needs: recursive stratified Datalog with field-navigation joins; aggregates in derive bodies incl. aggregate-comprehension where with rule-atom/predicate filters; stratified NAF; function application + projected-field terms as rule-atom arguments; an Allen-interval + date-arithmetic builtin; forall; denial constraints. It does not today exercise WFS-over-cycles, standpoints, or modal operators, and it models legal exceptions monotonically (conditional propositional content), not via rule defeat. But defeasibility and metric-temporal reasoning are confirmed near-term needs — design for them now, build them next.

Prior research: input, re-derived (not authority)

Per AGENTS.md the vault is research, not authority — the decision below stands on this RFD’s own rationale; the prior record is reconciled and re-derived, not deferred to. The vault ars-substrate program produced a decision record (R-3.1–R-3.8): a two-strategy architecture — DBSP for the bottom-up recursive tier (R-3.3, Feldera dbsp / differential-dataflow), SLG (chalk-engine-shaped) for top-down WFS/upper tiers (R-3.1), bilattice answers via the AFT pair construction (R-3.2), Salsa above both (R-3.4), provenance as a side-track (R-3.6). Four validation passes against that record surfaced the decisive fact:

The single most load-bearing unproven claim — recursive WFS riding DBSP via coupled alternating-fixpoint circuits, which the research itself says has no published precedent — is co-extensive with the WFS-over-cycles feature we are deferring. Everything our scoped target needs (positive recursion, stratified NAF, stratified aggregates, defeasibility’s Maher three-stratum compilation, stratified-NAF DatalogMTL) sits in DBSP’s proven, standard fragment.

So “build it correctly once” is more achievable than RP-009 implies: the scary part is the part we defer, and the AFT pair-encoding keeps the door open to add it (via SLG) without a data-model rewrite.


Decision

A new module realizing RFD 0003’s DBSPExecutor, plus the storage/invalidation/provenance substrate around it. Twelve decisions:

D1 — Value model: AFT pair-encoded Z-sets (the keystone)

Each Truth4 atom is a pair of ℤ-weighted Z-sets: tt (told-true / evidence-for support) and tf (told-false / evidence-against support), in the Belnap evidence-pair encoding (is = (1,0), not = (0,1), CAN/U = (0,0), BOTH = (1,1); a coordinate is present iff its support weight is non-zero). Each stream is a standard Z-set over an abelian group, so DBSP’s incremental machinery (chain rule, distinct, recursion via δ₀/∫) applies per-stream unmodified. Bilattice operations are per-coordinate: info-meet ⊗ = pointwise AND/min; info-join ⊕ (federation → BOTH) = pointwise OR/max; truth meet/join = the Belnap 4×4 table per atom; negation swaps the two streams (the sole cross-stream coupling). This gives, for free: K3 per-standpoint (⊕ statically unreachable within a standpoint; a clash is a diagnostic), FDE BOTH only at the federation boundary — matching the mechanized K3-per-standpoint / FDE-at-federation split (Foundation/Bilattice.lean, Standpoint/Federation.lean; per-standpoint state in Reasoning/State.lean) exactly. Cost ≈ 2× monotone Datalog (3× with symmetric T/F provenance).

Lean gate (D1), discharged. The value-level correspondence is mechanized in Standpoint/PairEncoding.lean (zero sorry/sorryAx; audited): encode/readoff form a bijection, and neg = stream-swap, = pointwise-OR, = pointwise-AND, plus federate evaluated in the pair representation reads back to the canonical Truth4 fold (readoff_pfederate). Mechanizing this corrected the encoding: the operations above (swap / pointwise OR-AND) hold under the evidence-for/against labels here, not the Fitting consistent-pair labels (T = (1,1), …) an earlier draft printed — under which neg is not a pure swap and is not pointwise-max. (This is the for/against form the Phase-0 spike already used.) The ℤ-multiplicity→support bridge for the assert-only fragment, and the retraction case, are the separate D5 obligation.

This is the keystone because it is also the shared substrate for the deferred SLG kernel (R-3.1): SLG’s answer-subsumption over a 2-D lattice is literally this pair. Adding WFS later is a new fixpoint strategy over the same state — not a rewrite.

Status (2026-07-03): unbuilt in Rust — the keystone is still open. The shipped substrate is a 2-valued signed-multiplicity Z-set (maintainer/mod.rs), and WFS is realized as a hardcoded batch strategy (executor/wfs.rs rewrites in-SCC not R against a frozen shadow EDB and reuses semi-naive), not a selectable fixpoint over a pair-encoded state. The Lean value-level correspondence (the blockquote above) is the only discharged half. The #640 AFT Phase-0/1 track is lifting the pair-encoding into Rust — assert-only first (D1), differential-oracle-gated tuple-for-tuple on both the true and $undefined:: fragments; the K3-vs-native-Both invariant (a Both leaking into the reasoner corrupts the fixpoint — spec/lean/Argon/Reasoning/AGENTS.md) is held as a loud I ⊆ J check pending its Lean non-corruption obligation.

D2 — Three time axes, kept strictly separate

  • Transaction/circuit time = the IVM stream index (each kernel commit is a Z-set delta: +1 assert / −1 retract, paired by axiom_id).
  • Valid time (VT) = a payload column on the Z-set key, never the circuit clock. Derived VT = intersection of body-atom VTs; derived TT = materialization tx-time.
  • Transaction time (TT) = the durable audit axis (tx_from/tx_to) for “AS OF” reads.

The tuple representation is interval-aware from day one even though metric-temporal ships later (D11), because retrofitting intervals is a rewrite. Metric-temporal reasoning is a separate DRedMTL maintainer over interval-set deltas (no engine implements interval-Z-sets), not a modified DBSP circuit.

D3 — Engine: fork Feldera dbsp (own it; do not wrap, do not fork differential-dataflow)

Revised by RFD 0021 D6. The in-memory reasoner adopts the DBSP model (every operator a pure Z-set→Z-set function, so incrementality is an additive outer loop) but does not fork Feldera’s dbsp crate: its flat binary-join Z-sets are the wrong substrate for the WCOJ / factorization / BYODS representations 0021 builds on. The Phase-0 spike below still stands as evidence that the model decouples latency from DB size; what changed is the substrate it runs on.

We fork Feldera dbsp (Apache-2.0) — the Lean-verified Z-set algebra itself — and own it under oxc-reasoning::executor::dbsp. We do not fork differential-dataflow/timely: DD’s value is its distributed/multi-worker machinery, which we would immediately strip on a single-node per-tenant engine. (This refines RFD 0003’s DBSPExecutor row, which said “differential dataflow / timely” — that was the stale default.) Strip: timely exchange/workers/progress-tracking, distributed coordination, any SQL frontend. Keep: the sorted Z-set traces/arrangements and the operator set (map, filter, join, antijoin = stratified NAF via distinct(I₁ − I₁⋈I₂), distinct, consolidate, iterate). Build on top: the D1 pair-encoding, the D2 interval payload, the D5 provenance side-track.

From kora-reason-rl (MIT/Apache, Argon-family — kora-reason-rl is itself “forked from the Orca kernel”) we borrow (lift into our tree, BTreeMap-convert, attribute), not depend on: its bit-parallel Warshall transitive-closure fast path (tc.rs/bitmatrix.rs) as a tier-dispatched operator; its forward/recursive derivation-tree provenance; its stratification (as a cross-check of ours); and its DatalogMTL→time-guarded-Datalog compilation approach (for D11). We skip its incremental.rs (DRed — over-delete+rederive, strictly inferior to DBSP exact deltas, falls back to full re-materialize on additions) and its hand-rolled datafrog_eval.rs (except as a differential-test oracle). This realizes RFD 0003’s “vendor Kora, own it” decision, scoped to the genuinely-reusable pieces.

D4 — Storage: CQRS — event-log write model + persisted, graph-optimized read model

Adopt the event-sourcing / CQRS split explicitly:

  • Write model = the append-only event log (axiom_events) — the single source of truth (audit, time-travel, bitemporal). It is the Z-set delta stream the engine consumes. Append scales; scanning to re-materialize is what dies — and that is exactly what D6/D7 eliminate.
  • Read model = a persisted, indexed, graph-optimized materialized representation — the durable form of the DBSP arrangements (D7). In Phase 1 it is in-memory; Phase 4 persists it so cold-start does not replay the whole log. Its layout borrows graph-database storage techniques (CSR / index-free adjacency, native edge stores) for traversal-heavy relations — Argon’s data is a graph (individuals + relation-edges).

This commits the scaling contract: no O(database) operation on any hot path, at any scale — mutation→query is O(Δ) (D6); cold-start is O(read the persisted read model), not O(replay log) (Phase 4). It aligns with the spec §20 reserved CQRS catalog tables and the StorageBackend “per-tenant LSM, XTDB-inspired, recency-sharded” slot. The StorageBackend trait is a clean seam now (Phase 1) so the persisted read model slots in without a reasoner rewrite, even though its implementation is Phase 4.

D5 — Provenance: dual-track (mandatory)

Provenance cannot ride the IVM Z-set stream (PosBool is an idempotent semiring with no additive inverse — Amsterdamer 2011 — so it breaks DBSP’s chain rule; answer-subsumption and variant dedup also erase per-derivation identity). So: the Z-set stream carries counting-multiplicity (ℕ) for IVM; why-provenance lives in the separate append-only derivation log (the existing D-097 PosBool-DNF column on axiom_events), joined by the erasing homomorphism ℕ[X] → PosBool[X]. Cheap (O(1)/conjunct: append-on-assert, remove-by-witness-on-retract, empty-DNF ⇒ retracted); bilattice = per-coordinate (separate T-witness and F-witness DNFs); each conjunct carries its own VT interval. Commit dual-track from day one — it is nearly free given the event log exists, but retrofitting onto a single-track answer table is expensive.

D6 — Incrementality + invalidation: Salsa above, generation-driven, one coherent path

Salsa sits above the engine (session granularity, per-(tenant, fork); the engine owns its internal incrementality). Build the §19.6 runtime OxcRuntimeDatabase with EventLogInput + the u64 generation counter as Salsa inputs, route query_derive through it, and unify it with oxc-serve’s hand-rolled runtime_cache so there is exactly one invalidation path: event-log delta → DBSP circuit (warm arrangements) → Salsa session cache, with the generation bump (already atomic with appends in oxc-storage-pg) as the single invalidation signal. “Warm derived state across queries” (Phase 1) is the prerequisite milestone; “warm across restarts” (Phase 4, via D4’s persisted read model) follows.

D7 — Tuple/index layout: typed, sorted arrangements on InternalId

Replace BTreeMap<CBOR-Vec<u8>, weight> + decode-per-tuple with typed, decode-once, partial-key-indexable arrangements (Materialize-style immutable sorted batches + trace) keyed on the existing InternalId (8-byte NonZeroU64, [kind:8 | partition:16 | sequence:40], range-scannable). All layouts are sorted (B-tree / arrangements / interval-trees), honoring the determinism-by-observability rule (RP-006) and the BTreeMap-not-HashMap convention. Automatic index selection (Subotić-style bipartite matching, ~500 LoC) is a compile-time pass portable from the Souffle literature (“steal the ideas, don’t port the C++”).

D8 — Conformance harness: Lean-first + dual-oracle differential testing

The correctness target is the strict-stratified perfect model. The DBSP engine is differential-tested against (a) the retained semi-naive evaluator as an independent oracle and (b) the Lean semantics on generated stratified programs, in addition to keeping the ~600 workspace tests + keystone.rs green throughout. Implement the proven modal discharge (box(anti_rigid) → false) statically; refuse the rest.

As shipped (2026-07-03). No second engine materialized (D6 of RFD 0021 kept ONE evaluator), so “dual-oracle” resolved to a different, stronger stack: FullRecomputeMaintainer (from-scratch Engine::evaluate as the standing IVM equality oracle, maintainer/mod.rs), the Lean evalProgram executed oracle (conformance_replay/conformance_fuzz diff the engine against lake exe eval-batch directly — no Rust port), and the RFD-0055 conformance corpus (hand-pinned goldens through the real ox CLI, covering the front half no self-differential can reach). Floor-scope correction: until 2026-07-03 the path-independent oracles pinned only the definitely-true fragment — the corpus goldens’ not derivable conflates FALSE with UNDEFINED, and the Lean oracle emitted only wfTrue — so a false↔undefined misclassification was visible only to the path-dependent maintainer diff. Corrected by vector schema v3: the Lean oracle now also emits the undefined fragment (wfPossible \ wfTrue) and the harnesses diff the engine’s $undefined::R companions against it.

D9 — Semi-naive is retained as oracle + cold/one-shot fallback (not retired)

DBSP subsumes semi-naive (its recursive operator is semi-naive internally; a circuit fed the database as one delta-from-empty computes the same bottom-up fixpoint), so the algorithm is never lost. The standalone semi-naive evaluator is kept as: (a) the independent differential oracle (D8) — validating the new engine against an independent implementation is the correct practice; and (b) a registered cold/one-shot fallback executor in the RFD-0003 dispatch (it wins only for query-once-tear-down workloads where arrangement maintenance is pure overhead). It is not grown — all expressivity (D11) lands once, in DBSP. One rule-IR + one Z-set semantics + one RelationCatalog exchange, two backends — not “build twice.” This realizes RFD 0003’s “DBSP preferred; SemiNaive stays as fallback + reference implementation.”

D10 — Executor unification

Collapse the live-eval / TierExecutor-stub split: DBSPExecutor becomes the real recursive-tier path through the RFD-0003 dispatch (registered first, ahead of SemiNaiveExecutor); fold compile/stratify’s stratification into the PhysicalPlan strata so the physical plan is the real execution unit; either move evaluate_to_fixpoint’s body into SemiNaiveExecutor::execute or delete the trait method’s stub. Fix the stale eval.rs “naive” header and replace the literal 1000 caps with a configured convergence/divergence policy (divergence stays a first-class, explained Err).

D11 — Scope cut (RP-009 §4)

Legend: BUILD-NEXT = ships on the stratified bottom-up core in this effort’s own later phases (no new kernel); BUILD-SOON = the next major arc after this effort (the SLG kernel), reached through the RFD-0003 dispatch seam; DEFER = not foreclosed, no current design work.

  • IN (this effort): recursive stratified Datalog (joins, field-navigation, collection iterators, comparisons); full in-body aggregates incl. aggregate-comprehension where with rule-atom/predicate filters; stratified NAF; function application + projected-field terms as rule-atom arguments; Allen-interval + date-arithmetic builtins; forall; denial constraints (assert ⇒ error); occurrence-typing type-tests; modal discharge soundness; indexing; incrementality; durable+warm derived state; observability; the scaling contract to 10M facts.

  • DESIGN-FOR-NOW, BUILD-NEXT (ride the stratified bottom-up core; no SLG kernel): defeasibility (Maher three-stratum at the closure tier; proof tags +Δ/−Δ/+∂/−∂ as derived-predicate labellings; defeater chains in the provenance side-track); metric-temporal (DatalogMTL compiled to time-guarded interval-Datalog; a separate DRedMTL maintainer over interval deltas).

  • DESIGN-FOR-NOW, BUILD-SOON (the next major arc, via the RFD-0003 seam): WFS-over-cycles — the SLG kernel (R-3.1). OE1309 stays reject-with-a-good-error now, architected as the future dispatch-to-SLG trigger. Coupling: ambiguity-propagating defeasibility = WFS-of-translation (Maier–Nute; +∂‖ = well-founded-true), so the SLG arc serves both WFS and the richer defeasibility.

    Superseded by events (2026-07-03): WFS-over-cycles shipped without the SLG kernel — executor/wfs.rs runs the Van Gelder alternating fixpoint (T = lfp(S_P∘S_P)) as a batch atop the semi-naive evaluator, materializing the definitely-true extent into R and the undefined extent into the $undefined::R companion. A NAF-cyclic SCC therefore evaluates (the old OE1309 acceptance behavior for that shape is gone); the honest boundary refusals are OE1365 (a higher-stratum not R(..) over a WFS relation would read undefined-as-false — refused pending three-valued propagation into derive heads, i.e. the D1 pair-encoding arc) and OE1317 (recursion through an aggregate). The SLG-tabled kernel remains reserved for the Expressive tier (classifier/mod.rs); it is no longer the WFS delivery vehicle.

  • DEFER, don’t foreclose: full Kripke modal; FOL/SMT (unsafe logic); KoraExtensionExecutor (DL via std::owl); standpoint-translation hardening; per-tenant eviction (a 10⁹ / ~2 TB-provenance concern); #[brave] stable models.

D12 — Phased plan, with a gating spike (no phase rebuilds a prior phase)

  • Phase 0 — de-risking spike (GATE before committing the fork). On a forked/wrapped dbsp: a bilattice-pair-encoded recursive rule (the overlay’s Met-style rule, or reachability) with stratified NAF, over bitemporal-interval-keyed tuples. Acceptance: a post-mutation query does not re-materialize — incremental mutation→query latency decouples from total DB size (latency ∝ |Δ|, not |DB|); cross-stream pair-encoding throughput is acceptable on a real rule mix. If the public dbsp API proves too lossy for the pair-encoding or the interval payload, that is the signal to fork internals (which we do anyway).
    • RESULT — PASSED (2026-06-05). Ran on stock dbsp 0.277 (rustc-1.92-compatible), release, 1 worker. (A) recursive transitive closure swept 1K→1M components (10K→10M derived facts): full re-eval grew ≈linearly (9.4 ms→5.78 s) while the steady-state incremental step stayed flat at ~400–850 µs — at 10M facts a mutation propagates in <1 ms vs 5.78 s full (~6,800×), ratio growing with DB size. The decoupling holds. (C) a valid-time [lo,hi) interval carried as a payload column (interval-intersection join) preserved the same flat incremental step (~390–863 µs) — VT rides as data, not the clock (§D2 validated; MTL can compile to interval-Datalog on this base). (B) the AFT pair-encoding (two ℤ-streams) + stratified NAF (antijoin) computed correctly at 1M (active = 500K, T/F disjoint within source, BOTH only at federation) with pair-encoding overhead 1.46× — under the ~2× the construction predicts (§D1 validated). Three Path-A frictions surfaced and reinforce D3 (fork & own): (i) upstream MSRV churn (latest dbsp needs rustc 1.93; we used 0.277); (ii) relations >65,535 records silently read empty without a storage backend configured; (iii) .output() (delta mailbox) reads empty at scale — must use .accumulate_output() (materialized snapshot). A vendored fork eliminates all three by giving us the spine/storage/read-model directly. (Harness + full results live at .local/spikes/dbsp-phase0/local-only / gitignored: a throwaway Path-A measurement harness, not shipped and not independently checkable from this tree. The tables and method above are the auditable record; reproduction is three cargo run --release -- {A,C,B} … lines against dbsp = "=0.277.0".)
  • Phase 1 — the build-once engine core. Forked dbsp IVM + D1 pair-encoding + D7 arrangements (on InternalId) + D10 executor unification + D5 dual-track provenance carriers + D6 runtime-Salsa/generation wiring (unified with serve’s cache) + the D4 StorageBackend seam. Conformance: D8 differential testing. Lean gate (Lean-first): the D5 retraction homomorphism (commutes-with-deletion) and the D1 pair-encoding↔bilattice correspondence are mechanized before the incremental-retract and cross-stream-negation paths they underwrite are trusted (see Consequences/Lean). Bench: mutation→query vs DB size (the headline).
  • Phase 2 — expressivity on the real engine (unblocks the overlay end-to-end; closes RP-008’s operational-evaluator debt as a by-product): aggregate-comprehension rule-atom/predicate filters, function application in rule bodies, projected-field rule-atom args, Allen/date builtins, modal discharge soundness.
  • Phase 3 — design-for features: defeasibility (proof-tag propagation) and metric-temporal (interval-Datalog + DRedMTL), each Lean-first + reject/accept tests.
  • Phase 4 — scale + ops: persisted graph read model (D4) + restart-warmth (checkpoint vs snapshot-replay), observability (structured logging/metrics/slow-query), resource limits, concurrency model, the benchmark suite to the 10M-fact target, IAM in oxc-serve. (Per-tenant eviction enters here only if the scale target rises.)

Each phase is a CI-gated PR sequence, complete + tested + benchmarked — no hollow features; a thing is done only when it runs and is proven.


Rationale

Why DBSP at all (vs indexed-semi-naive-with-caching). The product thesis is “the data system IS the reasoner”: a mutation must make a subsequent query cheap. That is incremental view maintenance, and DBSP is the provably-incremental, Lean-verified Z-set IVM substrate the codebase already committed to (runtime/relation.rs is “the day-one commitment to DBSP-shaped data”; arrangement_body is reserved). Indexed-semi-naive-with-caching gets indexing but not principled incrementality; it would be a stepping-stone we’d replace — i.e., build twice.

Why fork dbsp, not differential-dataflow, and not wrap. Single-node per-tenant means DD’s distributed/timely machinery is surface we’d strip, not value we’d keep; dbsp is the algebra itself with no distribution layer and a verified core (matching the Lean-first bar). Owning the fork (vs wrapping) is required to thread the pair-encoding, interval payloads, and provenance through the operators — and to honor the house constraints by fencing unsafe/HashMap behind a deny.toml-reviewed boundary rather than inheriting them opaquely.

Why the scope is safe to build correctly once. The only unproven piece in the ARS record (recursive-WFS-on-DBSP) is exactly the WFS feature we defer to the SLG kernel; everything in D11-IN is DBSP’s proven fragment. The AFT pair-encoding (D1) is what makes the deferral non-binding: WFS/SLG and the richer defeasibility slot in over the same state, via the same RFD-0003 dispatch seam, with no data-model rewrite. So designing-for-soon costs us a clean trait seam (D6/D10) and a value model (D1) we want regardless — not speculative engine work.

Why CQRS / dual representation. A single append-only log is a fine write model but a terrible read path at scale (re-materialization is O(DB)). Separating a persisted, indexed, graph-optimized read model (the durable DBSP arrangements) from the event-log write model is the standard event-sourcing answer, it is what the spec §20 already reserves, and it is what makes the no-O(DB)-on-any-hot-path contract achievable at all scales.

Why dual-track provenance, from day one. It is forced by three independent facts (group-vs-semiring impossibility, answer subsumption, variant dedup); it is nearly free given the event log already carries the PosBool-DNF column; and retrofitting it onto a single-track answer table later would be a rewrite of the hot path.

Why keep semi-naive. Validating the new engine against an independent implementation — not against itself — is the correct way to earn “completely correct.” Retiring the reference implementation to save tidiness is the wrong trade for a foundation. DBSP subsumes it, so this costs nothing in duplicated expressivity.


Alternatives considered

  • Indexed semi-naive + cross-query caching (no IVM). Smaller, stays in the BTreeMap world, gets indexing — but delivers caching, not principled incrementality, and would be replaced by DBSP. Rejected as a stepping-stone we’d build twice. (Its good ideas — arrangements, auto index selection — are absorbed by D7.)
  • Fork differential-dataflow (the ARS-record default Path B). Rejected: its distinguishing value is distributed/timely scale-out we don’t need; we’d fork-then-amputate. dbsp is the cleaner thing to own single-node.
  • Wrap Feldera dbsp as a black-box dependency (ARS Path A). Rejected as the product path (kept only as a throwaway Phase-0 measurement harness): wrapping can’t thread the pair-encoding/interval/provenance through operator internals, and drags in unsafe/HashMap behind an API we don’t control. “Build correctly once” + the fork-and-strip preference favor owning it.
  • DRedc as the shipping recursive substrate, DBSP as a v0.3 migration (ARS W5.S2/D-103). This existed as the safe fallback precisely because recursive-WFS-on-DBSP was unproven. Since we stay strict-stratified (where DBSP is proven), DRedc would be a stepping-stone we’d replace — rejected. (This reconciles the R-3.3-vs-D-103 internal inconsistency in the ARS record for our scope.)
  • Single-track provenance (on the answer row / on-stream). Structurally impossible under the bilattice + group structure (D5 rationale). Rejected.
  • Build recursive-WFS-on-DBSP now (the novel coupled-fixpoint construction). Unproven, no published precedent; the ARS record itself names it the gating risk. Rejected for this effort — WFS goes to the SLG kernel via the seam.
  • Event-log only (no persisted read model). The status quo; O(DB) re-materialization; dies at scale. Rejected (D4).
  • Retire semi-naive. Loses the independent oracle and the cold/one-shot fallback. Rejected (D9).

Consequences

  • Code structure. New oxc-reasoning::executor::dbsp (the forked, owned engine) registered first in the RFD-0003 dispatch; SemiNaiveExecutor implemented for real and retained as oracle/fallback; the orphaned logical/physical/optimizer scaffolding either wired into the real path or removed; oxc-runtime gains a salsa dependency and the OxcRuntimeDatabase; a StorageBackend trait seam for the future persisted read model; tc.rs/provenance/MTL-compile borrowed from kora-reason-rl under attribution.
  • Dependencies. The vendored dbsp fork enters via compiler/deny.toml allow-list + an explicit constraint review; unsafe is minimized and fenced (our new code stays #![forbid(unsafe_code)]); HashMap is permitted only in the vendored fork where it cannot affect observable order, justified in this RFD. num-rational/num-bigint already present (RFD 0016).
  • Lean (Lean-first for new semantics; evaluation-strategy obligations trail). AGENTS.md puts substrate semantics in the Lean-first lane, so the new obligations split by kind:
    • Gates the code it underwrites (Lean-first — these are new semantics not covered by the forward perfect-model theorems): (a) the ℕ[X]→PosBool[X] homomorphism commuting with deletion under DBSP’s chain rule (D5) — incremental retract is genuinely new semantics (stratified_fixpoint_unique is the forward model only), so it is mechanized before Phase 1’s incremental-retract path is trusted; (b) the D1 pair-encoding ↔ mechanized bilattice correspondence (representation adequacy for cross-stream negation = stream-swap, and ⊕-only-at-federation = Foundation/Bilattice.lean/Standpoint/Federation.lean) — mechanized before Phase 1 relies on the cross-stream coupling.
    • Trails implementation (evaluation strategy — reduces to the already-proven perfect model, sound for any converging strategy by stratified_fixpoint_unique): the operational RuleIR→extent evaluator (closes RP-008).
    • All anchored on porting Bogaerts–Cruz-Filipe 2024 (AFT-in-Coq) to Lean 4.
  • Conformance. The ~600 tests + keystone.rs stay green throughout; new differential + per-feature tests per phase.
  • Workflow. Branch off origin/main (this RFD is on rfd/0018-production-reasoner); per-PR CI-gated; commit at checkpoints; no “done” without a running/benchmarked proof.
  • Spec. Resolves RFD 0003’s deferred retraction/IVM open question; refines its DBSPExecutor row (Feldera dbsp fork, not DD/timely). The reference (spec/reference/src/{17,19,20}) follows once the design lands.
  • Relationship to RFD 0017 (refinement classification). RFD 0017’s two refinement kinds map cleanly onto this engine, with no scope expansion: a defined (iff) refinement is a derived-membership ruleiof(x, C) :- iof(x, parentᵢ), P(x) — i.e. an incrementally-maintained view (a mutation touching P(x) updates C-membership as a Z-set delta; this is the IVM win applied to classification), and is already covered by D11-IN’s recursive stratified Datalog. A primitive (where) refinement is a write-time invariant (OE0668 RefinementInvariantViolated) — a constraint check at the mutation boundary (Cat3-shaped), not a derived relation. RFD 0017 explicitly defers the iff realization theorem (iof(x,C) ↔ iof(x,parent) ∧ P(x)) to RP-007-adjacent work; this engine is its operational home, and that theorem is part of the D8 / RP-008 Lean conformance debt.

Open questions / tracked-future

  • The Phase-0 spike — PASSED (2026-06-05). Decoupling (∝|Δ| not |DB|) confirmed to 10M facts; AFT pair-encoding + stratified NAF correct at 1M with 1.46× overhead; VT-as-payload preserves incrementality. The three Path-A frictions found (MSRV churn; >65k-record relations need a storage backend; .output() reads empty at scale, must use .accumulate_output()) all reinforce D3 — fork & own. See the Phase-0 RESULT block in §Decision/D12.
  • AS OF tx-time = X × Salsa backdating — prototype with the Phase-1 Salsa layer; fallback = disable backdating for AS OF queries (ARS R-W5S2-04).
  • ℕ[X]→PosBool[X] commutes with deletion under DBSP’s chain rule (ARS R-W5.S3-4) — new semantics; gates Phase 1’s incremental-retract code (Lean-first; see Consequences/Lean). Not “tracked-future” — a Phase-1 prerequisite, listed here for visibility.
  • Persisted-read-model layout — a research item: graph-DB storage internals (CSR / index-free adjacency, native edge stores, triple-store indexing) → the Phase-4 read-model design.
  • PG schema canonicalization — spec §20.3 (TSTZRANGE+GIST, generated columns, JSONB derivation) vs the shipped migrations (INT8 ns columns, projection-index tables, TEXT derivation). Decide canonical before Phase 4 scaling.
  • The SLG kernel arc (WFS + ambiguity-propagating defeasibility) — the next major effort after Phase 2; its own RFD, building on D1’s pair-encoding and the D10 dispatch seam.
  • Per-tenant eviction — tracked, not built; enters when a tenant exceeds ~100 GB (provenance ≈ 2 TB/tenant at 10⁹ is the binding constraint); recommended composition = snapshot-at-τ₀ + continuous provenance compaction + tenant archival.
  • Cross-stream operator throughput for the pair-encoding — RESOLVED by the Phase-0 spike: measured at 1.46× at 1M facts, under the ~2× the construction predicts.

RFD 0019 — Mutation write-path correctness: construction, identity, read-your-writes, and exact values

  • State: accepted — partially implemented (RC1/RC2/RC4 landed; required-fields and within-body read-your-writes remain)
  • Opened: 2026-06-06
  • Decides: the surface and runtime semantics that take Argon’s write path — insert/construct → mint identity → persist fields and relations as ABox events → read them back within the same body and on reload — from silently incorrect to correct, durable, and Lean-conformant. Records the decisions taken on the design questions in RP-010 §6/§11. Refines RFD 0015 (the mutate body surface — insert disambiguation), and connects to RFD 0001 (identity), RFD 0006 (IndividualPropertyAssertion), RFD 0007 (required/missing fields under OWA), and RFD 0016 (exact Real). Its read/execution-path counterpart is RFD 0018 (RP-009); this RFD owns the write side and coordinates at the seams (§Consequences).

This RFD is the architecture decision record for the mutation write path. It is Lean-first where it touches semantics — the engine conforms to spec/lean/Argon/Runtime/MutationSemantics.lean (+ MutationFreshness.lean, AggregateExact.lean); divergence is a bug. It commits a plan and a set of surface-semantics decisions, not code. The full verified evidence lives in RP-010; this RFD states the decisions and why.


Question

Argon can parse and type a rich ontology and can reason over facts at small scale, but the write path is silently incorrect: real ontologies produce no usable instance data. Concretely (verified against origin/main @ 967cc03; see RP-010 §1 for the file:line evidence):

  • RC1 — positional insert Concept(...) does not construct. The parser dispatches purely syntactically (L_PAREN ⇒ relation-tuple op, L_BRACE ⇒ struct/construct op), never consulting whether the head resolves to a concept or a relation. So insert Person(name) lowers to a relation-tuple assertion that mints no individual, persists no fields, and lands under NameRef::DEFAULT (a garbage relation id). In let position it is totally silent: lower_let_stmt (mutate_lower.rs:271) only binds an INSERT_STRUCT_OP, so let p = insert Person(name) emits zero ops and binds nothing — and a later unbound p then collapses to a blake3("individual:{name}") content-hash id (oxc-runtime/src/lib.rs:3874), identical across runs. The brace form insert Type { f = v } is correct.
  • RC2 — no read-your-writes; navigation fields invisible. A Term::Proj reader in a mutate body reads committed (pre-mutation) state, not this body’s own buffered writes, so coll = coll + [x] clobbers. And from <rel>.endpoint navigation-view fields — which do resolve in the derive/query read path — are invisible inside mutate bodies.
  • RC4 — exact Real mishandled. The rule-body aggregate fold is exact BigRational (RFD 0016), but the mutate/term value path is f64-based: as_f64 has no Value::Real arm, so mutate-expression arithmetic/aggregates error on exact-Real operands.
  • §1.5 — required fields unchecked. No construction-time field-coverage check exists; insert Type { … } with missing required (incl. inherited) fields silently creates an incomplete individual. The relevant diagnostics (OE1908, OE1014) are defined-but-never-emitted.

What surface and runtime semantics make this path correct — and which of the “silent-wrong” behaviors become loud?

Context

The substrate is ahead of the implementation here. The Lean mechanizes a denotational mutate interpreter (Runtime/MutationSemantics.lean, 734 lines): a typed-literal insert mints a fresh IndividualId and emits an iof assertion plus per-field IndividualPropertyAssertions; a relation insert emits a RelationTuple; atomicity is structural (a failing run is Except.error, which carries no effects). MutationFreshness.lean proves mutate_run_fresh_ge (every minted id exceeds every committed one). AggregateExact.lean proves the folds are exact over Rat. So the correct behavior is mechanized; the Rust write path simply does not match it. This RFD’s job is to (a) settle the few surface-semantics calls the Lean does not pin down, and (b) commit the build order that makes the Rust conform.

The applied pressure is concrete: the residential-lease + accounting overlay (sharpe-ontology, ~5.2k LOC of .ar) cannot create a single faithful instance graph today — its entire Create* surface uses positional concept-insert, which silently persists nothing. This RFD is the prerequisite for the overlay (and therefore the production reasoner, RFD 0018) having real data to operate on.

Decision

1. Construction is brace-only; positional concept-insert is a hard error (RC1)

There is exactly one construct syntax: insert C { field = value, … }. The two surface forms are disjoint by bracket, and the bracket is the semantics:

  • insert C { … }construct. Mints a fresh IndividualId, asserts iof(id, C), and persists every supplied field (scalar, list, nested-individual, relation-typed, inherited) as ABox events. Value-producing: let p = insert C { … } binds p to the new individual.
  • insert R(args)relation-tuple assertion, only when R resolves to a relation. Emits a RelationTuple under R’s id.
  • insert C(args) where C resolves to a concepthard error (a new diagnostic, tentatively OE0212, finalized in grammar.toml at implementation): “positional insert of a concept constructs nothing; use the brace form insert C { … }.” The elaborator already has the module’s concept/relation index; it resolves the head and rejects rather than emitting a garbage tuple.

This kills the silent misroute, keeps the surface unambiguous (parens = relation, braces = construct), and avoids overloading parens to mean “construct sometimes.” The cost is an overlay migration (positional concept-inserts → brace form), which is mechanical and one-time.

The unbound-Var blake3 fallback (resolve_term_to_value:3874) is deleted: an unresolved Term::Var in a value position is a hard error, never a silently-minted content-hash id.

2. Identity: fresh monotonic surrogate, confirmed; no content-hash minting (RC1, RFD 0001)

The AtomicU64 surrogate minter is correct and stays. Distinct constructions get distinct ids; replay correctness comes from the event log replaying already-minted ids (MutationFreshness.mutate_run_fresh_ge is the backbone). A content/key-addressed “explicit identity key” (upsert/merge-on-key) is a future feature, explicitly out of scope for v1; if/when added it is opt-in, never the default. The only id-minting path is the surrogate counter.

3. Read-your-writes within the body (RC2)

A read of a field or collection inside a mutate body reflects this body’s own prior writes, not just committed state. So account.records = account.records + [r] accumulates correctly, and construct-then-read works. The value resolver consults the in-body buffered-write overlay (st.collections and the pending property/iof writes) before falling back to committed storage. Commit remains atomic at the body level (buffer → flush; a failing run flushes nothing — matching the Lean’s Except.error atomicity).

4. Navigation-view fields are computable in mutate bodies (RC2)

from <rel>.endpoint projections (and multi-hop chains like pair.book.account.records) resolve inside a mutate body exactly as they do in the derive/query read path (which already supports multi-hop projection — proven by keystone_met_integration). This removes the overlay’s parameter-passing workaround. The mutate evaluator materializes the navigation view on read (read-path parity), under the same world-assumption semantics.

5. One exact value model end to end (RC4, RFD 0016)

There is a single value model: exact BigRational for the exact tower (Real/Decimal/Money), with f64 reserved for explicitly-float types. Value::Real flows through the mutate/compute value path — as_f64/eval_binary and the mutate-path aggregate helpers (aggregate_sum/aggregate_extremum/aggregate_avg) gain a Value::Real arm and stay exact (no f64 promotion on the exact path). This is validated differentially against AggregateExact.lean. So require { value == sum(record.value …) } with value: Real evaluates exactly.

6. Required-field coverage is validated at build and at construction (§1.5, RFD 0007)

Construction with missing required (incl. inherited) fields is rejected with a real diagnostic:

  • Build-time (where statically knowable): a checker pass over insert C { … } against C’s field schema (walking <: for inherited required fields) emits a build diagnostic. The diagnostic-code reconciliation (OE0207 vs the existing OE1908/OE1014) is settled during implementation; the existing defined-but-unemitted codes are wired or replaced, not left dead.
  • Runtime: a construct-time guard catches the dynamically-unknowable cases at the mutate-rejection channel.

Required fields are CWA-at-construction even under concept-level OWA (an individual you are building now must satisfy its required structure), per RFD 0007’s intent distinctions; three-valued/OWA subtleties apply only where a field’s intent is epistemic/optional.

7. Loud failure is a deliverable (§1.7)

Every form that cannot execute correctly becomes a hard error or a build-time diagnostic, never a silent no-op or garbage write: positional concept-insert (§1), unbound-Var id (§2), construct with missing required fields (§6). Making these loud is part of the work, not a follow-on.

Rationale

  • Bracket-as-semantics is the least surprising surface. Once { } is the construct form (it already is, and it is correct), letting ( ) also construct — disambiguated only by a name lookup the reader must perform in their head — is the ambiguity that produced RC1 in the first place. Disjoint brackets mean a modeler (and the parser) can tell construct from relation-assertion locally, without resolving the head. The hard error on concept-(...) turns the one genuinely-ambiguous case into a teachable diagnostic.
  • The substrate already says so. The Lean InsertForm distinguishes typedLiteral (construct, mints identity) from relation (tuple, yields unit). Brace-only construction maps cleanly onto typedLiteral; positional-relation onto relation. The reject rule is the surface honoring a distinction the substrate already draws.
  • Read-your-writes is the transactional intuition every modeler brings; committed-only reads make in-body accumulation silently wrong, which is exactly the failure mode we are eliminating.
  • Exactness must be uniform or the numeric tower (RFD 0016) is a half-truth: a value that is exact in a rule body but lossy in a mutate expression is a latent correctness bug at the read/write seam.
  • Required-field enforcement at build is the earliest, loudest signal; the runtime guard covers what build cannot see. Silent incomplete individuals are the kind of garbage-in that defeats the reasoner downstream.

Alternatives

  • (RC1) Semantic disambiguation — positional insert Concept(...) constructs. The head resolves to a concept ⇒ Construct; to a relation ⇒ tuple. This was the initially-recommended option (more ergonomic; no overlay migration). Rejected in favor of brace-only: it overloads ( ) to mean two different things depending on a name lookup, keeps two construct syntaxes, and pushes head-resolution into a load-bearing position in the parser/elaborator. Brace-only is the more principled and locally-readable surface; the overlay migration is a bounded one-time cost.
  • (RC2) Committed-only reads. Simpler evaluator; rejected because it makes coll = coll + [x] silently truncate — a silent-wrong behavior this RFD exists to remove.
  • (§6) Runtime-only required-field check. Simpler, but defers feedback to execution; rejected in favor of build-time + runtime so statically-knowable omissions fail at ox build.
  • (RC2 nav fields) Reject navigation fields in mutate bodies. Keeps scope small but entrenches the overlay’s param-passing workaround and creates a read-path/write-path asymmetry; rejected in favor of parity.

Consequences

Phased implementation (each phase a complete, CI-gated PR, proven on a running example — no hollow features):

  1. RC1 — construct vs tuple + identity. Brace-only construction; positional concept-insert → hard error (OE0212); let x = insert C { … } binds; delete the unbound-Var hash fallback. Reconcile/extend MutationSemantics for the chosen surface (the reject is a surface rule; confirm the Lean models brace-construct + relation-tuple and add the positional-reject note). Proof: a keystone test constructs distinct individuals with all fields present and round-trips; the overlay’s Create* ops migrate to brace form and persist real data.
  2. RC2 — read-your-writes + navigation fields. Buffered writes feed in-body reads (scalar + collection); from/multi-hop projections compute in mutate bodies; resolve the update target: Type annotation requirement. Proof: update account set { records = account.records + [r] } accumulates across a for; the overlay’s Materialize* ops work without the direct-account workaround.
  3. RC4 — exact Real unification. Value::Real through as_f64/eval_binary/mutate aggregates. Proof: require { value == sum(record.value …) } with value: Real passes; differential test vs AggregateExact.lean.
  4. §6 — required-field validation. Build-time checker pass + runtime guard; diagnostic-code reconciliation. Proof: reject/accept tests; the overlay’s temporal entities enforce begin/end.
  5. Loudness sweep + reload fidelity. Remaining silent-wrong paths become diagnostics; round-trip/replay tests assert event-log fidelity for every emitted event kind.
  6. Overlay integration capstone. The lease Create*/Materialize*/Recognize* mutations build a faithful instance graph; Met/BreachedAt derives fire over real data (read side coordinated with RFD 0018).

Seams with RFD 0018 (RP-009). The two term evaluators (oxc-serve::eval_compute_term, oxc-runtime::resolve_term_to_value) are un-unified; RC4 and the reasoner’s compute work both touch them. Exact-Real plumbing touches oxc-reasoning::compile::Value, shared with the reasoner. The events this write path emits must be exactly what materialize_predicates reads. Ownership boundary on the shared evaluator is agreed with the RP-009 effort before refactoring shared code; the read/execution path (incrementality, indexed joins, function-application in derive bodies, tier coverage, modal soundness) is RFD 0018’s, not this RFD’s.

Drift. Any new/changed @[language_interface] shape (a CoreIR Operation, an event variant) updates the Lean inductive and the oxc-protocol mirror in lockstep (cargo xtask check-drift). The OE0212 reject is a diagnostic, not a wire shape — no drift impact.

Reference + Lean. The surface change (brace-only construct; positional-concept reject) lands in the reference (07-rules.md §7.5 + appendix-c-diagnostic-codes.md) and is reconciled with the Lean surface, per the surface-change workflow (RFD + reference → Lean → code).

Open questions

  1. update target: Type annotation. Today update target set { … } hard-errors; the annotation is required. Should it be inferred from the binder’s type (RC2 scope)? Leaning: infer where statically known, keep the annotation optional.
  2. Diagnostic-code reconciliation for required fields. OE0207 (spec-planned, nonexistent) vs OE1908 (IntrinsicPropertyMissing) vs OE1014 (RequiredFieldUnasserted) — which is the build-time code, which the runtime channel? Settled in the §6 phase against grammar.toml.
  3. property_id_for_field interning. The blake3("field:{Type}::{field}") field-key hash is a correct-but-stand-in for a real interned NameRef (RFD 0001). Promote to interned ids as part of the loudness/fidelity phase, or defer?
  4. Term-evaluator unification ownership. Who owns the unified term evaluator across the write path (this RFD) and the compute/read path (RFD 0018)? Agree the boundary before refactoring (§Consequences seam).

RFD 0020 — The runtime data engine: a composable query + reasoning pipeline

  • State: accepted — partially implemented (Phase 1 #98 landed; Phase 2 tracking issue #100 open)
  • Opened: 2026-06-06
  • Decides: the engine architecture of Argon’s runtime — the composable LogicalPlan → optimizer → PhysicalPlan → tiered execution pipeline that makes the runtime a full-blown, highly-optimized graph/knowledge database which (a) serves arbitrary ad-hoc queries and mutations (engine-configured), (b) maintains derived state incrementally as a reasoner, and (c) is the substrate the compiler/type-checker draws on — one composable IR, several physical backends, without becoming three incompatible engines or one monolith that compromises each role. This is the umbrella that frames RFD 0003 (the tier-dispatch seam), RFD 0018 (the recursive-tier read executor — the DBSP engine), and RFD 0019 (the write path), positioning each within the whole.

This RFD is the architecture decision record for the runtime engine. It is Lean-first where it touches reasoning semantics — the IR’s meaning conforms to spec/lean/Argon/Reasoning/ and the D1 pair-encoding correspondence (Standpoint/PairEncoding.lean); divergence there is a bug. But most of this RFD — the pipeline structure, physical operators, optimizer, storage layout — is engine architecture and ergonomics, which the Lean does not mechanize (per AGENTS.md scope) and which is settled from first principles here. It commits a plan, not code; orca-era decisions (D-NN) are cited only as corroborating prior experience, never as authority.


Question

Argon’s runtime is not “a reasoner with a storage backend.” It is a graph/knowledge database whose distinguishing feature is that the data system and the inference engine are the same system (oxc-reasoning/src/lib.rs:4-11: “Argon’s reasoner IS the data system… queries are sinks… the storage layer IS the reasoner’s state”). It must simultaneously be:

  1. A world-class graph database — accepting arbitrary ad-hoc queries and mutations (when the engine is configured to allow them), with traversals, pattern matching, aggregation, and recursion, optimized to compete with purpose-built graph stores at scale;
  2. An incremental reasoner — maintaining derived predicates (rules, bilattice/AFT, stratified NAF, aggregates; later WFS/defeasibility/MTL) over the same facts, incrementally on mutation (RFD 0018);
  3. The compiler/type-checker’s substrate — subtyping, refinement (where/iff), occurrence typing, and structural checks are queries over the type/ontology graph.

What is the engine architecture that serves all three without forcing the wrong shape on any of them? Concretely: what is the shared IR, what are the physical execution substrate(s), how do queries / rules / checker-goals / mutations relate, how is it optimized, how is data laid out at scale, and how do RFD 0003/0018/0019 compose inside it?


Context

Current state (verified against origin/main @ e7711a4)

  • The composable pipeline is designed, not wired. oxc-reasoning already declares the DataFusion-shaped stack: LogicalPlan (logical/mod.rs: Scan/Filter/Map/Join/AntiJoin/Distinct/Recurse/Sink), an OptimizerRule trait + an empty pipeline (optimizer/mod.rs), PhysicalPlan = Vec<Stratum> (physical/mod.rs), and an Engine dispatching Box<dyn TierExecutor> per stratum by tier (lib.rs:97-119). The module docs state the intent explicitly: “mirrors DataFusion’s ExecutionPlan pattern” and (compile/mod.rs) LogicalPlan is “the future surface for optimizer rules… when the optimizer materializes it will rewrite LogicalPlan → optimized → CompiledRule.”
  • The MVP took a shortcut around it. Today query_derive (oxc-runtime/src/lib.rs) goes AtomIR → CompiledRule → evaluate_to_fixpoint(&[CompiledRule], …) directly (6 call sites), bypassing LogicalPlan/optimizer/PhysicalPlan/TierExecutor. SemiNaiveExecutor::execute is an empty stub; the real (semi-naive) evaluator lives as free functions in executor/eval.rs. The whole RFD-0003 dispatch layer is currently orphaned — by staging, not by design error.
  • No ad-hoc query IR. Queries today are declared rules; there is no query expression/IR distinct from rules, and no general ad-hoc surface.
  • The checker does not yet use the reasoner. oxc-check is pure syntax-driven; the only shared artifact is the Tier enum. Role (3) is a goal, not wired fact.
  • Storage is naive. Relations are BTreeMap<CBOR-tuple, i64-weight> (runtime/relation.rs), decode-per-tuple in the join loop; no index-free adjacency / CSR / columnar / arrangements (the arrangement_body slot in .oxbin is reserved + inert).

What the roles demand (and what the references teach)

  • Graph-DB rolekuzu is the playbook: a strict bind → logical-plan → optimizer (visitor passes) → physical-mapper (1:N) → vectorized processor pipeline; factorization (flat vs unflat column groups → compact storage for many-to-many path patterns); multiway INTERSECT (worst-case-optimal joins) for cyclic patterns; CSR adjacency + columnar storage with semi-mask / predicate pushdown; clean extension hooks for pluggable operators. oxigraph adds index-permutation storage (SPO/POS/OSP), lazy iterator (volcano) evaluation, and the “a query is just a rule with a distinguished head” identity.
  • Reasoner rolekora teaches staged filtering (told-subsumers → EL-saturation → DL-saturation → tableau → FOL-escalation) and “absorption is a logical→physical lowering”; nous teaches the anti-patterns to avoid: rules-as-Rust-enums (rules must be data/IR), phases hardcoded into a loop (phases must be stratification metadata), and direct state mutation (rules must produce Z-set deltas, not mutate).
  • The physical insight — the vault’s Materialization Wall: for TC/1000, the bit-parallel computation is ~5 ms but converting the answer into joinable tuples is ~1100 ms (99.5%). The optimal compute representation (bitmatrix, CSR) differs from the optimal join representation (sorted tuples); converting between them dominates. The fix is BYODS — make the evaluator polymorphic over relation representations so the compute rep is the relation. This generalizes RFD 0018’s “arrangements (D7)” into a principle.
  • The IR principle — a reasoning logical IR is declarative, set-oriented, monotone, fixpoint-oriented (not SSA); FlowLog’s “explicit relational IR per rule, recursive control separated from the logical plan” is the right shape.
  • The optimizer trajectory — DataFusion’s own path: rule-based passes first; Cascades (memo + transformation/implementation rules + cost model; CMU’s optd is a Rust Cascades for DataFusion) when the search space justifies it.

Decision

A composable, multi-tier query+reasoning engine. Twelve decisions:

D1 — The runtime is a graph/knowledge database; reasoning is a capability within it

The product is a database: a durable, queryable, mutable store of individuals and relation-edges, with reasoning (derived predicates) as a first-class capability over the same data — not a bolt-on. Every other decision serves “world-class graph database that also reasons,” not “reasoner that also stores.” This reframes RFD 0018: the incremental reasoner is the engine’s view-maintenance subsystem, one tier among several.

D2 — One composable logical IR; three front-ends lower into it

LogicalPlan (a relational + graph + recursive algebra, D5) is the single shared surface. An ad-hoc query, a declared rule, and (at the boundary) a compiler/type-checker goal all lower to the same LogicalPlan: a rule adds a Recurse (fixpoint) node; a query adds a Sink/projection; a checker-goal is a bounded query over compile-time relations; a mutation is a write node (D11). This is why one engine can “serve all three” — they share the IR, like Substrait/DataFusion’s LogicalPlan is backend-agnostic. CompiledRule is reclassified as the physical lowering of a rule body (the mapper’s output), with LogicalPlan the optimizable form.

D3 — Two physical substrates, one IR (from first principles)

The IR is shared; the physical execution substrate is not one engine. Compile-time checking runs on Salsa-tracked memoized functions (demand-driven, per-definition invalidation, the rustc/rust-analyzer regime). Runtime queries/reasoning run on the DBSP/Z-set IVM engine (event-stream-driven, per-(tenant, fork) invalidation, RFD 0018). They meet at a boundary — compile artifacts feed the runtime; provenance composes — not in one executor. First-principles justification (not deference to orca’s D-09/D-10): the two halves have fundamentally different change regimes (source edits vs fact mutations), granularity (per-definition vs per-tuple), and lifetime (a build session vs a persistent multi-tenant store). A single physical substrate would force batch-IVM semantics onto fine-grained incremental type-checking, or Salsa’s recompute-on-demand onto a streaming fact firehose — compromising one half. The shared logical IR + tier ladder + provenance model is what unifies them; the physical backends are chosen per role.

D4 — The pipeline stages, strictly separated

front-end (parse → bind → type) → LogicalPlan → optimizer → physical mapper (1:N) → executor. Strict stage boundaries with typed IRs between them (kuzu’s discipline; and the vault’s canonical-pipeline-architecture post-mortem found that conflating analysis stages was the direct cause of a 4× diagnostic-count divergence — separation is a correctness property, not just hygiene). Each stage is independently testable; extension hooks (planner/mapper) allow pluggable operators and backends without editing the core.

D5 — The logical operator algebra

LogicalPlan extends the present relational core with graph-native and mutation nodes:

  • Relational: Scan, Filter, Map/Project, Join, AntiJoin (stratified NAF), Distinct, Aggregate, Union, Sink.
  • Graph-native: Extend (single-hop edge traversal), PathExtend (variable-length / recursive path), Intersect (multiway / worst-case-optimal join for cyclic patterns).
  • Recursive: Recurse (least-fixpoint — the rule-evaluation operator; “rules are the execution unit” lives here).
  • Mutation: Insert, Delete, Set, Merge (D11; the write path’s logical surface, coordinating with RFD 0019).

Graph-native nodes desugar to joins + Recurse for correctness, but the optimizer can lower them to native physical traversal operators (index-free adjacency, factorization) when the storage rep supports it — the kuzu performance win. The IR stays declarative / set-oriented / monotone / fixpoint-oriented.

D6 — BYODS: physical relations are polymorphic over representation

A physical Relation is an interface (membership, key-scan, range-scan, join-key iteration), not a fixed BTreeMap<CBOR, weight>. Representations coexist behind it: sorted arrangement on InternalId (RFD 0018 D7, the join workhorse), bitmatrix (transitive closure — kora-reason-rl’s bit-parallel Warshall), CSR index-free adjacency (graph traversal), factorized (D7), and virtual/lazy (generate tuples on demand). The optimizer/mapper picks the rep per relation; the engine never pays the Materialization Wall — the optimal compute rep is the relation, served directly to downstream operators. This subsumes and generalizes RFD 0018’s “arrangements.”

D7 — Factorization is in scope for v1

kuzu-style factorized query processing — flat vs unflat column groups, FLATTEN operators inserted by a rewriter pass, factorized intermediate results — is part of v1, not deferred. It is the difference between linear and Cartesian memory for the many-to-many graph patterns a knowledge graph is made of; deferring it would mean rebuilding the physical layer later. It is built early, alongside the Z-set/arrangement baseline, and reconciled with the Z-set model (a factorized Z-set is a compressed multiplicity-carrying batch).

D8 — The optimizer: rule-based visitor passes now, Cascades later

The optimizer is a chain of visitor-based rewrite passes (the OptimizerRule trait, made real): predicate/projection pushdown, join reordering (cardinality-guided), magic-sets / demand transformation (Datalog — restrict bottom-up rule evaluation to the query’s demand), stratum merging, factorization rewriting (D7), and backend/tier dispatch. This matches DataFusion’s shipping design. Cascades (optd-style memo + transformation/implementation rules + cost model over ontology/graph statistics) is the planned successor once the plan search space (graph join orders × representation choice × backend choice) outgrows hand-ordered passes — adopted then, not now.

D9 — Tier dispatch with pluggable executors (generalizes RFD 0003)

The 7-tier classifier (classifier/mod.rs) routes physical (sub)plans to executors, each owning its own incrementality over the shared relation catalog: Salsa-tracked functions (compile-time, structural/closure checking), semi-naive / DBSP (recursive runtime tier — RFD 0018), SLG (WFS-over-cycles — deferred), Kora (DL/expressive tier — embedded behind the seam, kora’s staged saturation/tableau), SMT (FOL under unsafe logic). Tier is compile-time metadata on rules/plans (not a runtime profile flag — the nous anti-pattern). This is RFD 0003’s TierExecutor seam, generalized from “reasoner backends” to “any physical-plan backend.”

D10 — Storage: CQRS, and the event-log-as-sole-store risk

The store splits write model (the append-only axiom_events log — source of truth, audit, bitemporal time-travel; the Z-set delta stream RFD 0018 consumes; the write path RFD 0019 produces) from read model (a persisted, indexed, graph-optimized materialization — CSR adjacency / columnar / factorized, the durable BYODS physical relations). The event log must not sit on any hot read path. A single append-only log is an excellent write/audit model but a catastrophic primary read store at scale (re-materialization is O(database); per-query log replay dies) — so the persisted read model is the primary served store, the log is checkpointed/compacted behind it, and cold-start reads the read model, not the log. (This is the storage-performance concern raised in discussion, made a hard contract.) Cross-refs RFD 0018 D4 (which this generalizes from the reasoner’s arrangements to the whole database’s read model) and the spec §20 CQRS catalog.

D11 — Ad-hoc queries and mutations are first-class (the database API), config-gated

The engine accepts arbitrary queries and mutations at runtime, not only declared rules/procedures — when the engine is configured to permit it (a deployment may restrict to declared operations for safety/perf). Ad-hoc queries are LogicalPlan trees built at runtime (D2/D5); ad-hoc mutations are write nodes producing Z-set deltas that the IVM engine maintains derived state against (D5/D10; semantics owned by RFD 0019). This is the read/write API surface of the database — gating is an engine policy, not a language restriction (tenancy/IAM stay in the serving layer per AGENTS.md).

D12 — Relationship to RFD 0003 / 0018 / 0019, and the build order

  • RFD 0003 (backend dispatch) is the TierExecutor seam — subsumed and generalized by D9.
  • RFD 0018 (DBSP engine) is the recursive-tier runtime read executor — the physical backend for Recurse/IVM. Correctly scoped; this RFD is the layer above it.
  • RFD 0019 (mutation write-path correctness) owns the write semantics; D5/D10/D11 host its logical surface and storage contract.
  • Build order: the “executor unification” work is no longer a standalone refactor — it is Phase 1 of this RFD: route the runtime through the Engine/dispatch over the proven rule path (with Recurse as the operator), make SemiNaiveExecutor::execute real, de-magic-number convergence, fix the stale headers. Then: Phase 2 — the LogicalPlan/optimizer/physical-mapper made real (the pipeline wired end-to-end) + BYODS reps; Phase 3 — graph-native operators + factorization + the persisted read model; Phase 4 — ad-hoc query/mutation surface + Cascades when justified. RFD 0018’s own phases (the DBSP engine) proceed in parallel as the recursive-tier executor.
    • As-built realization: RFD 0021. Phase 1 (dispatch through Engine::evaluate), Phase 2 (indexed + persistent-arrangement joins, WCOJ, BYODS/CSR reps, the SIP body-reorder optimizer), and the reasoner half of Phase 3 (graph-native joins + factorization) are now landed in oxc-reasoning; 0021 records that engine as built. The persisted read model + cross-query IVM (the remaining half of Phase 3) is still open — 0021 D6/D7 show why the operator discipline already in place makes it an additive layer rather than a rewrite.

Rationale

Why composable, not monolithic. Three roles with different consumers and change regimes cannot be served well by one hand-rolled evaluator (nous proved the failure mode: hardcoded phases, rules-as-enums, an EL++ ceiling). A composable IR + pluggable optimizer passes + pluggable executors is exactly how DataFusion serves dozens of embeddings and kuzu serves graph workloads — and it is what lets a new capability (a new operator, a new tier backend, a new physical rep) land without rewriting the engine.

Why one IR but two substrates. Unifying the logical layer is what makes the three roles coherent (one algebra, one tier ladder, one provenance model). Unifying the physical layer would be a category error: compile-time checking and runtime fact-streaming have different change granularity and lifetime; the right incremental machinery differs (Salsa vs DBSP). Share the meaning; specialize the mechanism.

Why BYODS / never materialize. The Materialization Wall is empirical (99.5% of TC time is representation conversion). A world-class graph database cannot pay that. Polymorphic relations let TC stay a bitmatrix, traversal stay CSR, joins stay sorted arrangements — each served through one interface, none converted.

Why factorization now. Knowledge graphs are many-to-many. Flat tuple materialization of path/pattern results is Cartesian; factorization is linear. It is structural to the physical layer, so it is cheaper to build in than to retrofit — hence v1.

Why CQRS with the log off the hot path. Event-sourcing gives audit, time-travel, and a clean IVM delta stream — but a log is a write model. Serving reads from it is O(database). The persisted, indexed read model is the only way to hit world-class read latency at scale; the log earns its keep on the write/audit side.

Why ad-hoc, gated. A database that only runs pre-declared procedures is a stored-procedure engine, not a database. Ad-hoc queries/mutations are the product; gating is an operational policy for deployments that want it.


Alternatives considered

  • Keep the MVP shortcut (no pipeline); grow the direct rule evaluator. Rejected: it cannot host ad-hoc queries, graph-native operators, an optimizer, or pluggable backends without becoming the monolith nous warns against; it is the stepping-stone we’d replace.
  • One physical engine for both compile-time and runtime. Rejected (D3 rationale): forces the wrong incremental regime on one half.
  • Relational-only IR; treat graph queries as sugar over joins. Rejected for v1’s graph-DB ambition: loses the native-traversal / factorization / WCO-join performance that defines a graph database (D5/D7).
  • Materialize everything into sorted tuples (no BYODS). Rejected: the Materialization Wall (D6).
  • Event-log as the primary read store (no persisted read model). Rejected: O(database) reads; dies at scale (D10) — the explicit storage concern.
  • Declared-queries-only (no ad-hoc). Rejected: not a database (D11).
  • Cascades optimizer from day one. Deferred, not rejected: rule-based passes are sufficient until the search space justifies a memo/cost-model engine (D8).

Consequences

  • Code structure. oxc-reasoning’s logical/optimizer/physical/executor modules become the real pipeline (not scaffolding); the runtime routes through Engine/dispatch instead of the bare free function; a physical Relation trait (BYODS) replaces the fixed BTreeMap<CBOR,weight>; new graph-native + mutation logical nodes; a factorized-batch physical layer; a persisted read-model store behind a StorageBackend seam.
  • Spec. This RFD frames RFD 0003/0018/0019; the reference (spec/reference/src/{17,19,20}) gains an engine-architecture chapter once the design lands. RFD 0018’s “the engine” framing is contextualized as the recursive-tier executor.
  • Lean / conformance. The IR’s reasoning semantics conform to spec/lean/Argon/Reasoning/ + the D1 pair-encoding correspondence; the pipeline structure / operators / optimizer / storage are engine architecture (outside the Lean’s mechanized scope per AGENTS.md) and are conformance-tested against the semi-naive oracle + the Lean (RFD 0018 D8). The optimizer’s rewrites must be semantics-preserving — a differential-test obligation (each pass: optimized plan ≡ unoptimized plan on generated inputs).
  • Workflow. Branch off origin/main; per-PR CI-gated; the executor-unification slice (Phase 1) is the first PR; no “done” without running/benchmarked proof; commit at checkpoints.
  • Performance posture. The scaling contract (RFD 0018 D4) is hereby a database contract, not just a reasoner one: no O(database) operation on any hot path, at any scale, for queries or mutations or reasoning.

Open questions / tracked-future

  • Event-log compaction / checkpointing design — how the read model is kept primary and the log is compacted/archived off the hot path (the storage-performance concern); interacts with RFD 0018 D4 + the spec §20 schema. A research item before Phase 3.
  • Factorization ↔ Z-set interplay — the precise representation of a factorized, multiplicity-carrying, possibly bilattice-pair-encoded batch (D7 × RFD 0018 D1/D5). Needs a concrete data-model design before Phase 3.
  • The checker-uses-the-reasoner boundary — when/how oxc-check starts issuing LogicalPlan goals (subtyping / refinement / occurrence) executed on the Salsa substrate; what the shared-IR contract between compile-time and runtime looks like in code. Role (3) is design-for-now; the seam is D2/D3, the wiring is later.
  • Cost model / statistics source — what cardinality/selectivity statistics the optimizer (and eventually Cascades) consumes, and how they are maintained incrementally over a mutating graph.
  • Ad-hoc safety / config-gating model — the engine-configuration surface for permitting/restricting ad-hoc queries and mutations (resource limits, allowed operators, tier ceilings); a Phase-4 design coordinated with the serving layer (RFD 0014).
  • WCO-join scope — how far to take worst-case-optimal / multiway joins (kuzu’s Intersect is star-pattern-restricted; general WCO is more) for cyclic graph patterns.

RFD 0021 — The reasoner execution engine (as built)

  • State: committed
  • Opened: 2026-06-06
  • Decides: the concrete query-execution engine of oxc-reasoning — the realization of RFD 0020’s Phase 2 (executor unification, the join engine, the optimizer) and the reasoner-side of Phase 3 (graph-native operators, factorization). It records the decisions settled by building: the execution model, the join algorithms, the optimizer passes, the Map operator, the factorization layer, the physical-relation layer, the incrementality discipline, and the correctness methodology. It also records two framing corrections that building surfaced — to RFD 0018 D3 (“fork Feldera”) and to the meaning of “full BYODS.” Built and merged across PRs #98, #102, #103, #104, #105, #108, #111, #114, #116, #118, #121, #122, #124 (and the forall triage #120).

This RFD records what was built and why. RFD 0020 is the umbrella vision; RFD 0018 is the incrementality plan; this is the engine that exists on main. It is Lean-first where it touches reasoning semantics: the executor conforms to the strict-stratified perfect model (spec/lean/Argon/Reasoning/Fixpoint.lean) and is held to it by differential testing against the semi-naive oracle. The rest — join algorithms, physical layout, optimizer, factorization — is engine architecture the Lean does not mechanize (per AGENTS.md scope), settled from first principles.


Question

RFD 0020 set the composable-engine vision but, at the time, the pipeline was “designed, not wired”: LogicalPlan/optimizer/PhysicalPlan/TierExecutor were orphaned, SemiNaiveExecutor::execute was a stub, the live path was AtomIR → CompiledRule → evaluate_to_fixpoint directly, storage was decode-per-tuple BTreeMap, and there were no graph-native operators, no computed-term evaluation, and no factorization.

How is the reasoner’s query-execution engine actually realized — execution model, join algorithms, optimizer, computed terms, factorization, physical-relation layer, and the incrementality discipline — and what concrete decisions did building it settle?


Context

  • RFD 0020 (umbrella) chose: one composable IR; graph-native operators (D5); BYODS (D6); factorization in v1 (D7); a rule-based optimizer (D8); tier dispatch (D9); CQRS storage (D10).
  • RFD 0018 (incrementality) chose: DBSP as the recursive-tier engine, originally forking Feldera dbsp (D3); AFT pair-encoded Z-sets (D1); arrangements on InternalId (D7); semi-naive as oracle (D9). Its Phase-0 spike de-risked IVM (<1 ms incremental at 10 M facts).
  • The constraint that shaped every slice: a correctness-first, no-placeholder, no-hollow discipline — each optimization had to be the genuine mechanism (not a known-incomplete shortcut), validated against an independent oracle, with a correct fallback for the cases it doesn’t yet handle.
  • A coordination seam with the parallel write-path track: the RelationCatalog public API (get/get_mut/ensure/insert/iter + the CatalogEntry.relation field). The reasoner owns the internal eval/join/representation; the write path consumes via the API.

Decision

D1 — Execution model: CompiledRule is the executed form; the “operator pipeline” is the set of operators it evaluates (#98)

The semi-naive CompiledRule evaluator (executor/eval.rs) is the executed form, routed through Engine::evaluate + ConvergencePolicy dispatch (the RFD 0003 seam made real; the six magic 1000s removed; SemiNaiveExecutor::execute real). RFD 0020’s “operator pipeline” (D2/D4) is realized as the set of operators the executor evaluatesPredicate (join), Comparison (filter), Naf (anti-join), Aggregate, Compute (Map) — not a separate operator-tree interpreter. Why (build-correctly-once): an operator-tree LogicalPlan that round-tripped back to the CompiledRule executor would be throwaway scaffolding the moment an operator-tree executor arrives; the optimizable LogicalPlan IR is reserved for that executor when graph-native physical operators + factorization genuinely demand it. The dead logical/physical/optimizer scaffolding stays reserved, not wired. (Now live — the operator tree was revived by RFD 0035’s federation consumer exactly as this “when a consumer demands the tree” trigger anticipated; see RFD 0065 for the FORK-2 resolution.)

D2 — The join engine: a Free-Join hybrid over a polymorphic physical layer

Joins dispatch by body shape, all anchored to the binary binding-extension evaluator as the differential oracle:

  • Indexed joins (#102) — arrangement-by-bound-key, tuples decoded once at build time. Replaced the O(|prior| × |rel|) decode-per-tuple nested scan. Output ordering byte-identical → a pure drop-in.
  • Persistent arrangements (#104) — arrangements cached across a stratum’s semi-naive iterations, evicted after each merge for exactly the relations whose extent changed (merge_into_state is the sole mutation point). Staleness is structurally impossible; stable relations (EDB, earlier strata) build their index once.
  • Worst-case-optimal join (#114, time-optimal #116) — for pure positive-predicate cyclic bodies (the triangle), a variable-at-a-time Generic Join over per-atom prefix tries: at each variable, drive the multiway intersection from the smallest candidate set and probe the others by membership (#116 — the leapfrog discipline that makes it O(M log N), not O(N²); #114 alone was memory-optimal but time-O(N²)). Binary stays for acyclic/linear-recursive bodies — the Free-Join hybrid. Gated by atoms ≥ distinct vars (perf-only; WCOJ is correct for any conjunctive body). Differential-tested identical to binary.
  • BYODS / CSR index-free adjacency (#118) — the arrangement is polymorphic (enum { Bucketed, Csr }, RFD 0020 D6). A binary relation keyed on a single column (the edge-traversal pattern) builds a CSR adjacency (sorted sources + contiguous neighbours); probing is a binary search + a contiguous slice. Built ephemerally from the canonical BTreeMapCatalogEntry.relation is unchanged, so coordination-free.

D3 — The optimizer: SIP reorder + projection-collapse (#103, #105, #122)

In Engine::evaluate, each rule body is reordered (optimizer/reorder.rs): filters first, then the most-constrained predicate (most already-bound / constant argument positions), then aggregates, with a cardinality tie-break (smaller non-rule-head relation first; a rule-head relation’s pre-eval size is unknown → deferred, never falsely “smallest”). Safety is structural — a greedy that only ever places an atom whose consumed variables are already bound (a correct over-approximation excluding NAF/aggregate-locals). Semantics-preserving.

Projection-collapse (#122) — eager projection-pushdown in the naive pass: after each atom, variables that are now dead (bound so far, but not in the head and not referenced by any later atom) are cleared and the binding set deduped, so a projected-away fan-out (HasBoth(p) :- hasPhone(p,ph), hasEmail(p,e) → stays at |distinct p|, never |ph|×|e|) collapses without enumeration. Result-preserving.

D4 — The Map operator: computed terms (#108, #111)

CompiledExpr + CompiledAtom::Compute evaluate computed scalar terms (exact-BigRational arithmetic with Int-collapse, comparisons → Bool, &&/||) — what CompiledTerm (variable/constant only) could not represent, the gate behind the issue-#56 “won’t evaluate” wall. The drift-gated AtomIR::Compute substrate node (Lean + protocol, the write-path track) lowers via compile_expr. The Map operator is linear → trivially incremental. Non-scalar terms (field projection, application) are a loud compile error, never a silent drop.

D5 — Factorization: the f-representation for the high-value cases (#121, #122, #124)

Realized for independent-factor bodies (groups sharing no body-local variable), computed over the factorized form rather than the enumerated cross-product:

  • count (#121) = per-group sizes.
  • projection-collapse (#122) — see D3 (the main-eval analog).
  • value folds (#124) — a value fold whose single projection variable lives in one group folds over that group; the others scale (sum: × ∏ other sizes) or gate (min/max/avg/count_distinct: repetition-invariant, gated on the others being non-empty).

Connected bodies, constant/outer-bound projections, and nested factorization stay on the correct flat path — real follow-ups, not silent shortcuts. Every path is differential-tested identical to the flat fold. This is the genuine f-representation: the general FBindings representation (deferred) would compute these the same way — these generalize, they are not placeholders.

D6 — Incrementality is an additive outer loop, not a rewrite — the DBSP model, not a Feldera fork (revises RFD 0018 D3)

Every operator is a pure Z-set → Z-set function: linear (commutes with the delta operator — Map, Filter, projection) or with an explicit delta-form over arrangements (the join product rule, antijoin’s signed-weight form). The substrate already is the incremental substrate: Relation = BTreeMap<tuple, i64-weight> is a Z-set, the persistent arrangements are DBSP’s integrated indexed state, and the semi-naive delta loop is the join product rule applied across iterations. Therefore cross-mutation IVM is an additive outer loop (integrate/differentiate at the boundaries + delta-seeded recursion), never an operator rewrite. Holding this discipline as each operator is built is what guarantees it.

This revises RFD 0018 D3 (“fork Feldera dbsp”) → “adopt the DBSP model; build our own operators.” Feldera’s flat binary-join Z-sets are the wrong substrate for the factorization and WCOJ that RFD 0020 D7 commits to for v1; grafting them onto Feldera is swimming upstream. We build our own operators on the DBSP model and keep Feldera + the semi-naive evaluator as differential oracles. RFD 0018’s Phase-0 spike still stands — it de-risked IVM-as-approach, not Feldera-the-codebase.

D7 — Cross-query reuse needs the persisted read-model, not a CatalogEntry.relation rep-swap (corrects the “full BYODS” framing)

The catalog is rebuilt per query (query_derive → materialize_predicates → RelationCatalog::new(); the Store holds no materialized catalog; query results are cached, the catalog is not). So making CatalogEntry.relation a persistent dense-InternalId representation buys nothing across queries — it dies with the catalog — and within-query reuse is already captured by the persistent arrangements (#104) + CSR (#118). The genuine cross-query-reuse and incrementality win is to persist the materialized read-model on the Store and maintain it incrementally on mutation — the CQRS persisted read-model + IVM (RFD 0020 D10, RFD 0018 Phase 4) — a storage-and-write-path-coordinated effort, not a reasoner representation change.

D8 — Correctness methodology: the independent oracle

Every optimization is validated against an independent implementation that must produce identical results: the binary/flat evaluator is the differential oracle for indexed joins, WCOJ, CSR, projection-collapse, and factorization; the semi-naive evaluator is the oracle for the eventual DBSP/IVM. Library code has no unwrap/expect/panic; BTreeMap/BTreeSet only; every PR is fmt + clippy clean and cargo nextest-green. This is why the engine could be transformed under load without regressions, and why a structurally-impossible-staleness or byte-identical-output argument backs each slice.


Rationale

  • Build-correctly-once over speculative IR (D1). The optimizable operator-tree LogicalPlan is real architecture, but wiring it to round-trip through the proven CompiledRule executor would be scaffolding discarded the moment an operator-tree executor lands. Optimizing the executed form and deferring the tree to its real consumer is the honest sequencing.
  • The oracle is the load-bearing safety property (D8). WCOJ, CSR, factorization, and projection-collapse are intricate and easy to get subtly wrong; anchoring each to the binary/flat evaluator turned “is it correct?” into a test. (#116 exists because the oracle-and-complexity analysis caught that #114 was memory-optimal but time-O(N²).)
  • The Z-set discipline is what keeps incrementality cheap to add (D6). Because the substrate is already a Z-set with arrangements, and every operator is a differentiable Z-set function, IVM is a wrapping, not a rewrite — which is precisely why forking Feldera (whose model can’t carry factorization/WCOJ) is the wrong trade.
  • Facts beat framings (D7). “Full BYODS for cross-query reuse” sounded right until the per-query catalog rebuild was verified; surfacing that prevented a large, coordinated, marginal-value rep-swap.

Alternatives considered

  • Wire the operator-tree LogicalPlan interpreter now. Rejected (D1): the LogicalPlan → CompiledRule round-trip is throwaway once an operator-tree executor exists, and the round-trip for aggregates/NAF is complex; defer the tree to its real consumer.
  • Fork Feldera dbsp (RFD 0018 D3 as written). Rejected (D6): flat binary-join Z-sets are the wrong substrate for factorization/WCOJ; adopt the model, build our own operators.
  • “Full BYODS” = make CatalogEntry.relation a persistent rep. Rejected (D7): the catalog is rebuilt per query, so it delivers no cross-query reuse; the real win is the persisted read-model.
  • WCOJ everywhere / always-on factorization. Rejected: WCOJ regresses acyclic bodies; factorization helps only specific shapes. Both are gated, with the proven path as the default and fallback.

Consequences

Built and on main: a query engine that is expressive (computed terms), optimized (SIP + cardinality reorder, indexed + persistent arrangements, projection-collapse), worst-case-optimal on cyclic graph patterns, index-free-adjacency-capable (CSR), factorized (count / projection / value-folds), and incrementality-ready (every operator a pure Z-set function). All correctness-first; the forall over-derivation was triaged to the lowering with a validated count-equality fix recipe (#120, handed to the write-path track).

Deferred (each a real follow-up, not a gap in what’s built):

  • delta-path projection-collapse (recursive rules) and nested factorization;
  • the general FBindings representation flowing through the eval — gated on real-workload evidence (no real models exist yet, so the winning-shape frequency is unknown);
  • the persisted read-model + IVM (D7) — the cross-query-reuse and incrementality payoff, the next big coordinated effort;
  • WFS / SLG for recursion-through-negation (OE1309 under strict stratification) — a real-model driver (RFD 0018, Gustavo’s breach calculus);
  • cardinality statistics for cost-based / per-stratum reorder.

Coordination: the RelationCatalog public-API seam stays the boundary with the write-path track; the catalog-owned (persistent) BYODS representation, when built, is the coordinated slice (touches CatalogEntry.relation).


Open questions / tracked-future

  • When does the general FBindings representation earn its cost? It is the uniform home for factorization, but it is the largest operator-model change; building it ahead of a real workload that exhibits the fan-out shapes would be over-engineering. Decide when real models exist.
  • The persisted read-model + IVM — the design (CQRS read model on the Store, generation-driven invalidation, incremental maintenance with retraction) is RFD 0020 D10 / RFD 0018 Phase 4; it is the next major, coordinated effort and warrants its own RFD when started.
  • Recursion-through-negation — needs WFS (the SLG kernel, RFD 0018 D11); strict stratification correctly rejects it (OE1309) today.

RFD 0022 — Package-path addressing (pkg) and the build evaluability gate

  • State: committed
  • Opened: 2026-06-07
  • Decides: two surface/contract decisions settled by building the real-package-layout module-resolution work — (1) how a path addresses modules, including the self-reference anchor and restricted visibility (pkg:: / pub(pkg), not crate:: / pub(crate)), and (2) that ox build refuses to emit an artifact containing a rule the runtime cannot evaluate, rather than silently dropping it. Built and merged across PR #128 (resolver anchoring, re-export propagation, pub(pkg), the build gate). Relates to Modules and the loudness stance of RFD 0019.

This RFD gives a home to two decisions the module-resolution PR made that change the surface (cratepkg) and the build contract (warn→error). Per AGENTS.md surface changes route through an RFD; these are recorded as built.


Question

The corpus examples were hand-flattened into a single namespace, so module resolution had never been exercised on a real nested package. Two questions surfaced when it was:

  1. How does a path name a module — and how does a package refer to itself? Rust uses crate:: for self-reference and pub(crate) for package-wide visibility. Argon is not Rust; it has packages, not crates. What is the canonical self-anchor and restricted-visibility spelling?
  2. What does ox build do with a rule the runtime cannot evaluate? The lowering admits rule shapes the executor’s compile_rule then refuses (a forall/exists quantifier → OE1315, not <aggregate> → OE1313, an unsupported aggregate kind → OE1312, …). Such a rule is silently dropped from evaluation.

Context

Resolution was filesystem-relative to the importing file, with no notion of the package root: pkg::a::b, super::, and any reference from a file deep in the tree all failed; only flat-relative paths from the package root happened to work (where relative coincides with absolute). The real overlay emitted ~190 OE0101 errors as a result.

Separately, ox build lowered every rule, loaded the artifact, and warned (non-fatally) on rules the runtime couldn’t evaluate — then wrote the artifact anyway. A consumer querying such a model gets wrong (under-derived) answers with no runtime error, the build warning easy to miss.


Decision

D1 — Path addressing: pkg is the sole self-anchor; no crate

A qualified path’s leading segment selects a root (Name resolution):

  • pkg — the current package’s root. The only way a package refers to itself; a package never names itself by its own package name. pkg::a::b::X names X in module a::b. Rename-safe: changing the package’s name in ox.toml doesn’t break internal paths.
  • self — the current module; super (repeatable) — an ancestor module.
  • A dependency package name ([dependencies]), and the always-available std root.
  • Otherwise the leading segment resolves against the scope chain (a submodule of the current module, or a use-imported name).

Restricted visibility is pub(pkg) (package-wide), not pub(crate). There is no crate keyword anywhere in the surface. pub(<anything-but-pkg>) is a parse error (OE0001) — it is not silently widened to pub.

pub use … ; / pub use … ::*; re-export (transitively); a plain use is a private import and does not re-export. Under the v0 world-assumption simplification pub(pkg) re-exports identically to pub (package boundaries aren’t yet modeled as a visibility cut).

D2 — ox build refuses to emit an un-evaluable artifact

If any rule in the lowered program is one the runtime’s compile_rule refuses, ox build fails and writes no artifact (it re-runs the runtime’s own rule compiler as the oracle, before write_oxbin). A built .oxbin therefore evaluates every rule it contains, or it does not exist.


Rationale

  • One obvious self-anchor. Supporting both pkg:: and the package’s own name would make every self-reference a silent style choice and blur the inside/outside boundary (a reader couldn’t assume a package-name path is a dependency). pkg:: is unambiguous and rename-safe; the package name stays the dependent-facing absolute path.
  • crate means nothing in Argon. The unit of distribution is a package ([package] in ox.toml); borrowing Rust’s crate vocabulary would be a false cognate.
  • Loud over silently-wrong (D2). A knowledge system that returns a plausible-but-wrong answer is worse than one that refuses — the same instinct as RFD 0019. A dropped rule is a silent under-derivation; refusing the artifact moves the failure to build time where it’s visible. The full examples corpus builds clean under this gate, so no real model relied on the warn-only behaviour.

Alternatives

  • Support both pkg:: and package-name self-reference (referential transparency: a symbol’s absolute path is identical inside and outside). Rejected: the cosmetic upside is outweighed by two-ways-to-say-it and rename-fragility; the external absolute path is still expressible.
  • Keep pub(crate) (Rust-familiar). Rejected: crate is not an Argon concept.
  • Warn, don’t fail, on un-evaluable rules (the prior behaviour). Rejected: it ships silently-incomplete artifacts.
  • A new diagnostic code for the visibility error. Deferred: reusing OE0001 with a clear message is sufficient; a dedicated code can come later if needed.

Consequences

  • Visibility::pubPackage (Lean Syntax/Decl.lean), Visibility::Package (Rust oxc-db), and the book §3.1/§3.4 are aligned on pub(pkg). There is no oxc-protocol Visibility mirror, so the rename is maintained by hand, not the drift gate.
  • self:: as a path-start segment does not yet parse (self is a lexer keyword for self.field); resolver support is in place. Accepting self/super/pkg as first-class path-root keywords is a follow-up.
  • Build-time refusal currently keys on the runtime rule compiler; an un-evaluable rule fails the whole build (no partial artifact). This is intentional for v0.

Open questions

  • Should pub(pkg) become a true visibility cut (distinct from pub) once package boundaries are modeled, rather than the v0 Package == Public simplification?
  • Should the unknown-pub(...) rejection get its own diagnostic code (vs the reused OE0001)?
  • Should the build gate ever support a partial/--allow-unevaluable mode for iterative authoring, or is whole-program evaluability the permanent contract?

RFD 0023 — Reflective TypeRef: type-as-value in the meta-calculus

  • State: committed
  • Opened: 2026-06-07
  • Decides: the substrate’s type-as-value facility — a reflective sort TypeRef whose values are references to declared types, the bounded form TypeRef<C>, the runtime/wire carrier, and the first-class (value-polymorphic) forms of the four reflection intrinsics (meta/iof/specializes/ extent). This is the realization of RP-003 GAP-1 (“expose the reflection predicates as first-class so library code can pass/count/quantify over type-values”). It is Lean-first: the foundation landed in spec/lean/Argon/CoreIR/Term.lean (Term.typeRef) and spec/lean/Argon/MetaCalculus/Reflect.lean (sorts, lattice, the iof/specializes/extent relation semantics) before the Rust layers.

This RFD records a settled design (see .local-staged discussion). It does not introduce any higher-order theory into the core: MLT / Potency / ML2 remain std::* libraries (RFD 0009, RP-003); TypeRef is the neutral substrate they build on.


Question

Ontological modeling stores references to types as values: actionType: TypeRef, roleType: TypeRef, relatorType: TypeRef (110+ sites in the sharpe-ontology overlay). Today such a field/param does not resolve (no TypeRef sort), and a mutation parameter that should accept a declared TBox class reference ("residential_lease::core::Lessor") is instead validated as an ABox entity reference (#i123) and rejected. Separately, RP-003 established that for any higher-order theory to be a library, the substrate must expose iof/specializes/extent/meta as first-class predicates over type-values (GAP-1), which the IR does not yet do (iof is a syntactic typeTest, meta compares against an Ident, specializes/extent have no IR at all).

What is a type-as-value in Argon — its sort, its bounded form, its runtime/wire representation, and the first-class form of the reflection intrinsics — without leaking any higher-order theory into the core?

Context

  • Meta-calculus (§4) already specifies the four reflection intrinsics and a 3-level tower (meta(Person)==kind, meta(kind)==Metatype, Metatype sealed/self-instantiating); RP-003 §10 (resolved) canonicalized the argument sorts as Entity (value-position) and type (type-position) and made the intrinsics substrate-scope (no use). The book uses these but the substrate never realized first-class, value-polymorphic forms — that is the gap.
  • Prior thinking pre-figures this and retires the central risk. The vault’s RP-001/D-132 MLT-parent-theory campaign already adjudicated Girard’s paradox: the reflective layer is a predicative, stratified universe of codes (hypothesis-2-type-theoretic/s2-MLT-mapping.md §9.4), not an impredicative Type:Type. A TypeRef value is a handle into the closed declared catalog (à la OWL-2 punning / Java Class<?> / Haskell TypeRep), so iof/extent are predicates over that catalog — no self-membership at a fixed level.
  • Carrier already exists. The runtime Value::Name(NameRef) (“a declared symbol reference”) is content- addressable (RFD 0001), bitemporal-safe, total-Ord (Z-set keys), and round-trips CBOR↔path-string — the legacy EntityRef::Concept / unified-EntityId idiom. No new Value variant is needed.
  • Overlay evidence. The overlay’s type-valued fields are consumed only by == equality today; the bounded need (“a reference to one of a set of declared subtypes of X”) shows up as the current enum workaround — i.e. exactly a bounded TypeRef<C>.

Decision

D1 — A reflective sort TypeRef

TypeRef is the sort whose values are references to declared types (concept / construct / relation / metatype). It is unsealed. The lattice:

Metatype  <:  TypeRef  <:  Entity

Entity is the universal sort of all entities (individuals + type-references); a type is an entity (so it can be classified by a higher-order type — MLT higher-order). Because TypeRef is unsealed, Metatype remains the only sealed primitive (preserving the redesign’s “Metatype is the only sealed primitive” commitment — this is not an amendment). Chosen name TypeRef (over core Type) avoids the four-way collision with Top, std::mlt::star::Type, UFO Type_, and a vocabulary’s own pub type Type, and avoids reopening the delexicalization that removed a Type keyword.

D2 — Bounded TypeRef<C> is a refinement, not a core type-former

TypeRef<C> ≝ { t: TypeRef where specializes(t, C) } (RFD 0017, where = primitive/asserted). TypeRef == TypeRef<Top>. Subtyping is covariantTypeRef<A> <: TypeRef<B> iff A <: B — which follows from the refinement ({t|t<:A} ⊆ {t|t<:B}), so no new structural subtyping rule is added. Membership is three-valued under OWA (per RFD 0007): specializes(t,C) unknown ⇒ not a member (success requires definite-true). Powertype / order / categorization semantics stay in std::mltTypeRef<C> carries none of them.

D3 — Runtime/wire carrier: Value::Name(NameRef)

A TypeRef value is a NameRef handle to a declared type (no new Value variant). On the wire, the SDK sends a qualified class-path ("residential_lease::core::Lessor"); the runtime resolves it to the type’s NameRef. coerce_json_arg_for_type gains one arm for TypeRef-typed params: path → resolve → Value::Name, short-circuiting before the ABox entity-ref path. Equality (==) already works. The TS SDK (oxc-gen) adds a class-path wire shape + serializer branch distinct from the existing #i… / entity-ref path.

D4 — First-class, value-polymorphic reflection intrinsics (GAP-1)

The four intrinsics accept/produce TypeRef values (a type-position argument is a Term — a Term.typeRef literal or a bound variable of sort TypeRef), so library code can quantify/count over types:

meta(x: Entity)                  -> TypeRef      // immediate classifier; amends book §4's `-> Metatype`
iof(x: Entity, t: TypeRef)       -> Bool         // sugar:  x : T
specializes(t1: TypeRef, t2: TypeRef) -> Bool    // sugar:  t1 <: t2
extent(t: TypeRef)               -> Set<Entity>

IR lowering convention (matches the temporal-operator precedent of reserved-head predicates — no new AtomIR constructors, so the Admittance/tier proofs are untouched):

  • iof(x,t)AtomIR.predicate ["iof"] [x, t]; specializes(t1,t2)predicate ["specializes"] [t1, t2].
  • extent(t)Term.app (var "extent") [t]; meta(x)Term.metaCall x (returns a TypeRef value).
  • The closed/literal sugar x : T keeps AtomIR.typeTest; x :: T keeps AtomIR.metaEq (fast path). The reasoner evaluates the reserved-head reflection atoms against the catalog’s iof/specializes graph.

D5 — Lean is canonical; the foundation landed first

Term.typeRef : Path → Term (the type-as-value primitive, @[language_interface]) and MetaCalculus/Reflect.lean (the sorts; reflectiveLeq proven a preorder so Subtyping.subtypeOf decides Metatype<:TypeRef<:Entity; ReflectCatalog with specializes proven a preorder, iof, and extentOf with its characterization x ∈ extentOf u t ↔ x ∈ u ∧ iof x t). The reflective sort edges are folded into the catalog’s <:-closure builder (not OR-ed at query time — a disjunction of two preorders is not transitive).

Rationale

  • Neutral substrate, theory as library — matches the meta-calculus pattern one layer up (RP-003): the core gains only “a value can reference a declared type” + first-class reflection; order/categorizes/ power_type_of live in std::mlt. The BFO/non-UFO smoke test still passes (a vocabulary uses TypeRef/meta with zero UFO/MLT in scope).
  • Sound by construction — codes-universe, not impredicative universe (D-132 / s2 §9.4). TypeRef unsealed keeps Metatype the lone sealed primitive.
  • Minimal blast radius — reuses Value::Name (carrier), RFD 0017 refinement (TypeRef<C>), the reserved-head-predicate IR convention (no AtomIR/tier-proof churn), and D-116’s precedent that a type-position may resolve to a handle (D-116 explicitly rejected “wrap a type in a synthetic concept”, validating a genuine sort).
  • Overlay-correct== over Value::Name works immediately; bounded TypeRef<C> is exactly the enum-workaround need, done principledly.

Alternatives considered

  • Core sort named Type (demote std::mlt::star::TypeOrderlessType). Most ergonomic word, but a four-way name collision and reopens the delexicalization wound; and Type with Metatype <: Type would amend “Metatype is the only sealed primitive.” Rejected for TypeRef.
  • Entity + metatype-as-sort only (no dedicated reflective sort; RP-003 §10 canonical Entity/type). Most minimal, but : type reads as a keyword and conflates the generic metatype with the universal type-reference sort; a dedicated unambiguous TypeRef is clearer craft.
  • First-class generic Type<C> core type-former (covariant primitive). Adds a powertype-flavored core form and reproves variance; the refinement route (D2) is more neutral and reuses machinery.
  • Runtime-only coercer fix (accept a class-path for any concept-typed param, no sort). Smallest, but a non-design — pushes the type/individual distinction into ad-hoc coercion. Rejected.
  • MLT-order-indexed Type@n / potency in core. This is precisely what neutrality forbids in the core.

Consequences

  • New surface: TypeRef, TypeRef<C> as type expressions; Term.typeRef IR; meta/iof/specializes/ extent value-polymorphic. Book §4 amended (meta -> TypeRef); RP-003 §10 type-position sort named TypeRef (lowercase type remains the generic metatype). SUMMARY/Appendix updated.
  • Drift gate: Term.typeRef is @[language_interface] — the Rust oxc-protocol Term mirror gains a matching TypeRef variant (arity 1 over the name/path id).
  • Lean module inherits Classical.choice at the metalevel (D-132 OQ3) — already used in Foundation/Truth4, not new debt.
  • TypeRef must stay crisply distinct in spelling/scope from Top, std::mlt::star::Type, and any vocabulary’s Type_ (D-073) — no respelling that blocks a vocabulary declaring its own.

Open questions

  • Codomain scopeTypeRef references concept/construct/relation/metatype; the intrinsics stay category-errors on struct/enum data values (§4). (Recommended: include relations.)
  • extent(TypeRef<C>) — enumerate all subtypes of C (symmetric with extent(Metatype)). (Recommended: yes.)
  • Narrowing — should t: TypeRef narrow to TypeRef<K> after iof(t, K) (occurrence typing)? Deferred.

Implementation status (layered, each checkpoint green)

  1. Lean foundationTerm.typeRef + MetaCalculus/Reflect.lean; lake build green (1034 jobs), 0 sorry (commit a84d9e3).
  2. Protocol + drift — mirror Term::TypeRef in oxc-protocol; document Value::Name carrier; drift green.
  3. Resolver + checker — resolve TypeRef/Entity; fold the reflective edges into the <:-closure; TypeRef<C> refinement + covariance; 3-valued OWA membership.
  4. Instantiate — lower the four intrinsics to the value-polymorphic atoms (sugar preserved; fast-path closed types).
  5. Reasoner — evaluate the reserved-head reflection atoms over the catalog graph.
  6. Runtime + wire — the coerce_json_arg_for_type TypeRef arm (path → NameRefValue::Name).
  7. SDK (oxc-gen) — class-path wire shape.
  8. Spec + overlay — §4 amendments; overlay migration where “any type-reference” is meant.

RFD 0024 — Allen interval algebra as a library (std::allen), not substrate operators

  • State: committed
  • Opened: 2026-06-08
  • Decides: Allen’s interval algebra is a standard-library theory (std::allen), written in pure .ar over the scalar Date/Duration value layer (#159) — not reserved infix operators in the substrate. The currently-committed-but-unbuilt Allen operators (Syntax/Operators.lean::AllenOp, Syntax/Expr.lean::RuleAtom.allenAtom, the tier:expressive grammar in book §7.3.1, and OE0713 AllenOverMTLDerived) are removed.

This is the temporal counterpart of the principle RFD 0009 / RP-003 already apply to higher-order theories: the substrate stays ontology-neutral; specific theories are libraries. MLT, UFO, and BFO are std::* packages, not language features. Allen’s interval algebra is a theory of time, and the same rule applies. Settled in discussion (the .ar value layer it rests on landed first, by design).


Question

Argon currently carries Allen’s 13 interval relations two ways at once: as reserved substrate operators (infix a before b, mechanized in Lean, drift-gated, tier-classified) and, implicitly, as something a modeler would otherwise define over interval boundaries. The substrate form is committed but unbuilt — no parser, elaborator, or executor path. With the scalar Date/Duration value layer now real (#159), an Allen relation is definable as an ordinary rule over {startsOn, endsOn} boundaries. So:

Does Allen’s interval algebra belong in the substrate (reserved operators), or as a library over the value layer?

Context

  • What is committed today (unbuilt). AllenOp enumerates 12 relations (before/after/meets/metBy/overlaps/overlappedBy/during/contains/starts/startedBy/ finishes/finishedBy — note it omits Allen’s 13th, equals) at spec/lean/Argon/Syntax/Operators.lean:79; the surface atom RuleAtom.allenAtom : FieldPath → AllenOp → FieldPath at Syntax/Expr.lean:222; the field-path allen-op field-path grammar at tier:expressive in book §7.3.1 (07-rules.md:127-150); and OE0713 AllenOverMTLDerived (Allen applied to an MTL-windowed predicate — ill-typed). All @[language_interface] and drift-gated; none of it is parsed, elaborated, or evaluated.
  • Allen reduces to boundary comparisons. a before b ≡ a.endsOn < b.startsOn; a meets b ≡ a.endsOn == b.startsOn; a equals b ≡ a.startsOn == b.startsOn ∧ a.endsOn == b.endsOn; and so on for all 13. Each is a conjunction of chronological comparisons on Date boundaries — exactly what the value layer of #159 now evaluates correctly (and which, pre-#159, the reasoner did as a silent lexicographic string compare).
  • The substrate’s stated posture. Argon is “ontology-neutral (UFO/MLT/BFO are stdlib theory packages, not language features)” (AGENTS.md). The substrate provides value computation (Money/Decimal/Date arithmetic, comparison); theories are libraries. Allen is a theory.
  • Allen is not the only interval algebra. There are variants — point vs. proper intervals, open vs. closed, fuzzy/probabilistic Allen, the coarser INDU and convex-relation algebras. A reserved operator set privileges one (and the committed set is already incomplete: no equals). A library is choosable and extensible.

Decision

  1. std::allen is a library, pure .ar, over #159. It models intervals and defines the 13 Allen relations (and any convenience dispatch) as ordinary derive rules over Date boundaries. It requires nothing from the engine beyond the scalar value layer that already shipped.
  2. Remove the reserved Allen operators from the substrate. AllenOp, RuleAtom.allenAtom, the §7.3.1 grammar, the tier:expressive Allen classification, and OE0713 are deleted. They are unbuilt and have no users, so this is a no-cost reversal taken now while it is free (no migration, nothing to break).
  3. No infix sugar. Library Allen is call/UFCS syntax (a.before(b) or before(a, b)), not infix a before b. For a niche qualitative algebra this is an acceptable — arguably clearer — trade for a smaller, neutral substrate. (Argon does not offer user-defined infix operators, and adding them for one library is not justified.)

The std::allen library (sketch)

Built on #159’s Date/Duration values + chronological comparison. An interval is modeled as a concept with two Date boundaries (a real entity, so its fields project in rule bodies; an opaque inline struct would not):

// std::allen (or std::time::allen)
pub kind TimeInterval { startsOn: Date, endsOn: Date }

pub derive before(a: TimeInterval, b: TimeInterval)  :- a.endsOn < b.startsOn;
pub derive meets(a: TimeInterval, b: TimeInterval)   :- a.endsOn == b.startsOn;
pub derive during(a: TimeInterval, b: TimeInterval)  :- b.startsOn < a.startsOn, a.endsOn < b.endsOn;
pub derive equals(a: TimeInterval, b: TimeInterval)  :- a.startsOn == b.startsOn, a.endsOn == b.endsOn;
// … the remaining relations + inverses, all boundary comparisons.
  • No relation-name magic. A user-declared pub rel AllenHolds(...) stays ordinary data; it gains meaning only if the model defines it from these rules (pub derive AllenHolds(a, Before, b) :- before(a, b);). The substrate never special-cases the name AllenHolds, nor sniffs {startsOn, endsOn} field names.
  • dateInterval / shift are library helpers; until the fn return-of-constructed-value surface matures, the robust form is to shift a boundary inline in the comparison — e.g. the workflow’s `allenAfter(timeInterval
    • relativeTime, dateInterval(checkDate))is(iv.startsOn + relativeTime) > checkDate (after ≡ a.startsOn > b.endsOn; a point interval's endsOn` is the date itself).
  • The library can be complete (all 13 relations incl. equals) and may offer an allenHolds(a, rel, b) dispatch over an AllenRelationType value — neither requires substrate support.

What is removed / changed

A surface change, so Lean-first (per the AGENTS.md workflow: surface decision → Lean → reference → drift mirror → parser/grammar):

  • Lean: delete AllenOp (Syntax/Operators.lean) and RuleAtom.allenAtom (Syntax/Expr.lean); drop the Allen arm from the tier classifier (Decidability/) and any Expr/@[language_interface] references.
  • Reference: remove the Allen grammar + prose from §7.3.1 (07-rules.md:127-150) and Allen mentions in §6/§15/crash-course; document std::allen in §15 (stdlib) instead.
  • Drift / Rust mirror: drop the AllenOp mirror in oxc-ast and its drift-gate entry; the parser/grammar never recognized the operators, so there is little Rust to remove.
  • Diagnostics: retire OE0713 AllenOverMTLDerived (it only exists to police an Allen×MTL interaction that no longer has substrate operators). Leave a tombstone in grammar.toml per code-allocation hygiene, or reclaim the number — decide at implementation.

Non-goals

  • Not the temporal reasoner. This is orthogonal to DatalogMTL metric operators (since/until/ever), bitemporal valid-time, and temporal modal operators — the “temporal operators” tracked separately. Allen here is qualitative interval relations as value-level boolean rules.
  • Not interval types in the substrate. The substrate gains no Interval type; an interval is a library concept with two Date fields.
  • No infix operators, no fuzzy/INDU variants in scope (those are further libraries if wanted).
  • DateTime/Time intervals and calendar-relative durations are out of scope — they ride the #159 follow-ons (sub-day resolution, P1M/P1Y).

Alternatives considered

  • Keep Allen in the substrate (status quo). Rejected: it privileges one interval theory, is library- definable with zero engine support, and the committed set is already incomplete. Mechanizing + drift-gating a theory the substrate doesn’t need is debt.
  • Ship Allen as built-in stdlib functions (allenBefore, …) wired into the engine. Rejected: same neutrality problem one layer down, and it would fork the algebra against the reserved operators. A pure-.ar library is cleaner and needs nothing special.
  • Leave the operators reserved-but-unbuilt. Rejected: a dangling surface that will fork against std::allen the moment the library exists; removing it now (unbuilt, no users) is the cheapest it will ever be.

Sequencing

  1. (Done — prerequisite) Scalar Date/Duration value layer + chronological comparison, #159.
  2. Remove the reserved Allen operators (Lean → reference → drift mirror → grammar), per What is removed.
  3. Ship std::allen as a pure-.ar package over #159.

Related: RFD 0009 (theories-as-libraries), RFD 0016 (the value tower this builds on), RP-003 (substrate neutrality). The today()/now() evaluation-context for date-relative rules is a separate RFD (deliberately out of scope here).

RFD 0025 — check discharge: vocabulary-staged compile-time and runtime constraint checking

  • State: accepted — implementation in flight
  • Opened: 2026-06-10
  • Decides: the complete semantics of the check rule mode — when and where a check discharges, what the => Diagnostic { … } payload is, what severity means at runtime, and how violations surface. Settles the question Gustavo Guizzardi and Tiago raised (“is a check compile-time or runtime?”) and Ivan’s original conception (checks as user-authored compiler diagnostics) into one model. Design discussion: .local/research/checks/DESIGN-2026-06-10.md.

Prior state (verified at 8deb583): checks parse and lower to RuleMode::Check Cat3 rules, the => Diagnostic expression is discarded at lowering, the payload shape is unvalidated, and no discharge path exists — RuleMode::Check has zero evaluation consumers. The body is evaluable (ox derive <oxbin> <CheckName> returns the violation set), so this RFD is the harness, payload, and contract around an engine that already computes the hard part.


D1 — One construct; the body’s vocabulary determines the discharge site

A check is a denial rule over the well-founded model: the body is the violation pattern, the payload is the per-violation report. The author never chooses a mode; discharge is staged by what the body reads — the third instance of an established pattern (refinement where staging D1Pred/D2Pred; modal static discharge):

Body vocabularyDischarge
Catalog-level — every head param and body variable is reflective-sorted (TypeRef, TraitRef, or Metatype; amended by RFD 0026 D6 — a free TraitRef variable must not demote a conformance check to instance-level); atoms read declaration structure (specializes, iof over metatypes, implements per RFD 0026)ox check / ox build / LSP. The catalog is closed at build; evaluation is total and final there.
Instance-level — any variable ranges over individualsRuntime, at transaction boundaries and on demand — and also at build, over whatever EDB the package itself declares (pub facts and seeded declarations are build-visible).

The classification rule is crisp and checkable: a check is catalog-level iff every variable in head and body is reflective-sorted (TypeRef, TraitRef, or Metatype). Mixed bodies are instance-level. (Amended by RFD 0026 D6, which introduces TraitRef; the original rule read “TypeRef-sorted”.)

#[static] (convention, Ivan 2026-06-10): compile-time-intent checks carry #[static] by convention. Discharge is still computed from the vocabulary; the attribute (a) makes intent legible at the declaration and (b) turns accidental instance-vocabulary drift into a hard error (new OE: static check reads instance vocabulary) instead of a silent reclassification to runtime. There is no #[runtime] twin — a catalog-level check evaluated at build is simply done.

D2 — Runtime Error semantics: guard on the delta

An Error-severity instance check rejects any mutation that creates new violations: violations(post) ∖ violations(pre) ≠ ∅ ⇒ abort, atomically (RFD 0015 whole-body atomicity — nothing flushes), with the rendered diagnostics returned to the caller. Precedent: OE0668 (where-invariant) and OE0211 already reject writes this way; user checks generalize that gate.

  • Delta, not absolute state. Pre-existing violations (e.g. a new artifact’s stricter rules over old data) are reported through the observe channel but never block writes — absolute semantics would brick every subsequent mutation on a store with one legacy violation.
  • Post-state realization: committed state + the transaction’s buffered events, read through an overlay view — the runtime realization of exactly the overlay semantics Runtime/MutationSemantics.lean already models for mutate bodies. No speculative store clone, no compensating events; the guard runs before flush.
  • v1 computes violations(pre)/violations(post) by evaluating the check predicates on both views and set-differencing; incremental maintenance of violation relations is explicitly the RFD 0018 persisted-IVM follow-on (the delta is what semi-naive computes natively).

D3 — Severity drives blocking

Error guards (D2); Warning/Info observe only. #[observe] on an Error check opts out of guarding (report-only errors are legitimate during migration). Severities are the closed set Severity::{Error, Warning, Info}.

D4 — K3: fire on is only

Check bodies evaluate over the WFM in K3. A violation fires iff definitely derived (is); undefined (can) does not fire. Surfacing can-grade violations (e.g. at Info) is deferred. Documentation must state the OWA reading plainly: not statute(c.cite) is NAF — “no derivably known statute” — not a claim about reality.

D5 — The Diagnostic { … } payload is check-surface syntax, not a user value

Diagnostic, Severity, and the field set are interpreted by the compiler (like #[strict] strengths) — no user-space Diagnostic type exists or is needed. Validated at ox check:

  • severity: — exactly Severity::Error | Severity::Warning | Severity::Info (nominal).
  • code: — string literal, namespaced: must contain :: (e.g. "Lease::E001"), and the OE/OW prefixes are reserved for the compiler (extends #150’s hygiene rule to user space).
  • message: — string literal, or format!("…{}…", args) with positional {} only; each argument must resolve against the body’s bindings (variables / field chains), checked like any body term. Named/spec’d interpolations ({name}, {:?}) are refused loudly.
  • Unknown fields are errors; at: is reserved (refused with a “reserved for span attribution” note) until the LSP consumes it.
  • All three fields are required. (Amended by RFD 0026’s 2026-06-11 amendment (check members may pin their severity), issue #230: when the check implements a trait member whose signature pins a severity — check Member(Self) => Severity::…;severity: may be omitted and inherits the pin; a divergent restatement is OE0676. code: and message: stay unconditionally required.)

format! becomes a real (dedicated) expression form in the parser — the §13 macro system is not implied; format! is recognized structurally, the way count { … } is.

D6 — Delivery: the diagnostic stream now, the Diagnostics sink contract forward

Semantically a fired check is an emission to a reserved typed sink Diagnostics: Diagnostic (§7.7.1) — that contract is recorded here so the sink machinery subsumes delivery when it lands, and checks become its first real producer. Until then, delivery is direct:

  • Static discharge → the build/LSP diagnostic stream: violations render exactly like compiler diagnostics, under the check’s own user code, at ox check and ox build (build does not fail on user Error checks over declared EDB? — it does: a firing Severity::Error check at build is a build failure, same as any other error diagnostic; Warning renders and passes).
  • Runtime discharge → mutation results carry rejected-guard diagnostics; dispatch responses (oxc-serve) gain a diagnostics section for observe-channel violations (amended 2026-06-11: the shipped wire key is diagnostics, not the originally drafted $diagnostics — the $ sigil is the reserved internal-relation namespace, the wrong register for a JSON response key); ox query renders them; the violation set is queryable on demand.

D7 — Wire and drift

RuleDeclBody gains additive fields: the lowered diagnostic template (severity, code, message parts as literal/argument segments) and the #[static]/#[observe] markers. Lean @[language_interface] carriers align; the drift gate covers the new shapes. Existing .oxbin artifacts predate any consumer of these fields; no migration concern.

Non-decisions (tracked, out of scope here)

  • Artifact-upgrade pre-existing-violation reporting moment (load-time observe pass) — open.
  • can-grade violation surfacing (D4 deferral).
  • Per-standpoint checks; check members in traits (companion trait design — their discharge follows this RFD unchanged once monomorphized).
  • Incremental violation maintenance (RFD 0018 phase 2).
  • ox check --runtime <store> verb cosmetics; ox derive <CheckName> stays the debug surface.

RFD 0026 — Trait rule members: clause-union dispatch, conformance, and the implements intrinsic

  • State: accepted — implementation planned
  • Opened: 2026-06-10
  • Decides: the complete semantics of the trait atom’s contents — what may be declared inside pub trait and provided by impl Trait for Type, how rule members evaluate (clause-union with a static coverage gate), how fn/mutate members dispatch (receiver-resolved), the conformance obligations (completeness, orphan, no-overlap, supertraits), the reflective conformance surface (TraitRef, implements), and member naming. Settles issues #202/#203/#204 (Gustavo’s trait report) and the ArgUFO // TODO: implement trait feature demand. Design discussion: .local/research/traits/DESIGN-2026-06-10.md. Companion: RFD 0025 (check discharge; merges first — see Sequencing) — trait check members monomorphize into ordinary RuleMode::Check rules whose discharge follows 0025 unchanged, and this RFD amends 0025 D1 (see D6).

Prior state (verified at 8deb583): trait/impl declarations parse and reach the wire as shapes; bodies are swallowed by eat_balanced and lowered as Vec::new() (§12.2), so a rule member written inside an impl silently never exists — check and build stay green (#202). No calling convention, no conformance checking, no way to test conformance in a rule body (#204). The book promises only fn signatures in traits; rule members are new surface. The parser accepts <: for supertraits (grammar.rs::trait_decl) while STATUS documents : — a pre-existing three-way drift this RFD resolves (D5.5).


Question

A trait that carries nothing is a name. The recorded design lineage (redesign charters; the five-atom settlement) made the trait atom Argon’s behavioral contract: concepts say what something is; traits say what something can do. For a language whose behavior is mostly rules, “what something can do” must include rules — Gustavo’s pattern:

pub trait Adulthood {
    derive Adult(Self)
}

impl Adulthood for USPerson {
    derive Adult(p: Self) :- p.age >= 18
}

pub derive IsAdult(p: USPerson) :- Adult(p)

What does Adult mean as a predicate, who provides its clauses, what may call it, what guarantees conformance, and how does a program ask whether a type implements a trait?

D1 — Rule members evaluate by type-guarded clause union; dispatch is derivation

A trait declares the member signature; each impl contributes a clause. The impl above elaborates to the ordinary rule

derive Adulthood::Adult(p: USPerson) :- p: USPerson, p.age >= 18

(the head-parameter annotation injects the membership guard through the existing type-test lowering, which closes correctly over <: since #199). A second impl for another type adds a second clause with the same head — Datalog unions same-head rules natively. No new evaluation machinery exists or is added: monomorphized member rules flow through the classifier, stratifier, the #171 evaluability gate, and the reasoner as ordinary RuleDecl events.

This is not a departure from Rust dispatch — it is what dispatch becomes in a bottom-up engine. The fixpoint evaluator has no call sites; an atom is a join. “Select the body by the value’s runtime type” (Rust dyn) transposes to “select the clause whose type guard the individual satisfies.” The no-overlap rule (D5.3) keeps clause selection single-valued per declared type: at most one impl covers any declared sortal type, statically checked. Per individual, Argon’s multiple classification admits one residual the static check cannot close (an individual instantiating two <:-incomparable covered types); its semantics is defined, not accidental — see D5.3.

Wherever a Rust-static reading would compile at all (the argument’s static type covered by exactly one impl), clause union gives identical answers. The programs where it gives more are exactly the polymorphic rules the feature exists for:

impl Adulthood for USPerson     { derive Adult(p: Self) :- p.age >= 18 }
impl Adulthood for GermanPerson { derive Adult(p: Self) :- p.age >= 18, p.has_residence }

pub derive CanVote(p: Person) :- Adult(p), p.registered   -- heterogeneous, per-subtype

Rust expresses that with bounded generics (<T: Adulthood>) or dyn — machinery v0.1 defers (OE0667). Clause union delivers it with what the engine already does.

Specialization and inheritance are rejected (Ivan, 2026-06-10): traits are Rust-exact. No “more-specific impl wins” — overlapping impl targets are an error (D5.3), and any future priority-between-clauses story belongs to defeasibility, not dispatch. Traits form no subsumption lattice and never enter the concept lattice.

D2 — Two planes: all five member forms, split by how they are consumed

PlaneMember formsSemantics
Rule plane (joined)derive, check, queryClause union (D1). A check member monomorphizes to an ordinary RuleMode::Check rule per impl; discharge, payload, severity, and delivery follow RFD 0025 unchanged. Query members: below.
Invocation plane (called)fn, mutateSingle resolution by receiver. fn members resolve statically per §12.4 (inherent impls, then trait impls; UFCS Trait::foo(x); bare x.foo() when unambiguous). A mutate member (mutate close(self, reason: String); impls provide RFD 0015 imperative bodies) monomorphizes per impl; invocation dispatches on the receiver individual’s actual type to the unique covering impl — see D5.3 for the ambiguous-receiver refusal — through the existing mutation-dispatch surface (CLI / serve / SDK, suffix-aware per #180) extended with receiver selection. Mutations do not call mutations today; member mutates do not change that.

Query members, precisely. The trait-side signature is the full §7.4 query signature: query Ident '(' member-params ')' '->' TypeExpr ';'. The impl provides a full §7.4 body (select … from …); the trait signature fixes the parameter sorts, the return type, and thereby the projection shape every impl must produce. Self is admitted in parameter positions only; Self in the return type is refused (OE0675) — a return-position Self makes the union endpoint’s type a union over impl targets, which needs union types (#184) or bounded generics (V1); tracked as an explicit non-decision. With a Self-free return, both consumptions are well-typed: in rule bodies the member’s head participates in the §7.3.1 vocabulary like any query head; as a dispatch endpoint, Trait::member(args) returns the union of the per-impl results (each clause guarded by its target).

Member signatures in the trait must mention Self in at least one parameter position (else the member is not about the implementing type and belongs at module level — the diagnostic says exactly that). Multiple Self positions are legal; every Self position is guarded/substituted.

Out of scope, refused loudly (not silently): trait-side default bodies (a default rule body needs structural bounds on Self to typecheck against fields — V1 bounded-generics territory, OE0667), generic members, bounded generics, return-position Self, per-standpoint impls (recorded open question; impls are standpoint-global), associated type/const (already reserved).

Grammar scope (corrects a §5.4 conflict found in review): the member grammar above is the item grammar for trait impls (impl Trait for Type). Bare impls (impl Type { … }) keep §5.4’s item set unchanged (any declaration kind, including rel and associated items); conformance obligations D5.1/D5.2 apply to trait impls only.

D3 — The coverage gate: bare atoms require full coverage; partiality requires an explicit guard

Clause union’s one honest weakness against Rust: in Rust, calling a method on a type with no impl is a compile error; under naive union, a member atom over an uncovered type is just false — the silently-vacuous class this project exterminates on sight.

Coverage, defined. Coverage is computed over the workspace-closed catalog using the metacalculus sortality metaxis (§4.1): a declared type S is instantiable iff S is sortal (non-sortals — categories, mixins — carry no identity principle and admit no direct instances: every individual is classified through some sortal, so non-sortals need no covering themselves; their sortal descendants do). A static type T is fully covered by a trait’s impl set iff every instantiable declared S ⊑ T (including T itself when sortal) satisfies S ⊑ targetᵢ for some impl target. This is a finite catalog scan — decidable at ox check/ox build.

The gate:

  1. A bare member atom Adult(p) requires p’s static type to be fully covered. Not fully covered ⇒ OE1327, whose help names the uncovered instantiable types and offers both fixes — add the impl, or write the guard.
  2. Partial coverage is legal only under an explicit conformance guard: implements(meta(p), Adulthood), Adult(p). Dispatch that can fail is always visible in the source; uncovered individuals are excluded by the guard, not by silence.
  3. A member atom over a type with no covering impl anywhere is always OE1327 (the atom can never fire; the guard would be vacuous too — the help says so).

Guard semantics under multiple classification. meta(p) is multi-valued (one row per <:-minimal classifier, §4.4). The guard is existential by constructionmeta is a relation and the conjunction is a join: implements(meta(p), Tr) holds iff some minimal classifier of p is covered. That is the intended reading: the member can fire for p exactly when some covered classification applies, and the clause that fires is the one whose guard that classifier satisfies.

D4 — Naming: Trait::member qualified heads; catalog keyed by qualified path

  • The member predicate is one name: pkg::mod::Adulthood::Adult (the trait’s module owns it). Per-clause rule identities qualify further by impl target, so clauses are distinct events with one shared head.
  • The rule catalog’s runtime keying moves from bare short name to qualified path, with short-name lookup retained as the unambiguous-suffix resolver (the #180 pattern applied to rules). This fixes the pre-existing cross-module derive Foo collision class as a forced move.
  • Call forms in rule bodies: Adulthood::Adult(p) (qualified — always legal; qualified predicate atoms get wired by this work); Adult(p) (bare — legal when exactly one provider of that name/arity is in lexical scope; ambiguity is an error naming the candidates, mirroring §12.4’s collision rule). No postfix p.Adult() sugar (Ivan, 2026-06-10): it buys spelling only and collides with field-access space (p.Adult is OE0204 territory). Method-call syntax arrives with the invocation plane, where §12.4 already promises it for fns.
  • OE0223/OE0204 become trait-aware: when an unresolved name matches a trait member in scope, the help states the member, its trait, and the legal call forms.

D5 — Conformance obligations (elaboration-time, not check rules)

All catalog-closed, all enforced at ox check and ox build; none depend on the RFD 0025 runtime path:

  1. Completeness — a trait impl provides every member its trait declares. OE0670.
  2. Extraneous / mismatched member — a trait impl provides a member the trait doesn’t declare, or with a different signature (arity, parameter types, return type, plane). OE0671. (Bare impls are exempt — they have no contract; §5.4 governs them.)
  3. No-overlap (coherence) — two impls of one trait are rejected (OE0673) when their targets are <:-comparable or share a declared common descendant (both catalog-decidable). The second arm is what multiple classification demands: impl T for Person + impl T for Customer with a declared pub kind Employee <: Person, Customer would put every Employee under two clauses. The worked counter-example for the first arm: impl Adulthood for Person + impl Adulthood for USPerson is OE0673 — the natural “default + override” pattern is rejected by design (no specialization; write disjoint targets, or one impl whose body branches). Residual, defined: an individual dynamically classified under two <:-incomparable covered targets with no declared common descendant (pure multiple classification) cannot be excluded statically under OWA. Semantics: in the rule plane, clauses are independent sufficient conditions — both may fire and the union is their disjunction (well-defined, documented; not an error). In the invocation plane, an ambiguous receiver is a loud runtime refusal naming both impls — never an arbitrary pick.
  4. Orphan ruleimpl Trait for Type must live in the package declaring Trait or the one declaring Type (§12.4, previously reserved). OE0672.
  5. Supertraits as requires-constraintsimpl Sub for T demands impl Super for T exist (Rust-exact; not inheritance). OE0674. Surface token: supertraits are spelled : (pub trait Repaintable: Drawable). Honestly stated: (a) the parser today implements <: (and the shared supertype_clause also admits the specializes keyword — both removed for traits in slice 0) for trait supertraits (grammar.rs::trait_decl), while STATUS.md documents : — STATUS was wrong about what’s built; (b) : in concept headers means MLT instantiation — a distinct, non-conflicting context (a trait header is never a concept header; Rust likewise overloads : by position). Slice 0 switches the trait production to :, makes <: after trait Ident a parse error with a fix-it (no in-repo source uses it), and corrects STATUS. <: stays the concept-lattice operator exclusively.
  6. Self disciplineSelf resolves only inside trait/impl bodies (trait: the obligation’s type parameter; impl: the target type); misuse, a trait member signature with no Self parameter position, or return-position Self (D2) is OE0675.

Member forms whose implementation slice has not landed are refused at parse with OE1326 (TraitMemberNotYet) — the gate that makes #202’s silent swallow structurally impossible from slice 0 onward.

D6 — Reflective conformance: TraitRef and implements

  • TraitRef — a new reflective sort parallel to TypeRef (RFD 0023): values are handles into the closed declared trait catalog, carrier Value::Name. Deliberately not folded into TypeRef — traits stay off the concept lattice. TraitRef does not join the Metatype <: TypeRef <: Entity chain; it is a sibling sort under Entity.

  • implements(t: TypeRef, tr: TraitRef) -> Bool — the fifth reflection intrinsic, materialized as the RT-closed reserved-head relation $implements from ImplDecl events (the $iof/$specializes pattern, #132) with two closures folded in: supertrait closure (implements(T, Sub) → implements(T, Super) — the D5.5 constraint’s logical consequence, not inheritance) and target upward-coverage along <: (an impl for Person covers USPerson), coherent with #199.

  • implements is exempt from OE0212 (MetaArgUnbound): unlike the type-position arguments of meta/iof/specializes/extent, both positions of implements may be free — $implements is a finite catalog-closed relation and enumeration is the intended use (free t: all implementing types; free tr: all traits of a type; instance-level composes with meta(x), #135). §4.4’s intrinsic invariants are amended accordingly (the “type-position argument is a TypeRef value” sentence and the OE0212 rule both carve out implements; its second argument is TraitRef-sorted). Being catalog-closed also makes not implements(…) stratification-safe. An implementors(Trait) -> Set<TypeRef> expression-plane intrinsic rides the same materialization.

  • implements is available in both planes: rule atoms (including NAF) and boolean expressions (if/match scrutinees in fn/mutate bodies, executing since #192/#194).

  • Amendment to RFD 0025 D1 (applied to 0025’s text when both RFDs are on main; 0025 merges first): the catalog-level classification sentence

    a check is catalog-level iff every variable in head and body is TypeRef-sorted

    becomes

    a check is catalog-level iff every variable in head and body is reflective-sorted (TypeRef, TraitRef, or Metatype)

    so that trait-conformance checks — Gustavo’s PersonImplementsAdulthood — classify catalog-level and discharge at ox check through 0025’s harness, #[static] by convention. This RFD builds the vocabulary; 0025 owns discharge. Without the amendment, a free tr: TraitRef variable would misclassify the check as instance-level.

D7 — Wire, drift, and what does NOT change

  • Member rules are emitted as ordinary RuleDecl/ComputeDecl events — the runtime and reasoner load them with zero changes. No new AxiomKind.
  • ImplDeclBody.items (already Vec<CborValue>, “rule_decl-shaped”) carries the member rules’ identities/provenance for tooling and conformance — not evaluation. TraitDeclBody.methods carries member signatures (name, arity, parameter types, return type, plane). Both fields exist today as empty vecs; the wire was built for this (§12.2’s stated intent).
  • Lean Syntax/Decl.lean already models TraitDecl.items/ImplBlock.items : List Decl — the Lean was ahead; Rust catches up. Stale Lean doc annotations the token change invalidates (TraitDecl.supers “via <:”; Substrate/Trait.lean/Conditional.lean citing “spec §13”) are corrected in the Lean PR. Drift gate: TraitAtom/TraitDecl/ImplBlock shapes unchanged; the member-signature carrier aligns by the usual @[language_interface] discipline.

Lean obligations (before the Rust slices, per workflow)

  1. Monomorphization conservativity — elaborating a trait/impl catalog adds a finite concrete rule set; the extended program’s well-founded model restricted to the old vocabulary is unchanged, and stratification is preserved given D5.3. Restates the legacy parametric-rules result on the modern Reasoning/Datalog/ spine. The load-bearing theorem.
  2. Coherence instantiation, by construction — a nominal impl target is a TraitBound in Conditional.lean’s sense: bound.satisfied T := T ⊑ target. The obligation is to build that instantiation and prove: the D5.3 catalog check (no <:-comparable targets, no declared common descendant) implies unique applicability over declared sortal types, whence coherent_of_unique_applicable applies. The per-individual multiple-classification residual (D5.3) is outside the theorem’s scope and documented as such — the theorem speaks about types, not individuals.
  3. Conformance decidability — completeness/orphan/overlap/coverage/implements are finite catalog scans; structural tier (§10 ladder).
  4. Substrate/Trait.lean member-signature model mirroring TraitDeclBody.methods (+ the doc fixes from D7).

Sequencing and implementation plan (each slice lands whole and loud)

Merge order: RFD 0025 (checks) → this RFD (which then applies the D6 amendment text to 0025) → Lean PR → slices. The 0025 cross-links in this RFD and §12 resolve when 0025 lands.

  • Slice 0 — real trait-item/impl-item parsing for trait impls (kills eat_balanced; bare impls keep §5.4 parsing), scoped Self, supertrait token <:: (parse error + fix-it for <:; STATUS corrected), OE1326 gating of every not-yet-landed member form, OE0675.
  • Slice 1 — rule plane end-to-end: elaboration (Self substitution, guard injection, RuleDecl emission, items/methods population), qualified-path naming + qualified atoms + bare-call resolution, conformance gates OE0670–OE0674, coverage gate OE1327 (full-coverage form; the partial-coverage guard escape activates in slice 2 with implements), trait-aware OE0223/OE0204. Keystones: Gustavo’s program verbatim; multi-impl union; NAF over a member; recursion through a member; cross-module impl; the OE0673 counter-examples (comparable targets; declared-common-descendant).
  • Slice 2TraitRef, $implements, implements in both planes, partial-coverage guard rule, the 0025-D1 amendment in force (reflective-sorted classifier), examples/trait_contracts/ (corpus-pinned, including a #[static] conformance check).
  • Slice 3 — invocation plane: fn members + static dispatch (§12.4), mutate members + receiver dispatch (ambiguous-receiver refusal) through CLI/serve/SDK, oxc-gen emission for member endpoints.

Diagnostics allocated

CodeNameSite
OE0670ImplMemberMissingcheck/build (elaboration)
OE0671ImplMemberExtraneouscheck/build (elaboration)
OE0672OrphanImplViolationcheck/build (elaboration; §12.4’s reserved code, now real)
OE0673ImplTargetsOverlapcheck/build (elaboration)
OE0674SupertraitUnsatisfiedcheck/build (elaboration)
OE0675SelfMisusecheck/build (parse/elaboration)
OE0676ImplMemberSeverityDivergescheck/build (conformance; 2026-06-11 amendment)
OE1326TraitMemberNotYetparse-time capability gate
OE1327TraitMemberUncoveredcheck/build (coverage gate)

Renumbering note (slice 1): this RFD originally allocated OE1323 for TraitMemberUncovered, but RFD 0025 (which merged first) took OE1323–OE1325 for its check-diagnostic payload codes (MalformedCheckDiagnostic / CheckCodeNamespace / CheckMessageArgUnbound); the coverage gate ships as OE1327.

Amendment (2026-06-11) — check members may pin their severity (issue #230)

The final-validation audit (aa7a7d0e) probed two impls of one check member declaring divergent severities: Limited::OverLimit as Error on Truck (mutations rejected) and Warning on Crane (mutations pass). D5.2 compares plane/arity/param-types/return only, so the trait could not pin the blocking semantics of its own obligation. Decided (issue #230, option a): a trait-side check member signature MAY pin its severity with the payload-arrow suffix, mirroring the check payload form —

pub trait Limited {
    check OverLimit(Self) => Severity::Error;
}

Only the severity is pinnable — code: and message: stay per-impl (a full Diagnostic { … } payload at the trait side is refused, OE1323). Pinning is optional; an unpinned member keeps the per-impl freedom above (documented status quo, not an error). Semantics when pinned:

  • An impl’s member-check payload may omit severity: — it inherits the pin (the ergonomic point). OE1323’s “all three fields required” relaxes to: code: + message: required, severity: required unless trait-pinned.
  • Restating the same severity: legal (harmless).
  • Stating a different severity: OE0676 ImplMemberSeverityDiverges at ox check/ox build (conformance pass, collect-all), naming the trait, the member, the pin, and the divergent severity. Rationale: a contract whose blocking behavior varies by implementor is a weak contract.
  • #[observe] on an impl member whose pinned severity is Error remains legal (observe is a discharge-mode opt-out, not a severity change — RFD 0025 D3).

Wire: TraitMemberSig gains severity: Option<DiagnosticSeverity> (additive, serde-default; always None for non-check members). Lean: Argon.Substrate.Trait.TraitMemberSig.severity, with the conformance gate Argon.TypeSystem.Conformance.SeverityPinned joining the Conformant conjunction (the OE0671 model comparison becomes severity-blind SigShapeEq; severityPinned_invariant states the implementor-invariance headline).

Non-decisions (tracked, out of scope)

  • Bounded generics / conditional impls / default member bodies / return-position Self — V1 (OE0667 stands; the Conditional.lean theory is ready for it; return-position Self also waits on union types, #184).
  • Per-standpoint impls (vault open question; impls are standpoint-global).
  • Postfix predicate sugar p.Adult() (revisit with the invocation plane if wanted).
  • Modeling guidance is reference material, not normative: a classification (Adult) is a phase/iff concept; a trait is a capability/obligation contract — §12.8.

RFD 0027 — The meta-property plane: axis bindings, catalog tiers, value-position resolution, and substrate-neutral modifiers

  • State: committed — implemented end-to-end (S0 #276, S1+S2 #283, S3 #287, S4 = this arc’s final PR; bugs #242/#243/#229 closed)
  • Opened: 2026-06-11
  • Decides: how metaxis declarations and metatype axis bindings become real — validated at elaboration, persisted on the wire, materialized as catalog relations, and queryable in rule bodies (meta(t).rigidity == rigidity::anti_rigid); one unified name-resolution pass for value positions in rule bodies (axis values, enum constants, individuals, qualified names — closing a family of silent mis-evaluations); metatype-tier $meta/$iof rows (settles #229); a Value::Symbol carrier shared with enum constants (fixing enum type-erasure); and the substrate-neutral modifiers abstract and fixed that replace every compiler read of user axis vocabulary (the OE1327 "sortality"/"sortal" magic strings, the §7.5 dynamic-iof gate, §10.2 static-discharge rigidity, RP-004 VT-persistence defaults). Settles audit findings ufo-01/ufo-02/ufo-03/ufo-04 and issue #148’s constants half; motivated by the first external user collision (Tiago’s ArgUFO overlay, 2026-06-11). PR #209 parked citing a planned “RFD 0027 ambient-vocabulary cleanup” that was never written; this RFD takes the number and completes that program.

Prior state (verified at aa7a7d0e): metatype axis bindings parse and are then droppedlower_metatype_decl emits axes: Vec::new() (oxc-instantiate/src/lower.rs:2870; same for metarels at :2897); the metaxis value set {anti_rigid < semi_rigid < rigid} is persisted nowhere (MetaxisDeclBody.value_type hardcoded Null, lower.rs:2844-2845); there is zero validationpub metatype weird = { nonexistent_axis::bogus_value }; passes ox check and ox build clean. $meta/$iof materialize individual-tier rows only (oxc-runtime/src/lib.rs:3923-4040, gap note at :4012-4014); meta(Person) == kind silently derives nothing (#229). In rule bodies, any multi-segment path lowers to a variable named by the joined path (atom_lower.rs:1649-1655 path_term), so rigidity::anti_rigid trips OE1303 as an “unbound variable” — and the same resolver hole makes enum constants work on comparison-RHS only, individuals in predicate-argument position silently match everything, and qualified predicate atoms silently derive zero rows. The trait coverage gate decides instantiability by token-scraping the literal strings "sortality"/"sortal" from metatype declarations (oxc-driver/src/trait_conformance.rs:398-399) — an unqualified match any module’s axis can satisfy, and a one-letter typo silently disables the gate. The book Note (04-meta-calculus.md:44) claims the language “ships” the rigidity/sortality/identity_provision axes.


Question

pub metaxis and the metatype binding form exist so that vocabularies can classify their own types along their own dimensions — the meta-calculus is atom 1 of the language. The book already commits to the semantics: axis values “are not labels — the reasoner reads them” (crash-course §1), and the storage chapter commits axis values as (axis, target, value) meta_property events (“No UFO axis is a column”, 18-storage.md:7). The Lean storage model carries them (Storage/AxiomBody.lean:96-132). The implementation honors none of it.

The first external user hit the gap within a day of the trait arc shipping. Tiago’s ArgUFO overlay — UFO built as user-space vocabulary, exactly what the no-ambient-vocabulary doctrine (#208, #213) prescribes — writes:

pub derive isAntiRigid(t: TypeRef) :-
    meta(t).rigidity == rigidity::anti_rigid

and gets OE1303 naming rigidity::anti_rigid as an unbound variable. No phrasing works: the constant doesn’t resolve, the metatype tier isn’t materialized, and the binding isn’t even in the artifact. Meanwhile the compiler itself does read axis vocabulary — by magic string — in the one place it needs an answer (OE1327 instantiability), which is the same bug class the §3.4 gate (#213) eliminated one level down.

Two questions, then. (1) What is the correct end-to-end design for declared meta-properties — declaration, validation, persistence, materialization, query surface? (2) Where is the boundary between user axis vocabulary and substrate semantics — what may the compiler read?

Design lineage

The vault records both questions being answered before, halfway:

  • argon-ontology-neutrality/rigidity-removal-storage (sub-RFC, accepted 2026-04-30): tore the built-in Rigidity/SortalDependence enums out of the legacy kernel — “the largest remaining UFO leak in nous” — replacing them with a generic per-target meta_properties: BTreeMap<AxisId, Vec<MetaValue>>, with MetaValue::Symbol interned per (axis, value-name) pair. The (axis, target, value) event shape descends from this. But the cluster kept legacy helpers (is_rigid()/is_sortal()/provides_identity()) that read the axes by string — the storage went neutral, the semantics did not. The OE1327 magic strings are that compromise carried forward.
  • Beyond-OntoClean research (vault scratch, 2026-04-18): the useful meta-property space is open-ended — Fine’s essence-vs-modality refinement of rigidity, unity as a parameterized family (topological/morphological/functional/intentional), Mizoguchi role-ness and context-dependence, Lowe individuation-dependence, BFO’s dependence typology. No finite axis list a substrate ships can be right. Rigidity/sortality are formal-ontology-neutral (UFO/BFO/DOLCE all use them) but they are still ontological commitments — and the substrate does not need them; it needs their operational shadows.

This RFD completes the neutrality program: the 04-30 sub-RFC neutralized the data; this RFD neutralizes the semantics.


D1 — One value-position name-resolution pass

Every term position in a rule body (predicate arguments, comparison operands both sides, compute operands, head arguments) resolves names through a single pass, in priority order:

  1. Bound rule variable (appears in a positive body atom or is a head parameter).
  2. Enum constantStatus::Active, qualified or imported.
  3. Axis valuerigidity::anti_rigid, where the first segment resolves to a visible pub metaxis and the second to a value in its declared domain.
  4. Type reference — single-segment and qualified type names (TypeRef constant).
  5. Declared individual — bare or qualified (alice, people::alice).

Refusals are loud:

  • A multi-segment path that resolves to nothing is a hard error (new OE), never a variable.
  • A bare identifier used both as a rule variable and resolving to a declared constant is a hard ambiguity error naming both candidates (the lesson of Rust’s bindings_with_variant_name, promoted from lint to refusal): rename the variable or qualify the constant.
  • The existing path :: Ident metaEq sugar (07-rules.md:128) applies only when the left side is a bound term, after constant resolution fails to claim the path — the resolution order above is the documented disambiguation for A::B (already flagged ambiguous at 07-rules.md:155).

This single pass closes five verified symptoms at once: axis values as OE1303 “variables” (Tiago); enum constants failing on comparison-LHS while working on RHS; x == alice refused (#148); knows(x, carol) silently treating carol as a wildcard and over-deriving (the worst member — silent wrong results, zero diagnostics); and qualified individuals refused. Qualified predicate atoms deriving zero rows (rule compile keys joined paths, catalog keys short names) is the predicate-position sibling fixed in the same slice.

D2 — Declarations become real: wire + validation

  • MetaxisDeclBody persists the domain: unordered(values), chain(values) (declaration order is the order; the book’s a < b chains), or typed(TypeExpr, refinement?). The Lean AxisDomain shape (MetaCalculus/Axis.lean:38-48) already models this; the Lean storage mirror does not and gains it.
  • MetatypeDeclBody.axes / MetarelDeclBody.axes are populated with resolved bindings: axis = qualified NameRef (post-#213 discipline — the wire does not carry bare text), value = symbol (enumerated domains, bound with ::) or literal (typed domains, bound with =, e.g. weight = 1.0; :: vs = follows the existing grammar and the RFD 0017 note that = belongs to typed domains).
  • Validation at elaboration (new diagnostics, meta-calculus OE19xx range): unknown axis; value not in the axis’s declared domain (or literal fails the typed domain’s refinement — the OE0606 precedent); axis tier mismatch (for type axes bind only on metatypes, for rel only on metarels); duplicate axis in one declaration (OE1903, already specified in Wellformed.lean).
  • Per-target assertions ride the existing MetaProperty event ((axis, target, value) — the channel MLT’s #[order(N)] already emits), now actually consumed (D3).
  • Lean storage mirrors for MetaxisDeclBody/MetatypeDeclBody/AxisBinding are added; the Rust doc-comment claiming “Mirrors Argon.Storage.AxiomBody.AxisBinding” currently cites a declaration that has never existed and becomes true.

D3 — Catalog tiers: metatype-tier $meta/$iof and the $axis relation (settles #229)

  • The runtime Module indexes concept_id → metatype_id at load (ConceptDeclBody.metatype_id is already resolved on the wire since #213; it currently has zero non-test readers).
  • $meta and $iof gain metatype-tier rows: (type, metatype) for every declared type, plus the tower the book’s worked examples promise (meta(Person) == kind, meta(kind) == Metatype, 04-meta-calculus.md:95-99). Catalog-closed ⇒ NAF-safe, the same justification as $implements. This also unblocks MLT’s CL-rules (iof over types) as a side effect.
  • A new catalog relation $axis(target, axis, value) materializes the effective axis assignments: metatype-level bindings (D2) unioned with per-target MetaProperty assertions, per-target assertions taking precedence, functional per (target, axis) — a conflicting pair is a load-time error, not a silent choice.
  • Valence: single-valued per (target, axis). The legacy design admitted Vec<MetaValue> (the vault’s mode metatype binding two nature values); in the IS/CAN/NOT calculus that case is correctly “the metatype does not determine the value” — a CAN, i.e. no binding — rather than two simultaneous ISes. Multi-valued axes are a recorded non-decision: revisit with a concrete consumer, against the vault lineage. Cross-axis condition clauses from the legacy engine likewise stay out until someone needs them.

D4 — The query surface

  • meta(t) over a type evaluates via the metatype-tier $meta join (it already lowers correctly; the rows now exist).

  • Sort-directed projection. base.name where base is reflective: a TypeRef-sorted base resolves name as a declared field on the type (the documented meta(bar).x walk-up, 05-constructs.md:158-173 — unchanged); a Metatype-sorted base resolves name as a visible metaxis and lowers to a $axis join. No overload collision: fields live on types, axes live on metatypes. Tiago’s rule works as written:

    pub derive isAntiRigid(t: TypeRef) :-
        meta(t).rigidity == rigidity::anti_rigid
    

    t binds via the catalog $meta join; .rigidity joins $axis; the right side is a constant (D1). meta(t).rigidity == r with r free binds r. An unbound axis (the metatype binds nothing for it) yields no row — ordinary Datalog absence.

  • Chain comparisons. For chain domains, </<=/>/>= compare by declared position (crash-course §1 promises exactly this). Comparing symbols of different owners, or of an unordered domain, is a loud compile-time error where statically known and a loud runtime refusal otherwise — never an enum-variant-order fallback.

  • Type-tests over reflective sorts are catalog atoms. t : TypeRef ranges over all declared types; m : Metatype over all declared metatypes (the predicate forms of the promised extent(TypeRef)/extent(Metatype), 04-meta-calculus.md:109). Both bind positively, catalog-closed. This legalizes Tiago’s second attempt rather than refusing it. x : Entity stays refused for now, with a diagnostic that names the supported sorts and the catalog-atom binding idiom (its extent spans individuals ∪ types and needs its own design).

D5 — Value::Symbol: one carrier for axis values and enum constants

A new runtime value sort:

#![allow(unused)]
fn main() {
Value::Symbol { owner: NameRef, name: SmolStr, ord: Option<u32> }
}
  • Identity is (owner, name) — the interned-(axis, value-name) design from the 04-30 sub-RFC. ord is populated from chain domains at build (None for unordered/enum owners); equality ignores it; ordered comparison requires Some on both sides and equal owners, else refuses loudly.
  • Enum constants migrate to the same carrier. Today’s enum runtime carrier is a type-erased CBOR tag ({tag: "<variant>"}) under which two enums sharing a variant name compare equal — a live defect this RFD fixes rather than duplicates. Pre-release, no artifact compatibility is owed; the corpus regenerates.
  • Full sort cost, paid in full (no hollow rendering): CBOR encoding in the event log; CLI render_value renders owner::name; serve JSON renders a typed envelope with the qualified owner path (the #182 pattern); oxc-gen emits branded TS types per owner with exact (de)serialization; Lean wire mirror + drift coverage.

D6 — Substrate-neutral modifiers: abstract and fixed

The compiler never reads a user axis name. Anywhere. The substrate’s three genuine needs reduce to two ontology-neutral, PL-precedented bits, declared as modifiers:

Substrate consumerTodayBecomes
OE1327 trait-coverage instantiability"sortality"=="sortal" token scrapeabstract — no direct instances; abstract types are exempt from impl-coverage obligations
§7.5 runtime re-classification gate (insert/delete iof on an existing individual)rigidity::anti_rigid per the book Note; unenforcedfixed — classification decided at construction; insert/delete iof against a fixed-introduced type refuses loudly
§10.2 static check discharge; RP-004 valid-time iof persistence defaultsrigidity (Lean Rigidity inductive; design docs)the same fixed bit — fixed ⇒ membership constant ⇒ static discharge sound, VT-persistent
  • Placement. Both modifiers attach to metatype declarations (pub abstract fixed metatype category = { … };) — the metatype is the behavior bundle — and abstract is additionally allowed per-type (pub abstract type Vehicle { … }, the UML/PL convention). No override semantics: a type introduced by an abstract metatype is abstract, full stop.
  • Polarity: dynamic by default, fixed is the opt-in restriction. The §7.5 gate governs re-classification of existing individuals, not construction; for a data-systems language, membership churn is the normal case and is today’s behavior throughout the examples. The restriction is the constraint you declare — the same posture as check. (The keyword is fixed; sealed was rejected as colliding with the closed-hierarchy meaning in Kotlin/Scala/C#.)
  • Consequences for the book. The Note at 04-meta-calculus.md:44 (“the language ships the rigidity/sortality/identity_provision axes”) is deleted. Anti-rigid admission “through the axis assignment” is rewritten: admission comes from the absence of fixed; identity_provision has zero substrate consumers and becomes pure user vocabulary like everything else. ArgUFO writes pub abstract fixed metatype category = { sortality::non_sortal, rigidity::rigid }; — the modifiers carry the behavior, the bindings carry their ontology, and importing a package can never change mutation semantics because its author named an axis rigidity.
  • StaticDischarge.lean’s Rigidity inductive (:38-50) is re-grounded as the Kripke-semantics justification of fixed (rigid designation = fixed classification), not as a privileged axis.

D7 — Lean obligations

Surface settled (this RFD); per the workflow table the substrate semantics go Lean-first:

  1. Canonical model = the ternary axis relation (target, axis, value) with per-(target, axis) functionality — the (axis,value)-point instantiation of State C A cannot express chains or typed domains; the relation reading covers all three domains uniformly. This resolves the deferred wiring note in IsCanNot.lean:48-50 (the A-instantiation becomes a derived view of the relation).
  2. Chain-order semantics for symbol comparison; refusal semantics for cross-owner/unordered comparison.
  3. Catalog-closure of $meta (both tiers), $axis, and the reflective-sort extents, with the NAF-safety corollary — the #221 (Catalog.conformant_iff) pattern, including a decidable checker and non-vacuity witnesses on a concrete catalog.
  4. Decidability: axis atoms, metatype-tier reflection atoms, and reflective-sort type-tests are D1-tier (catalog joins); the classifier admits them.
  5. Modifier semantics: fixed ⇒ the mutation-boundary refusal theorem (no iof delta on fixed-introduced types in any successful mutation — extends the RFD 0015/0019 mutation semantics); abstract ⇒ the OE1327 oracle reads a wire flag (decidable, no string predicate); the static-discharge soundness hypothesis re-stated over fixed.
  6. Wire mirrors (D2) under @[language_interface], acknowledging the known drift-gate limitation that structures are prose-aligned — the mirrors are still written, and the lying comment dies.

D8 — Diagnostics inventory

New codes in the meta-calculus range (numbers assigned at implementation against the live catalog; the trait arc’s OE1322→OE1326 renumbering is the cautionary precedent): unknown axis in binding; axis value not in domain / literal fails typed-domain refinement; axis tier mismatch; unresolvable multi-segment path in rule value position; variable/constant ambiguity; cross-owner or unordered symbol comparison; insert/delete iof against fixed; instantiating an abstract type; Entity type-test refusal (help names the supported idioms). Amended help texts: OE1303’s “variable(s)” wording when the offender is an unresolved path (point at the path, not at range restriction); OE0223 on reflective sorts (now legal per D4 — the remaining refusal case is Entity).

Alternatives considered

  • std-shipped well-known axes (std::meta::rigidity, …): rejected. Re-creates the #208/#213 leak one level up — a blessed package whose names carry compiler semantics — and the beyond-OntoClean record shows no finite list is right. The book Note that implied this dies.
  • Per-value marker attributes on axis declarations (#[dynamic_classification] on anti_rigid): rejected — the substrate bit is per-value in that encoding, the declaration site becomes clutter, and importing a vocabulary still changes substrate behavior.
  • Keep the magic strings: rejected; it is the audited bug (ufo-01), behaviorally confirmed — a typo silently disables a soundness gate.
  • Multi-valued axes (Vec<MetaValue>): deferred with the K3 ‘can’ reading as the principled alternative; see D3.
  • Static-by-default polarity for classification mutability: rejected for data-systems pragmatics — it makes the common case ceremonial and breaks every existing example; the ontological reading is recovered exactly by declaring fixed.
  • A separate axis-value sort distinct from enum constants: rejected; two symbol-like sorts with different equality and rendering rules is incoherence by construction, and the enum carrier needed the fix anyway.

Sequencing

  1. S0 — D1 resolution pass + the two filed silent bugs (individual-as-wildcard, qualified predicates). Independent of everything else; converts silent-wrong to loud immediately.
  2. S1 — D2 wire + validation + Lean storage mirrors.
  3. S2 — D3 catalog tiers ($meta/$iof metatype tier, $axis), closing #229.
  4. S3 — D4 query surface + D5 Value::Symbol (including the enum migration), full rendering chain.
  5. S4 — D6 modifiers + consumer migration (OE1327, §7.5 gate, static discharge, VT defaults)
    • the book rewrite (Note deletion, §7.5, §12.4, crash-course).
  6. Lean (D7) leads each slice’s semantics per the workflow table; the mutation-gate theorem lands with S4.

Each slice is loud-complete on its own: no slice ships a parsed-but-inert surface.

Relationship to existing issues

  • #229 — settled by D3 (metatype-tier rows) + D4 (the classifier admits catalog-level bodies).
  • #148 — the constants half is settled by D1; the equality-binding half stands.
  • #209’s “RFD 0027” parking reference — this document.
  • Audit register: ufo-01 (magic strings → D6), ufo-02 (hollow wire → D2/D3), ufo-03 (phantom Lean mirrors → D2), ufo-04 (the boundary ruling → D6), gs-09 (axis sugar diagnostics → D1/D8), plus the enum type-erasure finding (→ D5).
  • #150 — the new codes land catalog-first; no raw-string emissions.

RFD 0028 — Defeasibility redesign: honest heads, the defeat-directive plane, and strategy as a compilation scheme

  • State: accepted — implementation planned
  • Opened: 2026-06-11
  • Decides: the complete replacement of the §7.8 strength-attribute surface (#[strict]/#[defeasible]/#[defeater]/#[priority], pub priority) with honest-head rules plus a defeat-directive plane — unmarked rules stay strict/classical, #[default] marks an overridable rule, #[defeats(target(args))] declares the attack, #[label(name)] gives a clause an identity — with head-level, clause-level, and trait-qualified targeting all in v1; the strategy-as-compilation architecture: the core language owns four strategy-neutral hooks (rule identity in the catalog, defeat edges on the wire, a transform slot between lowering and stratification, a provenance channel), and Governatori-with-explicit-superiority is strategy #1, specified as its compilation to the core stratified/WFS semantics; proof tags (+Δ/−Δ/+∂/−∂) finally surfaced through the provenance channel; and a loud migration — no silent aliasing. Settles the #244 design thread (including both correction comments: the grammar-clause rejection and the final lock) and stage 2 of #245. Hard prerequisite: the directive registry (audit dc-01). This RFD leads the arc per the workflow table — language surface: RFD + reference draft → Lean → code.

Prior state (verified at d26dc626): the attribute walker maps the strength triple onto RuleDeclBody.rule_strength (oxc-instantiate/src/lower.rs:145-147; wire field oxc-protocol/src/storage.rs:784; Lean mirror RuleStrength in Locality/DefeasibleExtraction.lean under @[language_interface]), and a fused strength-stratified evaluator computes final[H] = strict[H] ∪ (defeasible[H] \ defeater[H]), keyed by head name, blocking by exact head-tuple match (oxc-runtime/src/lib.rs:4810-4823). #[priority(N)] is silently dropped — the audit executed the repro (sf-02, probe p18: a program written to §7.8’s priority semantics computes different conclusions with zero diagnostics; #245) — and the pub priority { } superiority blocks §7.8 documents don’t parse at all: book-ahead prose presented as available. Unknown attributes are silently ignored across the board (audit dc-01: #[defeasable] passes ox check and silently strictens the rule). The whole §7.8 diagnostics block — OE0411–OE0414 — is phantom: zero presence in the catalog or the compiler (audit dc-03). The wire proof_tag field exists (storage.rs:1283, “Governatori-Rotolo 4-slot proof tag”) but lowering always writes None (lower.rs:6755) and its only live writer is fork promotion squatting it with promoted_from:{fork} (oxc-serve/src/lib.rs:2568).


Question

We lost a morning to three lines of the canonical example (examples/legal_norms_can_vote/norms.ar):

#[strict]     pub derive can_vote(p) :- SpecialClass(p);
#[defeasible] pub derive can_vote(p) :- Adult(p);
#[defeater]   pub derive can_vote(p) :- Felon(p);

The third rule spells the head it denies. §7.8 itself defines a defeater as A ⇝ ¬B — blocks B without asserting either — yet the surface writes can_vote(p) :- Felon(p). Everyone reads it as “felons can vote.” The attribute silently inverts the polarity of the head at a distance, which is unacceptable as a modeling surface — and the book even ships a phantom diagnostic (OE0413 DefeaterAssertsConclusion) acknowledging the confusion it documents.

Four defects, one design (#244):

  1. The defeater writes the head it denies — the polarity flip lives in an attribute, invisible at the head.
  2. No rule identity, no targeted defeat — attack resolution is head-name-only; the only priority knob is a global integer that is the wrong abstraction (magic numbers don’t compose across modules or packages) and is silently discarded today; the pub priority blocks are unimplemented prose.
  3. No surface notion of a default rule#[defeasible] is logician-speak; modelers think in defaults and exceptions.
  4. Exceptions aren’t compositional — an exception must be written as the attacked predicate, in its terms; it can’t live in another module or impl under its own honest name. Post-#217 the rule catalog is qualified-path keyed, so the addressing substrate for cross-module targeting now exists.

Two questions, then. (1) What is the right surface — how does a modeler write defaults, exceptions, and attacks so that every rule reads true? (2) What does the core language own, versus what belongs to a particular flavor of defeasible reasoning — Governatori-style defeasible logic is one strategy among several (default logic, courteous LP, argumentation, ASP preferences), and the substrate must not privilege it.

D1 — Honest heads: a rule derives exactly what its head says

Unmarked = strict. A bare derive rule is classical, exactly as today — a Datalog program keeps meaning what it always meant, and strict conclusions cannot be overridden. #[default] marks an overridable rule (the Rust-specialization precedent — default fn — an established PL concept carrying exactly the right reading: this clause holds unless something more specific displaces it). An exception is an ordinary rule with its own honest head; the attack is carried by a directive (D2), never by the rule’s syntax. The canonical example becomes:

// strict = unmarked: special-class members vote, period
pub derive can_vote(p) :- SpecialClass(p);

// the overridable default
#[default]
#[label(adult)]
pub derive can_vote(p) :- Adult(p);

// the exception: an honest head, and the attack as a directive
#[defeats(can_vote(p))]
pub derive disenfranchised(p) :- Felon(p);

Every rule now reads true: special-class members can vote; adults can vote by default; felons are disenfranchised, and that disenfranchisement defeats the default. The pure defeater — block without asserting anything anyone consumes — is recovered as the degenerate case: a #[defeats(…)] rule whose head no other rule or query reads. Heads never lie; the polarity flip is gone because there is no flip.

Numeric priority is subsumed, not replaced like-for-like: lex specialis becomes the specific rule defeating the general clause’s label — an explicit, resolution-checked, cross-package-stable edge instead of a pair of magic integers that only mean something relative to each other.

D2 — The attack is a directive, not grammar

Defeat edges are meta-level — statements about rules, not conditions in them. The #244 body sketched a trailing defeats … clause after the rule body; that sketch is rejected (first correction comment): trailing position reads as part of the body, conflating the object level (what the rule derives) with the meta level (which other rules it displaces). The right surface is the directive plane above the rule — Argon’s established home for argument-bearing compiler directives (the §13.5 MLT relational decorators already resolve type arguments and emit classification wire events; this is the same shape with rule-and-variable arguments instead of type arguments).

No new grammar keywords. A default modifier keyword was also rejected: default and defeats are strategy vocabulary (D6), and strategy vocabulary must not be privileged in the core grammar — the same neutrality reasoning as the no-ambient-vocabulary doctrine (#208), applied to reasoning strategies. The directives:

DirectiveOnMeaning
#[default]a derive rulethis clause is overridable; it survives unless an applicable attacker blocks it
#[defeats(target(args))]a derive rulewhen this rule’s body fires, it blocks the targeted conclusion for the bound tuples (D3)
#[label(name)]a derive rulegives the clause an identity, referenced as head.label; duplicate labels per head refuse

Hard prerequisite: the directive registry (audit dc-01) lands before or with these directives. Today an unknown attribute is silently ignored — #[defeasable] silently strictens a rule — and with #[defeats] carrying legal meaning, a typo must never silently change what a norm program concludes. Unknown or malformed directives refuse loudly; the registry is the gate.

D3 — Targeting: head-level, clause-level, trait-qualified — all in v1

All three targeting forms ship in v1; there is no interim subset (locked: no shortcuts).

  • Head-level#[defeats(can_vote(p))]: attacks every #[default] clause of that head.
  • Clause-level#[defeats(can_vote.adult(p))]: attacks exactly the clause labeled adult. head.label is the reference form; labels are per-head identities (D2).
  • Trait-qualified#[defeats(Vote::can_vote(p) @ A)]: attacks a trait member’s clause at a given impl target, using the post-#217 qualified catalog naming (Trait::member @ Type).

Targets are resolution-checked at elaboration (goto-def-able): an unresolvable target — no such head, no such label, no such qualified member — refuses loudly.

v1 scope (amended 2026-06-13, PR #354). Defeat-edge resolution is file-local in v1: a #[defeats] target resolves against the rule catalog of the module that declares the attack, and a target naming a head/label/member outside that file refuses as unresolvable (OE0716). This is consistent with D7’s structural guarantee — selection is per module, so a connected defeat graph is single-strategy because edges resolve within the module that declared them. It narrows the cross-package aspiration sketched above for the trait-qualified grain (“a regulation package can defeat a clause it does not own”): the addressing substrate (post-#217 qualified catalog naming) exists, but workspace-scoped resolution requires lifting the defeat pass out of per-file elaboration into a combined-artifact pass — a build-pipeline change deferred so it does not ride alongside the two correctness fixes this PR lands. Cross-module composition through honest heads (strategy-independent) is unaffected and works today. Tracked: #362. Rationale recorded per the locked-design amendment discipline (AGENTS.md “RFDs record settled designs”).

Successor amendment (RFD 0082, discussion state — this note becomes settled when 0082 commits). The deferral above is discharged by that design: #[defeats] targets resolve over the importing module’s import-scoped catalog (its own rules plus the pub rules reachable through its use imports), in a combined-artifact pass at workspace assembly. Defeat edges stay in-package — the cross-package aspiration remains open (RFD 0082 D19).

Directive arguments resolve against the decorated rule’s variables (head + body). The argument binding ties the attacker’s tuples to the target’s: #[defeats(can_vote(p))] on disenfranchised(p) :- Felon(p) blocks can_vote exactly for the p the attacker derives — per-tuple blocking, not head-wide suppression. An argument name that does not bind in the decorated rule is a loud error, never a fresh variable — the #242 lesson: a name silently treated as a fresh variable is how knows(x, carol) matched everything. The same single resolution discipline as RFD 0027 D1 applies inside directive argument position.

D4 — Attack discipline

  • Strict conclusions are unattackable. A #[defeats] target that resolves to a head or clause not marked #[default] is a loud error. Adding rules to a classical program can only add conclusions; defeat exists only where overridability was declared. (Successor amendment, RFD 0082 D16: this refusal carries the consent story across the module boundary unchanged — #[default] is the token by which a rule’s author consents to being overridden from another file of the same package; no additional cross-module consent annotation exists.)
  • Defeat-graph cycles are refused loudly in v1. The graph is over resolved rule identities (clauses and heads), known at elaboration; acyclicity is decidable at build time (D10). Cyclic attack structures are exactly where the well-behaved compilation stories diverge; v1 refuses rather than picking one silently.
  • Defeated defeaters are legal. A #[defeats] rule may itself be #[default] and be the target of another attack — the exception to the exception — subject to the cycle gate.
  • Ambiguity blocking is retained (current behavior): a blocked tuple is simply absent from the head’s extent; it does not propagate a third truth value downstream. Ambiguity propagation is a different strategy (D7), not a switch on this one.
  • Team defeat, recorded explicitly: survival is support-based, per tuple. A tuple is in the head’s extent iff some clause not attacked on that tuple derives it — clause-level targeting blocks only the labeled clause’s contribution; head-level targeting blocks all #[default] clauses. An unbeaten teammate keeps the conclusion. This is the reading clause-union forces, and it is recorded here as the chosen semantics rather than left implicit; stricter team-defeat variants belong to future strategy vocabularies.

D5 — Strategy = compilation scheme, not engine mode: the four hooks

The architectural decision (locked): a defeasibility strategy is a compilation scheme onto the core semantics, never an engine mode. The engine — classifier, stratifier, reasoner — stays strategy-blind. The core language owns exactly four strategy-neutral hooks:

  1. Rule identity surviving lowering into the catalog. rule_id and qualified_path already survive (post-#217); #[label] extends identity to the clause grain. Identity is catalog-addressable — the same addressing that makes targets resolution-checked.
  2. Defeat edges as resolved wire metadata in the .oxbin. Each edge: attacker identity, resolved target set, argument binding. Resolved at elaboration — the wire never carries bare text to be re-resolved downstream (the #213 discipline).
  3. A transform slot in the pipeline, between lowering and stratification. The strategy compilation consumes the declared program + markers + edges and emits a core stratified/WFS program. Everything downstream of the slot sees ordinary rules.
  4. A provenance channel on derived tuples (D8) — the strategy maps its proof statuses onto it; the channel itself is strategy-neutral.

A strategy is then a quadruple: (directive vocabulary, compilation scheme, tag mapping, correctness proof). The RuleStrength wire field and its Lean mirror retire with the surface triple — hook 1 supersedes the strength field: once the artifact’s defeasibility metadata carries default markers, labels, edges, and the strategy id (D6), a wire-level strength is no longer a hook but a strategy-internal derivative, reconstructed inside the transform where the compilation needs it (the retirement is carried as an explicit Lean obligation, D10.6).

Module extraction (§3.5) operates pre-transform, over labeled rules + defeat edges — which is exactly the granularity Locality/DefeasibleExtraction.lean already mechanizes (defeat_complete_preserves over rule identities and a superiority relation). The hooks were, in this sense, already proven before they were named.

D6 — Strategy #1: Governatori with explicit superiority, as its compilation

The first (and v1 only) strategy is Governatori-style defeasible logic with explicit superiority and ambiguity blocking, specified as its Maher-2021-style compilation onto the core stratified/WFS semantics. §7.8 already commits that “no separate reasoner is invoked; the existing stratified-fixpoint machinery is reused” — this RFD promotes that from implementation note to definition: the meaning of a #[default]/#[defeats] program is the meaning of its compiled core program. Superiority is the explicit edge set (D3); there is no implicit priority anywhere.

The strategy id is recorded in the .oxbin defeasibility metadata. v1 programs get it implicitly (the exact identifier is assigned at implementation); the field exists from day one so that artifacts are honest about which compilation gave them their meaning, and so that per-module strategy selection (D7) has a place to land.

The existing fused strength-stratified evaluator may survive only as a fast path under the oracle obligation (D10): it must agree with the compiled program on every input, checked by the RFD 0018 differential discipline. If it can’t be kept in agreement, it dies; the compiled program is the semantics either way.

D7 — Strategy evolution: per-module vocabularies, editions, and the tier ladder

  • Future strategies arrive as use-imported macro-vocabulary packages once the macro atom matures (§13 V1): a package exports its directive vocabulary plus its compilation scheme. Selection is per module, which structurally guarantees that a connected defeat graph is single-strategy — edges resolve within the module that declared them, and cross-module composition happens only through honest heads, which are strategy-independent.
  • Migration off the implicit default is edition-shaped: a deprecation window plus a mechanical migration tool (the rustfix precedent). No flag-day; no silent reinterpretation of existing programs.
  • Cost containment: a strategy whose compilation needs more than stratified/WFS does not get a new engine — it lands on the §9 tier ladder: the #[brave]/stable-model tier or the ARS external-solver layer (§17). Strategy choice can change a program’s tier; it can never change the engine.

D8 — Provenance: the proof-tag channel, un-squatted

The Governatori proof tags — definitely provable, −Δ definitely refuted, +∂ defeasibly provable, −∂ defeasibly refuted — are finally surfaced on derived tuples through the provenance channel (hook 4). The tag mapping is strategy-owned: strategy #1 maps its compiled strata onto the four tags; a future strategy maps its own statuses. The §7.8 query surface for tags (match over /+∂/is unknown) stays as specified; this RFD supplies the channel it was always waiting for.

The wire proof_tag field is to be un-squatted: its only current writer is fork promotion stuffing promoted_from:{fork} into it (oxc-serve/src/lib.rs); fork lineage gets its own field, and proof_tag carries proof tags.

S3 status (amended 2026-06-13, PR #354). Proof tags are surfaced at query time today — Store::query_derive_explained computes each surviving tuple’s / +∂ tag from the compiled strata (the user-visible ox derive --explain surface, §7.8 tag queries). No proof tag is persisted to AxiomEvent.proof_tag (the comptime lifter writes None), so the fork-promotion squat does not collide with any proof-tag writer yet — but it is still a wrong use of the field. Moving fork lineage to its own field touches the @[language_interface] AxiomEvent wire shape (its Lean mirror, the oxbin codec, the Postgres column, and ~28 struct constructions), so the un-squat is deferred out of this correctness PR rather than landed beside the F1/F2 transform fixes. Tracked: #363.

D9 — Migration: loud deprecations, the §7.8 rewrite, diagnostic fates

  • #[strict], #[defeasible], #[defeater], #[priority], and pub priority become loud deprecation errors pointing at the new forms. No silent aliasing — a norm program’s meaning never changes without the author seeing it. #245’s stage 1 (the interim loud refusal of #[priority]) is independent and lands first; this RFD is stage 2, removing the attribute for good.
  • §7.8 is rewritten around the hooks/strategy split: the strength-attribute prose, the #[priority] section, and the pub priority blocks all die; the chapter specifies honest heads + the directive vocabulary as strategy #1 over the four hooks.
  • examples/legal_norms_can_vote is migrated to the D1 form — the canonical example must read correctly, since misreading it is what started this.
  • Diagnostic fates (all four are phantoms today — audit dc-03 — so nothing is removed from the catalog, only from the book): OE0413 (DefeaterAssertsConclusion) is moot — no rule can assert what a defeater denies, because no rule spells a head it denies. OE0412 (PriorityNotIntegral) dies with #[priority]. OE0414 (MixedStrengthAtHead) dissolves — strict and #[default] clauses sharing a head is the intended idiom (the special-class rule next to the adult default), not a hazard. OE0411’s concern (defeat cycle) survives as the new cycle-refusal code, under a fresh number (D11; no reuse).

D10 — Lean obligations

Surface settled (this RFD); per the workflow table the substrate semantics go Lean-first, leading each implementation slice:

  1. Transform-correctness theorem. The compiled core program, evaluated under the existing stratified/WFS semantics, realizes the declared defeasible semantics: a per-tuple defeasibleWarranted-style specification over labeled rules + defeat edges (the DefeasibleExtraction.lean shape, lifted from rule grain to tuple grain with argument bindings) agrees with the compiled program’s model.
  2. Fused-evaluator agreement. If the strength-stratified evaluator is retained as a fast path, it agrees with the compiled program as oracle on all inputs — the RFD 0018 differential-oracle discipline (semi-naive = oracle precedent).
  3. Defeat-graph acyclicity is decidable — decidable checker plus non-vacuity witnesses on a concrete program (the #221 conformant_iff pattern).
  4. Compatibility with defeat-aware module extraction (§3.5). defeat_complete_preserves already operates over rule identities + superiority edges, pre-transform; restate it over the new edge carrier and prove extraction-then-transform agrees with transform-then-extraction on the extraction signature.
  5. Narrowing soundness re-keyed. The existing restriction — only narrowings established by strict rules are preserved under defeasible attack (TypeSystem/Soundness/Defeasibility.lean) — is restated over #[default]: the counterexample and the strict-only soundness theorem carry over unchanged in substance.
  6. Wire mirrors under @[language_interface] for the defeasibility metadata (labels, edges, strategy id); the RuleStrength mirror retires with the surface triple.

D11 — Diagnostics inventory

All codes proposed; numbers assigned at implementation against the live catalog (the OE1322→OE1326 renumbering is the cautionary precedent; #150’s catalog-first discipline applies — no raw-string emissions). The 04xx range is proposed for the defeasibility plane: it is unallocated in appendix C’s range table, and the phantom OE0411–OE0414 die rather than being reused — no number reuse, per registry hygiene.

  • Unknown directive — the registry gate itself (dc-01; attribute subsystem, 07xx range): an unrecognized or malformed #[…] refuses loudly.
  • Unbound directive variable — a #[defeats] argument that binds in neither the head nor the body of the decorated rule; names the offender and the rule’s bound set; never a fresh variable.
  • Unresolvable defeat target — no such head, no such label on that head, no such qualified trait member.
  • Defeats a strict conclusion — the target resolves to a head/clause not marked #[default].
  • Defeat-graph cycle — names the cycle, rule by rule.
  • Duplicate label per head — two clauses of one head labeled identically.
  • Deprecated strength attribute#[strict]/#[defeasible]/#[defeater]/#[priority] or pub priority; the help text shows the new form for the specific case (migration, D9).

Alternatives considered

  • Trailing defeats … grammar clause (the #244 body sketch): rejected — it reads as part of the rule body, conflating the object level with the meta level; defeat edges are statements about rules, and the surface must say so (first correction comment).
  • default / defeats as grammar keywords: rejected — strategy vocabulary must not be privileged in the core grammar; the substrate stays neutral between defeasibility strategies exactly as it stays neutral between ontologies (#208’s reasoning, one level up). The directive plane carries the vocabulary; the grammar carries nothing.
  • Numeric #[priority(N)] + pub priority superiority blocks: removed — global integers are non-compositional across modules and packages (whose 10 beats whose 7?), and both are dead in practice: the attribute is silently discarded (sf-02, #245) and the blocks never parsed. Explicit edges subsume the use cases: lex specialis is the specific rule defeating the general clause’s label; derived superiority (lex posterior over enactment dates) belongs to a future strategy vocabulary that derives edges, not to core.
  • Engine-mode pluggable strategies (a strategy enum the reasoner switches on): rejected — the engine stays strategy-blind; compile-to-core keeps one trusted kernel and one mechanization target. The evidence the hooks suffice for the known strategy space: grounded argumentation semantics is the well-founded semantics of the argument meta-program (Dung 1995); courteous logic programs compile to LP with NAF (Grosof 1997); Governatori-style defeasible logic compiles to three strata of stratified Datalog (Maher 2021). Each is a quadruple over the same four hooks.
  • Head-level-only targeting in v1: rejected — no interim subsets (locked: do the full thing). Clause-level and trait-qualified targeting are where cross-module exceptions live — defect 4 — and the addressing substrate (post-#217 qualified catalog naming) already exists; shipping head-level-only would re-create the compositionality gap this RFD exists to close.

Recorded non-decisions

  • Trait-side strength pinning (the #240 shape): whether a trait can pin its members’ overridability, and whether one impl’s clause can be #[default] while another’s is strict — open, recorded in #244; nothing in this design forecloses it.
  • Ambiguity propagation and stricter team-defeat variants — future strategy vocabularies, not switches on strategy #1 (D4, D7).
  • Strategy-package format — the exact shape of a use-imported strategy vocabulary awaits the macro atom (§13 V1); only the hooks contract is fixed now.
  • Temporal proof tags (the 4-slot Governatori-Rotolo schema) — stay deferred exactly as §7.8 already records.

Sequencing

Each slice is loud-complete on its own — no slice ships a parsed-but-inert surface — and the Lean obligations (D10) lead each slice’s semantics per the workflow table. One deliberate deviation from the “RFD + reference draft” pairing: the §7.8 rewrite waits for S4 rather than landing with this RFD — rewriting the chapter now would describe an unimplemented surface, repeating the exact book-ahead-of-code failure (pub priority, OE0411–OE0414) this RFD retires; until S4, §7.8 carries a superseded-pending marker pointing here.

The slices:

  1. S1 — hooks. Rule identity and #[label] through lowering into the catalog; defeat edges as resolved wire metadata; the directive registry lands here as the prerequisite (dc-01) — unknown directives refuse loudly before any new directive exists to typo.
  2. S2 — vocabulary + transform. #[default]/#[defeats] resolution and validation (D3/D4 gates); the strategy transform in the pipeline slot; the compiled program is the semantics, with the fused evaluator retained only under the oracle check (D6, D10.2).
  3. S3 — provenance. Proof tags through the channel; proof_tag un-squatted (fork promotion gets its own field); the §7.8 tag-query surface lights up.
  4. S4 — migration. Loud deprecation errors for the old surface; legal_norms_can_vote migrated; §7.8 rewritten around the hooks/strategy split; the phantom OE041x block dies with it.

Relationship to existing issues

  • #244 — settled by this RFD, incorporating both correction comments (directive plane over grammar clause; the final lock).
  • #245 — stage 1 (interim loud refusal of #[priority]) is independent and lands first; stage 2 is settled here: the attribute and the pub priority prose are removed for good.
  • Audit dc-01 — the directive registry is the hard prerequisite (S1); dc-03 — the phantom OE0411–OE0414 citations die in the §7.8 rewrite; sf-02 — the executed priority-discard repro is resolved by removal, not implementation.
  • #242 / #243 — the loud-unbound rule for directive arguments and resolution-checked qualified targets are those lessons, applied at the directive plane (D3).
  • #240 — trait-side pinning stays open; recorded non-decision.
  • RFD 0010 (negative facts) — orthogonal: defeat blocks, it does not assert negative catalog facts.
  • RFD 0026 / #217 — qualified member naming is the targeting substrate for Trait::member @ Type defeat targets.
  • RFD 0018 — the differential-oracle discipline governs the fused evaluator (D10.2).
  • #134 — the WFS mechanization catch-up; the transform-correctness theorem (D10.1) lands against the same Reasoning/Datalog layer.

RFD 0029 — Derived values and aggregate terms: body-level binding, aggregate sources, rounding

  • State: accepted — implemented in this PR
  • Opened: 2026-06-12
  • Decides: the quantitative-modeling wall of the v0.2.1 program — that a rule can derive a computed numeric value and compare two aggregates. One mechanism: a body-level binding atom x = expr (single =, distinct from the comparison ==) that binds a fresh variable to the value of expr, where expr ranges over bound vars, literals, field projections, arithmetic over the exact tower, and aggregate expressions. Aggregate sources extend the existing comprehension form with comma-separated body atoms and admit relation atoms as sources; the brace form stays count/exists-only. Grouping is the outer bound variables (the standard Datalog reading). A rounding builtin family (round, round_half_even, trunc) is added to rule-body expressions and the fn/compute plane, banker’s rounding the money default. The exact-Decimal mutate-body status note is killed (the path already routes through the exact canonical core). The CLI/demo harness accepts decimal arguments for Decimal/Money/Real params. Settles register items R-B2, R-B3, R-B4, R-M4, R-M5, R-M7, and the harness half of R-B10.
  • Prior art: Datalog/Soufflé assignment (x = expr binds; range-restriction over the computed value) — Soufflé’s = constraint and arithmetic functors are the direct precedent ([Jordan, Scholz, Subotić, Soufflé: On Synthesis of Program Analyzers, CAV 2016]; the Soufflé manual’s “Assignments” and “Aggregates” chapters). Stratified-aggregate semantics follow Faber–Pfeifer–Leone 2011 (the aggregated predicate must live in a strictly-lower stratum).
  • Layer: language surface — RFD + reference draft → Lean → code (workflow table). The reasoner is the layer where Rust leads (AGENTS.md); the Lean obligation is recorded for #134’s catch-up scope, not mechanized here.

1. The problem

On origin/main @ v0.2.0, tax and accounting are not authorable. The domain-modeling lane proved it against the binary:

  • A rule head cannot carry a computed value. derive Tax(t, owed) :- appliesTo(b, t), owed == t.income * b.rate refuses with OE1303 — == is a filter over already-bound operands, never an assignment, so owed is never positively bound (R-B2).
  • Two aggregates cannot be compared. The double-entry invariant sum(debits) == sum(credits) is inexpressible: a comprehension aggregate parses only as the RHS of a field-path comp-op ___ comparison; on the LHS or in leading position it is OE0001 (R-B3).
  • Aggregates have no relation source and no grouped form. sum(amt for amt in rel(e, _, amt)) refuses (the comprehension source must be a materialized collection field); group by … having refuses with OE0007 (R-B4, R-M7).

Three adjacent gaps compound it: the §7 status claims mutate-body arithmetic is Real/f64 with “exact Decimal pending” (R-M4); there are no rounding primitives in any plane (R-M5); and the demo/CLI harness rejects a decimal mutation argument outright (R-B10, harness half).

2. The design — one mechanism

2.1 Body-level binding: x = expr

A new rule-body atom form binds a fresh variable to the value of an expression:

pub derive Tax(t, owed) :- appliesTo(b, t), owed = t.income * b.rate;

x = expr (single =) is assignment, distinct from x == e (double =, a filter). expr ranges over: bound variables, literals, field projections (t.income), arithmetic over the exact tower (a * b, a + b, exact /), nullary/other intrinsics, and aggregate expressions (§2.2).

This is the established PL concept — Datalog/Soufflé assignment. It is not a new IR constructor: the binding lowers to the existing AtomIR::Compute { result, expr } (the RFD 0020 D5 Map operator), whose result variable the range-restriction safety check already treats as a binding source (a Compute result is bound once its input variables are bound, closed to a fixpoint). The work is purely in the parser (recognize the = atom shape) and the lowering (emit Compute, relationalizing any Proj/aggregate sub-terms exactly as the existing comparison-operand hoist does).

Freshness and range restriction. The LHS x must be a FRESH variable, and the binding must be range-restricted. Two distinct refusal paths, never a silent Null:

  • Freshness (OE1335 BindingLhsAlreadyBound). If x is already bound — by a prior positive predicate atom, a head parameter bound elsewhere, a projection, an aggregate result, or an earlier binding — the = was silently degrading into an equality FILTER (x joined against the computed value) rather than a binding, collapsing the two readings of = (bind vs. compare) with no diagnostic. It refuses, naming x: use == to compare, or pick a fresh name. This is the path a rebind x = x + 1 takes when x is otherwise bound — the LHS is not fresh.
  • Range restriction (OE1303 RuleNotRangeRestricted). x = expr binds x positively iff every variable in expr is itself positively bound. An unbound RHS variable, or a pure self-reference / cycle where the result variable is bound by nothing else (x = x + 1 as x’s only binder; x = y, y = x), leaves x unbound under the binding fixpoint and refuses as unsafe. OE1303’s wording is extended to name the binding form.

So x = x + 1 refuses via OE1335 (freshness) when x is already bound elsewhere, or via OE1303 (range restriction) when x has no other binder — the two codes name two genuinely different errors. The freshness check runs first, so the freshness diagnostic wins when both could apply.

Why x = expr, not aggregate-on-LHS comparison sugar. a = sum(...), b = sum(...), a == b already solves the leading/LHS aggregate position (R-B3) with the same mechanism. We do not add a second spelling (sum(...) == sum(...) as direct comparison sugar): one mechanism, one spelling (house rule — nothing silently dropped, nothing redundantly admitted). A user who writes sum(...) == sum(...) directly is hoisted by the existing aggregate-operand relationalization (the aggregate becomes a Compute-bound var), so it also works — but the canonical, documented form is the binding.

2.2 Aggregates as bindable expressions

An aggregate call is a bindable expression:

pub derive trial(acct, bal) :- acct: Account,
    debits  = sum(e.amount for e in Entry, posted(e, acct), e.side == "D"),
    credits = sum(e.amount for e in Entry, posted(e, acct), e.side == "C"),
    bal     = debits - credits;

Because the aggregate binds a variable, comparing two aggregates is just comparing two bound variables (R-B3): a = sum(...), b = sum(...), a == b.

2.3 Aggregate sources — extend the comprehension, don’t add a form (R-B4)

The existing comprehension sum(e.value for e in S where φ) is extended so the body after the source is a comma-separated list of additional body atoms, replacing the single where φ:

agg( proj for binder in Source [, atom]* )
  • Source may be a type extent (for e in Entry) or a relation atom is expressed as one of the trailing atoms (posted(e, acct)).
  • The trailing atoms are ordinary rule-body filter atoms (predicates, comparisons, type tests, membership) lowered with the same machinery as the outer body.
  • Variables from the outer rule body are visible inside the comprehension. That visibility is the grouping (§2.4).

The legacy single-where form (… where φ) stays accepted as sugar for one trailing atom, so every existing example keeps parsing.

Bindings are NOT a trailing-atom form (decided under the no-hollow rule). An earlier draft of this enumeration listed nested bindings as a trailing-atom kind. They are not implemented and are removed from the design: the comprehension cond is a boolean-filter chain (right-nested &&), which has no assignment shape, and a fresh variable introduced inside an aggregate sub-scope is not safety-checked there (the outer range-restriction and freshness checks do not descend into aggregate bodies — a recorded conservative gap). Admitting a half-checked binding inside the fold would open a new silent-unsafe surface, and the useful shape is already expressible without it: sum(w for u in T, rel(t, u), w = u.v) is exactly sum(u.v for u in T, rel(t, u)) (project into the fold directly), and anything richer binds in the OUTER body and aggregates the bound variable. So a binding x = expr in a trailing position is refused with a directed hint, OE1336 BindingInComprehension, naming both rewrites — rather than a generic parse error from the unconsumed =. The comparison == is a real trailing filter and is unaffected.

The brace form stays count/exists-only. sum(expr){atoms} does not parse as a value aggregate; it refuses with a directed hint pointing at the comprehension form (OE1331). We do not add sum(expr){…} — one spelling for value aggregates (the comprehension), one for cardinality (the brace). This keeps the brace’s existing count { p in Person, R(a, p) } membership-join shape (tracked separately under #308) the only brace surface.

2.4 Grouping = the outer bound variables (R-M7)

Grouping is the standard Datalog reading: the group key is exactly the set of outer rule-body variables free in the aggregate. pub derive balance(acct, s) :- acct: Account, s = sum(e.amount for e in Entry, posted(e, acct)); groups per acct — the aggregate is evaluated once per binding of the outer variables, and acct appears in the aggregate body (posted(e, acct)), so each acct gets its own fold. This already matched the reasoner’s evaluation model (the aggregate sub-pipeline is seeded with the outer binding); the binding atom makes the result nameable so it can flow to the head or a second aggregate.

The SQL-ish group by … having keeps refusing (OE0007) but now with a directed hint showing the binding-form equivalent — a group by acct having sum(x) > 0 becomes acct: Account, g = sum(x for …), g > 0. We do not build group by: the Datalog grouping is the spelling, and a second one would be a silent-redundant surface.

Empty-group semantics — a deliberate split. When a group folds over no rows, the aggregators divide by role, and this is the documented behavior the double-entry example relies on:

  • sum / count / count_distinct emit a value for the empty group0. An additive/cardinality fold has a well-defined identity (the empty sum is zero, the empty count is zero), so the grouped row is produced with that identity.
  • min / max / avg drop the row — they have no value over an empty set (no least/greatest element; the mean is 0/0), so no grouped row is produced rather than fabricating one.

This split is load-bearing for the double-entry invariant (§4): sum(debits) == sum(credits) must catch an entry with credits but no debits. Because sum emits 0 for the empty debit side, the comparison is 0 == credits — which fails and flags the entry as unbalanced. Were sum to drop the empty group, the row would vanish and the imbalance would pass silently. The behavior is fixed in fold_aggregate (compiler/crates/oxc-reasoning/src/executor/eval.rs) and pinned by a test asserting BOTH halves (empty_group_split_sum_count_emit_zero_minmaxavg_drop).

2.5 Semantics and stratification

  • Stratification. An aggregate over a derived predicate requires the aggregated predicate stratified strictly below the rule (Faber–Pfeifer–Leone). This is already enforced: an in-SCC aggregate edge is recursion-through-aggregation and refuses loudly with OE1317 (issue #174). No change.
  • WFS interaction (#250). Aggregates evaluate over the definitely-true extent. If the aggregated atom set draws on a relation with a non-empty well-founded-undefined companion ($undefined::R), the fold would silently treat undefined as false — the #250 leak. We refuse the rule loudly at runtime for now (OE1332 / ReasoningError::AggregateOverUndefined) rather than fold undefined-as-false. #250 is the designed follow-up (three-valued aggregate intervals); this RFD does not close it, it stops the silent leak.

2.6 Exact-Decimal mutate-body path (R-M4)

The §7 status note (“mutate-body arithmetic is Real/f64; exact Decimal pending”) is false at HEAD and is removed. Mutate-body arithmetic (require guards, let/field expressions) already routes through eval_binary_values — the canonical exact core (RFD 0016) that the #272/#277 work made the single arithmetic path: pure-Int stays checked Int, any Real/Decimal operand promotes to exact BigRational, / is the exact field operation, no f64 in the arithmetic path. Money (carried as a structured CBOR value) folds through the same rational core. We delete the stale note and flip the §7 status row to ✓/✓, and add a money-arithmetic regression that proves 0.1 + 0.2 == 0.3 exactly in a mutate body.

2.7 Rounding (R-M5)

A builtin function family, available in both rule-body expressions and the fn/compute plane, exact-tower in/out (Decimal stays Decimal, never via f64):

BuiltinMeaning
round(x)nearest integer, ties away from zero
round(x, n)nearest multiple of 10^-n, ties away from zero
round_half_even(x, n)nearest multiple of 10^-n, ties to even (banker’s rounding)
trunc(x, n)toward zero at 10^-n

round_half_even is the money default — financial rounding rounds half to even to avoid the upward bias of half-away-from-zero. Documented in the book’s money section. All four operate on BigRational and return an exact value (integral results collapse to Int, as elsewhere in the tower). round / round_half_even / trunc are reserved builtin names: they resolve by surface name in both the rule plane (compile_expr) and the fn/compute plane (resolve_term_to_value), so a user-defined fn round does not shadow them. A wrong-arity call is OE1333 (RoundingBuiltinArity).

2.8 CLI/demo harness decimal arguments (R-B10, harness half)

demo.toml mutation args accept decimal values for Decimal/Money/Real params, two spellings:

  • A quoted decimal string via an explicit table form { decimal = "0.22" } / { money = "100.50" } — exact, parsed directly to the rational carrier (never via f64). This aligns with serve’s coerce_money_arg convention (a Money amount is a decimal string).
  • A bare TOML float (rate = 0.22) — converted via its shortest round-tripping decimal representation (the same value the user typed), then parsed exactly. The conversion is documented: a bare float is read as the shortest decimal that round-trips to that f64, so 0.22 becomes exactly 22/100, not the f64 artifact 0.2200000000000000011….

The serve-side wire half of R-B10 (decimal coercion at the HTTP boundary) belongs to arc2 and is not touched here.

3. Diagnostics

New codes (allocated next-free in the 13xx rule band, grammar.toml as truth, cargo xtask gen):

CodeNameMeaning
OE1331ValueAggregateBraceFormA value aggregate (sum/min/max/avg) was written in brace form sum(expr){…}; directs to the comprehension form sum(expr for x in S, …).
OE1332AggregateOverUndefinedAn aggregate folds over a relation with well-founded-undefined atoms; refused rather than silently treating undefined as false (#250 leak guard). The refusal is whole-relation, not per-group.
OE1333RoundingBuiltinArityA rounding builtin (round/round_half_even/trunc, §2.7) was called with the wrong number of arguments.
OE1334BindingLhsNotSimpleVarA binding atom x = expr (§2.1) has a non-fresh-variable LHS — a dotted projection (x.f = e) or a ::-qualified path (m::x = e); a binding’s LHS must be a bare identifier.
OE1335BindingLhsAlreadyBoundA binding atom x = expr (§2.1) names an LHS x already bound by a positive predicate, a head param, a projection, an aggregate result, or an earlier binding; the = would silently act as an equality filter. Use == to compare, or pick a fresh name. (Freshness path; see §2.1.)
OE1336BindingInComprehensionA binding x = expr appears as a comprehension trailing atom (§2.3), which is not a trailing-atom form; directs to project into the fold directly or bind in the outer body.

The Rust error variant for OE1333 is RuleCompileError::RoundingBuiltinArity, renamed from BuiltinArity to match the catalog name (only the rounding builtins reach this arity check).

Extended:

  • OE1303 (RuleNotRangeRestricted) — wording extended to name the binding form x = expr as a binding source and as a consumer of its RHS variables.
  • OE0007 (GroupByNotExecuting) — message extended with the binding-form equivalent hint.

4. Worked example: double-entry accounting

examples/double_entry_v0 is the journey’s exact wall made a running proof: accounts, journal entries with debit/credit lines, the balance invariant sum(debits) == sum(credits) as a check, and a trial-balance query that sums per account. It is corpus-pinned (an integration test asserts its extents) and demo.toml-driven.

5. Lean obligations

The reasoner is the layer where Rust leads (AGENTS.md): well-founded semantics, factorized aggregates, and the join machinery execute in Rust ahead of the Lean fixpoint. The binding atom and aggregate-source extension are evaluation-layer changes that ride the existing AtomIR::Compute / AtomIR::Aggregate constructors — already in the @[language_interface] wire shape — so no drift-gate change is required. Mechanizing the body-binding and grouped-aggregate semantics is added to issue #134’s catch-up scope (the Reasoning/Datalog/ layer), not mechanized in this PR.

6. Disposition against the register

ItemDisposition
R-B2 (derived value head)x = expr binding atom → AtomIR::Compute; head carries the bound var. LHS freshness enforced (OE1335) — a rebind-as-filter refuses loudly.
R-B3 (compare two aggregates)a = sum(…), b = sum(…), a == b via the binding atom.
R-B4 (aggregate sources)Comprehension extended: comma-separated trailing filter atoms + relation-atom sources; brace value-aggregate refused (OE1331). Trailing bindings are not a form — refused with a directed hint (OE1336); project into the fold or bind in the outer body.
R-M4 (exact mutate arith)Already exact via eval_binary_values; stale §7 note removed; money regression added.
R-M5 (rounding)round/round(_,n)/round_half_even/trunc builtins, exact tower, banker’s-default.
R-M7 (grouped aggregation)Grouping = outer bound vars (Datalog); group by … having refuses with binding-form hint.
R-B10 (harness half)demo.toml accepts { decimal/money = "…" } and bare floats (shortest-decimal).

RFD 0030 — Package dependencies ([dependencies], path deps v1)

  • State: committed
  • Opened: 2026-06-12
  • Decides: how an Argon package declares a dependency on another package and consumes its pub surface — the mechanism that makes vocabulary publishing real. A vocabulary team ships a package (e.g. argufo); a modeler in a separate package declares [dependencies] argufo = { path = "../argufo" }, writes use argufo::kind;, and uses argufo’s pub metatype kind as a declaration keyword in its own catalog. Built as the Wave-A arc7-packaging work of the v0.2.1 program; closes register blocker R-B1 and majors R-M1 (use-import diagnostics) and R-M12 (manifest honesty). Builds D1 of RFD 0022 (which reserved “a dependency package name ([dependencies])” as a resolution root) and the §3.3/§3.4 manifest/resolution surface. Relates to Modules and Build.

This RFD records, as built, the dependency mechanism the book described aspirationally (§3.3 [dependencies], §3.4 dependency-package roots, §16 the ox orchestrator’s package graph). v1 is path dependencies only; the registry/version/git surface is recognized and refused loudly, reserved for a later RFD.


Question

Until now ox.toml parsed only [package]/[project]/[schema], and the resolver registered only the embedded stdlib (std::*) plus intra-package paths. The two-package vocabulary journey — the central use case for an ontology-modeling language — was impossible: every vocabulary and every model that used it had to live as sibling modules in one package. The book documented the opposite in detail, “as if real.”

  1. How does a package name and resolve a dependency? What [dependencies] shape, what resolution semantics, what happens across the package boundary for use, qualified paths, and — the acceptance test — dependency-provided introducers (pub metatype / pub metarel)?
  2. What about the registry/version surface the book showed? ufo = "1.0" is a version requirement against a registry that does not exist. Accept it silently (the old failure mode), or refuse it?
  3. What is honest about an ox.toml the compiler does not fully consume — unknown sections, [lattice].max_tier?

Context

The substrate already composes a multi-file package into one artifact: the driver’s pre-elaboration sweep builds a WorkspaceSymbols table over every reachable module, the §3.4 introducer gate (resolve_metatype_introducer/resolve_metarel_introducer) resolves a declaration keyword against the pub metatype/pub metarel declarations visible in the workspace, and the resolver (oxc-resolver) resolves use/qualified paths over the workspace’s file set. The embedded stdlib is folded into that same file set via synthetic <stdlib>/std/<pkg>/root.ar paths (oxc_instantiate::stdlib_source_files). A dependency package is the same shape as the stdlib: a set of pub-surfaced modules folded into the consumer’s workspace.

So the mechanism is not a new subsystem. It is: parse the dependency, load its modules, give them a package-qualified namespace, fold them into the workspace exactly like the stdlib, and teach the resolver to treat the dependency package name as a path root. Everything downstream — the introducer gate, use, qualified paths, module extraction, the .oxbin — composes for free, because all of those already operate over the workspace.


Decision

D1 — Manifest: [dependencies], path deps only

[dependencies]
argufo = { path = "../argufo" }
  • The key is the dependency name the consumer uses as the path root (argufo::Person, use argufo::kind;). It must equal the dependency package’s own [package].name — a mismatch is refused with OE1241, naming both the declared key and the found package name. (A rename-on-import surface is a possible later extension; v1 keeps the package’s published name authoritative, as Cargo does by default.)
  • The only supported source is path = "<relative path to a package directory>". The path is resolved relative to the consumer package directory; it must point at a directory containing an ox.toml.
  • A shorthand string requirement (argufo = "1.0") or any of the registry/VCS keys (version, git, branch, tag, rev, registry) is recognized and refused with OE1240, naming the feature (“registry/version dependencies are not yet supported; cite RFD 0030”) rather than silently ignored. This is the loud-refusal discipline: a documented input is never silently dropped.

D2 — Resolution: a dependency is a package-qualified namespace folded into the workspace

For each declared path dependency, the workspace loader:

  1. Loads the dependency package (its own ox.toml + entry + reachable module closure), exactly as it loads the consumer package.
  2. Re-homes the dependency’s modules under synthetic VFS paths <dep>/<name>/… and assigns each a package-qualified module path rooted at the dependency name: the dependency’s root module becomes module argufo, its submodule endurant becomes argufo::endurant, and so on.
  3. Folds the dependency’s source files into the consumer’s Workspace file set and its modules into the package’s reachable-module list — the same fold the stdlib already gets.

The resolver (oxc-resolver) recognizes a leading path segment equal to a registered dependency name as a root (§3.4): argufo::Person and use argufo::kind; resolve into the dependency’s <dep>/argufo/… namespace, the way std::core::rel resolves into <stdlib>/std/core/…. A dependency root is detected from the synthetic-path pattern — a leading segment argufo is a dependency root iff the dependency’s root file <dep>/argufo/root.ar is in the workspace file set — so no separate dependency-name registry is threaded onto the Workspace input (the same way the stdlib is recognized by <stdlib>/std/<pkg>/root.ar). Inside a dependency, pkg:: anchors at that dependency’s root, so a multi-file dependency’s internal package-absolute paths resolve within its own subtree. Only the dependency’s pub (and pub use-re-exported) surface is visible to the consumer; module visibility rules (§3.1) are unchanged across the boundary.

Because the dependency’s pub metatype/pub metarel declarations are in the workspace symbol table, the §3.4 introducer gate resolves them transparently: after use argufo::kind;, a consumer declaration pub kind Person { … } resolves kind to argufo::kind and elaborates as a concept under that metatype — the acceptance property. A dependency-shipped catalog check (a pub check over iof/meta/specializes) runs on the consumer’s catalog at the consumer’s ox check, because the dependency’s rules and the consumer’s individuals share one elaborated event stream. That is what makes a vocabulary package real, and it is tested (see Consequences).

Transitive dependencies resolve recursively (a dependency’s own [dependencies] are loaded into the same workspace). Cycles are refused loudly with OE1242, naming the cycle path. Two declared dependencies resolving to the same canonical directory are one package (de-duplicated, not an error). The same dependency name resolving to different canonical directories is refused with OE1243, naming both paths.

D3 — Artifact: dependency modules embed into the consumer’s .oxbin (monolithic, v1)

Dependency modules are elaborated into the same event stream as the consumer’s modules and embed into the consumer’s single .oxbin, exactly as the stdlib embeds today (§16.3). v1 is one monolithic artifact per ox build; the per-package .oxc cache + workspace merge (§16.1) remains the reserved v0.2+ shape. Because the dependency’s elaborated content participates in the event stream, module/content hashes (§16.5) incorporate it: a change to a dependency changes the consumer’s artifact_hash.

D4 — No lockfile for path dependencies

Path dependencies are unlocked (Cargo precedent — a path dep is whatever is at that path now). There is no ox.lock for a path-only dependency graph. ox.lock is reserved here for the registry/git story (D1’s refused surface): a versioned/VCS dependency graph needs a lockfile to pin resolved versions; a path graph does not.

D5 — Use-import diagnostics (R-M1): loud, never silent

A use that does not resolve is refused loudly:

  • use a::b::C; where the path does not resolve emits OE0103 (UnresolvedUseImport) at the use itself, with a did-you-mean suggestion over the names visible in the target namespace (the dependency’s pub surface, a sibling module, an intra-package path). Previously a broken use was accepted clean and the only error was a misleading downstream OE0605 far from the cause.
  • A glob use pkg::*; / use argufo::*; resolves correctly (it already does, via the re-export-aware module_exports) — it brings in the target’s pub surface. A glob whose prefix does not resolve is the same OE0103 as a named import. A glob is never a silent no-op.

D6 — Manifest honesty (R-M12)

  • An unknown ox.toml section or key (anything outside the recognized [package]/[project]/[schema]/[dependencies]/[lattice]/[standpoints] surface, or an unrecognized key within them) emits a Cargo-style OW1240 (UnusedManifestKey) warning naming the section/key — it no longer parses with serde silently dropping it.
  • [lattice].max_tier is wired to the §10 tier classifier: it sets the artifact’s tier ceiling, and a declaration whose classified tier exceeds the ceiling is refused at ox check/ox build with OE1230 (TierCapExceeded, previously reserved). The ceiling string is validated against the seven-tier ladder names; an unknown tier name is OW1240.

Rationale

  • A dependency is the stdlib shape. The most robust, lowest-surface-area design reuses the fold-into-workspace machinery the stdlib already proves. The introducer gate, use resolution, qualified paths, module extraction, and the .oxbin all already operate over the workspace; a dependency that lives in the workspace inherits all of them with no new code path. This is why the acceptance property (imported introducers + imported checks fire on the consumer’s catalog) falls out rather than being special-cased.
  • Path-first, registry-refused. Path deps are the v1 vocabulary-authoring need (a Sharpe team’s packages live in one tree). A registry is a distribution-infrastructure project (provenance, resolution, lockfiles) orthogonal to the language. Recognizing-and-refusing the registry keys keeps the manifest forward-compatible and honest: the book can show the shape, and a user who writes it gets a feature-named refusal, not a silent no-op.
  • No lockfile for path deps matches Cargo and the “no ceremony you don’t need” instinct: a path graph is fully determined by the filesystem.
  • Loud manifest, loud imports. Both R-M1 and R-M12 are instances of the program’s silent-accept blocker class. A mistyped import or a typo’d manifest key that the compiler silently swallows is a trap; a warning/refusal that names the thing is debuggable.

Alternatives

  • A separate dependency resolver pass that produces per-package .oxc and a workspace merge (the full §16.1 orchestrator). Deferred: it is the right end state for incremental builds and a registry, but it is a large build-system project; folding into one workspace is correct for v1 and keeps the artifact monolithic (§16.3’s stated v0.x shape).
  • Allow version/git to parse and warn, resolving via path anyway. Rejected: a version requirement that silently resolves to a path is exactly the silent-wrong the program forbids — the user thinks they pinned a version.
  • Rename-on-import (argufo = { path = "…", package = "ufo_foundational" }). Deferred: a useful ergonomic, but v1 keeps the published [package].name authoritative (mismatch = OE1241), matching Cargo’s default.
  • A lockfile for path deps. Rejected (Cargo precedent): a path graph has nothing to lock.

Consequences

  • ox.toml now has a [dependencies] table and recognizes [lattice]/[standpoints]; unknown keys warn (OW1240). [lattice].max_tier is enforced (OE1230, no longer reserved).
  • No change to the Workspace salsa input: dependency roots are detected from the synthetic <dep>/<name>/root.ar path pattern in the file set (mirroring stdlib’s <stdlib>/std/<pkg>/…), so Workspace::new’s signature is unchanged.
  • New diagnostics (allocated in grammar.toml, generated via cargo xtask gen): OE0103 UnresolvedUseImport, OE0104 GlobImportUnsupported (reserved — glob currently resolves; held for a future feature-named refusal of an unsupported glob shape), OE1240 DependencyVersionUnsupported, OE1241 DependencyNameMismatch, OE1242 DependencyCycle, OE1243 DuplicateDependencyName, OE1244 DependencyPackageLoadFailed, OW1240 UnusedManifestKey. OE1230 TierCapExceeded is wired (was reserved).
  • The two-package journey is the living proof: examples/vocab_pkg_v0 (a UFO-shaped vocabulary package with pub metatype kind/category, a pub metarel, and a pub check catalog rule) and examples/vocab_consumer_v0 (a separate package, [dependencies] vocab_pkg_v0 = { path = ".." }, declaring pub kind Person via the dependency’s kind, with the dependency’s catalog check firing on the consumer’s catalog). Both are corpus-pinned and run in the examples harness; GO-journey (a) of the v0.2.1 release gate.

Open questions

  • When the registry/git story lands (its own RFD), ox.lock and the per-package .oxc + workspace merge (§16.1) become live; the path-dep fold described here stays as the unlocked fast path.
  • Should a future rename-on-import key (package = "…") be added once a registry makes the published name vs. local name distinction load-bearing?
  • Should pub(pkg) become a true visibility cut at the dependency boundary (RFD 0022 open question) — i.e. should a dependency’s pub(pkg) items be invisible to a consumer (today pub(pkg) == pub)? v1 keeps the RFD 0022 simplification.

RFD 0031 — The relation-constraint plane + meta-property completion

  • State: committed
  • Opened: 2026-06-12
  • Decides: how a vocabulary author expresses and the elaborator enforces the relation-level compile-time constraints an ontology needs — completing the meta-property and reflection surface the book promised but left unbuilt. Concretely: (1) metarel endpoint-metatype verification (§4.3 “the elaborator verifies the relation’s endpoint metatypes match the metarel’s positions” — #311); (2) relation-signature reflection$rel/$arm catalog atoms so a package-shipped check can quantify over declared relations, their classifying metarel, and endpoint types (#312); (3) per-target axis overrides keyed on a vocabulary’s own axis name (RFD 0027 D2 — R-M9); (4) reflection-intrinsic category errors on struct/enum carriers (§4.4.2 — R-M10); (5) the metaxis typed-axis where refinement parse (§4.1 — R-M11); and (6) relation bracket-cardinality enforcement (§4.3/§5 — #310). Built as the Wave-C arc4-metaproperty work of the v0.2.1 program. The macro atom is explicitly out of scope (its own later design discussion). Builds on RFD 0027 (the meta-property plane, whose D2 per-target plane and D4 reflection surface this completes) and RFD 0023 (reflective TypeRef). Relates to Meta-calculus and Constructs.

This RFD records, as built, the relation-constraint surface the readiness register (.local/research/readiness-2026-06-12) named as the vocabulary-authoring blocker half of the production bar: a vocabulary team (Gustavo/Tiago, building ArgUFO) cannot write the relation-level compile-time constraints their ontology needs — “an ability must inhere via an aspect, not a kind” is unenforceable, and there is no reflection over relations to write such a constraint as a package-shipped check. The substrate the meta-property plane (RFD 0027) built for types is here extended to relations, plus the residual meta-property gaps (R-M9/M10/M11) closed.


Question

RFD 0027 built the meta-property plane for the type tier: metaxes, metatypes, abstract/fixed modifiers, the $meta/$iof/$axis catalog relations, sort-directed meta(t).axis projection. It left the relation tier half-built and three meta-property gaps open:

  1. Metarel endpoints are unverified. pub metarel mediation(mediator: relator, mediated: kind) declares position metatypes, but pub mediation Bad(a: Vehicle, b: Person) — where Vehicle is kind-sorted, not relator-sorted — was accepted clean. The §4.3 promise (“the elaborator verifies the relation’s endpoint metatypes match the metarel’s positions”) was prose only; the position metatype names were parsed and discarded (never persisted on MetarelDeclBody). This is the core ask: an ability must inhere via an aspect, not a kind, and nothing enforced it.

  2. No reflection over relations. $iof/$meta/$axis/$implements reflect over types and traits; nothing reflects over relation arms. A vocabulary package could not write a check that quantifies over declared relations (their classifying metarel, their endpoint types) — so the metarel-endpoint discipline, even once enforced by the compiler, could not be extended or audited by package-shipped rules.

  3. Per-target axis overrides have no surface. RFD 0027 D2 specified per-target MetaProperty assertions (“metatype-level bindings unioned with per-target assertions, per-target taking precedence”), but the only emitter was the hardcoded MLT #[order(N)] magic string. A vocabulary declaring metaxis rigidity for type { … } gave consumers no way to override rigidity on one specific concept. In-body { rigidity::semi_rigid } → OE0001; the attribute form → OE0705.

  4. Reflection on struct/enum is silently clean. §4.4.2 promises a category error (“calling meta() on a struct/enum-declared value is a category error … emits a diagnostic at elaboration”). meta(p) == Point over a struct checked clean — no diagnostic existed.

  5. The metaxis where-refinement does not parse as printed. §4.1’s pub metaxis weight for type = Real where _ > 0.0; → OE0001 (the typed-domain type_expr greedily consumed the trailing where as a refined-type refinement expecting { … }). Only the braced = Real where { _ > 0.0 }; parsed.

  6. Relation bracket cardinalities are silently ignored. pub rel R(...) [1..1] [0..*]; parsed into a CARDINALITY token-soup node that no lowering read (#310). A documented modifier, honored nowhere.


Decisions

D1 — Metarel endpoint-metatype verification (#311)

The metarel’s position metatypes become real on the wire. MetarelDeclBody gains position_metatypes: Vec<Option<NameRef>> — one entry per declared endpoint, the resolved metatype NameRef named in that position (relator, kind, …), or None for a bare-typed position (metarel material(kind, kind) names a metatype per position; a position naming a primordial or a generic param binds None and is unconstrained). The generic stdlib pub metarel rel<E1: metatype, E2: metatype>(E1, E2) binds None at every position — it “accepts any endpoint metatypes” (§4.3), so it imposes no endpoint constraint, exactly as documented.

At relation lowering, the elaborator verifies. For a relation pub <metarel> R(a: A, b: B) classified by a metarel with position metatypes [Some(m₀), Some(m₁), …]:

  • each endpoint concept A’s declared metatype is resolved (the concept’s introducing classifier, already captured by the workspace symbol sweep), and
  • if the endpoint’s metatype is not the position metatype, the relation is refused with OE0631 (MetarelEndpointMetatypeMismatch), naming the position, the expected metatype, the endpoint concept, and its actual metatype.

A None position imposes no constraint. An endpoint whose type is a primordial (Int/String/…) or whose metatype cannot be resolved is not refused here (it is unconstrained or already refused by the §3.4 introducer gate) — OE0631 fires only when both the position metatype and the endpoint metatype are known and incompatible.

Elaboration path (v0). Metatypes are flat in v0 — there is no declared metatype <: graph — so the comparison is metatype short-name identity (MetarelDeclBody.position_metatypes carries the resolved NameRefs for the runtime; the elaboration-time gate compares the short names the workspace catalog resolves both endpoints to). It is still an identity comparison, not a magic string: the compiler never branches on a particular metatype/axis word (the §3.4 / RFD 0027 D6 doctrine — the compiler never branches on user vocabulary). The sub-metatype tolerance (an endpoint metatype that is a descendant of the position metatype satisfies the constraint) is reserved — it activates when metatype subtyping lands; until then the flat case is exact identity. This holds across the dependency boundary: a relation declared in a consumer package against a metarel imported from a dependency is verified against the dependency’s published position metatypes (the cross-package OE0631 path, #311).

D2 — Relation-signature reflection: $rel and $arm (#312)

Two new catalog-closed reflection relations, in the established $iof/$meta/$axis style (abstract reflection primitives, never ontology vocabulary):

  • $rel(r: TypeRef, m: TypeRef) — for every declared relation r (a TypeRef value naming the relation), m is its classifying metarel. Catalog-closed: one row per declared relation.
  • $arm(r: TypeRef, pos: Int, t: TypeRef) — for every declared relation r, pos is a 0-based arm position and t is the endpoint type declared at that position. One row per (relation, position).

Surface spelling mirrors the type-tier reflection sugar: rel(r, m) and arm(r, pos, t) are admitted in any rule body without a use (the rel/arm heads lower to the reserved-head predicates $rel/$arm, exactly as iof$iof). Both are first-class and all positions may be free — enumeration is the intended use, the same justification as $implements. A package-shipped check can now quantify:

// "every endpoint of a `characterization` relation must be aspect-sorted or the bearer kind"
pub check BadCharacterization(r: TypeRef) :-
    rel(r, characterization), arm(r, 1, t), meta(t) == kind, not meta(t) == aspect
    => Diagnostic { ... };

rel here is the vocabulary’s metarel name in value position (a TypeRef), not the stdlib std::core::rel introducer — they are distinct (one is a metarel name used as a value, the other the generic introducer keyword). The reflection head spelled rel(...)/arm(...) is the abstract primitive; resolution distinguishes the value-position metarel reference from the head.

D3 — Per-target axis overrides (R-M9, RFD 0027 D2 realized)

A concept declaration may carry per-target axis assignments in its body, in the same axis::value / axis = literal spelling a metatype body uses:

pub kind Person { rigidity::semi_rigid }          // override the metatype's rigidity binding
pub kind Worker { age: Int, rigidity::semi_rigid } // mixed with field declarations

An axis::value (or axis = literal) clause in a concept body lowers to a per-target MetaProperty event (axis, target=concept, value) — the same channel MLT’s #[order(N)] already emits, now reachable from a generic, vocabulary-named surface. The clause is validated exactly like a metatype binding (axis resolves to a visible pub metaxis whose for targets include type; value in domain — OE0622/OE0623/OE0624; duplicate — OE0625). The effective $axis relation unions metatype-level bindings with these per-target assertions, per-target taking precedence (RFD 0027 D3 — functional per (target, axis), OE0629 the load-time backstop).

The compiler never reads the axis name. The override mechanism resolves the axis to its NameRef and validates against the declared domain of whatever metaxis the vocabulary declared; a string-match on rigidity (or any axis/value name) appears nowhere. A vocabulary that declares metaxis foo for type { a, b } gets per-target foo::a overrides for free, with zero compiler changes — the mechanism is axis-name-agnostic by construction.

D4 — Reflection-intrinsic category error on struct/enum (R-M10)

meta/iof/specializes/extent applied to a carrier that is a struct- or enum-declared type (language-level data, not an ontologically-classified concept) is a category error, refused at elaboration with OE0632 (ReflectionOnUnclassified) — the feature-named diagnostic §4.4.2 promised, parallel to OE1016 (Truth4OfOnStruct). The check fires when the type-position argument of a reflection intrinsic statically resolves to a struct/enum declaration; reflection over concepts (the metatype-classified tower) is unaffected.

D5 — The metaxis where-refinement parse (R-M11)

The typed-domain metaxis body parses = TypeExpr ('where' refinement)? where the refinement is a bare predicate (_ > 0.0, self > 0.0) terminated by ;, or the braced { … } form. The parser stops type_expr from greedily consuming the trailing where: the metaxis-decl rule parses the base type expression without the refined-type where-suffix, then handles where itself (either a { … } block or a bare predicate to ;). Both printed spellings now parse and the refinement lowers into the typed-domain AxisDomainBody::Typed { refinement } already on the wire.

D6 — Relation bracket-cardinality enforcement (#310)

The [lo..hi] cardinality brackets parse into a structured Cardinality { lo, hi } (lo: u32, hi: Option<u32>* = None) per arm, persisted on RelationDeclBody.

  • Max-caps are CWA-checkable and enforced at the write path: an insert/update that would make an entity participate in more than hi tuples of relation R in the constrained position is refused with OE1014 family (the closed-world cardinality gate), feature-named OE1341 (RelationCardinalityExceeded). Max-caps are checkable at ox check/build only when statically decidable; the live enforcement is at write/serve.
  • Min-cardinalities need the evaluation channel. A [1..1] lower bound is an existence requirement that, under the closed-world default, would refuse an entity that does not (yet) participate — but staged construction (classify now, relate later) makes a build-time refusal wrong, and the field-access-time / evaluation-channel emitter that would surface staged incompleteness is RFD 0007’s design and not built in v0 (the same disposition as OE0207’s staged-construction note). Decision: enforce max-caps loudly; min-cards are recognized, validated for well-formedness (lo <= hi), persisted, and refused-or-deferred explicitly — a non-zero lo that cannot be statically discharged emits the reserved OW1342 (RelationMinCardinalityDeferred, a build-time note-severity diagnostic naming the unenforced bound) rather than silently accepting it as enforced. No silent-ignore: the bracket is honored (max) or explicitly flagged-as-deferred (min). Full min-card-under-OWA enforcement is the recorded follow-on, gated on RFD 0007’s evaluation channel.

This composes with D1: the metarel constraint plane (endpoint metatypes) and the cardinality plane together are the relation-level compile-time constraint surface.

D7 — mode un-reserved; dead-reservation sweep (#309)

mode is removed from the lexer keyword list — it lexes as IDENT, so pub metatype mode { … } (the most important UFO vocabulary word) is admitted. The documented graph-traversal mode-spec ::= 'mode' ('walk' | …) surface (§7.4) is unbuilt (allowlisted OE0001, arc6-debt); when it lands it recognizes mode contextually in the role-step position (lex-as-IDENT, match-by-text), the same discipline type/rel already use — no reserved keyword needed. The sweep also un-reserves ordered, a vestigial reservation with zero grammar consumers and no documented surface (order — distinct keyword — backs order by). The other zero-consumer reserved words (walk/trail/acyclic/simple/shortest/union/with/upsert/detach/…) stay reserved: each backs a documented future surface that refuses feature-named-or-OE0001 by design (the coverage allowlist’s burn-down class), so un-reserving them would let a modeler shadow a planned keyword.

D8 — Teaching hints (#313)

Three diagnostic-message improvements, no new codes:

  • A comma-separated refinement constraint list (where { a > 0, b < 10 }) gets a directed hint: “constraints combine with && — write a > 0 && b < 10” (one spelling; comma is not sugar).
  • OE0660 (bare field name in a refinement) gains “did you mean self.<field>?”.
  • The non-existent pub kind X : T = { field: value } declaration shape gets a directed hint naming the two real alternatives (pub kind X : T { field: … } for a typed instance, or a separate pub fact/construction).

Diagnostics inventory

CodeNameTier
OE0631MetarelEndpointMetatypeMismatchmeta-calculus (06xx) — D1
OE0632ReflectionOnUnclassifiedmeta-calculus (06xx) — D4
OE1341RelationCardinalityExceededruntime/write gate (13xx) — D6 max-cap
OW1342RelationMinCardinalityDeferredbuild composition (13xx) — D6 min-card, warning-severity

Out of scope

  • The macro atom — its own later design discussion.
  • Multi-valued axes — RFD 0027’s recorded non-decision stands.
  • Full min-cardinality-under-OWA enforcement — gated on RFD 0007’s evaluation channel; D6 enforces max-caps and explicitly flags deferred min-cards.
  • Cross-module relation-subsumption parent resolution — unchanged from RFD 0005.

Wire / drift-gate note

D1 (position_metatypes) and D6 (Cardinality) add fields to MetarelDeclBody / RelationDeclBody. These are storage-mirror shapes; the @[language_interface] drift gate covers the syntax inductives, not the storage bodies (the storage mirror is a documented known-limitation of the gate, RFD 0027 D9 §note). The Lean storage mirrors gain the fields in the catch-up; the Rust wire is canonical for these runtime-facing bodies.

RFD 0032 — oxup manages editor-extension installation

  • State: committed
  • Opened: 2026-06-13
  • Decides: how the Argon editor integration (the VS Code extension today; Neovim / Vim / Emacs later) is installed and kept in sync with the active toolchain — by oxup, abstracted over editors, rather than hand-installed. Closes the “users must hand-install the VS Code extension” gap. Builds on RFD 0013 (the oxup manager + the argon.sharpe-dev.com CDN) and RFD 0013 (the oxup manager + dist layout).
  • Implements: a new oxup extension (alias ext) subcommand; an EditorIntegration abstraction; a CDN asset path for the .vsix; auto-wiring from oxup init / oxup update.

This RFD records, as built, the editor-extension story. The first cut shipped the VS Code family (VS Code, Cursor, VSCodium, VS Code Insiders) via an --install-extension CLI; Neovim, Vim, and Emacs (#393) now install by file placement — the embedded plugin tree plus a managed config block in the user’s init file — since those editors have no install-CLI contract. Nothing is silently skipped (the no-hollow-features house rule).


Question

Today the Argon VS Code extension (editors/vscode/, id argon-lang.argon) is built by release.yml’s build-vsix job and attached to the GitHub Release. A user who wants it must find the .vsix, download it, and run code --install-extension by hand — there is no version coupling to the toolchain they installed, and nothing refreshes it when they oxup update. We already own the install story for the toolchain (oxup install fetches a version-matched, sha256-verified artifact from the CDN). The editor extension should ride the same rails.

  1. What is the command surface? One subcommand, abstracted over editors, so vim/neovim/emacs can slot in without a new top-level verb.
  2. How does the extension version stay coupled to the toolchain? A user on stable 0.2.1 must get the 0.2.1 extension, not “latest”.
  3. Where does the asset live and how is it verified? Same discipline as the toolchain fetch: immutable versioned CDN path, sha256 sidecar, fail-closed.
  4. What happens for editors we don’t yet support? Loud refusal or silent skip?

Decision

1. Command surface — oxup extension (alias ext)

oxup extension install   [--editor <vscode|cursor|codium|code-insiders|code-server|neovim|vim|emacs>] [--archive <path.vsix>] [--extensions-dir <dir>]
oxup extension uninstall [--editor <name>] [--extensions-dir <dir>]
oxup extension list
  • install (no --editor): auto-detect every installed VS Code-family editor (by its CLI on PATH) and install the extension matching the active toolchain version into each. With --editor, target exactly one. With --archive <path.vsix>, install a local .vsix (offline / a freshly built extension) instead of fetching from the CDN.
  • uninstall: remove argon-lang.argon from the detected (or --editor-named) editors.
  • list: show detected editors and, for each, the installed argon-lang.argon version (or “not installed”).

2. Editor abstraction — EditorIntegration

An enum Editor with a small behavioral surface (oxup/src/extension.rs):

methodmeaning
name() -> &strthe --editor key (vscode, cursor, codium, code-insiders, code-server, neovim, vim, emacs)
detect() -> Option<PathBuf>the editor’s CLI on PATH, or None
install_argv(vsix) -> Result<Vec<OsString>>the exact argv to install a .vsix
uninstall_argv() -> Result<Vec<OsString>>the exact argv to uninstall argon-lang.argon

The VS Code family maps vscode→code, cursor→cursor, codium→codium, code-insiders→code-insiders, code-server→code-server; detect by that CLI on PATH; install via <cli> --install-extension <vsix> --force; uninstall via <cli> --uninstall-extension argon-lang.argon.

code-server (browser-hosted, server-side VS Code — Daytona/ODE sandboxes) is a full member of the family: it honors the same --install-extension/--uninstall-extension contract. A server sandbox usually has only code-server on PATH (no desktop code), so it is in the auto-detect order — but last, after the desktop editors, so a desktop editor wins when both are present. Because the running code-server instance is launched with an explicit --extensions-dir, a fresh code-server --install-extension would otherwise land in the default dir; the optional --extensions-dir <DIR> argument on install/uninstall is appended to the editor argv to target the dir the live server actually reads. The flag is accept-and-passthrough for the desktop CLIs too (VS Code’s code supports it), optional, and omitted by default (the CLI’s default dir). code-server has no macOS .app bundle, so its detection is PATH-only (no bundle fallback).

Neovim, Vim, Emacs have no --install-extension CLI, so they install by file placement (oxup/src/editor_plugin.rs, #393): the plugin sources (editors/nvim/, editors/emacs/) are embedded into the oxup binary, placed into the editor’s native package dir, and the user’s init file gets a managed config block between begin/end sentinels that loads the plugin and points ox lsp at the active toolchain.

  • Neovim$XDG_CONFIG_HOME/nvim/pack/argon/start/argon (auto-loaded by Neovim’s built-in packages), managed block in init.lua.
  • Vim~/.vim/pack/argon/start/argon (the syntax + filetype-detection floor; the Lua LSP client needs Neovim 0.8+), managed block in ~/.vimrc.
  • Emacs~/.emacs.d/argon on load-path, managed block in ~/.emacs.d/init.el.

The block is idempotent and non-destructive: re-running rewrites only the region between the sentinels (one block, never duplicated), leaving hand-written config untouched; uninstall strips the block and removes the placed tree. The sentinels are commented in the init file’s own language (-- for Lua, " for Vimscript, ;; for Lisp) so the line is never a syntax error. The install_argv / uninstall_argv CLI helpers still refuse these editors loudly — they have no CLI contract — and redirect to the file-placement path; nothing is a silent no-op.

Version coupling for file-placement editors. There is no CDN .vsix to address. Instead the coupling is structural: oxup ships from the same release pipeline as the toolchain and carries the same version, so the embedded plugin (including its generated version.lua / argon-version.el stamp) matches the toolchain oxup installs, and oxup update (which self-updates oxup and re-runs the install) refreshes it. The managed block additionally points ox lsp at the active toolchain’s ox, so the editor always talks to the matching language server. Auto-wire stays VS-Code-only: oxup init/update never write into a user’s init.lua/init.el unbidden; file-placement editors install only on an explicit --editor.

The placement plan is constructed and asserted in tests over a temp tree without invoking a real editor; only the write/create_dir_all/remove side effects run at the CLI boundary.

3. Asset source + version coupling

The extension version tracks the toolchain version. In release.yml’s build-vsix job, the resolved $version is stamped into editors/vscode/package.json version before vsce package, and the artifact is named argon-<version>.vsix. publish-dist uploads it (plus a bare-hex .sha256 sidecar) to:

s3://argon-dist-sharpe/editors/vscode/<version>/argon-<version>.vsix
                       /editors/vscode/<version>/argon-<version>.vsix.sha256
CDN: https://argon.sharpe-dev.com/editors/vscode/<version>/argon-<version>.vsix

oxup extension install (no --archive) resolves the active toolchain’s concrete version (read from the installed toolchain’s manifest.toml version, so a stable channel maps to the real 0.2.1), fetches editors/vscode/<version>/argon-<version>.vsix, sha256-verifies it against the sidecar (same fail-closed discipline as the toolchain fetch), writes it to a temp file, and hands that path to the editor CLI’s --install-extension. The .vsix is also still attached to the GitHub Release (the secondary download path).

The CDN base URL is the existing dist_base_url() ($OXUP_DIST_URL, default https://argon.sharpe-dev.com), so a mirror / smoke host is honored end to end.

4. Auto-wire from init / update

After the toolchain is placed:

  • oxup init (non-minimal) detects installed VS Code-family editors and installs/refreshes the matching extension. --no-extension skips it; --minimal already skips it (it doesn’t fetch a toolchain at all). If no editor is detected, print a quiet one-line note — not an error.
  • oxup update refreshes the extension for the channel it updated, but only when the version changed: the installed extension version is recorded in settings.toml ([extension] installed_version), and update re-installs only if the new toolchain version differs.

A failed extension install during init/update is a soft failure (warn, don’t abort): the toolchain is what matters; the extension can be installed later with oxup extension install.

5. Editor-support matrix

EditorStatus
VS Code (code)supported
Cursor (cursor)supported
VSCodium (codium)supported
VS Code Insiders (code-insiders)supported
code-server (code-server)supported — server-side VS Code (Daytona/ODE); --extensions-dir for the live server dir
Neovim (neovim)supported — file placement into nvim/pack/argon/start/argon + managed init.lua block (#393)
Vim (vim)supported — syntax + ftdetect floor into ~/.vim/pack/... + managed .vimrc block (#393)
Emacs (emacs)supported — .el package into ~/.emacs.d/argon + managed init.el block (#393)

Why this shape

  • One verb, editor-abstracted. A single extension subcommand with an Editor enum keeps the vim/neovim/emacs work a matter of filling in install_argv, not adding CLI surface. The no---editor auto-detect mirrors how a user expects “install the extension” to just work across whatever VS Code-family editors they have.
  • Version coupling over “latest”. Stamping the toolchain version into the .vsix and addressing it at an immutable /editors/vscode/<version>/ path means a pinned toolchain gets a matching extension, and the CDN path is 1-year-cacheable like the toolchains. This avoids a “latest extension against an old toolchain” skew once the extension grows toolchain-coupled behavior (LSP protocol versions, server flags).
  • Reuse the fetch discipline. The .vsix fetch reuses dist_base_url() and the same sha256-verify-before-use path as the toolchain, so there is no second, weaker download path.
  • Loud, not silent, for unsupported editors. Recognizing vim/neovim/emacs and refusing with a specific pointer (and an issue number) is the house rule: a no-op that pretends to work is worse than an honest “not yet.”

Out of scope / deferred

  • Windows. v0.2 is macOS + Linux only; the VS Code CLIs exist on Windows but the rest of the oxup layout is unix-only (RFD 0013).
  • Marketplace / Open VSX publish. Argon is Sharpe-internal; the .vsix is distributed via the private CDN + the GitHub Release, not a public marketplace.

RFD 0033 — The ad-hoc query and mutation surface

  • State: accepted — implemented
  • Opened: 2026-06-14
  • Decides: that arbitrary, not-pre-declared (ad-hoc) queries and mutations are a first-class, default-on capability of the Argon runtime — submitted as source text at request time, parsed, lowered, and executed against the loaded module — with a deployment opt-out that restricts a server to declared invocables only. Establishes that the generic submission path is the substrate, and the declared pub query / pub mutate forms are a thin named wrapper over it — not the only door. Builds on RFD 0014 (the serving surface), RFD 0015 (the mutate body / Operation IR), RFD 0020 / RFD 0021 (the Engine/CompiledRule evaluation seam), and RFD 0022 (the build evaluability gate, whose runtime analogue this RFD must define).
  • Implements (as built): the query-provider Schema interface (oxc_types::Schema) with two parity-gated backends — WorkspaceSchema (build-time, over ASTs) and oxc_runtime::ModuleSchema (runtime, over a loaded .oxbin); the checker (oxc-check) fully routed through it; the runtime frontend (oxc-parser/oxc-check/oxc-instantiate now linked into oxc-runtime); Store::eval_{query,mutation}_source (parse → full type-check → lower → run, ill-typed bodies refused and never run); the POST /v1/{query,mutation}/adhoc HTTP surface + ox query --eval CLI; the AdhocPolicy opt-out (default-on); and the build-vs-runtime agreement gate (oxc-runtime/tests/adhoc_agreement.rs) asserting byte-identical diagnostics + lowered IR. The persisted-projection-cache / IVM materialization arc is a separate follow-on, out of this RFD’s scope.

Question

A data system you cannot query ad hoc is not a database. Argon’s design intent — stated repeatedly and recorded since 2026-05-29 — is that the runtime accepts arbitrary queries and mutations at request time, not only the “stored-procedure” pub query / pub mutate declarations that lower into .oxbin. The declared forms are meant to be a convenience layer over a generic ad-hoc path. A deployment may turn ad-hoc off (lock down to declared-only) for safety, but that is a gate you enable, not a default-closed wall.

Today that path is unbuilt at the edges, and — separately — the project’s own notes and one prior analysis have repeatedly mis-described it as “rejected by design.” It is not. This RFD settles:

  1. What the ad-hoc surface is (wire shape, CLI shape, semantics), for queries and mutations together.
  2. How a body submitted as source text is compiled at runtime, given that the compiler frontend is not currently linked into the serving binary.
  3. The resolution context: how names and types in an ad-hoc body resolve against the loaded module rather than a build-time Salsa workspace.
  4. How much type-checking an ad-hoc body receives (answer: the full amount), and what decidability-tier admittance applies at runtime (answer: build-gate parity by default, configurable).
  5. The security model: default-on, the deployment-level opt-out, and affordance parity — ad-hoc is governed by the same uniform capability scoping as declared invocation, never an ad-hoc-specific leash.

Context

The framing matters because it has been wrong. The corrected, code-verified picture:

The reasoner is rule-as-data, and the compile step already runs at request time. In Store::query_body_dispatch (compiler/crates/oxc-runtime/src/lib.rs:67996824) the runtime decodes a query’s AtomIR body + head Term, calls oxc_reasoning::compile::compile_rule(short, &head, &atoms) at dispatch time, pushes the fresh CompiledRule onto the module’s rules, and runs Engine::evaluate(&rules, &mut catalog, …). The engine consumes &[CompiledRule] as plain data; it has no notion of “pre-declared.” The only thing tying this to a declaration is the source of atoms/head: find_query_decls(name) looks them up from .oxbin-loaded QueryDeclBodys rather than from the request.

The mutation interpreter is already general and decl-agnostic. Store::run_body_op (oxc-runtime/src/lib.rs:3548+) interprets an Operation sequence (InsertIof, InsertTuple, Update, For, If, Return, … — oxc-protocol/src/core_ir.rs:389) and does not take the MutationDecl; the decl is consulted only for argument validation and capability checks at the boundary. The storage write methods (emit_iof_assertion, emit_relation_tuple, emit_individual_property_assertion) are origin-agnostic.

So the constraint is not semantic. It is three concrete wiring gaps:

  1. No request field for a body. DispatchDescriptor is { qualified_path, args, return_type } (oxc-serve/src/lib.rs:2671); resolution is query_decls.get(qualified_path) → 404 if absent. There is nowhere to put a body. (The runtime refusal ARGON_RUNTIME_UNSUPPORTED_QUERY_BODY at oxc-serve/src/lib.rs:6749 is a narrower executor gap — field projections in bodies are not yet executable — not an ad-hoc policy.)

  2. The compiler frontend is not linked into the serving binary. oxc-serve and oxc-runtime depend on oxc-reasoning, oxc-protocol, oxc-oxbin (+ storage) — and not oxc-parser, oxc-check, oxc-instantiate, oxc-resolver, or oxc-db (verified in both Cargo.tomls). The runtime can compile pre-lowered IR but cannot turn source text into IR.

  3. Name/type resolution is build-time. The frontend’s full type-checker (oxc-check) is bound to a Salsa OxcDb/Workspace/resolver.

The fourth fact reshapes the whole design and is why this is tractable:

Lowering is already decoupled from Salsa. oxc_parser::parse(source_text: &str) -> Parse (oxc-parser/src/lib.rs:44) is standalone — string in, parse tree out, no DB. Rule-body lowering is body_to_atoms_ctx(list: &SyntaxNode, ctx: &LowerCtx) -> Vec<AtomIR> (atom_lower.rs:49), and LowerCtx (expr_lower.rs:116) resolves names through plain closuresresolve_type: &dyn Fn(&str) -> Option<NameRef>, plus enum-variant and field-optionality resolvers — not Salsa. In oxc-instantiate/src/lower.rs, every &dyn OxcDb use is parse_file(db, file): the DB’s only job in the lowering data path is to produce the parse tree.

The genuinely Salsa-heavy component is oxc-check (reference resolution + full type inference via resolve_path(db, workspace, file, …) and lower_type_expr(…)), and it runs separately, after lowering. So “decouple frontend lowering from Salsa” splits into two very different tasks:

  • (a) Lowering is already call-site-decoupled. The work is to build a LowerCtx whose closures are backed by the runtime Module / .oxbin catalog instead of the build-time file pre-pass. Small.
  • (b) Type-checking is Salsa-bound. Reproducing it at runtime — or deciding ad-hoc bodies get lighter validation — is the large, separable decision.

What the runtime already exposes for (a): Module (oxc-runtime/src/lib.rs) carries concept_id, concept_id_by_short_name, relation_id, ancestor_concept_ids_including_self, resolve_predicate_key, resolve_rule_name, resolve_mutation_invocable, symbol_path, and (today private) declared_field. The .oxbin declaration bodies (oxc-protocol/src/storage.rs) carry field declarations with type expressions, refinement predicates, relation arg concepts/cardinalities, and query/mutation parameter types — encoded as CBOR. The information needed to back the LowerCtx closures exists; Module simply doesn’t yet expose a resolution surface over it (notably: resolving names inside a CBOR-encoded TypeExpr, field-type lookup, and a parameter catalog).

Decision

Adopt a two-tier surface, with the generic path as substrate and declared decls as a wrapper.

Tier A — the generic submission substrate

A submitted body flows through the same runtime seam declared invocables already use:

  • Queries: (head Term, Vec<AtomIR>)compile_rule → appended to module rules → Engine::evaluate → rows. (This is literally the query_body_dispatch path with the IR sourced from the request instead of find_query_decls.)
  • Mutations: Vec<Operation> (+ params) → the existing run_body_op interpreter, under the same atomicity, read-your-writes, and delta-guard contract as declared mutations (RFD 0015 / RFD 0019).

Tier A is reachable in two framings, in priority order:

  1. Source text (the product surface): the request carries an Argon query/mutation body string. The runtime parses and lowers it (Tier B) to the IR above, then runs it. This is what ox query '<body>', a REPL, and an /v1/query HTTP endpoint use.
  2. Pre-lowered IR (the substrate boundary): the IR itself is the unit Tier A executes. It is the internal contract the source-text path compiles down to, and declared decls already produce it. Whether IR is also a public client surface is left open deliberately (§Open) — it is a performance/optimization question (a precompiled/prepared-statement analogue), and the answer should be whatever is correct once the prepared-body / caching design is worked out, not a guess made here. Note that IR submitted directly would bypass the type-checker, so if exposed it must carry its own validation story — another reason to settle it with the performance design rather than now.

Declared pub query/pub mutate become wrappers: their dispatch resolves a name to stored IR and then enters the same Tier A execution. No second engine path.

Tier B — runtime parse + lower + check (the resolution contract)

Parsing is the easy part: oxc_parser::parse(source_text: &str) -> Parse is already standalone (no DB). The hard part — resolving and type-checking the body against the loaded schema — is solved by a single proven pattern, not by carrying source and not by a second checker.

The query-provider pattern (decision #3, refined 2026-06-15). Across every mature separately-compiled language — rustc (.rmeta as a query provider: tcx.type_of(def_id) is answered from local HIR or by decoding metadata, dispatched only by local-vs-extern), Go (go/typesImporter), OCaml/GHC/SML/Scala/F# (rehydrate serialized data into the same Env / TyThing / StaticEnv / typed-tree the checker already consumes) — the dominant, unanimous design is one type-checker whose environment access is an interface, answered either from source (local) or from already-resolved serialized facts (imported / loaded). Nobody re-elaborates the dependency’s source; nobody forks the checker. The PL-theory framing is the same (external prior art, cited as ideas not authority): F-ing modules’ “signatures are views over the kernel’s type structure, not a parallel type system,” and the .olean / .ttc interface-file precedent that the serialized environment is the type-checking source-of-truth.

Concretely for Argon:

  • Introduce a Schema interface — the narrow set of environment-access operations the frontend actually performs: resolve a name to a declared concept/relation/struct/enum; a concept’s fields and their types; subsumption/parent edges; relation argument arities and types; enum variants; query/mutation parameter types; and each concept’s world assumption (CWA/OWA) (so three-valued OWA refinement checking can’t silently diverge — a substrate-research caveat).
  • oxc-parser (standalone), oxc-instantiate body-lowering (already (&SyntaxNode, &LowerCtx), no DB), and oxc-check all resolve through Schema. The build-time backend answers from the Salsa workspace / ASTs (today’s code, behavior unchanged); the runtime backend answers from the loaded module. The inference and lowering logic is shared and untouched — only the environment-access surface is abstracted. This is the rustc local-vs-extern split, not a rewrite of the type system.

The runtime backend reads a projection over the event log — it serializes nothing new. This follows from how Argon storage works today (verified in current code, not assumed): storage is a single append-only axiom_events log (oxc-protocol’s AxiomEvent; the axiom_events table in oxc-storage-pg), and Module already builds its concept/relation/field indexes from that event stream at load. So the runtime Schema is a reader over the catalog projection the Module already builds from declaration eventsnot an embedded copy of source and not a separate schema section. The data it needs (resolved field TypeExprs, parent ids, relation arg types, params, refinement predicates) is already in the .oxbin decl bodies. We do not add a redundant representation of facts the log already holds; we expose them through the interface.

The artifact-identity and drift-guard machinery already exists; the Schema backend keys on it. The separate-compilation literature is unanimous that cross-boundary type identity must be a persistent content hash (rustc DefPathHash + StableCrateId; SML content-derived PIDs), never a structural match or an allocation-order stamp. Argon’s .oxbin already implements this: per-section BLAKE3 content hashes and a composition signature (oxc-oxbin/src/composition_signature.rs, content_hash.rs, section.rs), a multi-axis version preamble with strict-producer/liberal-consumer gating checked at the load site before any body section (versioning.rs; reader.rs), and a load-time tier gate (validation.rs layer1_validOE1204). So the boundary is already guarded two ways — a hard version/format header (deterministic refusal of an incompatible artifact) plus content fingerprints over the sections. The runtime Schema backend identifies its schema by the loaded module’s composition signature and section hashes; nothing new is invented here.

The genuine residual is narrower than “no identity”: artifact-level identity is solid, but it is not yet threaded to per-event / per-symbol identity inside the store — the storage-side gap where module_id is effectively constant, so two schemas’ symbol ids can collide at the event level (a known storage defect). Schema resolution must carry module/artifact identity down to per-symbol resolution; fixing that is shared with the storage-identity work, not additive to it.

Type-checking: full, no shortcuts (decision #1)

An ad-hoc body receives the same, complete type-checking a declared body gets — name resolution, reference checking, full inference — via the same oxc-check logic, now resolving through Schema. There is no “lighter validation” tier and no unchecked-but-executed path; a half-checked ad-hoc surface would be exactly the hollow feature the house rules forbid.

Parity is enforced as a canonical-input contract + agreement test — the same discipline the repo already runs at the Lean↔Rust boundary (the @[language_interface] drift test in oxc-protocol, where one logical contract is checked across two representations). Schema is the only way the frontend may touch the environment — no caller reaches around it to the AST or the catalog (make-illegal-states-unrepresentable) — and a CI agreement test asserts that the same body checked against the build-time and runtime Schema backends yields byte-identical diagnostics and identical lowered IR. Drift is a defect, gated like any spec↔code drift.

Decidability-tier admittance (decision #2)

By default an opted-in deployment admits the same tier ceiling as the build evaluability gate (RFD 0022) — ad-hoc bodies are held to the identical decidability bar as declared ones. The load-time tier gate that enforces parity already exists (oxc-oxbin/src/validation.rs layer1_valid, refusing max_tier_claimed beyond the runtime’s capability with OE1204); an ad-hoc body’s classified tier is checked against the same ceiling. The ceiling is intended to be configurable per deployment (a server may set a lower ad-hoc ceiling for untrusted callers) — that per-call/lenient mode is not yet built (today’s gate is artifact-level strict) — but the default is parity, and a deployment may not silently admit more than the build gate would.

Security: affordance parity, deployment-level control only

The governing principle (decision #4): ad-hoc queries and mutations have the same affordances as everything else. Ad-hoc is not a hobbled subset of the declared surface — it is the surface, with declared forms as the named convenience layer over it. We do not special-case what ad-hoc may express, read, or write relative to a declared invocable. The Postgres test applies: a system you cannot freely query and mutate is not a useful system.

Control is therefore deployment-level, applied uniformly, never an ad-hoc-specific leash:

  • Ad-hoc submission is on by default. A deployment may opt out to restrict to declared invocables only (lock-down), or run read-only (a normal database posture, not an ad-hoc penalty) — these are the same kinds of switches any database exposes.
  • Whatever capability / RBAC / tenant / fork / standpoint scoping exists applies equally to declared and ad-hoc invocation. An ad-hoc mutation that a caller’s capabilities permit is exactly as permitted as the equivalent declared mutation.
  • One capability exception — forget. Physical erasure (forget) is gated on the build-time #[allow_forget] grant, which is a declaration-site capability. A runtime-submitted body has no declaration site and so cannot confer it on itself; an ad-hoc forget is therefore refused (OE0730). This is not an ad-hoc-specific leash on affordance — it is that a request cannot forge a build-time capability grant (the same reason an ad-hoc body cannot, say, mark itself #[brave]). A declared #[allow_forget] mutate still erases; an ad-hoc body cannot. We record this as the deliberate exception to the otherwise-unqualified parity rather than pretend parity is total (originally this section asserted no Forget gate at all — that was the bug, not the code).
  • This is orthogonal to the generic-entity-write denial (POST /v1/entities → 404, oxc-serve/src/lib.rs:9565): that is an untyped-blob REST shape, a different axis. Ad-hoc writes go through the typed mutate/Operation mechanism with the full mutate affordance set. The two must not be conflated.

Forward compatibility: heterogeneous stores (keep this seam clean)

The stated future is specialized stores — relational / columnar / blob — that are “part of the Argon knowledge graph,” queried uniformly, with per-data placement configured in ox.toml. That design is not settled here, but this RFD must not foreclose it. Two principles, grounded in current-repo design intent (RFD 0020) and external prior art (database catalog/connector SPIs; the BYODS work, Sahebolamri et al., OOPSLA 2023):

  • Schema stays strictly store-agnostic. Schema answers type questions only; it must never know where bytes live. Physical placement is a separate layer — RFD 0020’s BYODS (D6: a physical Relation is an interface; representations coexist) plus the RuntimeStorageBackend seam, selected per-relation by ox.toml placement. This is the OBDA shape (data stays in place, queried through the ontology; ox.toml placement is the R2RML analogue), and the catalog/connector SPIs (Calcite Schema/Table.getRowType, Trino ConnectorMetadata) confirm the split: the engine owns the type system; sources map into it and never own planner type semantics.
  • The ad-hoc path lowers to LogicalPlan, not to a single in-memory catalog. RFD 0020 D2 already decided that ad-hoc queries, declared rules, and the type-checker goal all lower to one shared LogicalPlan (the IR scaffolded but currently dead in oxc-reasoning/src/logical/). Lowering ad-hoc bodies to that IR — rather than hard-wiring the current materialize_predicates pull-everything-into-memory model — is what keeps the surface multi-store-ready by construction. When pushdown arrives it follows the proven contract: an optimization never an obligation, negotiated as (handle-that-absorbed-work, remainder) with the residual always re-checkable in-engine (Trino/FDW), capability modeled as binding patterns (a blob/KV store can’t free-scan, TSIMMIS), and shippability gated on determinism + identical both-sides semantics.

This RFD is, in effect, the realization of RFD 0020 D11 (“ad-hoc queries and mutations are first-class … gating is an engine policy, not a language restriction”); its new contribution is the runtime-frontend mechanism (the Schema query-provider, content-addressed identity, parity discipline) that D11 left unspecified.

Rationale

  • Reuse over reinvention. The execution substrate (compile-at-dispatch for queries, the general Operation interpreter for mutations) already exists and already runs at request time. Tier A is mostly routing: let the IR come from a request. This is why “ad-hoc is impossible by design” was always wrong.
  • Lowering is already where we need it. Because parse is DB-free and LowerCtx is closure-based, the runtime lowering path is a Module-backed resolver + a dependency edge — not a rewrite of lowering.
  • One frontend, no drift. Reusing oxc-instantiate lowering and oxc-check type-checking against a Module-backed context (rather than runtime-only reimplementations) keeps build-time and runtime behavior identical, honoring the spec↔code drift discipline. Byte-for-byte diagnostic agreement is the acceptance test.
  • Full parity, no shortcuts. Ad-hoc bodies are type-checked exactly as declared bodies are (decision #1) and hold the same decidability ceiling by default (decision #2). A partially-checked ad-hoc surface would be a hollow feature; we do not ship one.
  • Ad-hoc is the surface, not a sandbox. Declared forms are sugar over the generic path; ad-hoc has full affordance parity (decision #4). Control is deployment-level and uniform, never an ad-hoc-specific restriction.
  • Default-on matches the product. Locking down is a deployment choice, not the substrate’s posture.

Alternatives considered

  1. Declared-only forever (status quo). Rejected: contradicts the stated design intent; “a database you can’t query ad hoc isn’t a database.”
  2. Source text only, IR never public. Likely, but not decided here: whether IR is also a public (prepared-statement-style) surface is folded into the performance/caching design (decision #3, §Open) so the answer is the correct one rather than a guess.
  3. A separate runtime-only frontend fed by an .oxbin catalog (decision-#3 option B). Rejected: faster to stand up but creates a second lowering/checking path that drifts from the build-time one — the exact failure mode the intent-node/drift-gate discipline exists to prevent.
  4. Ship ad-hoc with reduced/“lighter” validation first, full type-checking later. Rejected (decision #1): a half-checked surface is a hollow feature. Full oxc-check parity is in scope from the start, which is what pulls the checker into the runtime frontend.
  5. A special capability leash on ad-hoc writes (extra gates on Update/retract because they are ad-hoc). Rejected (decision #4): ad-hoc has affordance parity; control is uniform and deployment-level. The lone exception is forget, refused for ad-hoc — but that is not a leash on affordance, it is that forget’s #[allow_forget] capability is conferred at a declaration site a request doesn’t have, so the request can’t forge it (see Security).
  6. A generic untyped entity-write endpoint (POST /v1/entities). Rejected/kept-absent: ad-hoc writes belong to the typed mutate/Operation mechanism, not an untyped blob surface.

Consequences

  • New runtime dependencies: oxc-serve/oxc-runtime gain the frontend — oxc-parser, oxc-instantiate, and (per decision #1) oxc-check / oxc-resolver / oxc-types, once their environment access is routed through Schema. This is a substantial change to the runtime’s relationship to the frontend (the runtime/AGENTS.md “the reasoner was not built here” tombstone framing and the oxc-runtime/oxc-serve intent nodes all need updating). Introducing Schema as the sole environment-access contract — with the build-time backend over ASTs and the runtime backend over the event-log projection — is the bulk of the engineering and lands as its own arc before the surface is wired.
  • Artifact identity + drift guard already exist; per-symbol identity is the residual. Artifact identity (composition signature + per-section content hashes) and the version/tier load gates are already built (oxc-oxbin: composition_signature.rs, content_hash.rs, versioning.rs, validation.rs). The runtime backend reuses them. What remains is threading that identity to per-event/per-symbol resolution (the storage-side module_id collision gap) so two schemas’ symbol ids can’t alias — shared with the storage-identity fix, not additive.
  • New Schema-backing Module surface: name/type/parameter/world-assumption/refinement resolution over the CQRS catalog projection (additive; the facts are already in the .oxbin decl bodies — no new serialized representation, no embedded source).
  • New wire + CLI surface: a generic submission request shape and ox query '<body>' / REPL entry (exact shapes in the implementing PRs).
  • Spec/Lean: per the repo workflow, this is language-surface — RFD + reference draft → Lean → code. The reference (spec/reference/) gains an ad-hoc-submission section; the Lean substrate is unaffected in its semantics (an ad-hoc rule is just a rule), but the storage/runtime contract may need to record that evaluation admits request-sourced rules, and the security/opt-out posture should be described where the serving contract lives.
  • The “rejected by design” framing is retired in code comments, AGENTS nodes, and project memory.

Open questions

Decisions #1–#4 are settled above, and the resolution mechanism is settled as the query-provider Schema interface (one checker, build-time backend over ASTs, runtime backend over the event-log projection — the rustc/Go model). What remains genuinely open:

  1. The exact Schema operation set. The minimal trait surface (it must cover name→declaration resolution, field/param types, subsumption edges, enum variants, world-assumption, and refinement metadata) and how much it reuses the indexes Module already builds (concept_ids, relation_signatures, etc.) vs. adds. Identity/fingerprint is not open — the artifact already carries it (composition signature + section hashes); the backend keys on that. Lazy per-name materialization (the Idris .ttc pattern) is a future optimization, not needed for v1 since Module already eagerly indexes the (small) schema.
  2. The performance / prepared-body design (decision #3). The load-bearing open thread: compile-caching of recurring ad-hoc bodies (keyed by body hash + composition signature — the content-hash machinery already exists), whether a public prepared-IR fast-path is the correct surface, and how Salsa incrementality is reused at runtime. The IR-submission question is answered here, not in isolation.
  3. Materialization model. Ad-hoc reads today inherit the full in-memory materialize_predicates build (oxc-reasoning; SemiNaiveExecutor). The intended replacement — a content-addressed, generation-invalidated projection cache (the read-model section is already reserved in .oxbin and invalidation exists in oxc-storage-pg get_projection_cache, but it is not populated; the DBSP/IVM executor is drop-in-ready but gated) — is a real forward arc. The ad-hoc path should target that Engine/projection-cache seam rather than entrench the full-scan, and this overlaps the external/foreign-relation (“market oracle”) thread.
  4. Standpoint / fork / bitemporal scoping. Ad-hoc bodies need the same as_of / standpoint / fork context as declared dispatch; query_body_dispatch currently refuses across-standpoint parameterized bodies (oxc-runtime/src/lib.rs:6787). The ad-hoc path must reach full parity here, so that refusal is a gap to close, not a boundary.

RFD 0034 — Source text encoding and the Unicode lexical policy

  • State: committed
  • Opened: 2026-06-14
  • Decides: that Argon source is UTF-8 and that identifiers are Unicode per UAX #31, comments and string/char literals admit arbitrary UTF-8, and operators/punctuation/keywords stay ASCII (modulo the established // typeset aliases). Records two safety/correctness items as documented fast-follows: NFC normalization at the name-resolution layer (canonical equivalence) and a mixed-script confusable warning (UAX #39). Also records the module-file membership rule (Rust semantics: only mod/use-reachable files are part of a package) and its loud counterpart, OW0710.
  • Implements: §2.1/§2.3 of the reference; the lexer change in oxc-lexer; the reachable-closure workspace build in oxc-workspace; OW0710 (OrphanModuleFile) flipped from reserved to live.

This RFD records, as built, two adjacent lexical-layer decisions that surfaced together while diagnosing a real authoring incident: a tenant ontology package whose editor lit up with “unsupported non-ASCII character” diagnostics pointing at obviously-valid, pure-ASCII rule files.


Question

  1. Encoding. §2.1 already declared source UTF-8, but §2.3 defined identifiers as [A-Za-z_][A-Za-z0-9_]* (ASCII only), and the lexer rejected any non-ASCII byte outside string literals — including in comments. So an em-dash in a // comment was a hard lexer error. What is the real policy?
  2. Identifiers. Should identifiers be ASCII-only, or full Unicode? If Unicode, with what normalization, and how do we keep visually-confusable homoglyphs from silently denoting different names?
  3. Module membership. A .ar file sitting in a package’s source tree but declared by no mod was being lexed, parsed, and checked — and (through Salsa accumulation during cross-module name resolution) its lex errors bubbled up and were misattributed to sibling files. Is a non-mod-reachable file part of the package?

Decision

1. Source is UTF-8; non-ASCII is admitted in identifiers, comments, and literals

  • Comments (//, ///, //!, /* */) and string/char literals admit arbitrary UTF-8. (The lexer already scanned these byte-by-byte; the policy is now explicit and tested.)
  • Operators, punctuation, and keywords are ASCII. The only non-ASCII operator forms are the recognized typeset aliases (U+2291 → <:), (U+22A4 → Top), (U+22A5 → Bot). A non-ASCII codepoint in operator position is still a hard error (OE0001), now reported at the correct file and codepoint.

2. Identifiers are Unicode (UAX #31)

An identifier starts with a XID_Start character or _ and continues with XID_Continue characters (unicode-ident, the rustc-grade table). The ASCII subset is the common case and the recommended style. The token text is the raw source slice, byte-for-byte — see the lexer constraint below.

This follows the Rust/Cargo aesthetic (Rust accepts Unicode identifiers per UAX #31) and keeps the substrate ontology-neutral: a vocabulary authored in a non-Latin script is first-class.

Lexer constraint — token text must equal the source bytes. The parser rebuilds the rowan green tree from token text and derives every node’s text_range() by accumulating token byte-lengths. If the lexer rewrote an identifier’s text (e.g. folding a de-normalized spelling to NFC), the tree’s offset space would diverge from the raw-source offset space that the checker, the LSP LineIndex, and miette all index against — shifting every downstream span. So canonicalization does not happen in the lexer; the token carries the source bytes verbatim.

NFC normalization — fast-follow. Canonical equivalence (precomposed é U+00E9 vs e+combining-acute) should hold: two such spellings ought to denote the same name. Per the constraint above, that belongs at the name-resolution / interning layer (normalize the name key, not the token text) — the rust-analyzer model. Name comparison is currently spread across the resolver, checker, and elaborator on raw .text(), so doing this correctly is its own focused change. Until it lands, identifiers are matched by their exact source bytes (an NFD and an NFC spelling of the same glyphs are distinct names).

Confusable safety (UAX #39) — fast-follow. Permitting arbitrary scripts reopens the homoglyph surface (Latin A U+0041 vs Cyrillic А U+0410 read identically). The decided mitigation is a warning, not a refusal: a mixed-script confusable identifier is reported so the confusion is loud, never silent. It needs the unicode-security / unicode-script tables and a deny.toml license allowance, so it lands as a focused fast-follow. Until then, cross-script confusables are not yet flagged.

3. Module membership is the mod/use-reachable closure (Rust semantics)

A .ar file is part of a package iff it is reachable from the package entry through a chain of mod/use declarations — exactly as a .rs file is part of a Rust crate only when a mod brings it in. A sibling file no chain reaches is not compiled, not checked, not linted, and cannot contribute diagnostics.

The compiled workspace is therefore built from the reachable closure alone. Leaving unreachable files in the workspace was the root cause of the misattribution in the Question: checking a reachable file transitively parsed every workspace file during name resolution, and an unreachable file’s lex/parse diagnostics bubbled through Salsa accumulation onto whichever reachable file triggered the parse.

Loud counterpart — OW0710 (OrphanModuleFile), now live. Rust silently ignores an unreferenced source file (the IDE hints at it); Argon’s loud-not-silent doctrine and the already-reserved §3.1 code argue for surfacing it. We emit OW0710 as a warning (the build stays green, matching Rust’s non-fatal treatment) at ox check/ox build, naming each on-disk .ar under the schema root that no mod/use chain reaches. This is the diagnostic that would have immediately explained the incident (“rel_example.ar is not part of this package”).


Consequences

  • Vocabulary and model packages may use Unicode identifiers (matched by exact source bytes today; NFC canonical equivalence is the fast-follow above).
  • A scratch/tutorial .ar left in a package’s source tree no longer breaks the build with misattributed errors; it is ignored and surfaced as OW0710.
  • The confusable warning is owed; until it lands, a mixed-script identifier is accepted silently.
  • No change to operators/keywords; // aliases preserved.

RFD 0035 — The composable operator-tree execution pipeline

  • State: discussion
  • Opened: 2026-06-15
  • Decides: the realization of RFD 0020’s composable pipeline (D2/D4/D9) that RFD 0021 D1 deliberately reserved“the optimizable LogicalPlan IR is reserved for [an operator-tree] executor when graph-native physical operators + factorization genuinely demand it.” That consumer has arrived (RFD 0036: foreign-source federation + a relation-valued compute operator + a federation-split optimizer rewrite, none of which has a home in the current CompiledRule-direct path). This RFD builds the operator tree as the single shared lowering target for every front-end, a tree-level optimizer, a physical mapper, and a generalized operator-call executor contract — while preserving the proven semi-naive evaluator (RFD 0021) as the physical operator for the recursive/conjunctive core. It also introduces the relation-valued (table) operator IR — the relation→relation node absent from CompiledAtom today — co-designed with RFD 0029’s aggregate surface.

This RFD is co-designed with RFD 0036 (heterogeneous stores), which is its forcing consumer; 0035 is the engine layer, 0036 is the store layer that lands on it. It is Lean-first where it touches reasoning semantics — the executed meaning conforms to spec/lean/Argon/Reasoning/ (Fixpoint.lean, Datalog/Compiled.lean) and is held there by the differential oracle (RFD 0021 D8); the pipeline structure, operators, optimizer, and mapper are engine architecture the Lean does not mechanize (per AGENTS.md scope), settled from first principles here. It commits a plan, folded into the implementing PRs per the repo’s discussion-first practice.

Reconciled with the performance / distribution / consensus research campaign (2026-06-15). D4/D6/D7/D8 below are updated to record the campaign’s findings: the columnar content-addressed segment + IVM maintainer is the primary read-model (D7, a priority inversion — the architecture was already correct, only its sequencing was set without performance data); the analytical/columnar tier is Argon’s own native vectorized engine, not a delegated one (D4/D6 — Argon is the high-performance engine, never a “dumb” forwarder); and the IVM↔oracle equivalence joins the frozen-EDB theorem as a named obligation (D8). The campaign is research and decides nothing; these edits are the cut, settled in discussion.


Question

Argon’s runtime engine has, by deliberate staging, two halves that don’t meet. The reasoner (oxc-reasoning) is a fast, correctness-first semi-naive evaluator over CompiledRule — indexed + persistent arrangements, worst-case-optimal joins on cyclic bodies, CSR index-free adjacency, factorized aggregates, all anchored to a differential oracle (RFD 0021). Above it sits a designed-but-unwired operator-tree pipeline (logical/, optimizer/, physical/, runtime/operators.rs) that RFD 0021 D1 chose not to wire, on a “build-correctly-once” argument: a LogicalPlan that merely round-trips back into the CompiledRule executor is throwaway scaffolding until a real operator-tree executor exists.

That argument was right, and it carried an explicit trigger condition: the tree gets built when graph-native physical operators + factorization genuinely demand it. RFD 0036 is that demand, and sharper than anticipated:

  • a foreign-source scan is a new leaf the optimizer must rewrite filters/projections into (pushdown);
  • federation-split (absorbed, remainder) is a tree rewrite with no home in a flat rule body;
  • a relation-valued compute operator (e.g. k-means over a columnar securities master) reads a relation and returns a relation — no CompiledAtom variant expresses this (the five variants are Predicate, Comparison, Naf, Compute = scalar map, Aggregate = relation→scalar; verified compile/rule.rs:233-283);
  • binding-limited foreign sources need magic-sets/demand — a tree-level transformation.

What is the execution pipeline that hosts all of this — for declared rules, ad-hoc queries, checker goals, and mutations alike — without throwing away the proven evaluator, and without becoming the monolith nous warned against?


Context

Verified current state (against origin/main @ 1bdfa16)

Now live (see RFD 0065). This RFD’s decision has landed: the LogicalPlan → optimizer → PhysicalOp pipeline below — described here as orphaned/unwired — is now on the production query path (the logical::lower pipeline driven by RFD 0036’s federation consumer). The “two halves that don’t meet” framing and the “never instantiated by any front-end” notes are the pre-landing state; RFD 0065 records the FORK-2 resolution and the narrow genuinely-dead residue.

  • LogicalPlan is orphaned, relational-core only. logical/mod.rs:21Scan/Filter/Map/Join/AntiJoin/Distinct/Recurse/Sink, each Tier-tagged; Filter’s predicate is opaque CBOR “pending the CoreIR expression interpreter.” No graph-native, mutation, compute, or foreign nodes. Never instantiated by any front-end (oxc-reasoning/AGENTS.md:24-32 landmine note).
  • The front-ends bypass it entirely. All compilation goes AtomIR → compile::compile_rule → Engine::evaluate directly — verified at ~11 sites in oxc-runtime (checks.rs:628,644; lib.rs:1211,6807 + the .evaluate( sites 6819/7002/7086/7113/7147/7306/7402/7484/7527/7813/7867). Engine::evaluate (oxc-reasoning/src/lib.rs:144) reorders each body (SIP) and dispatches to a TierExecutor.
  • TierExecutor::execute is whole-program. executor/mod.rs:67-85execute(&self, rules: &[CompiledRule], catalog: &mut RelationCatalog, policy: ConvergencePolicy), writes derived facts into the catalog in place. SemiNaiveExecutor is the only real impl (covers Structural/Closure/Recursive); SLG/DBSP/SMT/Kripke/Kora are docstring stubs.
  • The optimizer that runs is one pass over CompiledRule. optimizer/reorder.rs — SIP/bound-set reorder + cardinality tie-break (RFD 0021 D3); it tracks bound: BTreeSet<VariableIdx> (reorder.rs:61) and consumes catalog sizes (reorder.rs:189-195). The OptimizerPipeline over LogicalPlan (optimizer/mod.rs) is part of the dead family.
  • runtime/operators.rs (map/filter/join/antijoin/distinct/integrate/differentiate) is the dead Z-set operator vocabulary — the semi-naive loop implements joins inline; it marks the future IVM boundary.
  • The catalog is rebuilt per query. query_derive → materialize_predicates → RelationCatalog::new(); the Store holds no materialized catalog (RFD 0021 D7). A CatalogEntry carries a tier + a Relation<Vec<u8>> = BTreeMap<Vec<u8>, Weight> (catalog/mod.rs:70, runtime/relation.rs).
  • The freeze discipline already exists — the wall clock is frozen for the duration of evaluate_to_fixpoint (eval.rs:170-174); stable relations’ arrangements are held across iterations (eval.rs:232-235).

What RFD 0021 established (and we keep)

RFD 0021 D1 made CompiledRule the executed form and the “operator pipeline” the set of operators the evaluator runs, deferring the operator-tree executor to its real consumer. D6 holds every operator a pure Z-set→Z-set function so IVM is an additive outer loop. D8 makes the semi-naive/binary evaluator the differential oracle for every optimization. These are load-bearing and survive intact.


Decision

Build the operator-tree pipeline RFD 0021 D1 reserved, as the orchestration+optimization layer above the preserved evaluator. Eight decisions.

D1 — LogicalPlan becomes the single shared lowering target (RFD 0020 D2 realized)

Every front-end — ad-hoc query, declared rule, compiler/type-checker goal, and mutation — lowers into one LogicalPlan. The ~11 direct compile_rule → Engine::evaluate call sites are replaced by lower-to-LogicalPlan → optimize → map → execute. CompiledRule is reclassified as the physical form of a Datalog-fixpoint sub-plan (the mapper’s output for Recurse/conjunctive nodes), not a front-end target. This is precisely the inversion RFD 0021 D1 said to perform “when a consumer demands the tree”: the tree is no longer scaffolding because it now carries front-ends a flat rule body cannot (foreign scans, table operators, federation rewrites, checker goals).

D2 — The evaluator is preserved as the fixpoint physical operator; the tree is coarse-grained

The proven eval.rs (WCOJ, CSR, persistent arrangements, factorization, oracle-validated) is not reified into per-join boxed operators. It is the physical implementation of a Recurse/conjunctive Datalog node. The operator tree reifies inter-operator / source / compute / recurse / sink structure; within a Datalog-fixpoint node the fused evaluator runs unchanged. Rationale: fine-grained reification would regress the tight semi-naive loop and dissolve the WCOJ/CSR/factorization fusion the oracle proved correct — discarding RFD 0021’s investment for no gain. Coarse reification honors D1’s “no throwaway” and keeps the engine.

D3 — The frozen-EDB materialization discipline is the composition mechanism

A non-Datalog sub-plan — a foreign scan (RFD 0036 D3), a relation-valued table operator (D4) — is realized by materializing its result into the RelationCatalog as a frozen EDB, after which the fixpoint operator consumes it natively. This generalizes the existing today()-pin (eval.rs:170-174) and stable-arrangement (eval.rs:232-235) discipline from a scalar/relation to any externally-produced relation, and it is the reason cross-source joins need no new physical join operator in v1 — the foreign/computed slice becomes an ordinary CatalogEntry and the existing evaluator joins it as it joins any EDB.

Soundness. The mechanized fixpoint operators range over Interp Atom = Set Atom and never inspect provenance — TP (Datalog/Program.lean:73) and gamma (the GL-reduct least model, Program.lean:107); the compiled engine’s immediate-consequence step equals TP over its grounding via the bridge theorem fire_eq_TP (Datalog/Compiled.lean:232), so provenance-freedom carries to the executed form. A frozen externally-produced atom is therefore semantically indistinguishable from a native one. The only Lean obligation is a semantics-preservation theorem — “a frozen slice injected as an EDB yields the same model as a native EDB of the same extent” — statable against Fixpoint.lean/Compiled.lean with no new machinery (a net-new C12 deliverable; the static case is immediate, the computed case — a frozen relation whose production is itself an inner fixpoint — is the one that needs the statement, see D4 and RFD 0036 D4).

D4 — The relation-valued (table) operator IR, co-designed with RFD 0029

Introduce the missing lowering target: a logical node (Apply / TableOp: relation(s) → relation) and a matching physical contract. It is the home for graph algorithms, windowing/ranking, and foreign analytical compute (RFD 0036’s k-means). It is co-designed with RFD 0029’s aggregate surface: an aggregate (relation→scalar) is the degenerate codomain of a table operator (relation→relation); the two surfaces share one design so a second seam cannot drift from the first. This is a language-level capability (table operators are wanted independently of federation), not federation plumbing.

A table operator’s physical realization is routed by the mapper (D6) to a tier executor; its result is materialized as a frozen EDB (D3). Determinism gate (net-new): a table operator admitted into a fixpoint position must be deterministic-given-its-frozen-inputs. The gate has two faces, and the distinction is load-bearing: for an in-engine operator it is statically checked (the engine sees the operator’s definition); for an opaque foreign compute provider Argon cannot verify determinism by inspection — it is contract-asserted (the connector declares it, as Soufflé functor-purity is author-asserted), and an operator that does not declare determinism is refused in a fixpoint position (RFD 0036 D3). Non-deterministic production (k-means training; any stochastic operator) runs outside the fixpoint and contributes only a frozen, content-addressed artifact; the deterministic re-entry (predict/assign) is what enters the fixpoint (the train/predict split — RFD 0036 D3). A non-deterministic operator in a fixpoint position is refused, never silently admitted (C9). The well-posedness order is fixed: this atom kind gates the executor contract (D6) and the freeze theorem (D3) — there is nothing to specify for an operator that cannot be named in the IR.

The analytical tier is Argon’s own native engine, not a delegated one (updated per the performance campaign, 2026-06-15). The table-operator / analytical-tier executor — vectorized columnar scan / filter / join / aggregate over the segment read-model (D7) and over foreign columnar sources — is built as Argon’s own native physical operators (extending RFD 0021’s engine). An off-the-shelf engine (DataFusion, DuckDB, Polars) is not Argon’s analytical engine; at most its TableProvider is a connector interface shape (RFD 0036 D3), or a clearly-temporary operational bridge — never the permanent execution engine. Two grounded reasons our own engine is forced, not merely preferred: (1) the segment must carry PosBool(M) why-provenance + the Governatori proof_tag + a BitemporalExtent inline (RFD 0036 D7), which every off-the-shelf columnar engine — provenance-free — structurally cannot; (2) the campaign’s measured result is that vectorized-vs-compiled is a small constant and the real cliff is representation (columnar), so a native vectorized engine lands within a small constant of DuckDB/DataFusion while owning the whole stack (provenance, the 4-axis segment, the Lean-conformant fixpoint). The CP3 cut is non-negotiable in either case: a SQL-style linear recursion engine cannot host Argon’s stratified semi-naive fixpoint, so Recurse/AntiJoin/Distinct/WFS never leave the native core.

D5 — The optimizer moves onto the tree

The live SIP/bound-set reorder (reorder.rs, over CompiledRule) is lifted to operate over LogicalPlan, preserving its bound-set propagation (bound: BTreeSet<VariableIdx> — the binding-pattern substrate RFD 0036 D3/D4 consume) and its cardinality tie-break (RFD 0021 D3). New passes land as semantics-preserving tree rewrites: predicate/projection pushdown (lifting RFD 0021 D3’s projection-collapse to the logical layer), magic-sets / demand transformation (the bounded-demand substrate RFD 0036’s binding-limited foreign sources need), federation-split (RFD 0036 D3 — folding Filter/Map into a ForeignScan leaf with a three-valued verdict), and stratum split + tier assignment. The OptimizerRule / OptimizerPipeline traits (optimizer/mod.rs) are made real. Every pass is a semantics-preserving rewrite, enforced as a differential-test obligation (D8): optimized plan ≡ unoptimized plan on generated inputs.

Hard prerequisite — the logical-layer expression interpreter. LogicalPlan::Filter carries its predicate as opaque CBOR today (logical/mod.rs:28, “pending the CoreIR expression interpreter”). Predicate pushdown, federation-split, and table-operator predicates all require a predicate the optimizer (and a connector — RFD 0036 D3 apply_filter) can inspect; an opaque blob cannot yield a three-valued pushdown verdict. So building the logical-layer expression interpreter that replaces the opaque CBOR is a sequencing prerequisite for these passes — not a deferrable open question. It lands before federation-split (RFD 0036 D3) is more than a stub.

D6 — The physical mapper (1:N) + the generalized operator-call executor contract

A physical mapper lowers an optimized LogicalPlan to a PhysicalPlan — per-node physical-operator choice + per-node executor assignment. The whole-program TierExecutor::execute(&[CompiledRule], …) (executor/mod.rs:79) generalizes to an operator-call / sub-plan dispatch so that a Recurse/conjunctive node routes to the semi-naive (later SLG) executor; a Scan to a catalog read and a ForeignScan to a connector (RFD 0036 D3); a table-operator node to a compute/analytical tier executor (RFD 0036 D3). Tier stays compile-time metadata on nodes (the Tier tag LogicalPlan already carries), never a runtime profile flag — the nous anti-pattern (RFD 0020 D9). The existing SemiNaiveExecutor is the first physical executor under the generalized contract; the SLG/DBSP/SMT/Kripke/Kora stubs slot in unchanged in shape. This is RFD 0003’s TierExecutor seam, generalized from “reasoner backends” to “any physical-plan node.”

Placement as a permanent per-workload router (updated per the performance campaign, 2026-06-15). Per-node executor assignment composes with [placement] (RFD 0036 D6): a node’s workload is routed to the store/engine that best serves it. Argon’s own engine is the first-class default; a specialized external store/engine is chosen when it is genuinely optimal for that workload (sub-ms operational point-lookup, a >TB columnar source Argon does not own). This dual stance — Argon is a first-class engine and a permanent orchestrator over heterogeneous stores — is permanent architecture, not scaffolding (RFD 0036 Decision lead): different workloads require different stores now and forever, while Argon itself stays a high-performance engine that never pawns off its own core.

D7 — The columnar content-addressed segment + IVM maintainer is the primary read-model; the frozen path is its correct fallback

Every operator stays a pure Z-set→Z-set function (RFD 0021 D6), so IVM is an additive outer loop — and that purity is exactly what makes the read-model cheap to wire and what keeps it bit-identical to the oracle.

The primary read-model (updated per the performance campaign, 2026-06-15). The persisted read-model is a columnar, content-addressed, immutable segment, maintained incrementally by the currently built-but-dead DBSP operators (runtime/operators.rsintegrate/differentiate/distinct, zero forward-path callers today) so that a mutate produces a delta, not a full catalog rebuild (today every mutate evicts the cache and re-runs the whole fixpoint). This is the convergence point of the campaign’s single-node-execution, storage, and IVM findings — one artifact seen from three angles — and it is the highest-leverage performance work, single-node-meaningful before any S3 or distribution exists. The single-node cliff the campaign measured is row-at-a-time over BTreeMap + per-tuple CBOR decode, which a columnar segment captures most of independent of execution model; so the segment is columnar (decode-once, keep-decoded, keyed by content_id). Retraction (statute sunsets, corrections) is mandatory for Argon and is what forces a real IVM algorithm — the algebra is settled by the prior IVM trilogy (DRedc / two-semiring DBSP; the maintenance loop + segment contract is the work, not the algebra; see RFD 0036 D7/D9). This is Argon’s own engine’s read-model, built natively (D4/D6), not delegated.

The frozen path is its correct fallback, not the headline. Lower → optimize → map → execute with non-Datalog inputs frozen-materialized per query (D3) is the correct fallback the segment is maintained against (the RFD 0021 D8 “genuine mechanism + correct fallback” discipline). What would be hollow is a LogicalPlan that round-trips into the old path, or federation that only works on an inert read-model — we ship neither. (An earlier draft of this RFD called frozen-per-query “the complete mechanism” and IVM “a named subsequent optimization”; the campaign inverts that priority while keeping the architecture — the Z-set purity here is precisely what makes the inversion free.)

D8 — Correctness methodology carries over: the differential oracle is load-bearing

The semi-naive/binary evaluator stays the differential oracle (RFD 0021 D8). Every optimizer pass (D5) and every physical mapping (D6) is diff-tested identical to the unoptimized/oracle path on generated inputs; the pipeline’s own correctness is a test, not an argument. No optimization ships without the oracle. Library discipline unchanged: no unwrap/expect/panic, BTreeMap/BTreeSet only, fmt + clippy clean, cargo nextest-green.

The IVM↔oracle equivalence obligation (added per the performance campaign, 2026-06-15). The incremental read-model maintainer (D7) introduces one net-new proof obligation, the CP3 hinge: maintaining the read-model M under a committed delta Δ must yield exactly the least fixpoint over the original EDB extended by Δ

maintain(M, Δ) ≡ lfp T_P (E ⊎ Δ)

— provenance- and time-free, on every mutation. The maintainer’s internal (time, diff, iteration) bookkeeping is maintenance state, not meaning; distinct projects the timestamped trace down to the reference Set Atom semantics (Program.lean:73). v1 discharge is the differential oracle: the maintainer’s output is diff-tested identical to a full semi-naive recompute over generated mutation sequences (the same discipline that proves wcoj ≡ binary). The Lean-level theorem joins the frozen-EDB preservation theorem (D3) as a named obligation, statable against Fixpoint.lean/Compiled.lean. No prior IVM system has this theorem because none maintains against an external reference semantics; provenance is preserved through the maintainer by the two-semiring split, so the distinct collapse and the provenance DNF do not fight (RFD 0036 D7).


Rationale

  • The trigger condition is met, not invented — and this RFD stands on its own. The table operator (D4) is wanted independently of federation — graph algorithms, windowing/ranking, ML all need a relation→relation node that CompiledAtom lacks — so 0035 has standalone motivation even while RFD 0036 is still in discussion. RFD 0021 D1 reserved the operator tree for the consumer that genuinely needs it; RFD 0036 is a (sharp) consumer, but not the only justification. Building it now is the staging RFD 0021 designed for — not a reversal.
  • Preserve the engine, build the layer above it (D2/D3). The freeze-into-catalog discipline lets the operator tree be an orchestration/optimization layer that produces frozen EDBs, leaving the proven fused evaluator as the fixpoint physical operator. We get the tree’s expressiveness without discarding RFD 0021’s WCOJ/CSR/factorization/oracle investment.
  • One IR, several physical backends (D1/D6). Unifying the logical layer is what lets one engine serve rules, ad-hoc, checker goals, and mutations coherently; specializing the physical backends (semi-naive, connector, compute tier) is what keeps each role fast. Share the meaning, specialize the mechanism (RFD 0020 D3).
  • The table operator is a language gap, not a federation gap (D4). Relation→relation operators (graph algorithms, windows, ML) have no lowering target today; designing the IR coherently with RFD 0029’s aggregates is the honest fix and prevents a second drifting surface.
  • Correctness stays a test (D8). Anchoring every pass and mapping to the oracle is what made RFD 0021 transformable under load without regressions; the same discipline is why the pipeline can be built correctly-once.

Alternatives considered

  • Wire a LogicalPlan that round-trips into the CompiledRule executor. Rejected — RFD 0021 D1’s original reason holds: throwaway the moment a real operator-tree executor exists. We build the executor (D6), not a round-trip.
  • Fine-grained operator reification (every join a boxed operator; runtime/operators.rs made live for evaluation). Rejected (D2): regresses the fused semi-naive loop and dissolves the WCOJ/CSR/factorization fusion. runtime/operators.rs stays the IVM-boundary vocabulary (D7), not the query-evaluation path.
  • Keep the direct compile_rule path; bolt federation onto it. Rejected: a flat rule body cannot host a foreign-scan leaf, a federation-split rewrite, a table operator, or a checker goal — it is exactly the monolith nous warns against.
  • Treat the persisted read-model + IVM as a someday-optimization. Rejected after the performance campaign (D7): the columnar content-addressed segment + IVM maintainer is the primary read-model and the highest-leverage work; frozen-per-query is its correct fallback, not the headline. The architecture (pure Z-set so IVM is additive) was already right; only the priority was wrong.
  • Delegate the analytical tier to an off-the-shelf engine (DataFusion/DuckDB as Argon’s execution engine). Rejected (D4): forced by the inline-provenance segment requirement and unjustified by perf (vectorized-vs-compiled is a small constant). Off-the-shelf engines are a connector shape or a temporary bridge, never Argon’s own engine.
  • Cascades optimizer from day one. Deferred (RFD 0020 D8): rule-based tree passes (D5) suffice until the plan search space justifies a memo/cost-model engine.

Consequences

  • Code structure. logical//optimizer//physical/ become the real pipeline; the front-end call sites in oxc-runtime re-point to lower-to-LogicalPlan; reorder.rs is lifted onto LogicalPlan; TierExecutor generalizes to operator-call dispatch; a new table-operator logical node + CompiledAtom/physical contract lands (D4); SemiNaiveExecutor becomes the fixpoint physical operator under the generalized contract. eval.rs and its oracle stay.
  • Lean / conformance. The IR’s reasoning semantics conform to spec/lean/Argon/Reasoning/; the frozen-EDB semantics-preservation theorem (D3) is a net-new, statable obligation. Pipeline structure/optimizer/mapper are engine architecture (outside mechanized scope) and are conformance-tested against the semi-naive oracle (D8).
  • Spec / reference. An engine-architecture chapter (spec/reference/src/{17,19}) lands once the pipeline is built; RFD 0021’s “as-built engine” framing is contextualized as the physical layer beneath this logical layer.
  • Coordination. The RelationCatalog public-API seam (RFD 0021) stays the boundary with the write-path track; RFD 0036 lands its connector/compute/store layers on D3/D4/D6.

Open questions / tracked-future

  • The exact table-operator IR shape and its coherence with RFD 0029 — the logical node, the CompiledAtom variant, and the shared aggregate↔table-operator design. The novel core; gates the executor contract and the freeze theorem (D4).
  • The generalized operator-call executor contract signature — how TierExecutor moves from whole-program to sub-plan/operator dispatch without losing the per-stratum stratification it does internally today.
  • Checker goals as LogicalPlan — whether oxc-check issues bounded LogicalPlan goals in this RFD or is designed-for (RFD 0020 D3 role 3 is design-for-now; the seam is D1).
  • Cost model / statistics — cardinality/selectivity over a mutating graph for the reorder + future federation-split + eventual Cascades (RFD 0021 left this open; RFD 0036’s foreign sources have no in-engine cardinality, sharpening it).
  • The magic-sets / demand interface RFD 0036’s binding-limited foreign sources consume (D5) — its precise shape (demand-stratify vs monotone bounded re-consultation) is settled with RFD 0036 D4.
  • Logical-layer expression interpreternot an open question but a stated prerequisite (D5): the opaque-CBOR LogicalPlan::Filter.predicate must become an inspectable expression before pushdown / federation-split / table-operator predicates work. What remains genuinely open is only its expression coverage (which operators/forms the logical layer interprets vs. defers).
  • The IVM maintainer’s checkpoint cadence (D7) — how often a mutation mints a new immutable segment: per-mutation (segment churn + GC pressure) vs batched (the in-memory materialization must then survive restart some other way, reintroducing a durability seam). A genuine open the loop surfaces; settled with RFD 0036 D9.
  • Enforcing the CP3 cut if an off-the-shelf engine is ever used as a bridge (D4) — whether confining it to non-fixpoint plans is a structural guarantee (fixpoint operators unrepresentable in the lowered sub-plan) or advisory Tier metadata; and the provenance-injection step a foreign analytical result needs to re-enter the provenance-carrying fixpoint (the frozen-foreign-EDB marker, RFD 0036 D7).

RFD 0036 — Heterogeneous and specialized data stores

  • State: discussion
  • Opened: 2026-06-15
  • Decides: how specialized / heterogeneous stores — a columnar analytical store (a >TB “market oracle” securities master), a blob store, a DynamoDB/KV store — become part of the Argon knowledge graph and are queried through it, alongside the default store (in-memory / Postgres). Establishes three distinct patterns (foreign federation, external-valued attributes, persistence-backend swap), the connector SPI, the mapping & placement surface, the recursion×federation discipline, the world-assumption-as-tier-input rule, and the provenance/freshness contract. Built on RFD 0035 (the operator-tree pipeline — its forcing consumer and substrate), RFD 0033 (the store-agnostic Schema query-provider and the ad-hoc surface), and RFD 0020/RFD 0021 (the engine).
  • Grounded in: a six-track literature campaign (polystore/federation, OBDA/VKG, pluggable persistence, recursion×federation, compute pushdown, consistency/provenance) whose decision-ready briefing this RFD selects from. External systems and PL theory are cited as evidence, never authority; current-repo code is cited at verified anchors.
  • Depends on: RFD 0033 (PR #401) — the store-agnostic Schema query-provider this RFD’s C1 rests on. Merge #401 first; on this branch the 0033-*.md links are forward-referential by design, not dangling.

This RFD is co-designed with RFD 0035: 0035 is the engine layer the foreign data flows through, 0036 is the store layer. Per the repo workflow this is language-surface + engine architecture (RFD + reference → Lean where a new semantic notion appears → code); the only net-new semantic obligation is the frozen-foreign-EDB preservation theorem (RFD 0035 D3), expressible against spec/lean/Argon/Reasoning/.

Reconciled with the performance / distribution / consensus research campaign (2026-06-15). D2/D3/D4/D7/D9/D10 are updated to record the campaign’s findings: the read-model is the primary path as a columnar content-addressed immutable segment maintained by IVM (D9), the >TB analytical bar is met by pushdown + push-compute-to-data + Argon’s own vectorized streaming rather than a BTreeMap freeze (D4), the segment carries provenance inline via a two-semiring maintainer (D7), the write spine is a scalar root pointing at an immutable content-addressed manifest (D9), and the financial path is three workloads with Argon as the read/OLAP DB beside a federated ledger (D10). The governing principle below frames all of it. The campaign is research and decides nothing; these edits are the cut.


Question

Argon’s value is the ontology + reasoning layer over data. Some of that data is too large, or too workload-specialized, to live as Argon axiom events: a >TB columnar securities master built for analytics; a blob store for documents; a KV store for point lookups. The intent — stated since the ad-hoc work began — is that such stores are part of the knowledge graph and queryable through Argon naturally, joined against native facts and reasoned over, without copying their data into Argon’s log.

The forcing example: “flag every security in the same k-means cluster as a known-distressed security, where clusters are computed over the market oracle’s return vectors.” distressed/1 is a small native relation; returns(...) lives in the columnar store; cluster_of(...) is computed by the store’s own engine. This single query exercises every hard axis: foreign data in a native shape, a relation→relation compute operator, a federated join, recursion-adjacency, world assumptions, and cross-store provenance.

What is the design by which heterogeneous stores join the knowledge graph — the connector contract, the placement/mapping surface, the recursion discipline, the world-assumption handling, and the provenance/freshness model — built correctly and completely, with no hollow path?


Context

What RFD 0035 provides

The operator-tree pipeline: LogicalPlan as the shared lowering target (D1), the frozen-EDB materialization discipline (D3 — a non-Datalog sub-plan’s result is materialized into the catalog as a frozen EDB the evaluator joins natively), the relation-valued table operator IR (D4), the tree optimizer with pushdown + magic-sets/demand + federation-split (D5), and the generalized operator-call executor (D6). 0036 attaches its connector/compute/store layers to these seams. Without 0035 there is no place to attach (verified: the live path is AtomIR → compile_rule → Engine::evaluate; LogicalPlan is orphaned).

What the substrate already provides (verified, not assumed)

  • AxiomEvent already is the native provenance/freshness token (oxc-protocol/src/storage.rs:1508): content_id (BLAKE3 — C10) + the four scope axes (tenant/fork/standpoint/module — C8) + a bitemporal extent + a proof_tag (defeasibility) + a derivation (PosBool(M) DNF why-provenance). Federation adds one leaf, not a new token.
  • Per-concept world assumption is already mechanizedWorldAssumptionMap, Locality/Cwa.lean, with cwa_owa_transfer proven (CWA-true ⇒ OWA-true; the reverse proved unsound), and diagnostics OE0901/OW0902 reserved-but-unbuilt. Argon is ahead of deployed OBDA here (uniformly OWA; Ontop never implemented closed predicates).
  • The freeze discipline is in the engine (eval.rs:170-174 clock pin; 232-235 stable arrangements) — RFD 0035 D3 generalizes it.
  • No store config exists. ox.toml (oxc-workspace/src/lib.rs:160-168) parses package/project/schema/dependencies/lattice only. The .oxbin/pg projection cache exists but is inert (None everywhere; oxc-storage-pg get/put_projection_cache tested, never called by the evaluator).
  • A RuntimeStorageBackend trait exists (oxc-runtime/src/lib.rs:2890) — a sync, in-process replay seam; PgStorage is a separate, async, CQRS-shaped durable layer reached by hydrate-then-replay (it does not implement that trait).

Decision

Governing principle — Argon is a first-class engine and a permanent orchestrator (both, forever). Two things are true at once and neither subsumes the other. (1) Argon itself is a high-performance database engine — never “dumb,” never a thin forwarder, never pawning off its own/core performance to another DB; the end-state includes Argon’s own custom engine, and for data Argon owns and reasons over, Argon’s own engine does the work. (2) Argon is also a permanent orchestrator/federator over heterogeneous external stores — now and forever, even after that engine exists — because different workloads genuinely require different stores (sub-ms operational KV, >TB columnar analytics, blob stores, time-series). This is first-class permanent architecture, not scaffolding to outgrow. The router between them is [placement] (D6): per-workload, which store serves it — Argon’s own engine the first-class default, a specialized external store chosen when genuinely optimal. The only thing that lessens over time is the current degree of reliance on externals (and outsourcing the durability backend for Argon’s own log, à la Datomic); the orchestration capability is permanent. “Don’t pawn off” is therefore narrow: it forbids delegating Argon’s own core performance and forbids Argon being a dumb passthrough — it does not mean retreating from external stores.

D1 — Three patterns, named and kept separate

PatternWhat it isDriving exampleSeam
P2 — Foreign federation (centerpiece)Foreign data in its native shape, mapped into the ontology, queried through Argon, never copied into the logthe market oracleconnector SPI (D3) + frozen-EDB (RFD 0035 D3)
P3 — External-valued attributesA property’s value is a content-addressed handle; bytes live in a blob storedocument on an individualRef<Blob> (D8)
P1 — Persistence swapArgon’s own event-log/read-model lives in a chosen durable backendlog in DynamoDBthin durable seam (D9)

Conflating them is rejected: P2 data has no axiom-event semantics (no bitemporality, polarity, standpoints, defeat tags); forcing those onto it is the trap. P1 is an Argon-own-model persistence concern; P3 is a typed value with a remote byte-store.

D2 — Foreign data is a virtual extensional predicate, outside the log, frozen per query; read-only in v1

A foreign relation is a virtual EDB produced on demand by a connector and materialized once into the catalog as a frozen EDB for the query’s duration (RFD 0035 D3). Argon stores only the mapping + a connection contract, never the foreign data. The axiom-event log stays source-of-truth for native facts; the foreign store for its own; they meet at query time. Foreign stores are read-only in v1 — and this is the correct steady-state architecture, not a limitation: per-entity ACID + idempotent cross-entity messaging is the right shape (Helland); 2PC is an anti-availability protocol that only works inside one store (Gray/Lamport). Cross-store write transactions stay out of scope, with the sole admissible exception being P3’s non-transactional content-put (D8). This is the realization of RFD 0020 D11 (“ad-hoc queries/mutations first-class … gating is engine policy”) extended across the federation boundary.

The BTreeMap freeze is for small slices and is not the >TB performance path (updated per the performance campaign, 2026-06-15). Materializing a slice into an in-memory BTreeMap CatalogEntry is correct for a small federated result (or as the fallback), but it is the O(database) cost CP2 forbids for a >TB store — and you cannot freeze a >TB slice into a BTreeMap. The >TB analytical bar is met by never moving the data: push filters/projections/aggregates down (D3), run analytical compute in-store where the store can (the k-means train/predict split — only the small result returns), and for what must run in Argon, stream it through Argon’s own vectorized engine (RFD 0035 D4) over columnar segments/sources — never a BTreeMap ingest. Only the small result re-enters as a frozen EDB to join native facts. Argon provides the performance; a “dumb” store provides only bytes — Argon is never the dumb layer (Decision lead).

D3 — The connector SPI: per-operation, three-valued pushdown verdict + binding patterns, object-safe

A foreign store implements an object-safe ForeignRelation trait (held as Arc<dyn> in a CatalogEntry, so one catalog holds heterogeneous connectors — DataFusion’s Arc<dyn TableProvider> precedent):

  • schema() — answers type questions into Argon’s vocabulary; never owns type semantics. Schema stays store-agnostic (C1, RFD 0033) — the connector sees the IR, never Schema.
  • binding_patterns() — capability as {b,f}^n adornments (C4; Rajaraman–Sagiv–Ullman): ff… free-scannable, bf… requires a bound key (a blob/KV store cannot free-scan). Consumed by the optimizer’s bound-set propagation (the same bound: BTreeSet<VariableIdx> reorder.rs already tracks, lifted to LogicalPlan — RFD 0035 D5).
  • apply_filter(&LogicalPlan) -> (handle, Absorption) with three-valued Absorption ∈ {Exact, Inexact, Unsupported} (DataFusion TableProviderFilterPushDown): Exact = no re-check; Inexact = the source prunes but the engine re-applies the whole predicate (so the residual is not the set-complement of the absorbed work); Unsupported = engine does it. Under Inexact the residual is the original typed predicate, so the type-identical residual demand (C2/C3) is met automatically.
  • scan(handle, demand) -> stream — lazy; a bounded binding-pushed slice via the b positions in demand (never a free count on the hot path — C5).

Rejected: the delegated-subplan-rewrite shape (datafusion-federation: the connector ships its own optimizer rule and self-determines the federated fragment). More expressive and a cleaner IR fit, but the engine cannot independently cost or type-check what the connector absorbed opaquely — irreconcilable with the type-identical-residual demand (C2/C3), which is non-negotiable for Argon. The async connector is reconciled with the sync fixpoint by the freeze rule: await the slice once, materialize it as a frozen EDB, iterate sync (RFD 0035 D3). Relation-provider and compute-provider are distinct seams — a relation provider negotiates binding patterns; a compute provider is a table operator (RFD 0035 D4) routed to an analytical-tier executor (RFD 0035 D6). The market-oracle k-means is the latter: train (non-deterministic) runs outside the fixpoint and emits a frozen content-addressed model artifact; predict/assign (deterministic given the model) re-enters as a frozen EDB. Compute determinism at the foreign boundary is contract-asserted (the connector declares it; undeclared compute operators are refused in a fixpoint position — RFD 0035 D4), since Argon cannot verify foreign code by inspection.

Connector verdicts are a trust boundary — two distinct dimensions, with different defenses; do not fuse them. A connector that wrongly returns Exact suppresses the in-engine re-check and yields silently wrong answers — the one outcome a correctness-first engine cannot tolerate (collation, NULL handling, numeric coercion are the classic mismatches). The differential oracle (RFD 0035 D8) is load-bearing for in-engine passes but structurally cannot test a connector — it has no foreign data. The two dimensions:

  • Data completeness (false negatives — a source omits rows it should return). Irreducibly trusted in both Exact and Inexact, because re-applying the predicate only re-filters the rows that came back — it can never recover omitted ones. “Always re-check” buys exactly nothing here. This is the Postgres-is-trusted dimension: a connector is, irreducibly, a trusted data source for the relations it serves.
  • Verdict honesty (false positives — a source claims Exact but returns rows that fail the predicate). Not irreducible — this is the connector’s code, not the source’s data, and it is closable.

Three measures follow from the split:

  • Inexact-by-default. An Exact claim is honored — i.e. allowed to suppress the residual re-check — only from a connector that has passed a conformance harness for the operations it claims; otherwise pushdown is treated as Inexact and the engine re-applies the whole predicate. This removes the engine-introduced footgun (honoring an unverified guarantee).
  • Audit mode is the “always-re-check” configuration. It re-runs every Exact verdict against the residual and flags divergence — run in CI / dev / canary, where the re-check is free, rather than taxing production. This captures everything a blanket “no Exact ever” would buy on the verdict-honesty dimension, at zero steady-state cost — the federation analogue of the WCOJ-soundness bug the Lean↔Rust conformance framework caught (1bdfa164f).
  • Honest scope. A perpetual production re-check would pay a hot-path tax to defend only verdict-honesty — the dimension audit already closes for free — while leaving data-completeness, the irreducible hole, exactly as open. So Inexact-default + earned-Exact + audit-in-CI is the calibrated answer, not “always re-check.” Conformance raises confidence in the completeness trust; it does not abolish it.

The store taxonomy — one SPI, heterogeneous roles (updated per the performance campaign, 2026-06-15). The same ForeignRelation SPI accommodates stores with very different capabilities because binding_patterns() negotiates them: a point-lookup-only KV store answers bf (needs a bound key), a free-scannable columnar store answers ff. Concretely: DynamoDB plays up to three distinct roles — the outsourced CAS / durability backend for Argon’s own log (D9), an operational point-lookup tier, and a federated source; DuckDB / Parquet / Arrow is a >TB columnar source (a D3 connector); S3 is the immutable-segment object-store truth (D9) and the Ref<Blob> byte store (D8). DataFusion’s TableProvider is borrowed only as the connector interface shape — it is not Argon’s execution engine (RFD 0035 D4: Argon’s own vectorized engine is forced by the inline-provenance segment and justified by the small vectorized-vs-compiled constant). Federating to such a store is correct precisely for data Argon does not own or has deliberately placed there (D6) — the permanent-orchestrator half of the Decision lead — while Argon’s own engine remains the first-class default for what Argon owns.

D4 — Recursion × federation: the frozen-foreign-EDB rule + the refusal gates

A foreign relation in a fixpoint body is frozen-materialized once (RFD 0035 D3) — sound because a frozen slice is indistinguishable from a native EDB at the level of the mechanized semantics. For free-scannable sources this is the complete v1 mechanism — within a materialization cardinality budget. Freezing a slice into an in-memory BTreeMap CatalogEntry is bounded by demand for a binding-limited (bf) source, but bounded by nothing for a large or non-selective free-scan (ff) — an O(foreign-database) pull into memory, in direct tension with C5. So free-scannable freezing carries an explicit cardinality guard: a slice projected to exceed the budget is refused with a remediation diagnostic (“add a selective filter or declare a binding pattern”), never silently materialized to OOM. (The market-oracle headline is safe — k-means runs in-store and only the small cluster_of returns; a generic free-scan federated join is the case the guard protects.) Push-compute-to-data + streaming columnar execution is load-bearing for the >TB bar, not a follow-on (updated per the performance campaign, 2026-06-15): the BTreeMap freeze fundamentally cannot do >TB, so the trading-grade analytical path is pushdown (D3) + in-store compute where possible + Argon’s own vectorized streaming engine over columnar data otherwise (RFD 0035 D4) — the frozen-into-BTreeMap EDB is reserved for the small result re-entering the fixpoint. (Spilling joins and pushing the join itself down remain genuine follow-ons that further lift the budget.) For binding-limited (bf) sources the demand is itself recursive (Duschka–Genesereth: binding-limited access compiles to a recursive demand program), so “freeze once” is naive; the demand is computed by magic-sets/demand transformation (RFD 0035 D5) and the bounded slice pulled, with the connector required idempotent and monotone under growing demand. The two resolutions — demand-stratify (compute the complete magic_F extent in a lower stratum, then freeze) vs monotone bounded re-consultation (consult per demand-growth round; F* only grows) — are settled in implementation; free-scannable is the floor, binding-limited the careful extension.

Refusal gates (C9 teeth — static, checkable, never silent mis-evaluation):

  • NAF over an OWA foreign relation — absence ≠ false in an open world; refuse (or thread three-valued).
  • A relation both foreign-mapped and rule-derived — intensional/extensional conflict; compile-time refusal.
  • Pushing recursion into a source — Li–Chang: decidable for conjunctive fragments, undecidable with recursion + integrity constraints; only bounded binding-slices push, the fixpoint stays in-engine.
  • Result-bounded incomplete foreign slices (paginated/rate-limited APIs) under CWA-NAF — an incomplete F* silently breaks closed-world negation; refuse until the slice is warranted complete (D5).

D5 — World assumption is a decidability-tier input, not a soundness flag

A foreign relation declares its CWA/OWA (C6); the mark propagates into the tier classifier (the Tier metadata LogicalPlan nodes carry — RFD 0035 D5/D6). Closing a predicate is a complexity cliff — CQ answering jumps from AC0 (DL-Lite) to coNP-hard the instant any predicate is closed, unless the query is quantifier-free. So Argon admits closed foreign predicates only inside the Lutz–Seylan–Wolter Thm-5 FO-rewritable island (quantifier-free UCQs, no open→closed role inclusion — a static syntactic gate, firing the reserved OE0901/OW0902), and refuses the rest rather than silently moving a query past its tier ceiling. The world-assumption mark also gates which fixpoint flavour a foreign relation may enter (a CWA relation admits NAF / a WFS-SCC; an OWA one does not — the C6×D4 hinge). The cross-boundary completeness warrant — what a connector must supply for the CWA→OWA transfer to be sound — is the same composite leaf as D7’s freshness token, specialized with a closed? flag; the in-engine theorem (cwa_owa_transfer, CwaOwa.lean) exists, the connector contract is net-new.

D6 — Mapping & placement: three levels + a compiled content-addressed artifact; placement versioned separately from schema

  • Level 1 — source annotation (vocabulary-free — C7): a declaration marks a relation/concept foreign, with its world assumption and an abstract field-correspondence to logical names. No ontology vocabulary, no store identity.
  • Level 2 — ox.toml [store] / [placement] (versioned package contract, no secrets — C11): the named store, its kind (columnar/blob/relational/kv), binding-pattern hints, and an RML-style mapping shape; credentials referenced only by an indirect @deploy: handle.
  • Level 3 — deployment config (not versioned): endpoints, credentials, the concrete connector instance.

The mapping compiles to a content-addressed artifact hashed against the Schema composition signature (the .oxbin discipline — oxc-oxbin already carries per-section BLAKE3 + a composition signature — C10), so schema↔mapping drift is a load-time refusal, not a runtime surprise. Placement is versioned separately from schema: a relation can move stores without a schema bump; the mapping pins to a schema hash. Schema stays store-agnostic throughout (C1) — it answers type-checking questions (subsumption, refinement, world assumption) for a foreign relation without learning where bytes live; placement is the parallel catalog layer beside it.

D7 — Provenance & freshness: a foreign leaf in the existing PosBool(M) DNF

AxiomEvent already is the native token (Context). Federation adds one generator leaf (source_id, mapping_content_hash, as_of, closed?) into the same derivation DNF — the engine’s ⊗/⊕ provenance composition is unchanged. The mapping_content_hash triple-duties: the OBDA mapping-axiom provenance label (Calvanese 2019) + the freshness coordinate + the C10 content-address. Freshness is a three-rung ladder gated by source capability: (a) TTL/staleness-bound (weakest; a liveness property, source needs no cooperation); (b) CDC/change-feed (push invalidation — the delta path, RFD 0035 D7); (c) per-source as_of barrier (strongest, read-your-writes). Argon’s bitemporal tt is a barrier coordinate, so the engine-side mechanism for rung (c) already exists — but the rung is not “free”: the connector must expose a monotonic source position and a mapping that aligns it with tt. That alignment is a real per-connector obligation, not a given. The mandatory floor and the composite freshness of a multi-source join (v1: meet-of-leaves — the answer is as fresh as its weakest leaf) are the open residuals.

Provenance under incremental maintenance — the two-semiring split; the segment carries provenance inline (updated per the performance campaign, 2026-06-15). The persisted read-model segment (D9) carries the PosBool(M) derivation DNF + the proof_tag + the BitemporalExtent inline — “answer why at segment granularity” — which no surveyed columnar store does (they are all provenance-free), and which is one of the two reasons Argon’s read-model needs Argon’s own engine rather than an off-the-shelf one (RFD 0035 D4). The IVM maintainer keeps the two concerns on separate semiring components so they cannot fight: ℤ weights drive the cardinality IVM (DRedc / two-semiring DBSP; insertion is free under semi-naive, retraction is what forces the real algorithm), while PosBool(M) why-provenance rides as a value-field payload via the Green et al. ℕ[X] → PosBool(M) homomorphism. distinct collapses only the ℤ multiplicity component to set semantics for the oracle-identity obligation (RFD 0035 D7/D8); it does not touch the PosBool(M) payload — so the equivalence collapse and the provenance-carrying obligation are discharged by construction, not in tension.

D8 — P3 external-valued attributes: Ref<Blob>, the one tractable cross-store write

A blob-valued property is a first-class Ref<Blob> handle type (explicit indirection, composing with the reflective-Type/refinement machinery — RFD 0023 — over a magic blob-typed field). The write decomposes into three ops with different guarantees:

  1. idempotent content-addressed puthandle = BLAKE3(bytes), outside any transaction; re-putting identical bytes is a no-op by content hash;
  2. a transactional single-entity reference write — a native AxiomEvent holding the handle (the token already exists);
  3. a background GC sweep for the orphan window (put succeeded, reference never written).

It is admissible precisely because it is not a distributed transaction — it never crosses an entity boundary. The genuinely hard part is GC over a bitemporal, four-axis, fork-branched, content-addressed log: a blob referenced in fork A but retracted in fork B is not orphaned; one referenced only outside the current as_of window is live-but-invisible. Convex’s flat refCount is insufficient. v1: conservative mark-and-sweep with a grace period — and its cost is named, not implied cheap: to prove a blob unreferenced the sweep must scan reachability across all forks × as_of windows (≈ O(log) per sweep over a branched history). It is a background job, so C5 (hot-path) does not bind it, but it is not free, and that cost is precisely why a maintained per-fork refCount CQRS projection (incremental, O(1) per reference event) is the named follow-on rather than the v1 default. Schema sees only the Ref<Blob> type (C1).

D9 — P1 persistence swap: a thin async durable seam below the existing replay seam

The existing sync RuntimeStorageBackend (replay seam, oxc-runtime/src/lib.rs:2890) is preserved. A P1 backend (DynamoDB / FoundationDB-layer / RocksDB / Cassandra) plugs in at a separate, thin, async durable layer — where PgStorage already sits — reached by hydrate-then-replay (the async/sync split is by design, not a defect; RDFox + Datomic confirm it as the normal shape). The contract is Datomic-shaped and tiny: a consistent kv-read + one linearizable CAS on the root/watermark; the bulk store needs only eventual consistency, because the stored data is immutable (Datomic stood up DynamoDB in ~2 weeks on exactly this). First-party backends behind a compile-time enum + an async builder trait for third-party “external storage composers” (SurrealDB’s actual hybrid — not the enum-vs-trait dichotomy a naive reading assumes). The external durability backend (DynamoDB/S3/FoundationDB) is a swappable durability primitive, not Argon’s engine — the engine over it is always Argon’s (Decision lead); ox.toml holds at most a backend-kind selector; URIs/secrets are deployment config (C11).

Read-model persistence is the primary read path, not a coupled afterthought (updated per the performance campaign, 2026-06-15). The live read-model is a columnar, content-addressed, immutable segment (Datomic/Materialize/TerminusDB lineage, made columnar — RFD 0035 D7), maintained incrementally by the IVM maintainer so a mutate is a delta, not a rebuild. It serves C5 (reads hit segments, never the log), discharges C10 (the read model is a content-addressed cache), survives restart, and is the campaign’s #1 single-node win. This is content-addressed immutable segments, not the current scope-versioned mutable cache — the immutable shape is load-bearing (it is also the replication and cache-placement unit, and the fork mechanism). The fork axis (C8) plausibly rides the same content-addressed-segment mechanism as the read-model (fork = a pointer-set over shared immutable segments — the Neon/Snowflake zero-copy-clone shape; the Datomic/TerminusDB C10↔C8 convergence), while tenant/standpoint/module stay scoping coordinates — a two-mechanism split, flagged for investigation, not forced here.

  • The segment manifest — “which segments compose the read-model at watermark W per (tenant, fork, standpoint, module) at as_of” — is runtime state, not config (the Neon IndexPart / Iceberg metadata role), and is 4-axis + bitemporal. It lives with the write-spine (the natural home for the linearizable watermark). It is distinct from D6’s foreign-placement artifact.
  • The write spine stays a single scalar linearizable CAS (Datomic “db root” shape — the Track C verdict). The structured 4-axis + bitemporal manifest does not force a structured CAS: the manifest is itself an immutable, content-addressed object, and advancing the frontier is write the new immutable manifest, then one scalar CAS swings the root pointer to its content_id. Atomicity is automatic (the manifest is written and content-addressed before the CAS makes it live), and the outsourced-single-CAS simplicity is preserved — the linearizable cell’s value stays scalar even though what it points at is arbitrarily structured. This resolves the campaign’s scalar-CAS-vs-structured-manifest tension.

D10 — Sequencing and the scope line (relationship to RFD 0035)

The complete, correct mechanism is frozen-per-query federation on the RFD 0035 pipeline — it lowers end-to-end for every stage or refuses at a checkable gate; nothing half-checked executes (C9). The persisted-read-model + IVM (cross-query reuse, delta-maintained freshness — D7 rung (b)) is the named subsequent optimization whose correct fallback is the frozen-per-query path (RFD 0035 D7) — not a hollow deferral. P1 (D9) and P3 (D8) are independent of the pipeline and can proceed in parallel. P2 splits cleanly: the relation-provider half for free-scannable sources rides existing semantics (frozen EDB ≡ native EDB); binding-limited sources extend it via demand (D4, RFD 0035 D5); the compute-provider half (table operators / analytical tier) is the genuinely novel IR work (RFD 0035 D4/D6). Two hard prerequisites gate P2’s federation-split (both are sequencing facts, not open questions): (i) the logical-layer expression interpreter (RFD 0035 D5) — without an inspectable predicate, apply_filter (D3) cannot return a verdict; and (ii) the connector conformance harness (D3) — without it, Exact cannot be honored, so federation runs Inexact-only (correct, just slower). Until both land, federation-split is a stub, and the RFDs say so plainly rather than implying it works. The implementation owns the ordering and the cut, subject to: no hollow path, every optimization a genuine mechanism with a correct fallback, the differential oracle gating each (RFD 0021 D8 / RFD 0035 D8).

The cut, informed by the performance campaign (2026-06-15). The campaign’s evidence-grounded sequencing (research, not a directive; the cut stays the implementation’s): (1) the single-node IVM + columnar content-addressed segment read-model — the highest-leverage win, no S3/distribution/consensus, the benchmark suite lands here (none exists today, CP8); (2) the financial read paths over those segments; (3) the outsourced-CAS write spine + batching, independent and parallelizable; (4) distribution — later and greenfield (distribute storage, keep compute local: distributing the fixpoint imposes a per-iteration barrier and distributed incremental-recursive Datalog does not exist to adopt — reasoning stays single-node in v1). This reorders the earlier framing, which treated the persisted read-model + IVM as a someday-optimization; the architecture was already right (pure Z-set, RFD 0035 D7), only the priority was set without performance data.

The financial path is three workloads, not one. “Trading query” decomposes into a point lookup (one instrument’s current state — the operational tier, sub-ms–5ms), an analytical slice (aggregate/k-means over a >TB returns slice — the columnar segment + Argon’s vectorized engine, ~100ms–1s), and a ledger write (contended debit-credit — the TigerBeetle pattern). TigerBeetle is an accelerator, not a system of record (“Write Last, Read First”), so Argon’s reasoning + query layer is the general-purpose DB beside the ledger — the ledger federates out (a frozen-foreign EDB of its user_data-linked facts), and the trading-query path is a read/OLAP problem, not an OLTP-ledger one. v1 targets the analytical/columnar path (the market-oracle headline). A dedicated sub-ms operational point-lookup tier over the 5-coordinate (4-axis + bitemporal) key — and whether it forces a second segment kind (a point-lookup index layout vs the analytical scan layout, opposite physical shapes over the same log) — is a named-later residual (no operational store does sub-ms on a 5-coordinate key today).

D11 — The async execution boundary: await once at the EDB-loading edge; the fixpoint stays synchronous

A real foreign connector (Postgres, S3, DynamoDB, DuckDB) does async network I/O; the Argon evaluator (executor/eval.rs) and RuntimeStorageBackend are synchronous (semi-naive over BTreeMap). The two are reconciled by the frozen-foreign-EDB rule (D4) itself: a connector’s scan is async, but it is awaited exactly once, at the EDB-loading edge, before the fixpoint — its bounded demand-slice (D4, RFD 0035 D5) is drained into the frozen CatalogEntry, and the synchronous fixpoint then iterates over a constant, in-memory snapshot. The async↔sync seam sits outside and above the fixpoint, never inside an iteration. This is the same async-durable-seam-below-sync-replay discipline D9 establishes for persistence, now for reads — the read-side counterpart of “hydrate-then-replay.”

This is forced, not chosen — await-inside-iteration is unsound, not merely awkward. A synchronous fixpoint’s monotonicity / WFS guarantees assume a fixed input relation; re-consulting a foreign source mid-fixpoint lets the EDB change under the operator — the case the proofs do not cover (the operator’s parameter, not just its argument, would vary — D4’s freeze rationale). The boundary is the convergent answer across every mature engine, on primary sources: Soufflé .input / Nemo @import load once before evaluation as stratum-0 EDBs; DDlog / Materialize / Differential Dataflow ingest async sources from outside the synchronous dataflow (SyncActivator wake + capability-stamped batches) and keep operators non-blocking; DataFusion’s own recursive-CTE operator iterates over an in-memory WorkTable, not re-issued remote scans; and PostgreSQL recursive-CTE-over-FDW re-scanning per iteration is the bug its Dec-2025 Material-node patch fixes (the negative control — freeze is the soundness fix, not an optimization). The connector-SPI strawman reached the same shape independently: await a bounded binding-pushed slice once, freeze it, iterate the fixpoint over the frozen synchronous snapshot.

The SPI is async, dyn-dispatched, and the connector lives in Store state. ForeignRelation::scan is async; the connector is held Arc<dyn> in Store state, never in CatalogEntry (which must stay Serialize / Eq for the read-model segments and the differential oracle — D9 / RFD 0035 D8). Native async fn in a trait is not dyn-compatible, so the trait carries #[async_trait] (the per-call box is noise against a network round-trip; dynosaur is the static-dispatch-by-default alternative if the box ever matters). A blocking-only source (rare; the targeted stores are natively async) bridges via spawn_blocking + a channel (the DataFusion pattern), never a blocking call on a runtime worker.

Where the await lives, per entry point. Serve handlers are already async; the foreign fetch is hoisted above the existing flavour-aware sync core (run_reasoner’s block_in_place-vs-inline, oxc-serve/src/lib.rs). Concretely, materialize_predicates splits into [sync: seed base + plan demand] → [async: fetch + freeze] → [sync: fixpoint], with the one await in the middle and the CPU-bound fixpoint kept off the executor exactly as today. The CLI (no runtime today) builds one current-thread runtime and block_ons the whole command, doing all fetching inside that single block_on before the sync core. Rejected alternatives, on mechanism: making the query stack async end-to-end (function-colouring contagion — it colours the recursive core async for zero benefit, since the core does no I/O, and forces a runtime into the CLI while dragging the maintainer and the differential oracle along); and block_on inside the sync freeze (it runs the CPU-bound fixpoint on a runtime worker — executor starvation — and panics at the CLI, which has no runtime).

The single-fetch soundness boundary → a new refusal gate (extends D4). A single bounded fetch is complete iff the foreign relation’s extension is independent of the IDB computed in that fixpoint — i.e. there is no recursion through the foreign source (it sits strictly below the IDB it feeds; magic-sets / limited-access-patterns theory — Duschka–Levy 1997, Nash–Ludäscher 2004). This is the dual of D4’s Li–Chang gate (which forbids pushing recursion into a source): here the source is a leaf, but a recursive cycle that derives new foreign demand from already-consumed foreign tuples would make one fetch incomplete. v1 gates it statically: recursion-through-a-foreign-source is a refusal, reusing the analytical tier’s theorem-backed transitive no-cycle / dependency-cone check (AvoidsVocab, F2.4 / Reasoning/Datalog/AnalyticalFreeze.lean) — a foreign scan in an SCC with the IDB deriving its demand is refused with a remediation diagnostic, never silently under-derived. The relaxation — admit it via monotone bounded re-consultation (iterative demand rounds owned by the orchestrator at the boundary, the connector required idempotent + monotone under growing demand — D4’s second resolution) — is a named, gated follow-on, not a v1 shortcut; the full sans-IO yield-demand state machine is explicitly not adopted (overkill for batch compute — the loop, when needed, lives in the orchestrator, not the evaluator). F2.3’s existing base-binder boundary (bf binders must be already-materialized base relations) already enforces a conservative form of the gate.


Rationale

  • OBDA over a federated executor, not Convex-style absorption. You cannot absorb a pre-existing >TB store into one integrated backend (Convex’s model); Argon’s value is the ontology/reasoning layer over data where it lives. Foreign data = virtual EDB mapped into the ontology, queried through it (D2/D3/D6) — the OBDA/Trino/DataFusion shape.
  • The substrate was designed well, and it shows (D5/D7). AxiomEvent already being the provenance/freshness token, and per-concept CWA/OWA already being mechanized and ahead of deployed OBDA, mean federation adds a leaf and a tier input, not new machinery.
  • The freeze rule is the spine, and it carries zero new reasoning semantics (D2/D4). Because the mechanized operators never inspect provenance, a frozen foreign/computed EDB is a native EDB; federation’s hardness is engineering (the pipeline, the SPI), not semantics.
  • Type-identity is non-negotiable, so the SPI is per-operation (D3). The one Argon demand no federation system has — both-sides type-check must agree — forces the per-operation, independently-re-derivable residual over the opaque delegated rewrite.
  • Read-only-foreign is correct, not conservative (D2). The distributed-systems literature treats per-entity ACID + idempotent messaging as the right steady state; P3’s content-put is the one admissible cross-store write because it isn’t a distributed transaction.

Alternatives considered

  • Ingest/mirror foreign data into axiom events. Rejected (D2): O(database) copy of a >TB store, stale by construction; defeats the premise.
  • Delegated-subplan-rewrite connector SPI (datafusion-federation). Rejected (D3): opaque absorption is irreconcilable with the type-identical-residual demand (C2/C3).
  • Mapping as hand-authored interpreted config (R2RML/RML verbatim). Kept as influence, not adopted whole (D6): drift becomes a runtime surprise; the compiled-content-addressed artifact makes it a load-time refusal.
  • A magic blob-typed field (P3). Rejected in favor of explicit Ref<Blob> (D8): honest indirection, composes with reflective-Type/refinement.
  • Generalize the sync RuntimeStorageBackend to durable backends (P1). Rejected (D9): every backend would have to speak ABox kinds / retraction tombstoning / deep-clone synchronously — a heavy contract few stores fit; the thin async kv+CAS seam below it admits the widest backend set.
  • 2PC / cross-store distributed transactions. Rejected (D2): anti-availability; only works inside one store.
  • Treat IVM / the persisted read-model as a someday-optimization. Rejected after the performance campaign (D9/D10): the columnar content-addressed segment + IVM is the primary read-model and the highest-leverage work; frozen-per-query is its correct fallback. The architecture was already right; only the priority was wrong.
  • Delegate the analytical engine to DataFusion/DuckDB. Rejected (D3, RFD 0035 D4): Argon’s own vectorized engine, forced by the inline-provenance segment and unjustified-against by the small vectorized-vs-compiled constant. Off-the-shelf engines are a connector shape / temporary bridge, never Argon’s own engine. (Federating to such a store for data Argon does not own remains correct and permanent — the orchestrator half of the Decision lead.)
  • Make the write spine a structured (4-axis) CAS to carry the manifest. Rejected (D9): the manifest is an immutable content-addressed object the scalar root points at; one scalar CAS advances it. A structured CAS would needlessly reopen the harder-consensus question.
  • Make the query stack async end-to-end, or block_on inside the sync freeze. Rejected (D11): the first colours the recursive core async for zero benefit (it does no I/O) and breaks the sync CLI + maintainer + oracle; the second runs the CPU-bound fixpoint on a runtime worker (executor starvation) and panics at the CLI. The connector await belongs at the EDB-loading edge, above the sync fixpoint — which the frozen-foreign-EDB rule (D4) makes both sound and natural.

Consequences

  • New runtime dependencies + seams. The connector SPI (ForeignRelation, async / #[async_trait], dyn-dispatched, held in Store state — D11), the table-operator/analytical-tier executor (RFD 0035 D4/D6), the [store]/[placement] ox.toml sections + the compiled mapping artifact, the foreign-provenance leaf in derivation, the world-assumption tier-input threading, the Ref<Blob> type + blob-put SPI + GC sweep, and the thin async durable persistence seam. Each lands behind RFD 0035’s pipeline seams. The async↔sync seam is the EDB-loading edge (D11): materialize_predicates gains a plan-demand → await-fetch → freeze → sync-fixpoint split, with the await hoisted above the existing sync evaluator core (serve) or a single per-command block_on (CLI).
  • Spec / reference / Lean. Reference gains a heterogeneous-stores chapter; the only net-new semantic obligation is the frozen-foreign-EDB preservation theorem (RFD 0035 D3) against Fixpoint.lean/Compiled.lean. The CWA→OWA completeness warrant reads against CwaOwa.lean.
  • AGENTS nodes. oxc-reasoning, oxc-runtime, oxc-serve, and a new store-layer node updated once seams land; the “reasoner was not built here” / “rejected by design” framings retired (coordinated with RFD 0033).
  • Performance posture. The RFD 0020 scaling contract holds across the federation boundary: no O(database) on any hot path; the bounded binding-pushed slice + frozen EDB is the realization; the foreign store’s cost is the unknown the v1 cost model treats degenerately (push only when strictly better, RFD 0035 D5).

Open questions / tracked-future

  1. Engine-driven verdict (chosen) vs provider-driven callback — settled to per-operation (D3); the iterate-to-fixpoint negotiation (Trino) is deferred until a connector needs it. The genuinely-open piece is the connector conformance harness that gates the Exact claim (D3): its shape (a generated battery of predicates checked source-vs-oracle? a declared semantic profile — collation/NULL/coercion — the engine validates?) is net-new and unattested in the surveyed systems.
  2. Where mapping compilation lives — engine-side off-line (Ontop T-mappings) vs pushed into the source as views (Ultrawrap); and the exact [store]/[placement] schema (D6).
  3. Demand-stratify vs monotone bounded re-consultation for recursive foreign demand (D4) — settled with RFD 0035 D5’s magic-sets interface.
  4. Freeze vs delta as steady state (D7 rung (b)) — when a foreign CDC/change-feed earns its cost; the IVM follow-on (D10, RFD 0035 D7).
  5. Composite multi-source freshness beyond meet-of-leaves; read-your-writes across a mixed native-exact / foreign-barrier boundary (D7).
  6. The cross-boundary CWA→OWA completeness warrant — the connector contract that discharges cwa_owa_transfer for a closed foreign predicate, and how OE0901 fires when it is absent (D5).
  7. Blob GC over the bitemporal/fork-branched/content-addressed log — conservative mark-and-sweep vs a maintained per-fork refCount projection (D8).
  8. Whether content-addressed immutable layers unify C10 identity with the C8 fork axis (D9) — a deeper storage simplification to investigate.
  9. The per-symbol identity residual (RFD 0033): artifact identity is solid (composition signature + section hashes); threading it to per-event/per-symbol resolution (the module_id collision) is shared with the storage-identity fix and bears on the foreign-leaf source_id.
  10. Read-model segment GC over the 4-axis + bitemporal + fork space (D9): a single segment is referenced from many (tenant, fork) coordinates (cheap branching = pointer-sets over shared segments), so it is collectable only when no axis-coordinate’s manifest references it — a cross-axis reachability computation no surveyed system does (Neon GCs by single-axis LSN-horizon). The cheap-branching win and the GC-reachability cost are in direct tension; the algorithm is unsketched.
  11. The IVM maintainer’s checkpoint cadence (D9, RFD 0035 D7): per-mutation segment minting (churn + GC pressure) vs batched (the in-memory materialization must survive restart some other way). A genuine open the maintainer loop surfaces.
  12. Composed cross-tier freshness (extends D7’s meet-of-leaves): when one fixpoint joins facts from a point-lookup (operational tier), an analytical slice (segment watermark), and a federated ledger (as_of barrier), the derived conclusion is consistent only relative to the weakest of the three barriers — a three-way compose sharper than the two-source (native + foreign) case D7 states.

RFD 0037 — The macro atom: a phase-separated, hygienic, declarative-first expander over surface syntax

  • State: discussion
  • Opened: 2026-06-16
  • Decides: how Argon realizes its macro atom — the last unbuilt of the five substrate atoms (meta-calculus, constructs, rule, trait, macro). Settles: (1) expansion is a real pipeline phase between parse and resolve, not a rewrite smuggled into elaboration; (2) a macro expands to surface syntax, which is re-parsed and re-elaborated through the one existing path — never to events directly; (3) hygiene is scope-sets + Racket-style binding spaces, white-box, over Argon’s existing binder namespaces; (4) rule variables are alpha-canonicalized at AST→event lowering — a content-addressing correctness fix that also dissolves the macro determinism hazard; (5) v1 is purely declarative (pub macro, pattern→template) — verified sufficient for every current client including MLT; the procedural / analytic layer is deferred with its end-state shape committed (total structural recursion over reflected syntax, Lean-mechanizable); (6) migration re-homes the relation-property family to a library declarative macro byte-identically, and the MLT decorators via a library-surface-stub / privileged-native-expander split (RFD 0009 RP-003 GAP-3); (7) collisions resolve by uniqueness-at-registration (loud OE0705-class, not silent priority); (8) the Lean line holds at the typed AST (rung a) with a re-check obligation. Two acceptance tests gate v1 (§8). This RFD leads the arc per the workflow table — language surface: RFD + reference draft → Lean → code. Hard prerequisite for the directive surface (RFD 0028) and the std theory libraries (std::temporal, std::lifecycle, std::mlt).

Prior state (verified at 2c2854959):

  • The surface parses; nothing expands. pub macro Name { … } parses to MACRO_DECL with the body eaten as opaque balanced tokens — no pattern/template structure, no fragment specifiers recognized (oxc-parser/src/grammar.rs:2134, eat_balanced). #[procmacro] is a reserved directive (OE0706, directives.rs:468); there is no TokenStream type. name!(…) bang-invocation does not parse at all — no MACRO_INVOCATION node, the BANG token is unused in expression position (expr.rs:238). Lowering intentionally skips macro decls (oxc-instantiate/src/lower.rs:5).
  • A working proto-macro already exists. synthesize_relation_property_rules (lower.rs:5095) is a string-template macro in all but name: it format!s Argon source — "pub derive {rel}(x, z) :- {rel}(x, y), {rel}(y, z)" (lower.rs:5172) — then parse_files it, extracts the Item, and lowers it with the live ctx so the body resolves to the right qualified paths (lower.rs:5135). It works only because it runs inside instantiate on self-contained synthesis whose introduced names nothing else resolves against.
  • The pipeline is hard-gated, parse-frozen. parse → [error gate] → resolve/check → [error gate] → instantiate → tier-classify → discharge → write (oxc-driver/src/lib.rs:1520– 1647). parse_file(db, SourceFile) is a memoized salsa query (oxc-db/src/lib.rs). Each phase assumes the prior AST is immutable; there is no phase that rewrites the AST before resolution.
  • Rule variables are name-carried and un-canonicalized. Term::Var { name: Ident { text: String } } (core_ir.rs:240,142); content_id = BLAKE3(encode_rule_decl(body)) (lower.rs:3969); no alpha-canonicalization anywhere (grep across compiler/ is empty). So alpha-variant rules get different AxiomKeys — contradicting the stated intent “same proposition asserted twice has the same AxiomKey” (ids.rs:393). #[forall] lowering already mints counter-named $fa_N vars (atom_lower.rs:226) — a latent reproducibility hazard.
  • The §3.4 gate and head resolution are concrete and reusable. resolve_metatype_introducer / resolve_metarel_introducer (lower.rs:1171/1209): three arms (local pub metatype → ambient std::core → workspace-unique, else OE0605/0606). Rule-atom resolution is local-first → unique-workspace → ambiguous-refuse (“never a silent union or an arbitrary pick”, lower.rs:1528). Same-module same-head derives union; cross-module same-name refuses.
  • MLT is declarative templating, not computed expansion. #[categorizes(T)] reads only T’s name and emits a MetaProperty event (lower.rs:4331); it does not inspect T’s structure. The Lean model is a five-kind enumeration with a round-trip theorem (MLTKinds.lean, classify_declOfKind).

Question

Macros are the last unbuilt atom and the extensibility substrate the ontology-neutral doctrine depends on: every hard-coded compiler substitute is a macro waiting to exist — the #[…] directive registry, the elaborator-native MLT decorators (RFD 0009 commits to re-homing them byte-identically), the just-built relation-property directives (#434, a string-template proto-macro), std::temporal’s ten DatalogMTL operators, std::lifecycle, and derive. Without macros, “the core stays small; theories are libraries” cannot hold — every vocabulary a user wants needs a compiler patch.

The campaign that precedes this RFD (vault: Efforts/On/Argon/research/macro-system/) converged on a design and then code verification overturned two of its premises, which this RFD encodes:

  1. The research assumed expansion could sit “between parse and elaborate” on the existing pipeline. It cannot — the pipeline is parse-frozen and hard-gated (prior state). v1 requires a real expansion phase, a driver restructuring. This is the correct solution; hacking expansion into instantiate (as the proto-macro does) only works for self-contained synthesis and breaks the moment a macro introduces a name another declaration resolves against, or rewrites a rule body the resolver would otherwise reject as unknown atoms (since/ever).
  2. The research treated the procedural fork (declarative-first “C” vs bootstrapped-Argon “B”) as the central decision. Verification collapses it: every current client — relation-property, the ten temporal operators, std::lifecycle sugar, navigation, and even MLT — is declarative (pattern→template ± name resolution). The only workload needing analytic computation (inspect a concept’s fields) is genuine derive-class, which has no current client. So v1 is declarative, full stop; the analytic layer is deferred, not staged-around.

A worked motivating case (a real user’s tax-code rule):

pub derive realizes_gain(pcr: PCR) :-
    Performs(pcr, perf), HasContent(perf, content),
    HasObjectConstraint(content, oc), ConstrainsObject(oc, obj),
    compute_1001b(pcr) > obj.basis_for_gain;

The long relational-navigation chain is exactly what a declarative navigation macro should desugar; the transitive-closure sugar x.Role+(y) is already a loud refusal telling users to hand-write the recursion (rule_atom.rs:338, #297). These are the macro engine’s first clients, and they are all pattern→template.


Decision

D1 — Expansion is a phase between parse and resolve

Insert a new compilation phase. The pipeline becomes:

lex → parse → EXPAND (to a fixed point) → resolve → check → instantiate → tier-classify → discharge → write

EXPAND consumes the parsed module set and produces an expanded module set (fresh green trees / synthetic SourceFiles) that resolve/check/instantiate consume as if hand-written. It is a memoized salsa query keyed on the parsed input + the in-scope macro definitions. Expansion runs to a fixed point (macros producing macro invocations re-expand) with a fuel cap that errors on exhaustion (OE-coded). The relation-property and MLT synthesis currently living inside instantiate move to this phase (or, for MLT, to the stub/expander split of D8).

Why a phase, not a rewrite in instantiate (the correctness call): a macro that introduces a concept/relation other code references, or body sugar the resolver would reject pre-expansion, must expand before name resolution runs. The proto-macro’s instantiate-time re-parse only works because its output references already-declared names and introduces nothing referenced elsewhere. Generalizing requires the phase. The error gates re-order accordingly: a parse gate on the original source, then expansion (which may itself emit diagnostics), then the resolve/check gate on the expanded tree.

D2 — A macro expands to surface syntax, re-parsed and re-elaborated

A declarative macro is Syntax → Syntax: it matches a token pattern and produces surface tokens, which are spliced into the module and re-parsed, then flow through the one existing parse→resolve→check→instantiate→lower path. This is the model the proto-macro already validates (emit source text → parse_file → lower). Consequences, all verified to hold:

  • The drift contract (S2) is preserved for free — the only thing that ever produces events is the unchanged lowering, now running over post-expansion AST.
  • The §3.4 gate runs unchanged — a macro-introduced concept flows through resolve_metatype_introducer like any other (lower.rs:1171).
  • Tier classification is correct by construction — the classifier already runs last, after instantiate, on lowered events (oxc-driver/lib.rs:1571); it sees post-expansion reality (S4).
  • Head composition is sound — a macro emits into its invocation module, where its rules union with the user’s (same-module same-head) and structurally cannot pollute another module’s heads (cross-module is ambiguous-refuse, lower.rs:1528).

Direct event emission is rejected for user macros (it forks the drift contract and bypasses the gate and classifier). The one exception is the privileged MLT-style axis-event emitter (D8), which has no surface form and stays a compiler builtin behind a library surface.

D3 — Hygiene: scope-sets + binding spaces, white-box

Adopt Flatt-2016 scope-sets with Racket-style binding spaces — each of Argon’s existing binder namespaces (rule vars, concept/type, rel/metarel, metatype/metaxis + axis values, individuals, trait members) is an interned scope in one scope-set; the maximal-subset resolution rule is unchanged. Hygiene is white-box (Lean-style effectful quotation): the expander does not stamp scopes globally — the quotation applies a fresh macro scope to the identifiers it introduces. (Black-box mark-and-invert is quadratic on Argon’s per-axiom-event structure.)

Integration is an extension of existing resolution: the resolver already does local-first → candidate-set → ambiguous-refuse (lower.rs:1528) and tracks bound_vars: BTreeSet<String>. Hygiene adds a scope dimension to candidate filtering; bound_vars becomes scope-tagged. The §3.4 gate rides candidate-set disambiguation as a commuting pass — it rejects an ill-formed introducer but never re-points a reference (candidate Lean theorem #1, deferred per D7).

Hygiene splits cleanly by what reaches identity (see D4):

  • Rule variables (bound, clause-local): hygiene need only guarantee non-capture — alpha-canonicalization (D4) makes their names irrelevant to identity and the content hash.
  • Introduced vocabulary (concepts/rels/metatypes a macro declares): names are semantic and referenceable, so hygiene must produce a content-derived, stable name (derived from macro-identity ⊕ argument-content ⊕ expansion-path, content not source span.oxbin must survive non-semantic edits). For macro-producing macros this is a content-derived path.

unhygienic! is not shipped in v1: a $crate-style targeted self-reference plus syntax-parameter keywords cover the real needs; any future raw escape must be namespace-indexed (name which binding space).

Implementation (realization Y). The white-box scope-set model above is realized over the expand-to-surface carrier (D2) by rewriting each transcriber-literal identifier as expansion runs, classified against the macro’s definition module: a reference to a global is qualified to its package-anchored canonical path (reference hygiene); a macro-introduced variable is freshened to a content-derived name in a reserved namespace (variable hygiene, marker · / U+00B7, forbidden in user source — OE0725); metavariable substitutions (use-site syntax) are left untouched. The classification reuses the resolver’s own local-first → workspace-candidate → ambiguous-refuse discipline at the definition site, so the embedded §3.4 gate rides along unchanged. The carrier-level rewrite (oxc_parser::hygiene

  • oxc_workspace::hygiene) was validated before implementation: it is proven to induce the same binding as the scope-set specification — resolves_equiv in spec/lean/Scratch/MacroHygiene.lean (branch research/0037-macro-hygiene; design memo spec/rfd/0037-macro-atom-hygiene-design.md), with non-capture resting on the reserved namespace’s disjointness — and that proof depends on no project-specific axioms. v1 covers single-level expansion and the variable/reference partition; reference hygiene of a metaequality metatype target (c :: T) is the one documented carrier-level gap (the variable c is still freshened), pending the nested-scope generalization.

D4 — Alpha-canonicalize rule variables at AST→event lowering

Before computing content_id, rename every bound rule variable to a positional canonical form (_0, _1, … by first-occurrence traversal), consistently across all ~25–30 binder sites: head args, body predicate args, Comparison/Compute/Aggregate/TypeTest operands and outputs, Comprehension binders (respecting shadowing), and the #[forall] $fa_N vars. Surface names are preserved in a side table for diagnostics (errors keep the user’s names; the hash sees the canonical form).

This is a content-addressing correctness fix in its own right, independent of macros: it makes rule identity up-to-alpha (honoring the ids.rs:393 intent), fixes the latent $fa_N counter non-determinism, and lets alpha-equivalent rules dedup and share lineage. For macros it is the lever that dissolves the gensym→hash hazard: hygiene’s fresh variable names never reach the hash, so hygiene reduces to non-capture and reproducibility is automatic. Verified sound — variables are clause-local, string-identity-only, and carry no semantic weight beyond binding (no reflection, no match-by-name, defeat-edge resolution is by-name but maps through the side table).

D5 — Fragment specifiers: a closed v1 set, each a binding-space-targeted parse

v1 specifiers: concept, rel, metatype, rule, plus the syntactic Rust-lineage set (expr, ident, ty, literal, path, tt, and $( … )*/+/? repetition). standpoint is cut from v1 (no forcing client; admit later if needed). Each ontological specifier is “parse this syntax category, bind/reference in the corresponding binding space”: $r:rel binds r in the rel space and re-resolves use-site; $c:concept clears the §3.4 gate at disambiguation. S7 (ontology-neutrality) and S4 (tier-honesty) hold by construction — the gate runs on emitted references, and no specifier carries a tier (the classifier assigns it post-expansion).

$x:rule is the subtle one: it is a quotation of rule-plane syntax carrying its own sub-bindings (the rule variables inside the matched fragment must keep their use-site identity and not be captured by the macro’s introduced vars). It is the specifier the v1 prototype must exercise hardest.

This requires real grammar for the macro body (today an opaque token blob): a (pattern) => { template } form with $name:spec metavariables — net-new parser work.

D6 — Invariants: classifier-last, strong normalization, denied sources

  • Tier-honesty (S4): structural, by D1+D2 — the classifier runs last on lowered post-expansion events. “Nothing produces events after the classifier” is a checked pipeline invariant. No tier monotonicity rule — the classifier measures the true post-expansion tier; there is nothing to police (a tier cap, if a package declares one, is enforced on the measured tier with the breadcrumb pointing back to the invocation).
  • Termination (S5): the declarative layer is strongly normalizing by construction (finite templates; macro-calls-macro bounded by a structural measure), with a fuel cap as a backstop against bugs, not the primary mechanism.
  • Determinism (S5): the declarative layer has no I/O sources to deny; emitted collections are emitted in canonical (sorted-by-content) order; and D4 removes the fresh-variable hazard. Result: byte-identical .oxbin across builds with no author discipline required.

D7 — Lean line: hold at the typed AST (rung a) + re-check obligation

The expander is untrusted by design. Assurance comes from re-checking its output: the tier classifier runs last (D6), the .oxbin content hash is re-derivable, and the drift gate covers the @[language_interface] shape of MacroAtom = declarative MacroDecl | procedural ProcMacroDecl. Mechanize the re-checker, not the producer — the CompCert pole, which §13.7 already follows, and which the headline ITP precedent (Ullrich & de Moura) actually uses (it mechanized nothing; trust = kernel re-check). Do not mechanize expansion in v1. The tier-honesty commutation theorem (classify ∘ expand commutes with the true decidability class) is the one worth pursuing later — it is Argon’s distinctive invariant — but it is statable only once expand is a Lean object, i.e. only under the analytic layer (D9). Hygiene-algebra mechanization (POPLmark-scale) and full expansion-preservation (likely ill-defined per Leroy — a macro’s meaning is its expansion) are out.

D8 — Migration: re-home by emit-target, surface-stable at every step

  • Relation-property family (#[transitive]/#[irreflexive]/#[asymmetric]/#[functional]): re-homes to std::rel, but splits by what the declarative layer can express (design revised during implementation — the family is not uniformly declarative; PR #549/#556/#560):
    • #[transitive] is a genuine declarative pub macro: it emits the closure rule (which has surface) into the invocation module, and because the library template is byte-identical to the proto-macro’s string ({rel}(x, z) :- {rel}(x, y), {rel}(y, z)), the lowered events are byte-identical — the v1 differential test (§8).
    • #[irreflexive]/#[asymmetric]/#[functional] are builtin-backed macros — the same library-stub / privileged-expander split as the MLT decorators below. Their synthesized check has a computed head (__{rel}_{prop}), which the splice-only declarative layer cannot construct (no concat_idents), and #[functional]-on-rel is a cardinality cap, not a surface rule. So a std::rel library stub marked #[builtin] (a new directive on a pub macro decl — Argon’s analogue of Rust’s #[rustc_builtin_macro]) owns the importable surface, the EXPAND phase leaves the attribute in place, and the unchanged privileged synthesis stays the implementation (byte-identical). They become genuine declarative macros once the procedural layer (D9) lands. All four require use std::rel::{…} (nothing is ambient — RFD 0038); a bare one is refused (transitive: OE0705; the checks: OE1362).
  • MLT decorators (#[categorizes(T)] …): re-home the surface to std::mlt via the Rust #[rustc_builtin_macro] pattern — a library stub owns the surface (name, stability, visibility, importability, docs; byte-identical per RFD 0009 RP-003 GAP-3) while the expander stays a privileged compiler builtin that emits the MetaProperty event (which has no user-writable surface). The expander flips to library Argon only when the analytic layer (D9) lands and an axis-assertion surface exists — both phases surface-stable.
  • std::temporal / navigation / std::lifecycle: declarative library macros; a native-or-library scoping choice per operator once the engine exists.

D9 — Collisions, identity, and the deferred analytic layer

  • Collision resolution: uniqueness-at-registration (Lean model) — one implementation per #[name]; a user macro colliding with a builtin is an OE0705-class error, not a silent shadow (matches Argon’s loud-refusal posture and the existing ambiguous-refuse resolver). use mod::foo is the module-qualified escape for distinct cross-package names. Identity is keyed to a stable diagnostic-item-style handle, not the path, giving resolution-invariance under re-homing. Implemented for duplicate macro definitions — two macro name declarations in one module are refused with OE0726, never silently merged into one invocation name; builtin-name collision in the shared #[…] attribute namespace follows with the attribute-macro surface.
  • The procedural / analytic layer is deferred, with its end-state shape committed: a total, structurally-recursive meta-language over reflected syntaxnot general-purpose Argon. This is the correct end-state because totality is what the substrate’s own doctrine demands (deterministic content-addressed builds, decidability tiers, Lean-canonical): it is strongly-normalizing and deterministic by construction, and — being a total function over an inductive Syntax type — it is the natural object for the D7 tier-honesty theorem. A Turing-complete compile-time Argon would be more powerful and less correct. The layer’s trigger is the first genuine derive-class client (inspect a concept’s fields), and its acceptance test is the MLT expander flip.

v1 scope and the two acceptance tests

In v1: the EXPAND phase (D1); the declarative pub macro engine (structured pattern→template grammar, fragment specifiers per D5, expand-to-surface→reparse per D2); scope-set/binding-space hygiene (D3); alpha-canonicalization (D4); invocation surface (name!(…) parser + MACRO_INVOCATION node; #[name(args)] argument plumbing, lifting OE0709 for macro-bearing attributes); the relation-property re-home (D8); the MLT surface stub (D8).

Deferred: the analytic/procedural layer and derive-class (D9); type-directed/elab-class expansion (no forcing client; the real ergonomic need — legal scoping — is context-directed, which binding-space resolution already serves); standpoint specifier; unhygienic!.

v1 ships only when both tests pass:

  1. Migration proof (byte-identical): #[transitive] (and the relation-property family), re-homed from the hard-coded synthesis to a library pub macro, produces byte-identical lowered events vs the current path — a differential test, trivially satisfiable because the template is the same and D4 makes variable naming hash-irrelevant.
  2. Grow-a-language proof: at least one genuinely user-defined pub macro expands end-to-end through the full pipeline (parse → EXPAND → resolve → check → instantiate → classify), clearing the §3.4 gate and landing on its true tier.

What this RFD does not leave implicit

  1. Expansion is a phase, not an instantiate-time rewrite (D1) — the verified architectural correction.
  2. Alpha-canonicalization (D4) is a committed correctness fix, prerequisite to the clean hygiene story and valuable independently.
  3. The analytic layer’s end-state shape (total recursion over reflected syntax) and its trigger (first derive-class client; MLT-expander-flip acceptance test) (D9).
  4. The origin breadcrumb for diagnostics (mapping a lowered event back to its macro invocation) is metadata excluded from the semantic content hash, carried in a separate diagnostic index — decoupling reproducibility (D6), re-homing identity (D9), and diagnostics. No full unexpander in v1.
  5. The $x:rule fragment’s sub-binding hygiene (D5) — the prototype’s hardest case.

Open items for ratification

  • The exact content-derivation function for introduced-vocabulary scopes (D3).
  • The fragment-specifier grammar for the macro body (D5) — the parser work that replaces the opaque-token-blob body.
  • Whether std::temporal/std::lifecycle operators land native or library in the first cut (D8) — a scoping call, not a design blocker.
  • The driver/salsa shape of the EXPAND phase (D1) — incremental re-expansion granularity.

RFD 0038 — The prelude, ambient scope, and symbol-precise stdlib loading

  • Status: discussion
  • Depends on: RFD 0009 (MLT-as-library / nothing-privileged), RFD 0030 (path dependencies)
  • Blocks: RFD 0037 D8 (relation-property re-home needs a real prelude to surface library attribute-macros without ceremony)

Summary

Argon’s prelude is specified but unbuilt: the reference designs a full Rust-like prelude (§3.4 four-tier resolution; §15 std::prelude::v1 with an auto-import set + #![no_implicit_prelude] opt-out), but the compiler implements none of it — three ad-hoc mechanisms stand in for it, it diverges from the reference, and the reference contradicts itself on the most basic question: whether type/rel are ambient. This RFD settles the ambient-scope architecture:

  1. Tier 0 — substrate. A fixed, hardcoded, non-opt-outable set: the names that are the language, not library declarations.
  2. Tier 1 — the prelude. A real, configurable, auto-imported (opt-out) prelude that carries no ontology by defaultno type/rel. The prelude is a package feature (ox), not a compiler feature: there is no compiler-default prelude.
  3. Tier 2 — explicit use, with stdlib and dependencies loaded symbol-precisely (⊥-locality extraction) from the transitive use-graph — only what the program actually uses, never whole packages.

The frame is the existing oxc = rustc, ox = cargo split (the binaries already encode it: oxc is “the language compiler … pure source-to-artifact passes”; ox is “the project CLI”). oxc compiles loose files with substrate + explicit uses and no prelude; ox is the package tool where the manifest supplies the prelude, dependencies, tier, and world. We copy Rust’s split and manifest model but deliberately do not copy rustc’s rich auto-applied std::prelude — Rust can privilege std because it is a universal library; Argon structurally cannot privilege any vocabulary (RFD 0009). So the governing principle is: nothing from the standard library is privileged into ambient scope — not categorizes, and not the no-commitment baseline type/rel. Ontological commitment is always explicit and visible at the top of a module. This resolves the reference-manual contradiction in favor of its own better half (§4 / §15.0.1) and deletes the “auto-injected ambient type/rel” claims (§5.2 / §15).

Background — the validated problem

Ground truth, verified against the compiler (compiler/crates/) and by ox check:

  • The specified prelude is unbuilt. The reference fully specifies a four-tier resolution (local → use → auto-prelude std::prelude::v1::* → primordials, 03-modules.md §3.4) with #![no_implicit_prelude] opt-out, and §15 even enumerates a rich Rust-like auto-prelude (Option/Result/Ordering/traits/ macros). But the compiler builds none of tier 3: no std::prelude::v1, no #![no_implicit_prelude] (listed as a directive in §9 but absent from DIRECTIVE_REGISTRY, so it parses then fails OE0705). The prelude exists on paper, not in code.
  • The substrate/prelude boundary (clarification). Of §15’s auto-prelude list, the items that actually resolve today — Option/Result/Ordering/List/ Set/Map/Range/Truth4/Diagnostic/Severity — are Tier-0 substrate (hardcoded is_builtin_type_form + the check surface), not a library prelude. So “empty default prelude” (D2) never strips these — a package always has them. The empty default applies to the ontology/library layer (type/rel/ vocabulary). The not-yet-shipped traits/macros (Display/format!/…) are deferred: substrate-vs-prelude is decided when they land (lean: language macros and core operator-traits are substrate).
  • All seven stdlib packages load unconditionally. STDLIB_SOURCES (oxc-instantiate/src/lower.rs) is a fixed seven-element list; load_stdlib iterates it with no dependency graph, no opt-in, no opt-out. Every artifact carries std::mlt/std::kripke/std::fin/… axioms whether used or not.
  • Ambient leak in classifier position (demonstrated). Type references require use (pub rel R(World, World) with no import → OE0101, test T1). But metatype/metarel classifiers resolve local → ambient pin → workspace-unique (resolve_metatype_introducer/resolve_metarel_introducer, lower.rs:1171/1209); because every std package is loaded into the workspace, the workspace-unique arm makes every loaded metatype/metarel an ambient classifierpub categorizes AB(A, B) checks clean with no use (test T2). This directly violates RFD 0009 (“MLT is an explicitly-imported library, nothing privileged”) and §5.2.
  • type/rel are hardcoded pins. if classifier == "type" { return std::core::type } (lower.rs:1182; :1220 for rel) — special-cased, not resolved through any inclusion mechanism.
  • Top/Bot have two sources of truth. Defined both as primordials (resolve.rs primordial_kind) and as pub types in std/core/root.ar.
  • The reference contradicts itself. §15.0.1: “No vocabulary ships with the language or the stdlib … external vocabularies may re-export std::core::rel in their own prelude as a convenience”; §4: after use std::core::rel; the modeler can write pub rel … — both explicit-inclusion. Yet §5.2 / §15: std::core is auto-injected … pub type Foo works with no use.” These cannot both hold.

Infrastructure that already exists and we build on: ox.toml package manifests with [package]/[dependencies] (RFD 0030); inner-attribute parsing (#![...]ATTR_INNER CST, validated against DIRECTIVE_REGISTRY); the prelude.ar / pub use pkg::prelude::* convention (§3). Infrastructure that does not exist: selective stdlib loading, a transitive use-graph (only mod-chain reachability and per-file import maps exist), and any prelude-control directive.

Decision

D0 — oxc is the compiler, ox is the package tool (rustc / cargo)

The two binaries (crates/oxc-driver/src/bin/) already encode the split:

  • oxc (rustc): compiles loose source files — “the pure source-to-artifact passes.” A loose file is not a package, so it has no prelude: only the Tier-0 substrate is in scope, and everything else is an explicit use. A bare oxc foo.ar with pub type Person {} and no import is an error (OE0605, unresolved metatype type) with a hint to use std::core::{type, rel}.
  • ox (cargo): the package tool. It requires an ox.toml, and the manifest supplies the prelude ([package].prelude, D4), dependencies, tier ceiling, and world. ox invokes the same oxc passes with that manifest context — exactly as cargo drives rustc.

Consequence: the prelude is a package feature, not a compiler default. The current standalone path single_file_workspace_with_stdlib (oxc-driver), which injects all seven stdlib packages into any loose file, is deleted — it is the auto-load-everything behavior this RFD removes. Loose-file compilation keeps working under oxc with substrate + explicit uses + symbol-precise extraction (D3); there is no separate “standalone mode” with divergent rules.

D1 — Tier 0: the substrate is fixed and is not the library

The always-in-scope, non-opt-outable set is exactly the language primitives — the carriers of the meta-calculus and type system, which have no library declaration form:

  • Primordials: Nat, Int, Real, Decimal, Money, Date, Time, DateTime, Duration, Bool, String, Top, Bot (and /).
  • Builtin type forms: List, Set, Map, Range, Option, Result, Ordering, Truth4, Truth4Of, Path, Metatype, TypeRef, Entity.
  • Reflection intrinsics: meta, iof, specializes, extent, implements, implementors.
  • Aggregate heads: count, count_distinct, sum, min, max, avg, exists.
  • Modal operators, operators, true/false, the check surface (Diagnostic/Severity).

These stay hardcoded (resolved before imports), and the set is closed: adding to it is a language change, not a library change. Top/Bot are primordials (Tier 0); the duplicate pub type Top/Bot in std/core are removed (D5).

D2 — Tier 1: ONE prelude, carrying no ontology by default

There is exactly one auto-import mechanism. Two distinct roles were being conflated under the word “prelude” — separating them is what unsticks this (both exist in Rust too):

  • The auto-import set (role 1 — the prelude): what every module of a package gets without writing use. Configured by ox.toml [package].prelude (D4), default empty, opt-out per-module via #![no_implicit_prelude]. This is the one thing called “the prelude,” and it is a package concept.
  • A package’s exported public module (role 2): a normal module conventionally named prelude that pub uses the package’s public surface; consumers opt in explicitly with use pkg::prelude::*, or feed it into their own role-1 config. No special mechanism — just a module. The §3 prelude.ar convention is this. It is not a competing auto-prelude.

The earlier confusion (std::prelude::v1 and std::core::prelude both acting as global auto-preludes) is gone: there is one role-1 prelude per package, empty by default — the extension point, not a dumping ground.

type/rel are opt-in, with no special prelude for them. A package that wants the baseline auto-available in its own modules lists it in role 1 — [package].prelude = ["std::core::{type, rel}"]. A module (or a loose oxc file) that wants it without a package prelude writes use std::core::{type, rel} directly. A foundational-ontology package sets [package].prelude = ["std::ufo::prelude::*"] and never sees type/rel. The hardcoded pins (lower.rs:1182/1220) are removed; type/rel resolve through the prelude / imports like every other classifier.

A future #[cfg(...)]-style mechanism (Argon has none today) could conditionally configure the prelude — a clean home for “this build wants the core baseline” — but the manifest config covers the need now without a new language feature; cfg is noted as a later generalization, not a v1 dependency.

Rationale. The no-commitment baseline is itself a commitment — to neutrality. Defaulting it privileges that choice and forces every committed package to opt out of a commitment it never made (backwards). Making it opt-in (a) keeps the language neutral (RFD 0009), (b) makes every module’s metatype basis visible at its top, and (c) unifies resolution: classifiers and references both resolve local → imports/prelude → substrate, eliminating the T1/T2 asymmetry.

D3 — Tier 2: explicit use, symbol-precise extraction (not whole-package)

Loading is symbol-precise, not package-granular. use A::b::{c, D} brings in c and D and the transitive closure of declarations they depend on (endpoint types, supertypes, referenced relations, the symbols their bodies mention) — and nothing else from A::b. Pulling in all of A::b::* because one symbol was named is a hollow approximation and is rejected: the artifact must contain only what the program actually uses.

This is exactly ⊥-locality module extraction, which the substrate already mechanizes and proves conservative (spec/lean/Argon/Locality/; AGENTS.md §3.5 — “Σ-scoped CWA-conservativity, domain-conservative extraction for ghost individuals, chained extraction across import chains”). The compiler does not implement it today (all stdlib loads whole); D3 wires the existing, proven theory into the build:

  • Build the transitive use-graph — a new pass over use edges plus the dependency edges between declarations — seeded from the entry module.
  • Mint events for exactly the reachable declaration closure, across stdlib and path dependencies alike. use std::fin::{MonetaryAmount}MonetaryAmount and its dependency closure, not the Currency/RoundingMode surface it never references; no use of std::fin ⇒ zero std::fin axioms.
  • The metatype/metarel introducer’s workspace-unique arm sees only the extracted/in-scope metatypes (imported or in the prelude) — never every loaded package. This is what closes the T2 leak.

Conservativity is a composition of two properties (validated by reading the whole spec/lean/Argon/ tree), both now Lean-proven for their core:

  • Ontology/concept layer — ⊥-locality module extraction → Σ-scoped CWA-conservativity (concept entailment): proven (Locality/ScopedConservativity.lean, DomainConservative, ChainedCwa).
  • Datalog/derived layer — a program slice (the rules in the dependency-closure of the used predicates) preserves the least-fixpoint on the closure predicates, i.e. derived extents + checks: proven for positive Datalog (Scratch/DatalogSliceRelevance.lean, agree_on_closure, axiom-free — Lean obligation L1, discharged). The existing Locality/ mechanization did not cover this (Seminaive.lean proves the module-id column is harmless — a different result). The extension to stratified negation + aggregates is L2 (below), gated meanwhile by the differential test.

The differential and determinism tests below guard the implementation; L1 discharges the positive-Datalog core of the derived-layer proof gap.

D4 — Prelude configuration: manifest default + per-module opt-out

Two controls, both honored:

  • ox.toml [package].prelude — an array of use-tails, each parsed exactly as if written use <entry>; and auto-prepended to every module of the package. Reusing the real use parser gives single / brace-list / glob / alias forms for free, with identical resolution and identical participation in the D3 use-graph (a prelude entry is an implicit use). Default empty. An entry that fails to parse or resolve is a hard error (explicit author intent), not an OW1240 warning (OW1240 stays for unknown keys).

    [package]
    prelude = [
      "std::core::{type, rel}",   # opt into the no-commitment baseline
      "std::ufo::prelude::*",     # or glob a foundational ontology's public prelude
    ]
    

    Large preludes: point at your own prelude module. For a non-trivial prelude, the cleanest shape is a one-line config that globs the package’s own prelude module, with the actual re-exports written as real Argon code:

    [package]
    prelude = ["pkg::prelude::*"]
    
    // prelude.ar
    pub use std::core::{type, rel};
    pub use std::ufo::prelude::*;
    // … grows here, commentable, reviewable
    

    Validated against the resolver: pkg:: is the package self-anchor (resolve.rs:680, and it anchors at that package’s root even as a dependency, :681-684); a glob use pkg::prelude::* pulls in the module’s public surface including its pub use re-exports, which resolve transitively and cycle-guarded (resolve.rs:410-416, :452-453). This is role-1 (auto-import) pointing at role-2 (the exported module) — one mechanism, composed. Caveat (new): the prelude module must be exempt from auto-prelude injection into itself (else use pkg::prelude::* is prepended to pkg::prelude); the resolver’s cycle-guard catches the loop, but exempting the prelude module is cleaner.

  • #![no_implicit_prelude] — a per-module override. Add it to DIRECTIVE_REGISTRY (module position, ATTR_INNER); expose inner attributes on the SourceFile AST (today argon.ungrammar has only items:Item*); thread the flag into prelude injection. A module with the flag gets Tier 0 only — no auto-prelude — and must use everything else explicitly.

D5 — Resolve the spec contradiction; dedupe Top/Bot

The reference is amended to a single consistent rule: nothing from the stdlib is ambient; type/rel require use std::core::{type, rel} (or a prelude that re-exports them). Delete the §5.2 / §15 “auto-injected ambient type/rel” clauses; keep §4 / §15.0.1. Remove the pub type Top/Bot declarations from std/core/root.ar (Top/Bot are Tier 0 primordials).

Migration

This is a breaking change for any code relying on the leak or on the deleted standalone auto-load. Accepted (the corpus is ours).

  • Delete single_file_workspace_with_stdlib (oxc-driver). Loose-file compilation runs under oxc with substrate + explicit uses + extraction.
  • A corpus migration pass: every module that declares pub type/pub rel, or uses a stdlib classifier without importing it, gains the right use (std::core::{type, rel}, std::mlt::*, …) — or, if it is a package, the entry in [package].prelude.
  • Examples and single-file tests either run under oxc with an explicit use std::core::{type, rel}, or gain a one-line ox.toml (prelude = ["std::core::{type, rel}"]); the test harness (write_temp_source) writes whichever it needs.
  • The OE0605/OE0606 unresolved-introducer diagnostics gain a hint suggesting the likely use (e.g. “did you mean use std::core::{type, rel}?”).
  • The stdlib packages themselves declare their own dependencies via use (e.g. std::mlt uses std::core).

Acceptance tests

  1. Leak closed: pub categorizes AB(A, B) with no useOE0606 (unresolved metarel introducer), not ok. (Inverts test T2.)
  2. Baseline opt-in: pub type Foo {} with no use and #![no_implicit_prelude]OE0605; with use std::core::{type}ok.
  3. Symbol-precise extraction: use std::fin::{MonetaryAmount} yields an artifact with MonetaryAmount and its dependency closure but not the std::fin declarations it never references; no use of std::fin ⇒ zero std::fin events. Assert over the event stream.
  4. No regression for committed vocabularies: use std::ufo::prelude::*; pub kind Person {} checks clean with no std::core in scope.
  5. Determinism preserved: byte-identical .oxbin across builds (S5) holds under extraction (the extracted set is a deterministic function of the use-graph).
  6. Extraction is conservative: for any program, the answers (derived extents, check firings, tiers) under symbol-precise extraction equal those under load-everything — spot-checked differentially over the corpus. The DL/concept half is the Lean-proven ⊥-locality property (Locality/); the positive-Datalog half is L1 (Scratch/DatalogSliceRelevance.lean, proven, axiom-free); the stratified-negation/aggregate cases (L2) rest on the differential test until mechanized.

Lean obligations

  • L1 — positive Datalog slice-relevance: DISCHARGED. Proven in spec/lean/Scratch/DatalogSliceRelevance.lean (theorem agree_on_closure): for a ground positive program, the slice (rules whose head predicate is in the dependency-closure of the used predicate set Q) and the full program agree, at every fixpoint iteration — hence at the lfp — on every atom whose predicate is in the closure of Q. So loading only the use-graph closure yields the same derived extents over the used predicates. Self-contained (no Mathlib), no sorry, and #print axioms reports it depends on no axioms at all (fully constructive) — the agreement is proven by induction on the fixpoint chain, not assumed ([[no-axiomatizing-the-conclusion]]). This is D3’s derived-layer conservativity for the monotone core, composed with the ⊥-locality result below.
  • L2 — stratified negation + aggregates (remaining). L1’s T is monotone (positive Datalog). Argon’s reasoning is stratified well-founded with negation and aggregates; the relevance result extends in the standard way, but a faithful proof must model the strata, so L1 does not by itself cover the non-positive cases. Until L2 is mechanized, the differential test (#6) gates those. (Per AGENTS.md the Rust reasoner leads the Lean on the reasoning layer.)
  • Already discharged (ontology layer): ⊥-locality → Σ-scoped CWA-conservativity (Locality/ScopedConservativity.lean), domain-conservative extraction (DomainConservative), chained across imports (ChainedCwa).

Spec reconciliation (reference edits)

  • 05-constructs.md §5.2, 15-stdlib.md §15/§15.0.1: delete “auto-injected ambient type/rel”; state the explicit-inclusion rule and the prelude tiers.
  • 03-modules.md §3.4: make the resolution order match D1–D3 (substrate is the floor, not auto-prelude-then-primordials); document #![no_implicit_prelude].
  • appendix-a-reserved-keywords.md: type/rel are not reserved ambient names.

Resolved (this RFD)

  • oxc = rustc, ox = cargo. The prelude is a package feature; there is no compiler-default prelude. Loose oxc files use substrate + explicit uses. single_file_workspace_with_stdlib is deleted.
  • One auto-import mechanism (role 1), distinct from a package’s exported prelude module (role 2). No std::prelude::v1, no std::core::prelude.
  • [package].prelude = an array of use-tails, default empty; per-module opt-out #![no_implicit_prelude]. Bad entry = hard error.
  • type/rel opt-in via [package].prelude or explicit use — never privileged ambient.
  • Loading is symbol-precise (⊥-locality extraction), never whole-package.

Open questions

  1. Extraction staging. Symbol-precise ⊥-locality extraction is substantial. It may land in stages, but the hollow package-level version must not ship — if an interim step is needed it must still be sound (over-approximate only in ways that never change answers, and log/document any conservatism). Sequence the use-graph pass and the extraction pass against the existing Lean theory.
  2. #[cfg(...)] as the eventual conditional-prelude mechanism — out of scope for this RFD, noted as the natural later home for build-conditional baselines.

RFD 0039 — Composable mutations: nested invocation and derived reads

  • State: accepted — implemented (nested invocation Operation::Invoke #566; derived-extent reads Operation::ForEach #571; closed-nesting soundness mechanized on the never-merged scratch/nested-mutation-soundness branch)
  • Opened: 2026-06-19
  • Decides: how a mutate body (1) invokes another mutation — including a trait mutate member — and (2) reads the deductive plane (a derive/query result) to drive effects, so that the “rules that apply rules” pattern is expressible inside the language rather than only via host-side orchestration. Builds on RFD 0015 (the atomic buffer→prevalidate→commit body), RFD 0025 (the check delta-guard), RFD 0026 (trait members + receiver dispatch), and the pipeline slot RFD 0035 D1 reserved for mutations.

This RFD is Lean-first where it touches soundness: the atomicity of nested composition and the confluence of snapshot reads are the executed meaning, held in spec/lean/Argon/Runtime/ (extending MutationSemantics.lean’s runMutation_error_noop) and gated by the differential oracle. The surface grammar, the operation IR, and the call-graph analysis are engine architecture settled here. It commits a plan, folded into the implementing PRs per the discussion-first practice.


Question

A modeler writes a generic workflow rule as a trait and an impl:

pub trait Rule {
    derive Applicable(Self)
    mutate Apply(self)
}
impl Rule for LateFeeRule {
    derive Applicable(rule: Self) :- /* … conditions over rule.* … */
    mutate Apply(self) { /* create a RightDutyPair; enqueue a WorkflowRequest; update self.applied = true */ }
}

The deductive half works — Applicable(rule) derives, and a query returns the applicable rule ids. The modeler then wants to apply them, in Argon:

pub mutate ApplyApplicableRules() {
    for rule in Applicable {        // read the derived set
        Rule::Apply(rule);          // invoke each rule's mutation
    }
}

Today neither line works. There is no operation for invoking a mutation — a call in statement position is silently dropped (oxc-instantiate/src/mutate_lower.rs, the _ => {} arm of lower_stmt_expr) — and there is no path from a mutate body into the reasoner, so a for cannot range over a derived predicate. What are the correct semantics for a mutation that calls a mutation, and for a mutation that reads a derived set to drive its effects?


Context

The two-plane substrate

Argon already firewalls a deductive plane (derive/query: monotone, fixpoint, no effects) from an effect plane (mutate: insert/update/delete, executed as one atomic all-or-nothing transaction over a discardable overlay — RFD 0015, proven by runMutation_error_noop in Runtime/MutationSemantics.lean). The check delta-guard (RFD 0025) already invokes the reasoner over the committed ⊎ buffered overlay at one defined point, so a controlled deductive read at a transaction boundary is not a new capability — only a newly surfaced one.

Trait-member dispatch already works

RFD 0026’s receiver dispatch is live at runtime: resolve_mutation_invocable recognizes a trait-member callable, member_receiver_individual extracts the self receiver from the args map, and select_member_impl picks the unique covering impl by the receiver’s actual classification (refusing on zero or on <:-incomparable multiple covers). What is missing is (a) self lowering in expression position (a separate bug, #550) and (b) an operation that drives this dispatch from inside a body.

Prior art (the design is not invented here)

Six independent traditions converge on one model:

  • Transaction Logic (Bonner & Kifer, TCS 133, 1994): a transaction is evaluated over a path of states; serial conjunction is “do φ then ψ”; a sub-transaction call splices its body inline into the caller’s path, so the whole transitive call tree is one path that commits-or-aborts wholesale by construction. Tests (reads) interleave with updates and see prior writes.
  • Nested transactions / closed nesting (Moss 1981; Gray & Reuter savepoints): a subtransaction’s effects merge into the parent on subcommit and become durable only at top-level commit; a child abort is a partial rollback the parent may catch. Closed nesting is exactly the model that preserves top-level all-or-nothing.
  • Dedalus / Bloom + CALM (Alvaro, Hellerstein et al.): deductions are instantaneous within a step over a frozen snapshot; state change is deferred to a step boundary. CALM explains why: applying a non-monotone write (delete/aggregate) mid-fixpoint makes derivations order-dependent and destroys confluence.
  • LogicBlox / Rel (Aref et al., SIGMOD 2015; RelationalAI 2025): EDB/IDB firewall; updates are a delta plane recomputed over an immutable snapshot at named stages (@start/@final); one composite transactional fixpoint; integrity checked at the boundary, abort wholesale.
  • Active-rule termination & confluence (Aiken, Widom, Hellerstein, SIGMOD 1992): set-oriented (statement-level) firing over the net-effect delta; termination guaranteed by an acyclic triggering graph (sufficient, conservative); confluence requires commutativity over the transitive-trigger closure.
  • Golog / IndiGolog and Flix: a body as an ordered sequence of tests and primitive actions with read-your-writes; a static type-and-effect wall with effect polymorphism (a caller’s effect subsumes its callee’s, by inference).

Decision

D1 — A mutate body is reads-over-a-snapshot serially composed with deferred writes

The guiding principle, and the one-line semantics:

A mutate body is a serial composition of state-preserving reads (against the deductive-fixpoint snapshot) and deferred writes; a sub-mutation call is closed-nested — its writes merge into the parent’s single transactional buffer and become durable only at the top-level commit, so the whole transitive call tree is one atomic all-or-nothing unit.

D2 — Nested invocation: Operation::Invoke, closed-nested

Add an Operation::Invoke { callable, args } to the core IR. At runtime it does not call execute_mutation (which commits and runs its own check cycle). It resolves the callable (reusing resolve_mutation_invocable + select_member_impl) and runs the callee’s operations into the caller’s BodyExec — the same pending effect buffer and collections overlay — under a child binding scope for the callee’s parameters. That is closed nesting realized directly on the RFD 0015 overlay: effects merge up, the check delta-guard runs once at the outer commit, and atomicity composes for free.

  • Termination: the mutation call graph must be statically acyclic in v1 (no recursive mutation cycles). This is the decidable, safe choice and matches the decidability-tier philosophy. Mutual recursion / fixpoint-to-quiescence is a separately-gated future feature.
  • Failure: a sub-mutation’s failed require aborts the whole top-level transaction. Argon has no try/recover surface; catchable savepoint-style partial rollback is a deliberate deferral (it needs a recovery construct first).
  • Effect discipline: adopt the call-purity ladder fn ⊆ query ⊆ derive ⊆ mutation; a caller may invoke only equal-or-lower-impurity callees, and mutation → mutation composes into one atomic transaction. The effect is inferred (Flix-style polymorphism), not annotated.

D3 — Derived reads: snapshot at a fixpoint boundary, set-oriented, fire-once

A for x in <derived-extent> iterates a snapshot of the derived predicate taken at the deductive fixpoint of the body-entry state — never a partially-applied relation (CALM). This needs a read-goal term that invokes the reasoner from the mutation runtime, which is the mutation slot RFD 0035 D1 reserved.

  • Default = snapshot-once: compute the applicable set at entry, fire each Apply exactly once. Trivially terminating; confluent when the rule mutations have disjoint footprints. This is the set-oriented / statement-level reading.
  • Iterate-to-quiescence (recompute the set after each firing) is a strictly-more-expressive, strictly-more-dangerous later feature, gated on a static acyclicity check of the triggering graph (the stratification analysis in another hat) plus a loud runtime iteration bound. Not in v1.

D4 — Keep the two planes’ read semantics distinct

The apparent tension between Transaction-Logic read-your-writes and CALM snapshot-reads dissolves once the planes are separated:

  • Reading a derived predicate → a stable snapshot at a fixpoint boundary (D3).
  • Reading the body’s own direct writes (a scalar/collection it set) → read-your-writes (already partial via buffered_collection, RFD 0015 RC2).

Rationale

The decision is the intersection of all six traditions, and — crucially — it requires no new execution substrate. Operation::Invoke-into-the-parent-BodyExec is simultaneously Transaction Logic’s serial-conjunction splice, Moss’s closed-nested merge-up, and Bloom’s defer-to-boundary; the snapshot read is LogicBlox’s staged @final and Dedalus’s frozen-snapshot fixpoint; the fire-once default is Aiken–Widom–Hellerstein’s set-oriented semantics with a guaranteed-terminating triggering graph. Each maps onto machinery Argon already has (the overlay, the reasoner-over-overlay in check discharge, the stratification/tier analysis), which is why the soundness obligation is an extension of an existing theorem rather than a new framework.


Alternatives

  • Open nesting (a sub-mutation’s effects escape early, atomicity restored via compensating inverses). Rejected: it breaks top-level all-or-nothing and owes hand-written inverses; closed nesting is strictly safer and is what the overlay already supports.
  • Iterate-to-quiescence as the default. Rejected as default: it is what some workflow modelers expect, but it is exactly where non-termination and order-dependence live. It is offered only behind the static guard above.
  • Nested calls each commit independently (no closed nesting). Rejected: it shatters atomicity — a failure in a later sub-call could not undo an earlier committed one — contradicting the §7.5 “any error emits nothing” guarantee.
  • Host-side-only orchestration (the status quo: query, then loop and dispatch from the SDK). Retained as always-valid, but insufficient — and even it was blocked by the self bug (#550). “Rules that apply rules” is a first-class modeling pattern, not glue.

Consequences

  • A new Operation::Invoke IR variant and a read-goal term; the lowering replaces the silent _ => {} drop. The runtime threads Invoke into the current BodyExec rather than a fresh one.
  • The Lean gains a closed-nesting atomicity lemma (composition preserves runMutation_error_noop) and a snapshot-read confluence statement; the differential oracle gates the executor.
  • The reference manual §7.5 grows the invocation and derived-read forms; §7.7 (emit/sinks) is orthogonal.
  • Supersedes the nested-call scope creep folded into #74. Tracking issue: #551.

Soundness plan (Lean-first, scratch-mechanized before the executor)

Per the discipline that novel soundness frameworks are mechanized in scratch Lean before implementation and held by the differential oracle, here is the obligation, scoped against the existing model in Argon/Runtime/MutationSemantics.lean.

What the existing model already gives us (verified)

  • A mutate body is evalMutate : MutateDecl → Env → ReadView → Except Abort (List Effect × Value). It threads a MutState := { fresh, effects : List Effect } through evalStmts; the read-view rv is a read-only argument — it never changes during a body. Effects accumulate in MutState.effects; commit folds applyEffect over them only on the .ok branch; runMutation no-ops on .error (runMutation_error_noop, require_fail_atomic).
  • Consequence for D3 (snapshot read), already structural: because rv is read-only through evalStmts/evalExpr, any derived read inside a body observes the body-entry committed state by construction. The v1 snapshot semantics needs no new invariant — it is the only thing the model can express. (The overlay-staged re-derivation variant — the iterate-to-quiescence door — is what would require new machinery.)

What to add

  1. A statement constructor Stmt.invoke (callee : MutateDecl) (args : List Expr) (the Lean image of Operation::Invoke).
  2. An evalStmt (.invoke callee argExprs) case that evaluates the argument expressions against the current (env, rv, st), binds the callee’s parameters, and runs the callee’s require guards + body statements into the same MutState — appending the callee’s effects to st.effects and threading st.fresh. Aborts propagate through Except unchanged.

Theorems to prove

  • T1 — Closed-nesting atomicity (the headline). For any body containing invoke statements, runMutation is all-or-nothing: if evalMutate = .error err then runMutation = rv. Strategy: this is runMutation_error_noop unchanged — it depends only on evalMutate returning effects solely on .ok, which the shared-buffer/Except-propagation invoke case preserves. The proof lifts for free; that is the entire point of running the callee into the parent buffer rather than committing it.
  • T2 — Effect-buffer monotonicity. On .ok, an invoke extends st.effects by exactly the callee’s emitted effects (a list append), in callee-emission order spliced at the call site — the Lean form of Transaction Logic’s serial-conjunction splice / Moss’s merge-up. Grounds “the transitive call tree is one effect list committed once.”
  • T3 — Fire-once confluence (D3). For a snapshot set S iterated by for x in S { Apply(x) }, if the per-element effect sets have disjoint footprints (no two write the same (id, field) / (id, concept) / relation tuple), then commit is independent of the iteration order of S. Strategy: a commutativity lemma on applyEffect for footprint-disjoint effects, lifted over foldl.

Mechanization result (scratch branch scratch/nested-mutation-soundness)

A faithful self-contained miniature of MutationSemantics.lean proved all three (zero sorry/axiom; only propext/Classical.choice/Quot.sound), with one consequential refinement on T3:

  • T1 and T2 lift cleanly, no extra hypotheses. runMutation_error_noop survives the shared-buffer Stmt.invoke verbatim — confirming the formal payoff of run-into-parent over commit-nested.
  • T3 holds only up to observational equivalence, not structural equality — and this is mechanized as a counterexample theorem, not a caveat. The literal goal commit (commit rv A) B = commit (commit rv B) A is false even for footprint-disjoint effects, because applyEffect prepends to the classification/relation lists ((id,c) :: …, as the production applyEffect does too), so two disjoint asserts land in opposite list order. Confluence is true against an Obs interface — equal classification/relation membership, equal property lookups, equal nextFresh — i.e. up to what reads can observe. T3 is proved up to Obs, lifted over List.Perm.

Consequence for the executor. Order-independence of the apply-all loop is a read-interface property, not a representation property. The Invoke executor needs no commit-order canonicalization provided reads go through the membership/lookup interface, and the differential oracle must compare observable reads, not raw buffer/store layout.

The one new obligation: termination

Adding invoke breaks the structural termination_by (sizeOf s, 0) measure — a call runs the callee’s body, which is not a sub-term of the call statement. This is precisely why D2 mandates a statically acyclic mutation call graph: it is what makes the composed evaluator terminating and the recursion well-founded. Two encodings, in increasing fidelity:

  • (a) Fuel/gas — a depth parameter that decreases per invoke; models the runtime depth bound. Cheapest; lets T1–T3 be proved immediately, parametrically over fuel.
  • (b) Well-founded recursion on the acyclic call-graph rank — measure = (topological rank of the callee in the dependency DAG, then sizeOf within a body). This is the faithful image of D2’s static check and the honest termination story; it is the harder mechanization.

Recommended cut: prove T1–T3 under (a) fuel first (they are about atomicity/confluence, orthogonal to why evaluation terminates), and discharge termination separately under (b) as the acyclicity decision’s own lemma. Keep all of this on a scratch branch (never merged); the differential oracle gates the Rust executor against the executed meaning, exactly as for the reasoner.


Open questions

  • Lean first: mechanize closed-nesting atomicity and snapshot-read confluence in scratch Lean before the executor lands (the F2.4 bar). What is the minimal lemma shape that composes with the existing overlay proof? Resolved (scratch branch scratch/nested-mutation-soundness): T1/T2 lift unchanged; T3 confluence holds up to observational equivalence (see Mechanization result above). The minimal shape is runMutation_error_noop verbatim for atomicity + an Obs-quotiented commutativity for confluence.
  • Dispatch over buffered classification: should select_member_impl at an Invoke site read the receiver’s buffered (read-your-writes) classification or the committed one? Committed is the v1 answer; buffered is a coherent enhancement once scalar RYW lands.
  • Derived-read staging: is the snapshot taken at body-entry committed state, or re-derived over the committed ⊎ buffered overlay at the point of the for (as check discharge does)? D3 fixes v1 at body-entry; the overlay-staged variant is the iterate-to-quiescence door.
  • Quiescence guard: the exact triggering-graph construction over mutation effects and its relationship to the existing stratification pass.

RFD 0040 — The procedural macro layer

  • Status: P2 built — all builtin stubs retired. Reflection-types-as-real-types (#850) and the Lean line (D7) remain.
  • Depends on: RFD 0037 (the macro atom — declarative layer, D7 Lean line, D9 deferred analytic layer), RFD 0038 (nothing-ambient / import discipline)
  • Blocks: the genuine re-home of the relation-property checks and the MLT decorators (today shipped as builtin-backed P0 stubs, RFD 0037 D8); the classify ∘ expand tier-honesty theorem (RFD 0037 D7)
  • Implements: RFD 0037 D9 — “the procedural / analytic layer is deferred, with its end-state shape committed.”

Amendment (P2 built) — bounded structural iteration

D4 below commits to general structural recursion checked at declaration. Building P2 ruled the meta-language’s traversal to be bounded structural iteration instead, and this amendment records that decision (it supersedes the recursion framing in D4 / D6 stage 3 / Acceptance test 2):

  • The one iteration form is $( for x in item.fields ) { … } (and item.params) inside quote — a bounded loop over a finite reflected child-list, total by construction, nestable for depth. There is no user-writable recursion or call form (only the concat_idents paste builtin). So totality needs no decreasing-argument analysis: the body is a closed fragment {let-paste, quote + $(for) + ${…}/if, match on a reflected scalar, list-lit of quote/artifacts}, and anything outside it is refused at the declaration (OE0729 — Acceptance test 2, reframed from “non-structural recursion refused” to “out-of-fragment construct refused”).
  • Rationale (not effort): strong normalization without a checker that could itself be wrong; alignment with Argon’s determinism + decidability + cheap-Lean doctrine; the warning in this RFD’s own Background that compile-time-Argon must not grow “more powerful and less correct.” Every real client needs ≤2 levels of named children, which nested bounded iteration covers.

Landed: P2 structural field reflection + the derive-class capability (proven by #[reflect_fields], Acceptance test 3); the OE0729 totality guard (Acceptance test 2); #[functional] retired — the last #[builtin] (it is now a std::rel procmacro that dispatches on item.kind: a rel re-emits with a [0..1] target cardinality cap via structural re-emission, a metarel pastes the __{rel}_functional check — Acceptance test 1’s third). The MLT re-home (Acceptance test 4) had already landed under RFD 0043. OE1362 (the #[functional] import gate) is retired — an unimported one is now OE0705, like its siblings.

Remaining: the reflection vocabulary as real resolvable types (#850; the hover symptom is already fixed) and the Lean Syntax carrier + tier-honesty theorem (D7). Reference §13.5/§13.8 + §05 prose lift from “deferred”/“builtin” to “built” — coordinated with the docs work, not in the compiler branch.

Summary

RFD 0037 shipped the macro atom’s declarative layer (pub macro, pattern→template, hygiene, expand-to-surface) and committed — but did not design — the procedural layer: “a total, structurally-recursive meta-language over reflected syntaxnot general-purpose Argon.” This RFD designs that layer.

The motivation is honesty, not novelty. Two macro clients ship today as P0 stubs — a library surface whose implementation is still a privileged compiler builtin, because the declarative layer cannot express them:

  • the relation-property checks (#[irreflexive]/#[asymmetric]) need to construct an identifier (__{rel}_irreflexive) — paste — which a splice-only language has no operator for. (#[functional] is the family’s third member but is not paste-shaped: one of its two arms re-emits a rel with a modified cardinality bracket, which is structural re-emission — P2 — so it stays a builtin stub past P1; see D6 / Acceptance test 1.)
  • the MLT decorators (#[categorizes(T)] …) need to read the decorated declaration’s name and emit a metaproperty — which has no user-writable surface.

A stub is a deferral, not a re-home: the library owns the surface, the compiler still owns the implementation. The procedural layer is what turns these into genuine macros and lets the library migrate its implementations. It is deliberately smaller than “compile-time Argon”: a total fragment, strongly-normalizing by construction, so it preserves Argon’s determinism (content-addressed builds), decidability-tier honesty, and Lean-mechanizability.

This RFD is design-first: it crystallizes the surface, the reflection model, the totality discipline, the staged delivery, and the Lean line. The committed first implementation step — P1, identifier construction (paste) — is now built (#567): it retires the #[irreflexive] + #[asymmetric] stubs (genuine #[procmacro]s, byte-identical events). #[functional] stays a builtin stub until P2 (its cardinality-cap arm is structural re-emission, not paste).

Background — what’s a stub today, and why

The declarative layer is Syntax → Syntax by splice: it matches a token pattern and substitutes captured fragments into a template. It has no operation that (a) builds a new identifier from pieces, or (b) inspects a declaration’s structure and computes output from it. Both current stubs need exactly one of these:

ClientWhat it needsWhy declarative can’tStatus today
#[irreflexive]/#[asymmetric]head __{rel}_{prop} built from the relation nameno concat_idents / pasteP1: genuine #[procmacro]s (#567); OE0705 if unimported
#[functional]a metarel-check head (paste-able) and a rel-cardinality-cap [0..1] re-emit (structural)one attribute can’t be half-builtin/half-procmacro; the cap arm needs structural re-emissionbuiltin stub (P2); OE1362 import-gate
#[categorizes(T)]/#[partitions]/#[subordinate_to]/#[power_type_of]read the decorated concept’s name; emit MetaProperty(subject, axis, T)no reflection; no metaproperty surfacebuiltin-backed stub (RFD 0037 D8)
derive-class (inspect a concept’s fields, synthesize per-field)structural recursion over a declarationno reflectionthe capability P2 builds (this RFD, stage 3)

The research campaign (research/macro-system, D4) recommended C-then-B: declarative now, then a bootstrapped-in-Argon procedural layer on a zero-capability Wasm substrate, fuel-bounded. RFD 0037 D9 narrowed that to a total meta-language — strongly-normalizing by construction rather than fuel-bounded-and-Turing-complete — because totality is what Argon’s own doctrine demands (deterministic builds, the decidability ladder, Lean-canonical substrate) and “a Turing-complete compile-time Argon would be more powerful and less correct.” This RFD designs the committed total meta-language; Wasm is then an implementation option for its sandboxed substrate, never the user-facing surface.

Decision

D1 — Surface: #[procmacro] pub fn, body in a total fragment

A procedural macro is a function marked #[procmacro], in the one macro namespace (RFD 0037 D9; the !/#[…] sigils are invocation markers, not namespace tags). This is the shape already carried by the Lean substrate (Macro.lean: MacroAtom.procedural : ProcMacroDecl → MacroAtom, p.fn.name/p.fn.isPub).

#[procmacro]
pub fn transitive(item: Decl) -> Syntax = …      // a derive-style decorator

The body is not general-purpose Argon. It is a total fragment: structural recursion over reflected Syntax, the construction/quotation builtins (D3), and pure expressions — no I/O, no clock/RNG/filesystem, no unbounded recursion. The body’s totality is checked at declaration (D4), so a #[procmacro] either is strongly-normalizing-by-construction or is refused — never “trusted to terminate.” #[procmacro] is currently reserved (OE0706); this RFD lifts the reservation in stages (D6).

Rationale: reusing pub fn (not a bespoke DSL keyword) keeps the surface familiar and the namespace single; restricting the body to the total fragment is what buys determinism + decidability + mechanizability. This mirrors Argon’s own tier ladder — the meta-language is, in effect, a low-tier total sublanguage applied to syntax.

D2 — Reflection model: an inductive Syntax, read-only, finite

The macro receives its input as a value of an inductive Syntax type — the reflected post-parse AST (a deep embedding mirroring the typed AST that @[language_interface] already governs). Inputs by invocation position:

  • function-like name!(tokens)Syntax of the argument fragment;
  • attribute / derive #[name(args)] <item> → the decorated Decl (a Syntax subtype) plus the attribute args fragment.

Reflection is read-only and finite: a Decl exposes its name, kind, generics, parameters, fields, and attributes as Syntax children; a macro pattern-matches and recurses on those children, which are structurally smaller. There is no reflection of resolved identity (DefIds), types post-elaboration, or tiers — expansion runs before resolve/check (RFD 0037 D1), and reflecting post-resolution facts would break the phase ordering and S4 (tier-honesty). The Syntax shape is itself an @[language_interface] carrier, so the drift gate covers it (D7).

D3 — The construction builtins: paste, quote, splice

The meta-language’s output side is three primitives, each total:

  • quote { … } / splice — build Syntax from a literal template with $-holes (the declarative transcriber, now first-class and nestable). Output is surface text re-parsed by the ordinary path (RFD 0037 D2) — never events directly.
  • concat_idents(a, b, …) (paste) — construct a new identifier from identifier/literal pieces. This is the single capability the three checks need (concat_idents("__", rel_name, "_irreflexive")). It is total (string-level), non-recursive, and deterministic.
  • structural match over Syntax — case-split a reflected node and recurse on its children (D4).

A pasted identifier is raw (definition-site, not freshened): the macro intends it as the actual program name — like the check head __{rel}_irreflexive, which must be a stable, collision-resistant identity the classifier and the .oxbin see. Paste therefore composes with hygiene (RFD 0037 D3) as a definition-site name in the reserved-marker namespace where collision-resistance is needed, and as a plain raw name where the macro author wants a predictable public name. (The exact hygiene interaction of paste is the one sub-decision flagged in Open Questions.)

As shipped in P1: concat_idents emits the pasted head as a raw definition-site name with no freshening — exactly the former synthesis’s __{rel}_{prop}. Splices ($item, $item.name, $item.params[i].ty) are substitution barriers: the reflected text is dropped in verbatim, carrying its own use-site identity, not re-scoped by the macro. So P1 does no hygienic renaming at all — which is correct for these checks (they want the stable public head), and is the conservative floor the Open-Questions paste×hygiene decision will build on.

D4 — Totality: structural recursion, checked at declaration

Termination is by construction, not by fuel. The only recursion the meta-language admits is structural: a recursive #[procmacro] (or meta-helper) may recurse only on a Syntax value that is a proper child of its argument. The compiler checks this at declaration (a decreasing-argument check over the finite Syntax tree — the same well-founded-recursion discipline Lean uses for structural recursion). A body that cannot be shown structurally decreasing is refused (a new OE07xx), not accepted-and-fuel-capped.

A fuel cap (RFD 0037’s OE0727 runaway diagnostic) remains as a backstop against implementation bugs, but it is not the termination argument: the argument is strong normalization of the total fragment. This is what makes expand a total function over an inductive type — the precondition for the D7 theorem — and what distinguishes this layer from the research’s fuel-bounded Option B.

Determinism: no non-determinism sources are in scope (no clock/RNG/FS/hash-seed); emitted collections are emitted in canonical (content-sorted) order; fresh names are content-derived (RFD 0037 D3). The drift contract (S2) and tier-honesty (S4) hold because the classifier still runs last on lowered events (RFD 0037 D6) — expansion is upstream of, and invisible to, the trusted re-check.

D5 — The MLT path: an axis-assertion surface

The MLT decorators are not paste and not field-reflection — they read the decorated concept’s name and emit a MetaProperty(subject = the concept, axis = the metarel, value = T). Today that event has no user-writable surface, which is why the expander stays native. The procedural layer’s MLT deliverable is therefore a surface for asserting a metaproperty, e.g.

#[procmacro]
pub fn categorizes(item: Decl, target: Ident) -> Syntax =
    quote { assert std::mlt::categorizes($item.name, $target) }

where assert <metarel>(a, b) is a surface form that lowers to a MetaProperty event. With (a) light decl-reflection ($item.name) and (b) this axis-assertion surface, the four MLT decorators become genuine procedural macros emitting through the ordinary path — and the native lower_relational_mlt_decorators emitter retires. The metaproperty surface is partly independent of the meta-language (it is an event surface question), so it is its own decision here and could land separately.

D6 — Staging: the full layer, built in sequence

The procedural layer ships in full, in three stages built in order — each stage retires real debt, and stage 3 follows stages 1 and 2; no stage is gated on a hypothetical future client. (“Built when a derive-class client forces it” — the framing RFD 0037 D9 and the research used — is a hollow deferral that lets the capability never get built; this RFD rejects it. The capability is the deliverable.)

  1. P1 — paste (concat_idents) + the #[procmacro] surface for non-recursive bodies. Total trivially (no recursion). SHIPPED: retires the #[irreflexive] + #[asymmetric] P0 stub — they become genuine std::rel procedural macros emitting byte-identical events (their arms in synthesize_relation_property_rules are deleted; OE0706 reservation for #[procmacro] is lifted; the new evaluator is oxc-workspace/src/procmacro.rs). #[functional] does NOT migrate in P1: its rel-cardinality-cap arm needs to re-emit a rel with a modified cardinality bracket = structural re-emission = P2, and a single attribute name cannot be half-builtin/half-procmacro — so functional stays a #[builtin] stub (OE1362-gated) until P2. This was the committed first implementation step; it is now built.
  2. The axis-assertion surface (D5). Retires the MLT P0 stub — the four std::mlt decorators become genuine procedural macros.
  3. P2 — full structural recursion over reflected Syntax (D2+D4) — the committed end-state meta-language. Built once stages 1 and 2 are done. It is exercised + proven by writing a real derive-class macro (one that inspects a concept’s fields and synthesizes per-field output — e.g. a #[derive(Reflect)]-style macro) as part of the deliverable, not by waiting for one to “arrive.” This stage also makes expand a total Lean object, unlocking the D7 theorem.

Each stage is surface-stable: a stub-backed #[name] and its procedural-macro implementation have byte-identical lowered events, so migration never changes a program’s meaning (RFD 0037 D8 / S3). RFD 0037 D9 deferred this layer “until a forcing client”; this RFD supersedes that — the full layer is built in sequence.

D7 — The Lean line: hold the AST boundary; pursue tier-honesty when expand is a Lean object

Per RFD 0037 D7 and the research (track-F: Lean 4, CompCert, translation-validation all mechanize the re-checker, not the producer), the expander is untrusted by design. Assurance is the downstream re-check: the tier classifier runs last, the .oxbin content hash is re-derivable, and the drift gate covers the MacroAtom = declarative MacroDecl | procedural ProcMacroDecl shape plus the Syntax carrier (D2).

What changes with this RFD: the total meta-language makes expand a total function over an inductive Syntax type — the precondition for stating the tier-honesty commutation theorem (classify ∘ expand lands a program on the same decidability class as its expansion). RFD 0037 named this “the one worth pursuing.” This RFD commits: when P2 lands, expand becomes a Lean object and the tier-honesty theorem is the rung-(c) deliverable — not a full MacroExpansion.lean expansion-preservation proof (rung d, CakeML-scale, out), not hygiene-algebra mechanization (rung b, POPLmark-scale, out). The totality discipline (D4) is precisely what keeps that theorem statable and the substrate honest.

D8 — Conservative monotonicity: a macro may only raise the tier

A macro’s expansion may land its program at a higher decidability tier than the source spelled, never silently lower it (the program is classified last, on the truth). Sound tier-lowering desugarings — where a macro provably produces a lower-tier equivalent — are a later, proof-carrying escape hatch, out of this RFD. This keeps S4 a structural guarantee during expansion, not a per-macro audit.

Migration

  • P1 landedstd::rel’s #[irreflexive] and #[asymmetric] are now genuine #[procmacro] pub fn … -> Syntaxs: each re-emits the decorated declaration ($item), projects the relation’s name and endpoint types ($item.name, $item.params[i].ty), and pastes a guarding pub check whose head is concat_idents("__", item.name, "_{prop}"). Their arms in the native synthesize_relation_property_rules are deleted, and their import gate shifted from OE1362 to OE0705 (a bare unimported #[irreflexive] is now an unknown attribute macro, exactly like transitive). The byte-identical differential test (relation_property_rehome.rs) now asserts the procedural macro’s events equal the hand-written expansion. #[functional] is deferred to P2 and stays the #[builtin] stub (OE1362-gated): its rel-cardinality-cap arm needs structural re-emission, and an attribute is builtin or procmacro, not both.
  • Axis-surface lands → rewrite the four std::mlt decorators as #[procmacro]s; delete lower_relational_mlt_decorators. Closes #483 genuinely (not as a surface stub).
  • No source migration for users: #[transitive], #[irreflexive], #[categorizes(T)] are spelled identically before and after; only the import (use std::{rel,mlt}::{…}, already required post-RFD-0038) and the byte-identical events are observable. S3 holds at every step.

Acceptance tests

  1. P1 retires the checks’ stub (byte-identical): #[irreflexive] and #[asymmetric] are re-expressed as #[procmacro]s — each re-emits the decorated declaration ($item) and pastes a guarding pub check whose head is built by concat_idents("__", item.name, "_{prop}") — and produce events byte-identical to the former synthesis (relation_property_rehome.rs); their arms in synthesize_relation_property_rules are deleted. #[functional] stays a #[builtin] stub, not a procmacro: it is one attribute that covers BOTH a metarel-check (paste-able) AND a rel-cardinality-cap [0..1] on the target endpoint (lower.rs, the cap arm) — and re-emitting a rel with a modified cardinality bracket is structural reflection, which is P2. Because an attribute name is either a builtin or a procmacro (not both halves split across the two), functional stays entirely a #[builtin] stub (both the rel-cap and the metarel-check synthesized natively, OE1362 import-gate retained) until P2 builds structural re-emission. So P1 retires two of the three checks byte-identically; the third is deferred because the capability (structural re-emission) is unbuilt — not for want of a client.
  2. Totality is enforced, not trusted: a #[procmacro] whose body recurses non-structurally is refused at declaration (the new totality OE07xx), with no fuel-cap fallback masking it.
  3. A genuine derive-class macro (P2): a real procedural macro that inspects a concept’s fields and synthesizes per-field output — written as part of the P2 deliverable — expands end-to-end (parse → EXPAND → resolve → check → instantiate → classify), clears the §3.4 gate, and lands on its true tier.
  4. MLT genuinely re-homed: the four MLT decorators are #[procmacro]s emitting via the axis-assertion surface; lower_relational_mlt_decorators is deleted; events byte-identical.

Lean obligations

  • Extend the @[language_interface] drift coverage to the reflected Syntax carrier (D2), so the Rust Syntax reflection type and the Lean Syntax inductive stay aligned by the existing gate.
  • No MacroExpansion.lean in P1/P2-surface. When P2’s expand becomes a total Lean function, schedule the tier-honesty commutation theorem (rung c) — the distinctive Argon invariant — as the macro layer’s first mechanized theorem (D7).

Spec reconciliation (reference edits)

  • §13.5 (“Procedural macros (deferred)”) → describe this design: the #[procmacro] pub fn surface, the total meta-language, reflection, paste, the axis-surface, and the full staged build (P1 → axis-surface → P2, in sequence). Lift “deferred” to “built (P1 first)” — drop the “first derive-class client” trigger.
  • §13.8 (Lean correspondence) → add the Syntax carrier + the tier-honesty theorem schedule.

Resolved (this RFD)

  • The procedural model is the total meta-language (RFD 0037 D9), not the research’s fuel-bounded bootstrapped-Argon (D4 R1) — committed and now designed.
  • Totality is by construction (structural recursion, checked at declaration), not fuel-bounded (D4).
  • No client-gating: the full layer is built in sequence (P1 → axis-surface → P2); P2 is built once 1 and 2 are done, not “when a derive-class client forces it.” This supersedes RFD 0037 D9’s forcing-client trigger (a hollow deferral) (D6).
  • P1 (paste) is built — it retires the #[irreflexive] + #[asymmetric] stubs byte-identically; #[functional] stays a #[builtin] stub pending P2 structural re-emission (D6, Acceptance test 1).
  • quote’s ${expr} antiquotation is expansion-time. The in-string ${…} form inside a quote { … } body (e.g. the diagnostic message "relation ${item.name} is #[irreflexive] …") is resolved by the macro renderer at EXPAND, substituting the reflected/let-bound value into the rendered surface text before re-parse. It needs no runtime string concatenation — it is a render-time splice, total and deterministic.

Open questions

  1. Paste × hygiene: the precise hygiene treatment of a concat_idents result — raw definition-site name vs reserved-marker freshening — per use (D3). The three checks want a stable public-ish __{rel}_{prop}; a general paste in user code may want freshening.
  2. The axis-assertion surface shape (D5): assert categorizes(a, b) vs a metaproperty-bearing declaration form — an event-surface question that may warrant its own short RFD.
  3. Reflection accessor API for P2: the precise Decl/Syntax accessor set — settled when building P2 as a complete reflection of a declaration’s structure (name, kind, generics, parameters, fields, attributes), not a minimal subset and not deferred to a client.
  4. ${} spelling-overlap with runtime string interpolation. P1’s quote antiquotation ${expr} is an expansion-time render splice (resolved above, Resolved). It is spelled the same as a hypothetical runtime string-interpolation form "…${x}…" that would lower to string concatenation and const-fold — but that is a separate, unbuilt foundational arc (tracked as issue #575): the runtime today has no Text + Text operator and no const-fold pass, so general runtime interpolation does not exist. Do not assume the two ${…} spellings unify — whether the render-time antiquotation and a future runtime interpolation should share a surface (or must be kept distinct, since one is total-by-construction at EXPAND and the other is value-level) is an open question for the #575 arc, not settled here.
  5. elab-class (type-directed) expansion — firing a macro over the expected metatype/sort — is a distinct mechanism (type-direction), not part of the procedural layer (computing an expansion). It stays out of this RFD’s scope; the real ergonomic need it was floated for (legal scoping) is already served by binding-space resolution. If type-direction is wanted, it is its own design, not a deferred piece of this one.

RFD 0042 — The re-checkable emission boundary: a self-validating .oxbin and sound direct artifact emission

  • State: discussion
  • Depends on: RFD 0037 (the macro atom — this amends D2), RFD 0040 (the procedural macro layer — this extends it, and revises its shipped P1 surface), RFD 0036 (heterogeneous stores — the MappingArtifact build-section gate), RFD 0031 (relation-constraint plane — cardinality), RFD 0027 (meta-property plane), RFD 0009 (MLT)
  • Supersedes: RFD 0041 (pub metafact — a surface-per-metadata-kind fix; this RFD makes it unnecessary). RFD 0040 stage-2’s MLT axis-assertion surface became RFD 0041; with 0041 superseded, MLT re-homes through this RFD’s emission gate, not a metafact surface.
  • Blocks: the genuine, builtin-free re-home of the MLT decorators (#483); a library foreign! (RFD 0036 placement)

Question

The macro atom (RFD 0037 D2) committed that a macro expands to surface syntax only, never to events — so everything a macro produces is re-checked by the one trusted parse→resolve→check→instantiate→classify path. That is sound, but it has a cost the campaign made concrete: any vocabulary whose effect has no surface form must be implemented as a privileged compiler builtin (a doctrine violation — the core ships vocabulary) or be granted a new substrate surface per metadata-kind (accretion; libraries gated on compiler changes). Both are paying with the wrong currency. The MLT decorators (→ MetaProperty, no surface), the relation-property checks (→ a pasted __{rel}_{prop} head), and foreign! (→ a .oxbin MappingArtifact, RFD 0036) are all stuck on this. Can a macro emit a substrate artifact directly — keeping D2’s safety — without a builtin, an unsafe escape, or surface-accretion?

Context

D2’s deepest content (RFD 0037 D7) is the CompCert / Ullrich-&-de-Moura posture: the expander is untrusted; assurance comes from re-checking its output. D2 then made one move too many — it conflated re-checked with re-parsed, and required output to be surface. But the trust comes from the re-check, not from the surface provenance: if the substrate re-checks an emitted artifact as thoroughly as a lowered one, direct emission is exactly as safe.

A two-round adversarial review (2026-06-19), verified against the merged substrate, established the load-bearing facts:

  1. Today’s substrate does NOT re-check emitted events. classify runs only on RuleDecl bodies; resolve validates names; encode validates nothing. The load-bearing event validations live in elaboration pre-passes / lower.rs / the runtime write-path — not over the event stream. A directly-emitted event enters the runtime through ingest_locked/seed_from (an infallible index insert), a separate ingress from the write-path (execute_mutation/apply_operation). So a hand-built or macro-emitted RelationTuple smuggles past endpoint-existence (OE0232), arity/endpoint-type (OE0221/OE0222) and max-cardinality (OE1341); a MetaProperty smuggles past axis domain/literal/refinement (OE0622OE0624); a property/iof assertion past refinement (OE0668), required-field (OE0207), value-type (OE0236) and iof-on-defined (OE0211). The lone exceptions are three hand-added load-time backstops — IofAssertion abstract-target (OE0233), axis same-precedence (OE0629), and property-id injectivity (OE0231) — one of whose comments says “a hand-built artifact must not bypass the elaborator’s gate.” So direct emission is unsound on the substrate as it stands, and the gap is broader than first thought: the authors plugged three holes and the rest remain open. This latent debt exists independently of macros — any corrupt, hand-built, or foreign .oxbin already bypasses these checks today.

  2. There is a third output kind. A MappingArtifact (placement) is a .oxbin section (id 11), deliberately versioned independently of the schema (RFD 0036 D6) and explicitly forbidden from carrying axiom-event semantics (RFD 0036 D1). It is neither quote!-able surface nor an AxiomEvent. So {surface | event} is not exhaustive. Crucially, build-sections have a different and simpler soundness story than events (see D4): the trusted production path is config-driven, and a macro can feed the identical struct into the identical sink — parity, not a new gate.

  3. The event re-check set is bounded but larger than a first pass suggested — ~11–12 event-derivable load-bearing validations, not “5–6”. The census (verified file:line in the review record) is:

    • Relation tuples: endpoint-existence OE0232, arity OE0221, endpoint-type OE0222, max-cardinality OE1341 (incl. the #[functional] [0..1] rel-cap, same mechanism).
    • Meta-properties: axis-value-in-domain OE0622, axis-literal-type/refinement OE0623, axis-tier OE0624.
    • Iof / individuals: abstract-target OE0233 (already at load), iof-on-defined OE0211, fixed-reclassification OE0234 (see Fork A — needs an encoding discriminator), property-id injectivity OE0231 (already at load).
    • Construct/property: refinement invariant OE0668, required-field OE0207, value-type OE0236.
    • Axis precedence: OE0629 (already at load).

    Two clarifications the first pass got wrong:

    • The relation-property checks OE1359 (irreflexive) / OE1360 (asymmetric) are NOT in this set. They are enforced by the check rules the macros emit (surface, re-parsed), which run over the relation’s full extent regardless of how a tuple entered — so they self-enforce and need no load-time pass.
    • The remaining validations (arg-shape, match exhaustiveness, defeasibility well-formedness OE0716OE0721, temporal-qualifier forms, in-body construction completeness, the §3.4 introducer-resolution gate, relation-subsumption OE0150OE0154, metarel endpoint-metatype OE0631) gate surface declarations and stay on the lowering path — macros produce those via quote!. A handful (OE0631, OE0150OE0154, OE0222 endpoint-category) are load-bearing but not event-derivable; they bound what is emittable as an event (such artifacts stay quote!-surface).
  4. The .oxbin validation framework exists but is unwired and mis-shaped for this. Layer2Invariants (oxc-oxbin/src/validation.rs) is six always_ok() stubs (symbol_resolution, lattice_acyclicity, provenance_well_formed, composition_consistency, tier_consistency, doc_links_resolve), drift-paired Rust↔Lean (Argon/BuildArtifact/Validation.lean). None of the six slots maps onto the census checks (the near-misses are false friends: symbol_resolution ≠ “endpoint exists as an individual”; tier_consistency = “rule tier ≤ envelope” ≠ “axis binds at the target’s tier”). And Module::load never calls the framework — its sole caller is oxc-serve, passing the stubs. So this RFD does not “fill stubs”: it adds net-new predicates to the framework, wires the framework into Module::load, and thereby grows the trusted base (and the Rust↔Lean drift surface). That is the honest characterisation, and it is fine — see the Rationale.

So the missing piece is not a new trust model — it is making the substrate re-check what it already should: a self-validating .oxbin.

Decision

D1 — The re-checkable emission boundary

A procedural macro may emit, in one expansion, any mix of:

  • Syntax — built with quote!, re-parsed and lowered through the ordinary path (RFD 0037 D2’s mechanism, retained);
  • typed substrate artifacts for which the substrate has a complete re-check gate over the artifact itselfAxiomEvents (the event-stream gate, D3) and build-sections like MappingArtifact (its existing gate, D4).

An artifact-kind with no complete gate is not directly emittable — it must be produced as Syntax (via quote!). There is no unsafe escape: emission is sound by construction because the gate re-checks every emitted artifact to the same standard a lowered one faces. The restriction is not “flat events only” but “only what the substrate fully re-checks.”

The canonical example of the not-directly-emittable case is a raw IofAssertion: its OE0234 fixed-reclassification check is not event-derivable under the current encoding (Fork A), so iof is emitted as Syntax until the discriminator lands. No current client needs to emit a raw iof event (MLT emits MetaProperty; foreign! emits MappingArtifact; the relation-property family emits check rules), so this costs nothing today — and the boundary stays honest rather than pretending the gate is complete when it is not.

D2 — Amend RFD 0037 D2

Lift D2 from “a macro expands to surface syntax, never to events” to: “a macro expands to surface syntax or to re-checked substrate artifacts.” The invariants D2 protected are preserved — not by surface provenance but by the gate (D3/D4): the drift contract (S2 — events are still typed @[language_interface] values), the §3.4 ontology-neutrality discipline (emitted names resolve through the resolver), tier-honesty (S4 — classification runs on emitted events via the gate), and determinism (canonical encoding). The privileged axis-event-emitter carve-out D2/D8 reserved is retired: MLT and the relation-property family become library macros emitting through the gate.

D3 — The self-validating .oxbin (a second trusted checker)

Implement the event-derivable load-bearing validations of Context §3 as .oxbin load-time predicates over the decoded event stream, and wire them into Module::load (which does not call the validation framework today). This is net-new trusted code, not a stub-fill: the existing Layer2Invariants slots do not cover these checks (Context §4), so the framework grows by ~11–12 predicates, drift-paired into Lean (D7).

Reading: whole-module, not single-event. The re-checker validates each event against the fully decoded module (all RelationDecl/MetaxisDecl/ConceptDecl events + the individual set), not against the event’s own body in isolation. Module::load already materialises the entire event stream before any validation could run (decode_eventsVec<AxiomEvent>), so whole-module visibility is available. This is load-bearing: OE0232 needs the workspace individual set; OE0622OE0624 need a join of the MetaProperty event against its MetaxisDecl on axis_id (the domain/targets/tier live on the decl, not the property) — so OE0624’s tier is event-derivable via that join, resolving the earlier open question.

Architecture — shared cores where they exist, fresh load checks where they don’t. Two shapes, do not pretend they are one:

  • AST-shaped checks with an extractable core (OE0232, OE0622, OE0623, and the value/refinement family OE0668/OE0207/OE0236): extract the predicate core keyed on resolved ids/values, behind two thin adapters — an AST-time adapter (lowering, behavior unchanged) and an event-time adapter (load). The two MUST NOT diverge on the decision (see comparability below).
  • Set-level aggregations with no AST core (OE1341 max-cardinality): lowering only records the [lo..hi] bracket; the actual check is a stateful runtime tuple-count over scan_live, per (tenant, fork). There is no AST predicate to factor — the load form is a fresh whole-event-stream aggregation, partitioned by (tenant, fork). Build it as such; do not force it into the two-adapter mould.

Comparability — decision-agreement, not byte-identical diagnostics. The two checkers cannot emit byte-identical diagnostics: the AST-time error carries a source offset and source-level names; the event-time error has no source span and names individuals by interned #i ids. The honest, enforceable contract is decision-agreement: on the same logical violation, both emit the same diagnostic code (and the closest available message). Golden tests pin agreement at the decision level (code fires / does not fire on a corpus of matched cases). This is weaker than byte-identity — it cannot catch message/hint drift or pin spans — and the RFD says so plainly rather than overclaiming.

Independent value (the clincher): this makes every .oxbin self-validating against corrupt, hand-built, foreign, or macro-emitted artifacts — paying down latent soundness debt that exists today regardless of macros (Context §1). So D3 is a substrate-trust feature macros happen to need, and ships first on its own merits. (Note: min-cardinality OW1342 is recorded-but-unenforced today even in lowering; D3 may close it at load, but it is out of the load-bearing soundness set and tracked separately.)

D4 — Build-sections re-check by their own gate (the third kind)

A MappingArtifact is re-checked by validate_composition_signature (OE1213, which re-derives the composition signature from the event legs and anchors the mapping’s pin to the actual elaborated wiring) + check_against_schema (OE1245), both already invoked at load. A macro emitting a build-section is admitted iff that kind’s gate runs.

The soundness story for build-sections is parity, not a new gate. The trusted production path for a MappingArtifact is config-driven (ox.toml [store]/[placement]MappingSectionsoxbin_for_events). A macro that feeds the same PlacementDecl struct into the same MappingSections the config path feeds produces an artifact indistinguishable downstream from a config one → it gets identical compile, pin, and load treatment. The soundness bar is parity with the trusted config path, which holds by construction. (Placement mapping directives are connector-opaque and validated at scan-time for any placement; a macro-emitted one carries no more risk than a config one.) So build-section emission needs no new event-validation (D3) — it rides the existing gate, and is unblocked independently of the self-validating-.oxbin substrate lift.

Phase-ordering protocol (the one real wrinkle). The composition-signature pin is a fixpoint over the entire elaborated event set, computed at build time (oxbin_for_events), after macros run at expand. So a macro cannot and must not supply the pin. The protocol:

  1. Expand: the macro emits an unpinned placement payload — a PlacementDecl (store name + mapping directives) keyed by the logical relation name. Everything in placement_hash() is knowable at expand from the macro’s input; schema_signature is left unset.
  2. Build: the existing MappingSections::compile(composition_signature) stamps the pin over the merged placements — identically to the config path.
  3. Load: OE1213 + OE1245 run unchanged.

Wiring task (the RFD 0036 seam). MappingSections is populated today exclusively by parse_mapping_sections from ox.toml. The emission boundary opens a second source: macro-emitted PlacementDecls, surfaced from the EXPAND phase (D5), unioned into the MappingSections the driver hands to oxbin_for_events. A placement for the same logical relation declared by both config and a macro is a loud refusal (a new OE code — never the silent BTreeMap::insert overwrite). ox.toml [placement] remains supported as an alternative. The macro emits only the PlacementDecl; the StoreDecl (store kind + @deploy: handle — a deployment concern, RFD 0036 C11) stays in ox.toml.

D5 — The emission surface (extends, and revises, RFD 0040)

A procedural macro returns an Expansion — a sequence of emissions, each either a Syntax value or a typed substrate artifact:

#[procmacro]
pub fn foreign(item: Decl, attr: Args) -> Expansion =
    [ quote! { $item },                                            // Syntax → re-parsed (the pure `pub rel`)
      Placement { rel: item.name, store: attr.store, mapping: attr.mapping } ];  // build-section → MappingArtifact gate (D4), unpinned
#[procmacro]
pub fn categorizes(item: Decl, target: Ident) -> Expansion =
    [ quote! { $item },
      MetaProperty { subject: item.name, axis: std::mlt::categorizes, value: target } ];  // event → re-checked at load (D3)
  • quote! builds Syntax; splices ($item, $item.name) interpolate reflected values. A single Syntax auto-lifts to an Expansion, so a surface-only macro keeps returning -> Syntax (the shipped P1 procmacros are unchanged).
  • A typed-constructor literal (MetaProperty { … }, Placement { … }) builds an artifact; its names are symbolic and re-resolved by the gate.
  • No emit keyword (taken by the emit/sink feature) and no unsafe — the macro returns its emissions; soundness is the gate’s job.

This revises RFD 0040’s shipped P1 surface — own it. P1 (#581) ships quote { … } (a brace form), -> Syntax, $item as a substitution barrier, ${expr} antiquotation. This RFD re-specs quote! (a !-invoked builtin, not a keyword — already contextual from P1, consistent with format!), -> Expansion, and $item as a splice. That is a code migration of live surface, the P1 parser recognition (quote_brace_at), the shipped std::rel irreflexive/asymmetric procmacros, and their tests — not a greenfield addition. The migration may ride this RFD’s implementation or land as a focused surface-migration PR first; either way D5 owns that it changes shipped P1, and silently resolves the ${} antiquotation spelling that #575 left open (string interpolation as a runtime feature remains a separate arc, #575).

foreign! surface (Ivan, 2026-06-19): the attribute form #[foreign(...)] ships first; the item form foreign! { … } is a later addition (both spellings eventually). The attribute form requires threading ATTR_ARGS into the macro invocation as a second attr input (Rust’s #[proc_macro_attribute] fn(attr, item) shape) — today rewrite_attribute_macro parses but drops attribute args (expand.rs:236); this is a bounded, contained change. #[foreign] is consistent with the shipped #[transitive]/#[irreflexive] attribute spelling (Argon has no @-decorators; #[name] is the shipped form).

D6 — The soundness contract

An emitted artifact is admitted iff its kind’s re-check gate subjects it to the same validations a lowered/config one faces: names through the resolver; the event-stream validations (D3) for events, or the section gate (D4) for build-sections; tier classification; canonical encoding. The standing invariant that makes “no unsafe” true: no event-derivable load-bearing validation may live only in lowering or only on the write-path. That invariant is currently violated by the substrate itself across the ~8 write-path/lowering-only checks of Context §1 — D3 pays that debt down. It is then kept honest by the one-core/two-adapter discipline plus decision-agreement golden tests, and (Open Q b) a structural CI gate asserting every relocated core has an event-time adapter, at decision-agreement granularity.

D7 — The Lean line

The @[language_interface] event types are already drift-gated. What this RFD adds to the Lean substrate:

  • State the new load-time validations as total predicates over the event model. These are net-new and grow the drift-paired Layer2 struct in both Rust and Lean — there is no existing slot to reuse (Context §4).
  • Prove the bridge lemma “a lowered event and a directly-emitted event satisfy the same validation predicate.” For the AST-shaped checks this is the formal content of “one core, two adapters.” For OE1341 the lemma is over an event set (a partitioned aggregation with order-insensitive count), not per-event — state it at that granularity; the per-event form does not type-check against the actual check.
  • Correction to the draft’s earlier claim: there is today no Lean model of fact/metaproperty → event lowering (CoreIR/Lowering.lean proves only structural count-preservation; full term lowering is stubbed). So the bridge lemma rests on net-new Lean infra, not an existing lowering model. The event-side predicate scaffolding (AxisBindingValid.lean) exists and is reusable.
  • RFD 0040 D7’s tier-honesty theorem (classify ∘ expand) is unaffected (classification already runs on emitted events through the gate) and remains gated on P2 making expand a Lean object. No expansion-preservation proof is owed (RFD 0037 D7 still holds: mechanize the re-checker, not the producer — D3 is the re-checker, now total and emission-agnostic).

Fork A — OE0234 fixed-reclassification: quote! now, the discriminator later (committed)

OE0234 rejects a re-classification — an iof that is not the construction site — onto a fixed type. The distinguishing information is the operation variant (Construct vs InsertIof), which is erased at emit: both paths call the identical encoder, and IofAssertionBody = {concept_id, individual_id} carries no construction-site marker. So a load pass over the flat event stream cannot re-check OE0234 under the current encoding.

Resolution (Ivan, 2026-06-19): for now, iof stays Syntax-only-emittable (macros quote! it; the elaborator, which has the operation variant, checks OE0234). This is principled per D1 (an artifact whose gate is incomplete is produced as Syntax) and costs nothing today (no client needs raw iof emission).

This is a committed deferral with a real prerequisite, NOT “no client.” We intend to implement the full construction-site discriminator — a wire-format change so construction and reclassification encode (and content-hash) differently, drift-gated — if it is the correct choice, which makes raw iof directly-emittable and OE0234 event-derivable. The capability is genuinely gated on that unbuilt encoding, not on a hypothetical future client. Tracked as real scope.

Rationale

The design pays in the right currency. D2’s safety came from re-checking, not from surface; we keep the re-check and drop the incidental surface requirement — and in doing so we are forced to make the re-check actually run on artifacts (the self-validating .oxbin), which is independently the correct hardening of a content-addressed artifact format whose soundness debt the review made concrete. The alternative currencies are wrong: a builtin spends ontology-neutrality; a surface-per-kind spends substrate minimality and gates libraries on compiler work; an unsafe escape spends soundness (the review showed it would genuinely be unsound, so the escape would be load-bearing, not cosmetic). “Restrict to what the substrate fully re-checks, and make the substrate re-check fully” is the only option that spends nothing — and it grows the trusted base honestly (D3 is a second trusted checker, declared as such), which is the correct place to spend, because a re-checker is exactly what the CompCert posture asks us to trust.

The scope is larger than a first pass claimed (~11–12 event checks, a wire-format discriminator owed for iof, the Layer2/Lean drift surface grows). That is not the design weakening — it is the review revealing pre-existing soundness debt that this work pays down. Build-sections (foreign!) need none of it and ship first.

Alternatives

  • RFD 0041 (pub metafact surface). Sound (it lowers through the validating path) and neutral, but it is the surface-per-metadata-kind accretion: MLT needs metafact, foreign! needs a placement form, the next vocabulary needs the next form — libraries perpetually gated on compiler surface work. Superseded.
  • unsafe direct emission. Cheap, neutral, no accretion — but a genuine trust-waiver: the review proved a directly-emitted event bypasses real checks, so unsafe would be load-bearing, and a content-addressed .oxbin would still be un-self-validating against corrupt input. Rejected in favor of fixing the substrate.
  • Privileged builtins (status quo). The doctrine violation this whole arc exists to kill.

Consequences

  • foreign! becomes a library macro (attribute form #[foreign(...)] first, item form later) emitting a MappingArtifact placement through the build-section gate (D4). Unblocked now — it needs only the Expansion surface (D5), the MappingSections union + collision OE code, and the #[foreign] attr-args plumbing; no Layer2 work. This is the first client and the proof of the mixed-emission boundary.
  • MLT (#[categorizes(T)] …) and #[order(N)] become library #[procmacro]s emitting MetaProperty; lower_relational_mlt_decorators / lower_order_decorator are deleted; #483 closes genuinely. Added scope the review surfaced: this depends on (a) the MetaProperty event checks OE0622OE0624 at load (D3), and (b) building the MLT concept-membership semantics OE190x/OE1904 (partition disjointness, order) which are currently reserved/unbuilt even in lowering — so “MLT emits MetaProperty” is bigger than deleting the builtin. Also resolve #517 (MLT metarel signatures disagree across book/std::mlt/Lean) during the re-home.
  • The relation-property family keeps its current re-home: #[irreflexive]/#[asymmetric] are already genuine procmacros emitting check rules (self-enforcing, no Layer2); #[functional] on a rel stays a #[builtin] stub until P2 (its [0..1] cardinality-cap needs structural re-emission); the metarel-functional check is already re-homed.
  • .oxbin becomes self-validating at load against any malformed artifact — a robustness win beyond macros, paying down the Context §1 debt.
  • No user-source migration anywhere: #[categorizes(T)] / #[foreign(...)] are spelled identically; only the (already-required) imports and the byte-identical/parity artifacts are observable (RFD 0037 S3).
  • #587 / RFD 0041 closes as superseded (done).

Implementation sequence:

  1. The Expansion/emission surface (D5) in the procmacro evaluator (procmacro.rs): the Expansion return type + auto-lift, the EXPAND Vec<EmittedArtifact> buffer, surfaced from expand_package_workspace. Includes the quote{}quote! P1 migration (or a preceding focused PR).
  2. foreign! (build-section track) — in parallel, no Layer2 dependency: the Placement constructor in procmacro.rs, the MappingSections union + collision OE, the #[foreign] attr-args plumbing. (Heterostore team owns the connector + MappingSections seam; macro side owns the emission buffer.)
  3. The self-validating .oxbin (D3, substrate-trust track, ships on its own merit): the ~11–12 event predicates, wired into Module::load; the Layer2 framework grown + drift-paired; decision-agreement golden tests.
  4. MLT re-home (#483) — needs (1)+(3) plus building OE190x/OE1904; delete the builtins.
  5. The Lean predicates + bridge lemmas (D7), including the set-level OE1341 lemma.
  6. (committed, later) the iof construction-site discriminator (Fork A) → raw iof becomes directly-emittable.

Open questions

a. OE0624 (axis tier). Resolved — event-derivable via the MetaPropertyMetaxisDecl join on axis_id (the tier lives in MetaxisDeclBody.targets). Confirm the join is reliable at load. b. Enforcing the D6 invariant structurally. Make “every relocated predicate core has an event-time adapter” a drift/CI gate (at decision-agreement granularity), so a future lowering-only check can’t silently re-open the hole. c. Expansion ergonomics. The exact type for the mixed-emission return (heterogeneous list vs a named Expansion builder) and the single-Syntax auto-lift surface. d. fixed-reclassification (OE0234). Resolved by Fork Aquote!-only for now; the construction-site discriminator is committed scope. e. Which other build-sections become emittable (DocBlocks, IndividualNames) — each only when it carries a re-check gate (D4). Per the census, most .oxbin sections have no load gate today (not even byte-integrity recompute), so they stay non-emittable until one is added. f. OE1341 partitioning. Confirm the load aggregation partitions per (tenant, fork) exactly as the write-path does, to avoid false rejections.

RFD 0043 — Theory packages and the neutrality boundary: where ontologies and higher-order theories live

  • State: discussion
  • Depends on: RFD 0030 (package dependencies — path deps, the on-ramp), RFD 0027 (meta-property plane — the neutral axis machinery a theory rides), RFD 0042 (the re-checkable emission boundary — the event-emission gate a theory’s decorators emit through), RFD 0037 / 0040 (the macro atom + procedural layer — the authoring surface), RFD 0009 (MLT — the first theory re-homed)
  • Amends: RFD 0009 (relocates MLT out of std to a first-party package) · RFD 0042 (realizes its “blocks the genuine, builtin-free re-home of the MLT decorators” clause)
  • Blocks: the genuine re-home of the MLT decorators (#483); ArgUFO authorable end-to-end (milestone #8)

Question

Where do higher-order type theories and foundational ontologies live, and what may std contain?

Today the asymmetry is the tell: UFO is an external package (correct — the substrate ships no ontological category), but MLT is embedded in std and expanded by privileged compiler builtins — ambient theory vocabulary at four layers. MLT and UFO are the same kind of thing: a committed theory the substrate is deliberately neutral about. The doctrine already says “no ambient ontology vocabulary” and “higher-order theories ship as libraries” — but those are two rules without one operational test, and the MLT leak slipped through the gap between them.

So: what is the single test that decides whether a thing belongs in the substrate, in std, or in a package — and where, concretely, does a theory like MLT live?

Context

The leak surface runs four layers. MLT-specific vocabulary is wired into:

  1. the compiler directive registry — categorizes et al. registered as known directives with hard-coded arg-shapes (oxc-instantiate/src/directives.rs);
  2. the lowering path — lower_relational_mlt_decorators / lower_order_decorator natively emit MetaProperty (oxc-instantiate/src/lower.rs);
  3. std itself — std/mlt/ is include_str!-embedded and elaborated in a fixed order after std::core (oxc-instantiate/src/lower.rs);
  4. the diagnostic registry — OE1903–OE1907 reserved for MLT’s specific violations (oxc-diagnostics, oxc-syntax/grammar.toml);
  5. the substrate mechanization — MetaCalculus/MLTKinds.lean names categorizes/partitions/subordinates carriers (its Wellformed.lean predicates are already parametric/neutral — the names are the residue).

The neutral substrate the theories need already exists. RFD 0027 gives axes the compiler never interprets; the meta-calculus exposes a generic metarel carrier, iof/specializes, reflection, and MetaProperty events; RFD 0042 adds (in progress) the re-checked event-emission gate a decorator emits through. None of this names a theory.

The packaging on-ramp already exists. RFD 0030 shipped [dependencies] name = { path = "…" } end-to-end — transitive closure, cycle/collision diagnostics, package-qualified module paths, a two-package acceptance test. A non-std package on disk is buildable and dependable today; the include_str! embed is how std is distributed, not a constraint on path deps. (Versioning, a registry, a lockfile, [workspace], and ox new/add/publish do not yet exist — that is a separate cargo-parity arc, below, and it does not block this RFD.)

Decision

D1 — The neutrality test

A capability belongs in the substrate or std only if it passes the reflect-not-smuggle test:

std (and the substrate) may reflect a commitment the substrate has already made. It may not smuggle a commitment the substrate deliberately withholds.

The competing-theory rule is its corollary: a competing formalization exists exactly where the substrate withheld commitment, so if multiple credible theories occupy the same layer, none of them belongs in std — shipping one privileges a theory.

Applied to the current std set (each verified against its header / the substrate it rides):

PackageVerdictWhy
std::corereflectthe substrate’s own classification floor (Top/Bot, primordials)
std::relreflectconventions over the relation mechanism; transitivity has no competing theory
std::kripkereflectthe substrate committed to Kripke frames (Decidability/Modal.lean, Standpoint/, Locality/SheafEquivalence.lean); box/diamond are substrate operators
std::finreflectconventions (currency identity, rounding-mode vocabulary) over the substrate-owned exact Money/Decimal tower; no competing arithmetic
std::datetime / std::path / std::temporal / std::storereflectutility/convention over substrate primitives
std::mltsmuggle → OUTthe substrate is neutral on multi-level theory; MLT competes with potency (Atkinson–Kühne), ML2, powertype (Cardelli–Odell)

The same verdict puts UFO, BFO, DOLCE, potency, and ML2 in packages — none in std, none built-in.

D2 — Three layers

  • Substrate (intrinsic, inert, neutral): the meta-calculus — a generic metarel carrier, axes the compiler never interprets (including a neutral ordinal axis a level-theory rides), iof/specializes, reflection, MetaProperty events, the partition/disjointness mechanism, and the RFD 0042 re-checked event-emission boundary. The substrate names no theory.
  • std (neutral, non-theory utilities): everything that passes D1. No higher-order type theory, no foundational ontology.
  • Packages (theories and ontologies, none privileged): MLT, UFO, BFO, potency, ML2. Each declares its own metarels, authors its decorators as library #[procmacro]s emitting MetaProperty through the RFD 0042 gate, expresses its semantics as derive/check rules, and emits its own diagnostics via RFD 0025 check-discharge.

There is no “higher-order type utilities” tier in std: anything generic enough to be theory-neutral already is the substrate (generic metarel + ordinal axis); anything more specific smuggles a theory’s commitments.

D3 — The packages/ directory

First-party libraries that are not std live in a new top-level packages/ directory (this RFD authorizes the new top-level directory). A package there is an ordinary ox package with its own ox.toml, depended on as name = { path = "packages/name" } and imported as use name::…. packages/ is visibly distinct from examples/: packages/ holds publishable libraries; examples/ holds demonstrations. The future registry’s naming aligns with packages/ (the three-layer story maps to substrate / std / registry, ≈ language / stdlib / crates.io).

D4 — MLT re-homes to packages/mlt as an unprivileged package

Delete the four-layer privilege (D1 context items 1–5): the directive-registry entries, the lower_* builtins, the std/mlt/ embed + its fixed elaboration order, the OE1903–OE1907 codes, and the MLT names in MLTKinds.lean (leaving the neutral parametric mechanism). packages/mlt declares its metarels, authors #[categorizes(T)] et al. as #[procmacro]s emitting MetaProperty, and emits its own diagnostics via RFD 0025. User source is unchanged — #[categorizes(T)] is spelled identically; only the (already-required) import and the now-unprivileged provenance differ.

The re-home is two-phase, because its two halves have different dependencies:

  • Phase 1 — emission/classification neutrality (independent; do now). The RFD 0042 EmittedArtifact::Event boundary + routing an emitted MetaProperty into the event stream where the shipped D3 load re-checks (OE0622–OE0624) gate it; delete the privilege; the packages/mlt skeleton (metarels + #[procmacro]s). This closes #483’s emission scope by construction and touches none of the reasoning fixpoint — its files are the emission boundary, the lowering-deletion sites, the diagnostic registry, and packages/; not oxc-reasoning’s disjointness/classifier semantics.
  • Phase 2 — reasoning-time function (converges with the substrate keystone). MLT functioning: the declarable-disjointness primitive (the OE1904-Reserved mechanism, downstream of the CWA/OWA write-side ruling), the ordinal axis, and the reflective #[static] checks that express partitions ⇒ disjoint + cover and the order arithmetic. These are not macro work — they are the shared substrate keystone that also makes ArgUFO authorable (milestone #8). They are designed once, in the keystone arc’s own RFD and tracker (see Consequences), and consumed by packages/mlt. The full acceptance test (D6) is met only here.

D5 — Package soundness-proof home

A package’s soundness proofs travel with the package, and must not pollute the substrate’s proof corpus:

  • In-repo first-party packages home their Lean under spec/lean/Packages/<Name>/ — in the same lake build (so they are CI-gated and held to the same no-sorry / no-uncited-axiom discipline as the substrate), but namespaced out of Argon/ so the Argon/↔Rust @[language_interface] drift gate and the substrate theorem corpus stay pure. packages/mlt’s categorizes ⇒ order+1 / partitions ⇒ disjoint+cover soundness lives at spec/lean/Packages/Mlt/, proved against the neutral substrate it imports.
  • External packages carry their proofs in their own repository, importing the substrate Lean as a lake dependency.

This sets the precedent for every theory package (UFO, BFO, potency).

D6 — Acceptance: an unprivileged package authors a full type-theory

The neutrality proof is that a package with zero compiler privilege authors a complete type-theory:

  • Phase 1: packages/mlt’s #[procmacro] decorators expand end-to-end, emit MetaProperty events that are re-checked at load by the substrate’s own gate, classify on their true tier, and round-trip an .oxbin — with no builtin, no std embed, no reserved diagnostic code. The criterion is parity, not byte-identity: the metarel path moves (std::mlt::categorizesmlt::categorizes), so the emitted event’s axis_id interns a different string and cannot be byte-identical to the old builtin output (unlike the relation-property re-home, which stayed at std::rel). Parity = the semantically-equivalent re-checkable event (correct axis metarel, target, value), load re-check clean, .oxbin round-trip.
  • Phase 2: packages/mlt’s #[static] checks enforce its well-formedness (disjointness, order) at reasoning time over the neutral substrate primitives, emitting its own diagnostics.

Rationale

Why reflect-not-smuggle over competing-theory alone. “No competing theory” is true but secondary — it explains why a layer is neutral (a competitor exists precisely because the substrate withheld commitment) but does not, on its own, justify keeping std::kripke (modal logic has competing semantics — neighborhood, algebraic, topological). The primary fact is that the substrate already committed to Kripke frames in its mechanized modal/standpoint semantics; std::kripke reflects that commitment honestly. The test has to be about what the substrate committed to, with competing-theory as the diagnostic for “did it withhold here?”. That ordering is what makes “keep kripke, drop mlt” a consequence of doctrine rather than a judgment call.

Why packages/, not std and not external-only. std-membership is the privilege we are removing. External-only (like UFO today) loses the in-CI neutrality proof. An in-repo first-party package gets both: zero privilege and a CI-gated proof that the substrate is neutral — co-evolving with the substrate work that unblocks it.

Why two-phase. The emission re-home is genuinely independent of the reasoning substrate; gating it on the keystone would stall a clean, provable win behind a multi-session research arc. Splitting also exposes the real shape: MLT is a second forcing function for the keystone (alongside ArgUFO), not the owner of it. One disjointness primitive, designed in the keystone, consumed by every theory — never a parallel mechanism invented inside the macro arc.

Why package proofs under spec/lean/Packages/. The substrate’s quality bar (no sorry, drift-gated, one lake build) is worth extending to first-party theory proofs, but the substrate’s purity (the Argon/↔Rust contract, the neutral theorem corpus) must not absorb theory-specific lemmas. A sibling namespace in the same build is the only option that keeps both.

Consequences

  • std is purged of theory vocabulary. MLT moves to packages/mlt; UFO/BFO/potency/ML2 follow the same path. std::kripke/std::fin/std::rel/std::core stay, justified by D1. (std::kripke’s “elaborate after std::core and std::mlt” wiring simplifies to “after std::core”.)
  • #483 closes its emission scope by construction (Phase 1) — the strongest neutrality statement we can make: an unprivileged package authors decorators that emit re-checked substrate events with no compiler involvement. #517 (the MLT signature disagreement) becomes a package-internal naming choice aligned to the neutral substrate.
  • The reasoning-time keystone (Phase 2) is owned by the substrate keystone arc, not this RFD. The declarable-disjointness primitive (CWA/OWA write-side ruling), the reflective #[static]-check plane, and the set-valued metaxis work are one coordinated substrate cluster currently spread across the macros, tiago-meta, and design-review threads, all editing oxc-reasoning / the classifier / the meta-plane. They share one design and one tracker (a converged keystone epic), with packages/mlt Phase 2 and ArgUFO as its two independent forcing functions. This RFD references that arc; it does not design it.
  • The package registry / ox publish / lockfile / [workspace] / ox new/add is a distinct cargo-parity arc. Path deps (RFD 0030) already carry the re-home. MLT is the forcing function for “the first published package” once the registry lands; it does not block here.
  • D5 sets the package-proof precedent for every future theory package.

Alternatives considered

  • Keep MLT in std, just delete the builtins (library-surface-stub + privileged-native-expander). Rejected: relocates the surface while the privileged expander stays — a hollow re-home, and std-membership is itself the privilege. RFD 0042 already commits to the genuine version.
  • MLT external-only, like UFO. Workable, but forfeits the in-CI neutrality proof and the co-evolution with the substrate work. In-repo first-party (packages/) dominates.
  • A neutral “higher-order type utilities” tier in std. Rejected: any such util generic enough to be neutral already is the substrate; anything more specific smuggles a theory (see D2).

RFD 0044 — Package registry, workspaces, and distribution

  • State: discussion
  • Depends on: RFD 0030 (package dependencies — path deps, the on-ramp this extends), RFD 0022 (package-path addressing + the build gate), RFD 0038 (prelude & ambient scope — the import model), RFD 0043 (theory packages — the first publishable package, packages/mlt), RFD 0013 (toolchain distribution — the CDN infra this reuses; it explicitly deferred the registry to here)
  • Blocks: a publishable/installable package ecosystem (milestone #11 “Package ecosystem”); packages/mlt distribution (the modeling team’s MLT vendor-vs-registry decision)
  • Tracking: epic #688; children #689–#703

Question

Argon has path dependencies (RFD 0030) but no workspace, no lockfile, no version resolution, and no package registry — every layer above local path deps is recognized-and-refused (OE1240) and deferred to “a later RFD.” How should Argon distribute packages? Concretely: what is the registry substrate (it must not be a GitHub repo), the workspace + lockfile + resolution model, and what does Argon’s nominal type system dictate about identity across package versions?

Context

Where we are. ox = the package orchestrator (cargo), oxc = the single-package compiler (rustc) — the committed frame (book §16), realizing Backpack-’17’s two-phase pipeline (ox computes a wiring diagram + composition signature; oxc instantiates per package). Path deps fold a dependency’s modules into the consumer’s workspace and embed into one .oxbin; the compiler front-end already resolves imports through resolved deps. version/edition parse but are inert; [workspace] does not exist; the ~/.argon/packages content-addressed cache and ox.lock are reserved, not built.

The prototype (orca-mvp) is prior art, not ground truth. Its registry was a GitHub repo (registry.json on a branch + GitHub Releases + the GitHub API), which we are replacing. It got real things right — a deterministic tarball, a bivalent hash (a BLAKE3 byte hash and a constructs Merkle root over per-declaration semantic signatures), a content-addressed cache, a lockfile, PubGrub — and real things wrong: two manifest parsers that disagreed, a compiler that never saw resolved deps (the deepest bug), two publish pipelines that produced different hashes, and one hardcoded GitHub repo with a mutable index under concurrent writers.

Argon is unusually well-positioned. It already owns the two most expensive ingredients of a modern registry — a content-addressed byte hash (content_hash) and a semantic Merkle root (constructs, which no surveyed system has) — plus the exact S3 + CloudFront topology cache.nixos.org runs in production (the infra oxup already uses). A five-system prior-art sweep (Unison, Nix, Go modules, Dhall, Sigstore/TUF | Cargo, JSR, PubGrub, pnpm) converges on one architecture, recorded below; the research lives at .local/research/package-system/DESIGN.md.

Decision

D1 — Concept identity is nominal/path; the resolved graph is single-version-per-package-name

Argon’s type identity is nominal, by qualified path — verified in both the substrate and the research. In code: a concept is identified by NameRef (its canonical-symbol-table position for a qualified path) and DefId = (file, start, name, kind, visibility), never by a hash of its structure; subtyping is nominal end-to-end (Lean TypeSystem/Subtyping.lean: “Subtyping of named types is nominal”; Rust oxc-check types_compatible = schema.concept_ancestors(a).contains(b)); the BLAKE3 content-ids that exist (ContentId/AxiomKey/CompositionSignature) are body/build fingerprints kept separate from symbol identity. The vault’s identity research is explicit: “a naive content hash would make every schema edit a new type — the opposite of what a nominal type system wants … Unison’s model fits structural identity; Argon is largely nominal.”

It follows that:

  1. Person@1 and Person@2 are the same type by path. A field addition or refinement edit does not mint a new type — it trips the drift fingerprint, not identity. Identity is the full qualified path: pkg::mod_a::Person and pkg::mod_b::Person are simply two distinct concepts (like two Error types in different Rust modules), no conflict.
  2. The resolved graph is single-version-per-package-name. A package name is one namespace root mapping to exactly one package (already enforced: OE1241 name = published [package].name; OE1243 a name cannot denote two package directories). This is the applicative shared-base model (track-F module research; D-77 shared immutable base appears once). This differs deliberately from Cargo, which permits multiple semver-incompatible versions to coexist via name-mangling: Argon must not, because two concepts at one path cannot both be “the” type. The resolver (D2) therefore resolves each package to exactly one version graph-wide, or fails loudly.

Cross-version compatibility is a drift question, not an identity question (see D4 / Open questions).

D2 — Version resolution is PubGrub over a SemVer VersionSet

Use pubgrub-rs (already proven in the prototype). PubGrub is generic over a VersionSet, so Argon can later define its own range algebra (the 4-axis versioning) without being locked to caret SemVer; its derivation-graph errors route into Argon’s OE-coded diagnostics (a named root cause + fix, fitting the loud-over-silent ethos). Resolution enforces the D1 single-version invariant: one version per package across the graph, or a loud refusal. Features/optional-deps are encoded as virtual packages from the start. (MVS was considered — see Alternatives.)

D3 — The registry is a static, content-addressed store over our own CDN — not a GitHub repo

Three layers over S3 + CloudFront (argon.sharpe-dev.com, the infra oxup uses), with no trusted live service on the read/integrity path:

  • Layer A — immutable content-addressed blob store + sparse index. The S3 object key is the BLAKE3 content_hash (blobs/<blake3>), served immutable/cache-forever; beside each blob a tiny signed metadata sidecar (Nix .narinfo shape: size, constructs root, dependency closure, provenance, signature). A Cargo-style sparse-index protocol (config.json + per-package append-only NDJSON version records, uniform hash-prefix sharding, mandatory ETag/If-None-Match). Yank is an append-only event, never in-place mutation — every object is write-once, which structurally removes the prototype’s concurrent-index contention. Publish source (.ar) as canonical with a per-file content manifest (JSR’s model); compile .oxbin on demand, cached by source-manifest hash. Nothing opaque is ever published.
  • Layer B — a transparency log of both hashes. A Go-sumdb / Certificate-Transparency-style append-only Merkle log of package@version → (content_hash, constructs_root), with signed tree heads and static tiles on the same CDN; ox verifies inclusion + consistency proofs and fails loudly. Logging the semantic constructs root next to the byte hash makes the log tamper-evident over meaning, auditable at per-declaration granularity via subset Merkle proofs — a property no surveyed registry has, costing ~nothing once the log exists.
  • Layer C — a thin TUF metadata cap. timestamp + snapshot for freshness and anti-rollback/freeze/mix-and-match (essential because CloudFront caches stale objects), over a threshold offline root + targets key for key-compromise survival and in-band rotation. This makes S3 + CloudFront fully untrusted transport; trust anchors in offline keys + the public log. Fulcio / keyless OIDC, delegated targets, and SLSA attestations are deferred until many external publishers exist.

Because the registry is just static files, a local directory or file:// is a conformant registry — which yields offline builds, air-gapped mirrors, CI fixtures, and a “local registry” for free, with no special-casing. The ~/.argon/packages content-addressed cache (D-78, fail-closed) is retained; ox vendor covers fully-pinned reproducible builds.

The alternative considered and rejected is an OCI registry (ECR): standard auth/mirroring, but heavier and a dependency we don’t need given we already own a CDN.

D4 — v1 is minimal-correct; trust hardening and the correctness-oracle edge are follow-ons

v1: one unified manifest with [workspace] virtual manifests + inheritance + a shared ox.lock; PubGrub resolution; a static content-addressed sparse-registry client with integrity verification; one authoritative deterministic publish builder + the ox package CLI; packages/mlt as the first published package. Follow-ons: the transparency log (Layer B), the TUF cap (Layer C), tokenless-OIDC + Sigstore/Rekor provenance, and the JSR-inspired publish-time correctness oracle — the registry runs ox check + the tier classifier + the drift gate at publish and publishes correctness metadata (decidability tier, CWA/OWA cleanliness, silent-accept count, provenance) as first-class, hard-weighted data. Argon’s trust-first posture turns the registry into a correctness oracle, not an opaque host.

Rationale

  • Nominal/path identity (D1) is forced, not chosen — it is what the substrate already implements and what a nominal-plus-refinement type system requires. The registry design conforms to the substrate, not the reverse. “No dependency hell” (Unison) does not vanish; it relocates into cross-version compatibility, which Argon answers with constructs drift rather than by silently re-identifying types.
  • Single-version-per-package-name is the only coherent rule when identity is the path: it is already enforced, it matches the applicative shared-base model, and it gives a stronger guarantee than Cargo’s name-mangling — appropriate for a KR language where vocabulary identity must be stable.
  • PubGrub (D2) is greenfield-appropriate (no legacy resolver to preserve bug-for-bug), already in hand, and its error quality + generic VersionSet are direct wins.
  • The static content-addressed CDN (D3) is the convergent state of the art (Nix’s binary cache, Go’s proxy + sumdb, Cargo’s sparse index, JSR’s static API) and reuses infra we own; the transparency log of the semantic root is where Argon’s existing constructs signature lets it exceed every prior art.
  • Source-published + compiled-on-demand keeps packages auditable (the Deno lesson: URL-as-identity was the mistake, the hash was the safety net) and avoids opaque binaries.

Alternatives

  • GitHub-repo-as-registry (the prototype, D-27). Rejected: mutable index under concurrent writers, no namespacing/mirroring, couples distribution to a VCS host. The static CA store subsumes its every use.
  • OCI registry (ECR). Rejected for v1 (see D3): heavier, an unneeded dependency.
  • MVS instead of PubGrub. Considered seriously — Go’s minimal version selection is deterministic, lock-free, and carries a genuine safety argument for a KR language (“a transitive release has no effect until you ask”). Rejected for v1 because PubGrub is already in hand, gives superior errors, and its generic VersionSet future-proofs Argon’s own range algebra; the MVS safety intuition is preserved by the single-version invariant + the publish-time compatibility gate.
  • Unison-style structural / content-addressed type identity. Rejected at the language level (D1): it fits structural identity, but Argon is nominal — a content hash would mint a new type on every edit.

Consequences

  • The OE1240 manifest refusal of registry/version dep-forms is replaced by real resolution; version/ edition become load-bearing; [workspace] lands; ox.lock lands (bivalent: content_hash + constructs root).
  • A new authoritative deterministic publish builder is the only artifact producer (the prototype’s two-pipeline divergence does not recur).
  • The registry infra reuses the oxup CDN/account; the toolchain CDN and the package registry remain distinct surfaces sharing transport.
  • packages/mlt becomes installable, unblocking the MLT vendor-vs-registry decision.
  • Two substrate prerequisites become correctness floors (Open questions): freezing the constructs canonicalization, and authoring the breaking-change taxonomy.

Open questions

  1. The breaking-change / compatibility taxonomy (#697). Cross-version compatibility rides constructs drift: additive (new pub decl, widened bound) = compatible; narrowing a refinement, removing/renaming a pub decl, a CWA→OWA flip = breaking. The vault scoped a Java-binary-compatibility-style ruleset but never authored it. Per the mechanize-soundness-first directive, the compatibility condition is a scratch-Lean candidate before implementation. This is the soundness-bearing piece, and it ties to the keystone disjointness work (#628) and the R1 CWA/OWA write-side ruling.
  2. constructs canonicalization freeze (#696) — landed. The semantic Merkle is specified, frozen, and versioned independently of the hash input (Dhall’s v6.0.0 lesson: the spec version is a constant, never folded into the hash), with cycle hashing pinned (Unison’s #x.n recipe). The canonicalization lives in oxc_protocol::constructs (the per-pub-declaration signature projection → BLAKE3 leaf → D-114-alphabetical Merkle root, with NAF clauses kept distinct from positive boundaries per the #697 oracle), wired to the build via oxc_workspace::constructs and recorded in the ox.lock constructs column. Vocabulary reconciled: the vault’s D-026 calls constructs “semantic identity”; functionally it is the drift fingerprintcontent_hash = byte fingerprint, constructs = semantic drift/compatibility fingerprint, nominal identity = the qualified path (no separate identity hash).
  3. Namespacing/scopes. JSR’s scoped names (@scope/pkg, admins-not-owners) kill squatting and fit internal teams. Whether to adopt scopes from v1 or start flat is open.
  4. Asymmetric publish tokens (Cargo PASETO v3.public) vs the deferred OIDC path for the internal bootstrap window.
  5. An Argon-native non-SemVer VersionSet over the 4-axis versioning (#703) — deferred until a concrete substrate need forces it.

RFD 0045 — The world-assumption write-side: refuse-on-K3-not and the #[world] opt-in

  • State: discussion
  • Depends on: RFD 0025 (check discharge — the delta-guard this rides), RFD 0017 (the where/iff refinement split — the owned-vs-derived escape), RFD 0027 (the meta-property plane and the $axis catalog), RFD 0031 (the relation-constraint plane and the D-013 disjoint/complete/partition block surface)
  • Blocks: #627 (set-valued metaxis — its membership read needs the world-side semantics settled), #697 (the breaking-change taxonomy’s closed→open arm), #249 residue, packages/mlt Phase 2
  • Implements: the §6.9 per-concept world assumption (today specified but inert), the meta-plane keystone ruling R1 (#628)

Question

A modeler declares partition Vehicle { Car, Truck } and then inserts a Vehicle that is neither a Car nor a Truck. What happens? Three answers are on the table: refuse the write, derive one of the missing memberships, or cascade some repair. The same question recurs one plane up, on derived membership: a Person becomes an Adult when iff age >= 18 holds — but under open-world, age may be absent, so the refinement is neither true nor false. Does the derived Adult membership get asserted, refused, or left undecided?

These are not taste calls. The substrate already fixes the answer; this RFD records it and builds the one surface that lets a modeler opt out of the default. Concretely: (1) what is the write-side rule when a covering or derived-membership constraint is unmet, and (2) how does a concept declare that it lives under the open-world assumption instead of the closed-world default?

Context

Where we are. Three pieces are already on main, and they constrain the answer.

The world-assumption substrate exists. WorldAssumption (oxc-protocol/src/world_assumption.rs) mirrors the Lean Argon.Schema.WorldAssumption.WorldAssumptionClosed and Open, with WorldAssumptionMap the total function CN → WorldAssumption over a sparse override map plus a default. The type is wired; nothing reads it to change behavior yet.

The #[world] directive parses but is refused. oxc-instantiate/src/directives.rs:520 registers world and rejects it: “per-concept world assumption (§6.9) is specified but not built; the engine evaluates closed-world unconditionally.” So every concept is closed today, unconditionally.

Covering already ships under the closed default. The D-013 block surface (disjoint/complete/partition, RFD 0031) lowers to RFD-0025 checks: OE0240 (overlap), OE0241 (runtime covering, delta-guard-enforced at write), OE0242 (build-time static covering over the <: graph), OE0243 (non-subtype member). A partition whose cover is unmet at write is refused today. The covering question is therefore not whether covering is enforced — it is — but under which world assumption the enforcement should soften.

Why the answer is forced, not chosen. The closed-world write-side rule composes theorems already proven in spec/lean/Argon/, with no new framework and no bridge lemma. The composition was validated against the source, not assumed:

  • cwaCollapse_is_iff (TypeSystem/Soundness/CwaOwa.lean:110) — collapsing a classification yields is only if the input was already is. The closed-world collapse turns can into not, never into is. It cannot fabricate positive evidence.
  • collapse_conclusion_lacks_positive_evidence (CwaOwa.lean:197) — ¬ cwaTrue .can: a conclusion that exists only because the collapse invented a not carries no positive backing.
  • guard_iff (Reasoning/Checks.lean:288) — the RFD-0025 delta-guard passes iff violations post ⊆ violations pre. A write that creates a violation is refused; pre-existing violations are observed but do not block.
  • violations_mono_of_positive (Checks.lean:298) and the Truth4 joins (Foundation/Truth4.lean, can ∨ can = can, is ∨ _ = is) — the closure under which an unmet covering classifies as not and the check fires.

Put together: an unmet covering under the closed default is not, the covering check fires, and the delta-guard refuses the write. Refuse is the closed-world behavior the substrate already enforces; this RFD names it and makes the open-world alternative declarable.

Decision

D1 — The write-side rule: refuse-on-K3-not

When a covering, partition, or derived-membership constraint is unmet at a write under the prevailing world assumption, refuse the write iff the constraint classifies as K3 not (a definite violation). Under the closed default, an unmet covering collapses can → not and is refused — the behavior on main. Under #[world(open)] the same unmet covering stays can and is tolerated (incomplete is not violated; see D2).

This is one rule, applied at two planes:

  • Covering / partition (complete/partition): an instance of the parent whose membership in no declared variant can be established is not under closed-world → refused (OE0241), can under open-world → tolerated.
  • Derived membership (iff, RFD 0017): a computed membership whose refinement lands K3-undefined is not under closed-world → the membership is absent and any check depending on it fires; under open-world the refinement reads three-valued and a membership that would be asserted only by collapsing can → is is refused, never asserted.

Derive is rejected as unsound, not declined as a preference. Deriving the missing membership asserts is where the model only supports can. cwaCollapse_is_iff forbids exactly this move, and a fabricated is does not survive re-evaluation: cwa_owa_transfer (CwaOwa.lean:133) carries genuine is conclusions from closed to open worlds, but a derive-fabricated membership has no is to carry, so an open-world reader would find can. The two planes would disagree about the same fact. Cascade is rejected on a separate ground: a repairing write breaks the fixed-gate constancy theorems and the append-only event log, and it smuggles the same abductive guess into the store.

D2 — #[world(open)] becomes a real per-scope opt-in

Lift #[world(open)] from reserved-and-refused (directives.rs:520) to a real per-scope annotation that writes the concept’s entry in the WorldAssumptionMap.

  • The default stays closed. An un-annotated concept behaves exactly as it does today. Shipping this RFD is observably a no-op until a concept is opted into open. This is forced by the CWA-to-OWA monotone-transfer theorem: closed is the sound default, and opening a concept is the modeler’s explicit declaration that absence means ignorance, not falsity.
  • #[world(open)] on a concept makes its refinement reads three-valued: an absent fact reads can (unknown), not not (false). A where-asserted membership is still admitted (it is owned — D3); an iff-derived membership that lands can is refused (D1).
  • #[world(open)] on a concept governs the generalization sets declared under it. A partition/complete cover under an open concept softens from refuse to K3-tolerate: an instance not yet placed in a variant is can, not a violation. Only covering softens. Disjointness is world-assumption-invariant — a disjointness violation (OE0240) is two memberships that are both is, a positive overlap, and a positive overlap is a definite violation under any world assumption. The keystone rule softens refuse-on-K3-not (covering); it has nothing to tolerate on refuse-on-positive-overlap (disjointness), which is a separate, invariant rule.

D3 — The owned-vs-derived escape already exists: where / iff

No new assertion surface is needed for “I take responsibility for this membership.” RFD 0017 already split it:

  • where = asserted / primitive. The modeler owns the membership; insert iof(x, C) is permitted; violation is OE0668. Under open-world, an absent field permits the write — information-absence is not violation.
  • iff = defined / derived. Membership is computed; explicit insert iof is rejected (OE0211); under open-world it refuses-on-K3-not (D1).

So the modeler’s lever is the refinement keyword they already choose. Use where where the model asserts a membership it owns; expect iff-derived membership to refuse rather than guess when the evidence is absent.

D4 — Scope, attachment, and the relationship to store placement

#[world(open)] attaches to a concept declaration — the surface §6.9 / 06-types.md:230 already documents — giving the directive the real positions it lacks today (directives.rs:520). It writes a per-concept override into WorldAssumptionMap; the default remains Closed. There is no separate per-generalization-set annotation: a concept’s world assumption governs the covers declared under it (D2), so the surface stays exactly what the book scopes — concepts only.

This is the modeling opt-in — a statement about a concept’s domain (is the set of Persons in this model closed, or partial?). It is distinct from, and composes with, the store-side world tiering in RFD 0036 D6 (feat/store-placement-and-world-assumption), where a federated external store may force open-world reads on the data it owns. Where both apply, the store tier and the concept annotation must agree or the existing mixed-world conflict gate (MixedWorldAssumptionConflict) fires.

Coordination hazard — one WorldAssumptionMap writer, not two. This RFD’s per-concept opt-in and RFD 0036 D6’s store-side tiering both write the same WorldAssumptionMap. They must converge on a single writer reconciled through MixedWorldAssumptionConflict; two independent writers would silently disagree. This is a real blocking coupling between the two arcs, not a prose aside — whichever lands second builds on the first’s writer.

The decidability cost is real and must be honored in the tier ladder: mixing closed predicates into an open-world base can push data complexity from PTIME to coNP-hard (Lutz et al. 2013). A per-concept #[world] is not free; the §10 classifier accounts for it.

D5 — Diagnostics

  • OE0706 stays as the general reserved-directive gate — it still serves #[brave], #[intrinsic], and the other reserved directives. Once #[world] is built it simply stops firing for #[world].
  • OE0241 (covering) softens under #[world(open)] rather than being replaced: the same check, evaluated three-valued, tolerates can. OE0240 (disjointness) does not soften — a positive overlap is a definite violation under any world assumption (D2). OE0242 (build-time static covering) is unaffected — it is a catalog-level well-formedness check over the <: graph, independent of instance data and world assumption.
  • No new error code is required. One discretionary micro-call remains (the only piece of this RFD that is preference, not consequence): when a write is tolerated under open-world that would have been refused under closed-world, emit an informational diagnostic or stay silent? Recommended: silent. Opting a scope into open is itself the declaration that incompleteness is intended; a note on every such write is noise. If a lint is ever wanted, it belongs in the allow/warn/deny lint plane (#707), not as a hard diagnostic.

D6 — Lean obligations

The closed-world write-side rule (D1) is already discharged by compositioncwaCollapse_is_iff + guard_iff + violations_mono_of_positive + the Truth4 joins, as walked in Context. No new theorem, no bridge lemma.

The #[world(open)] softening (D2) rides the existing three-valued CwaOwa semantics: tolerating-on-can is the sound direction (it weakens refusal, never strengthens an assertion), so it needs no new framework.

One Lean-catchup item is owed, and it does not gate this RFD (Rust leads the reasoner here). The covering check is implemented in Rust, but its closed-world covering-completeness soundness is not yet mechanized: CwaOwa.lean proves positive-evidence transfer, not “the covering check fires whenever coverage is unmet.” That theorem — sitting between Storage/AxiomBody.lean’s PartitionAxiomBody and Reasoning/Checks.lean — is tracked as #760. File it; proceed on the softening under existing semantics.

Alternatives considered

  • Derive the missing membership. Rejected as unsound (D1): it asserts is from can, which cwaCollapse_is_iff forbids, and the fabricated fact fails cwa_owa_transfer re-evaluation. This is the option a forward-chaining engine cannot take soundly — a covering axiom is an open-world disjunction (“in at least one variant”), and a CWA-NAF engine cannot represent the disjunction, so deriving a specific variant is an abductive guess.
  • Cascade a repair. Rejected: breaks the fixed-gate constancy theorems and the append-only event log, and carries the same abductive guess.
  • Make open-world the default. Rejected: closed is the sound default by the monotone-transfer theorem, and it is the established §6.9 doctrine (“extents are the authority”). Opening is the exception a modeler declares, not the baseline.
  • A new assert-style membership statement for the owned case. Unnecessary: where/iff (RFD 0017) is already the owned-vs-derived lever.

This rule is the integrity-constraints-as-selective-CWA pattern from the description-logic literature (Motik, Horrocks, Sattler 2007; Tao, Sirin, Bao, McGuinness 2010): treat a covering or partition as a CWA-checked integrity constraint — denial-on-violation — layered over an OWA base. Per-concept world assumption is itself prior art (Reiter 1978; the Open/Closed/ClosedWithDefault tiers of earlier systems). The delta-guard-check model is exactly that pattern.

Sequencing

  1. R1 (this RFD) — record the write-side rule; no code change, the closed behavior already ships.

  2. #[world] surface — lift it from reserved (directives.rs:520) to a real per-concept annotation writing WorldAssumptionMap; soften OE0241 (covering) under open, leaving OE0240 (disjointness) invariant; refuse-on-K3-not on the iff path. Sequenced after the meta-plane classifier seat clears oxc-instantiate — one seat in the checker at a time. The same change satisfies the book↔engine drift gate: rewrite §6.9 / 06-types.md’s “refuses today” language to the built behavior.

    Prerequisite — the reasoner must consult the world assumption for negation (a live gap). The package-wide default_world is already a reachable manifest field (oxc-workspace/src/lib.rs:385, "open"/"closed", RFD 0036 D5), but the reasoner consults it nowhere — NAF is evaluated unconditionally closed-world. Under default_world = "open", NAF over a catalog-closed axis relation ($axis, and $setAxis once #627 lands) wrongly reads not where the open world demands can. Threading WorldAssumptionMap into the oxc-reasoning executor so NAF over these relations is world-gated (v ∉ Snot under closed, can under open) is a prerequisite this RFD’s open softening rests on, and it closes the pre-existing single-valued $axis gap at the same time. The soundness condition is already mechanized (AxisRelation.lean’s “K3-honest only under CWA”; the set analogue in Scratch/SetValuedAxis.lean’s setMembership_k3_honest); the executor is the catch-up.

  3. #760 Lean-catchup — mechanize covering-completeness soundness, ahead of or alongside the softening.

  4. #627 set-valued metaxis — now unblocked on the world-side semantics; its own set-membership K3 soundness is mechanize-first scratch-Lean, separate from this RFD.

Relationship to existing issues

  • #628 / R1 — this RFD is the ruling that issue tracks as gating the meta-plane keystone.
  • #627 — the membership read over a set-valued axis needs D1/D2 settled; this RFD settles them. The set machinery itself is #627’s own work.
  • #697closed → open is now a real per-concept property, so the breaking-change taxonomy’s world-flip arm is definable (a flip is breaking iff a NAF clause depended on the closed collapse).
  • #760 — the owed covering-completeness Lean-catchup (D6).
  • RFD 0036 D6 — the store-side world tiering composes with the per-concept opt-in via the existing mixed-world conflict gate (D4).

RFD 0046 — Derivation serving surfaces: query, delta, explain, trace

  • State: discussion
  • Depends on: RFD 0018 (the reasoner — semi-naive/WFS engine these surfaces read), RFD 0020 (runtime engine — Engine::evaluate, the one evaluation path), RFD 0036 (heterogeneous stores / IVM — the incremental read-model the delta surface rides), RFD 0028 (defeasibility — the proof tags a proof tree’s nodes carry)
  • Prior art: orca-mvp’s two-subsystem design — the OTel-like hierarchical trace (crates/nous/src/reasoning/trace/) and the fact-keyed derivation DAG (crates/datalog/src/provenance.rs); orca-mvp RFD 0007 (first-class queries/mutations + why-provenance); the vault note Provenance Under Tabling and Bilattice.

Question

How should the runtime expose derived state to clients — the derived-fact results a program needs, the per-write “what changed”, explanations of why a fact holds, and execution traces for debugging — and what is the single rule that keeps these from collapsing into the O(N·F) anti-pattern they collapsed into today?

Context

The runtime today exposes a derive/trace/explain HTTP plane in oxc-serve whose core is a loop:

#![allow(unused)]
fn main() {
// collect_derived_facts / derive_trace_value
for rule in module.rule_short_names() {
    let tuples = store.query_derive(module, rule);   // a full fixpoint, per rule
}
}

For a non-monotone program (recursion-through-negation, e.g. a breach/fulfilment calculus Fulfilled :- … not BreachedAt(…)) query_derive takes the uncached path and re-runs the whole stratified fixpoint from scratch on every call. So one whole-fork /derive is N fixpoints (N = number of derive heads), and for a layered calculus the shared lower layers are recomputed once per dependent head. The consumer — the ODE workflow runner — then calls this whole-program derive ~3× per write (/derive before, /derive after, /derive/trace) to render a per-step visualization diff. Net per workflow: ~3·W·N from-scratch fixpoints, each non-monotone-expensive. That is “derives take forever.”

Two facts reframe the fix:

  1. Correctness never touches /derive. Workflow decisions flow exclusively through dispatch/query and dispatch/mutation; an Argon query already evaluates against the materialized (derived) model, so the derived facts a decision needs arrive in the query result. No control path reads a /derive projection. The author-facing derive() op returns nothing, is absent from the generated SDK, and is called by zero workflows. Removing the per-rule derive plane loses zero correctness.

  2. Every /derive + /derive/trace call is display. It serves three real needs — a per-step delta of derived facts for a timeline, a derived snapshot for an instance graph + post-run validation, and an on-demand explanation/trace of a fact a user clicks. The ODE fakes the delta by diffing two full projections; its own code names the gap: “new Argon does not yet expose mutation-scoped proof trees for full causal provenance.”

The substrate the correct surfaces need already exists or is designed: the IVM read-model (RFD 0036; the maintainer wired into the strict path) already computes a per-commit delta; the reasoner runs one Engine::evaluate (RFD 0020); RFD 0028 defines proof tags. And the orca-mvp prototype already built the right shape — two distinct subsystems: an OTel-like hierarchical execution trace (spans: derive → engine → stratum → rule-firing), and a fact-keyed AND/OR derivation DAG (ProvenanceStore/DerivationTree) reconstructible into a proof tree on demand, reused by DRed for incremental deletion. The vault’s verdict is explicit and load-bearing: no production system stores full how-provenance in the answer table; the answer table stores answers, a side-track derivation log stores justifications, and explanation is reconstructed lazily. Why-provenance is PosBool(M) DNF; incremental maintenance uses the counting semiring; the ℕ[X] → PosBool homomorphism bridges them — two semirings, two jobs.

Decision

Four needs, four distinct surfaces. The governing rule:

The runtime materializes the derived model once; clients QUERY it. Derives are never “called” one at a time, and explanation/trace are reconstructed from a single materialization — never by re-evaluating per rule.

D1 — Correctness: query the materialized model (unchanged)

dispatch/{query,mutation,compute} stay the correctness surface. A declared pub query selecting over a derived head returns the derived facts a decision needs, evaluated against the one materialized model. There is no client-facing “evaluate this derive” primitive; deriving is the runtime’s job, querying is the client’s.

D2 — Per-step derived delta from the IVM read-model

A mutation already maintains the derived read-model incrementally (RFD 0036; monotone programs maintain in place, non-monotone rebuild once). Expose the derived delta of a commit{added, removed} derived facts (optionally per head) — as a by-product of dispatch/mutation, computed from the read-model maintenance that already happens. This replaces the “two full /derive projections + diff per write” pattern with a cheap commit-scoped delta. It is the mutation-scoped change feed the ODE timeline actually wants.

D3 — On-demand per-fact explanation (proof tree)

Maintain a provenance store populated during the single derive pass: a forward index fact → [RuleApplication{rule, substitution, input_facts}] and a reverse index input_fact → {derived facts} (the orca-mvp ProvenanceStore shape — and the reverse index is the same one DRed needs, so it pays for itself). Expose explain(fact) that reconstructs an AND/OR proof tree on demand (input_facts within one application = AND; multiple applications = OR; recursion bounded by a visited set + depth cap, the truncation point becoming an expand-on-demand hole). No re-evaluation: explanation is a pure read against the materialized store, O(proof size) not O(rules · facts).

Reconcile with RFD 0028: a proof-tree node carries its proof tag as a verdict. → a strict acyclic tree; +∂ → the supporting argument plus the defeat-check substructure (which attackers were considered and out-prioritized), built from the same DAG on demand; −∂/−Δ → why-not, a failure graph (which rule heads could have produced the fact and which body atom failed), computed by reverse reasoning over the reverse index. One model, four tags.

Store why-provenance as PosBool(M) DNF (the side-track log; not full how-provenance in the answer table). Keep IVM on the counting semiring; the ℕ[X] → PosBool homomorphism is the bridge — do not make one structure do both jobs.

D4 — Execution trace as a separate, off-by-default debug surface

Port orca-mvp’s OTel-like hierarchical span model (root deriveengine.<kind>stratum.<n>, with rule-firings/rounds/clashes as span events) as a distinct debug surface, emitted from the same single derive, behind a depth parameter, off by default (a zero-overhead NoOp sink, devirtualized in release — the orca-mvp contract). This answers “what happened, in order” for a step-debugger. It is never the explain path: explanation is per-fact and logical; the trace is bulk and temporal.

D5 — Remove the per-rule derive plane

Delete the per-rule loop (collect_derived_facts, derive_trace_value, explain_*_value and the HTTP routes built only on them) and the dead derive() author op. If a “all derived facts” debug dump is ever genuinely needed, it is one materialization projected over every head (O(F + N·project)), not O(N·F) — but D1/D2 make it unnecessary for production.

Rationale

The anti-pattern is a category error: it treats a derive as a callable procedure to invoke per head, when the engine computes the entire least/well-founded model in one stratified pass. Once “materialize once, query/project/explain from that” is the rule, all four needs fall out cheaply: decisions are queries (D1), change is a maintenance by-product (D2), explanation is a read of a side-track log written during the one pass (D3), and the temporal trace is an opt-in instrumentation of that same pass (D4). The orca-mvp prototype already separated the temporal trace from the logical proof DAG; the regression was collapsing them into one per-rule loop. The vault’s economics decide the split: explanation queries are rare and per-fact, tracing is bulk and opt-in, maintenance is per-commit — three different cadences, three different mechanisms, paid where each cost is incurred.

Alternatives

  • Fix collect_derived_facts to one-pass-project, keep the dump-all endpoint. Removes the N× multiplier but keeps the wrong shape — clients still pull “all derivations” rather than querying what they need, and it gives no proof trees. Rejected as the end state; acceptable only as a stopgap.
  • Always-on full how-provenance in the answer table. The semiring-complete ℕ[X] answer. Rejected per the vault / the literature (XSB, Souffle, PUG): every cache-hit pays a provenance update, and answer-table dedup discards alternative proofs anyway. Side-track log + lazy reconstruction is the established design.
  • Keep the ODE per-write capture but cache it. Still display-coupled, still no proof trees, still two full projections. D2’s maintenance-delta is strictly cheaper and is the change feed the ODE actually wants.

Consequences

  • oxc-serve: the per-rule derive/trace/explain plane is removed (D5); dispatch/mutation gains a derived-delta in its response (D2); new on-demand explain(fact) (D3) and an opt-in debug trace (D4) are added on the single-materialization substrate.
  • Reasoner: gains a provenance store written during Engine::evaluate (D3), sharing the reverse index with DRed; and an opt-in trace sink (D4). The counting-semiring IVM is unchanged; PosBool why-provenance is the new explain-side artifact.
  • ODE (devbox-workflow-runner, playground): stops the per-write /derive×2 + /derive/trace capture; consumes the mutation delta for the timeline, one derived snapshot per step (not per write), and on-demand explain/trace only on user click; the dead derive() op is dropped. A PR lands against ontology-tooling.
  • Sequencing. Phase 1: remove the per-rule plane + expose the D2 delta (kills the O(N·F) immediately, loses no correctness). Phase 2: the D3 provenance store + on-demand explain. Phase 3: the D4 debug trace; the ODE rewire. Phase 1 is independent of and unblocks the #781 workspace-resolution work.

Open questions

  • The in-memory representation of the PosBool DNF provenance and whether/when it is persisted (the storage-backed read-model #455 is the natural home; in-memory suffices for Phase 2).
  • Scope of why-not (−∂) explanations in Phase 2 — full PUG-style failure graph vs. a first cut that names the failed body atom.
  • Depth of defeasible (+∂) explanation — how much of the ASPIC+ argument/defeat structure to reconstruct vs. summarize.
  • The D4 trace wire format — a custom JSON/SSE now (orca-mvp shape) vs. OTLP later (a separate observability concern).

RFD 0047 — The temporal value library, and the value/ontology boundary

  • State: discussion
  • Depends on: RFD 0043 (theory packages + the neutrality boundary — std reflects, never smuggles, commitment), RFD 0044 (packages — how a vendored backing is distributed)
  • Relates to: std::temporal (the DatalogMTL operators, §17 — a distinct namespace, not this), std::datetime (the existing neutral value layer this enriches)

Question

What is Argon’s standard date/time support, and where is the line between a temporal value (a thing you compute with) and a temporal commitment (a thing the ontology is about)?

Today the temporal layer is ad-hoc: Date/DateTime/Duration are opaque ISO-text literal primordials (Literal::Date("2026-01-01")) with no Rust temporal library behind them — no arithmetic, no time zones, no parsing/formatting, no calendar math, only < comparison. std::datetime (174 lines) builds the Allen interval calculus + a TimeInterval value on top of that thin base. Domain ontologies then re-implement TimeInterval/AllenRelationType/AllenHolds again (the sharpe-ontology common/datetime duplicates std::datetime verbatim). The result: every temporal need re-derives the same primitives over string-shaped dates, and anything beyond < (durations, time zones, “the third Tuesday”, DST) is unavailable.

Context

  • Date/DateTime/Duration are primordials carrying ISO text, lowered in oxc-instantiate (expr_lower.rs); the runtime stores the string and compares it. No chrono/jiff/temporal_rs dependency exists.
  • std::datetime is the ontology-neutral value home: TimeInterval { startsOn: Date, endsOn: Date }, AllenRelationType, the thirteen Allen derives, AllenHolds. Correct as far as it goes; starved of a real value layer beneath it.
  • std::temporal is a different thing — the reserved namespace for the DatalogMTL convenience operators (ever/always/since_event/…, §7.3.2), behind the V1 macro system. The temporal logic plane. Do not conflate it with the value library; do not reuse its name.
  • TC39 Temporal is the modern, settled design for date/time values (it fixed the legacy Date/Joda/java.time pitfalls: no ambiguous “month 0”, explicit time-zone vs wall-clock, immutable, calendar-aware). Two mature Rust implementations track it: temporal_rs (the Boa reference impl, spec-faithful, churns) and jiff (BurntSushi, Temporal-inspired, stability-first API). temporal_rs is, notably, the crate whose version drift broke a local build during this work.
  • Doctrine (RFD 0043): std may reflect a commitment the substrate already made, never smuggle one it withholds. A temporal value makes no ontological commitment — so it belongs in std, free of ufo.

Decision

D1 — Adopt the TC39 Temporal model for Argon’s temporal value layer

Argon’s temporal values are the Temporal type set, ontology-neutral, in std::datetime (the existing neutral home, enriched — not a new namespace):

  • Instant (a fixed point on the timeline, UTC), Duration (calendar-aware span);
  • PlainDate, PlainTime, PlainDateTime (wall-clock, no zone);
  • ZonedDateTime (instant + TimeZone + calendar);
  • TimeZone, and a value-level Calendar system (ISO-8601/Gregorian arithmetic — leap years, month lengths);
  • the operations: construction, parse/format (ISO-8601), comparison, arithmetic (add/subtract/until/since/round), field access.

The existing TimeInterval + Allen calculus stays, now expressed over these richer values rather than bare ISO strings.

D2 — The value/ontology boundary (the rule)

A temporal value — an instant, a date, a duration, a zone, a calendar system — is ontology-neutral and lives in std. Temporal ontology — a calendar as a social artifact, a reified interval that mediates individuals, time-indexed facts — is committed and lives in a ufo-based package.

A PlainDate is a value like an Int; you do not ontologically commit to “2026-06-24”. The Gregorian calendar as ISO arithmetic is a value-level Calendar system (std::datetime); the Gregorian calendar as a NormativeDescription a society adopts is a domain kind (the ontology layer). Both exist, separated by this line. Not everything temporal needs ontological commitment — most of it is values.

D3 — Runtime-back the primitives; do not reimplement temporal math

The Date/DateTime/Duration/… primordials gain a real backing: their arithmetic, parsing, formatting, time-zone resolution, and calendar math are runtime intrinsics that delegate to a vendored Rust Temporal implementation — not hand-rolled, not computed in Argon source. Calendar and time-zone arithmetic is a notorious correctness sink; we consume a spec-tracking library, we do not author one.

Backing choice: temporal_rs is spec-faithful but churns; jiff is stability-first. For a runtime intrinsic that must pin to a stable API, lean jiff — but the Argon surface is TC39 Temporal regardless of which backs it, so this is an isolated, reversible impl decision (kept behind the intrinsic boundary). Pin it deliberately (RFD 0044 distribution).

D4 — Two temporal namespaces, kept distinct

std::datetime = the value library (D1). std::temporal = the DatalogMTL operators (§17). They compose (an MTL operator ranges over events stamped with std::datetime instants) but are different layers — the value algebra vs. the temporal logic. No rename; this RFD only clarifies the boundary.

D5 — Domain temporal layers depend on std::datetime, never re-implement it

A domain/ontology package needing time uses std::datetime for values + the Allen calculus, and adds only commitment on top (e.g. Calendar <: NormativeDescription, a reified TimeInterval-as-individual when it must mediate). The sharpe-ontology common/datetime duplication of TimeInterval/AllenRelationType/AllenHolds collapses to a re-export of / dependency on std::datetime.

Rationale

The current ISO-text primordials are a floor, not a library — anything past < is absent, and every consumer re-derives the same intervals. TC39 Temporal is the one date/time model worth standardizing on (it is the lesson learned from every prior date/time API), and two Rust impls already exist, so the cost is binding, not authoring. The value/ontology split (D2) is forced by the neutrality doctrine and by common sense: a date is a value; a calendar-as-institution is a commitment. Keeping the math in a vendored intrinsic (D3) avoids the single most error-prone thing a language can try to write itself.

Alternatives

  • Keep the ad-hoc ISO-text primordials. Rejected — no arithmetic/zones/calendars; every domain re-derives intervals over strings; correctness hazards (DST, leap) unaddressed.
  • Author temporal math from scratch (Argon or hand-rolled Rust). Rejected — calendar/time-zone arithmetic is a correctness sink with a maintained standard impl available.
  • Model everything temporal ontologically (ufo-committed dates). Rejected — a value is not a commitment; it would force ufo into std and make trivial date math a reasoning problem.

Consequences

  • The Date/DateTime/Duration primordials are enriched with runtime intrinsics backed by the chosen Rust impl; new value types (Instant, ZonedDateTime, TimeZone, …) are added to std::datetime.
  • std::datetime grows from a 174-line Allen layer into the Temporal value library; its Allen calculus re-expresses over the richer values.
  • Domain temporal duplication (sharpe-ontology common/datetime) is removed in favor of depending on std::datetime.
  • A new vendored runtime dependency (jiff/temporal_rs), pinned; the only place temporal math lives.
  • std::temporal (MTL) is unaffected beyond the documented boundary.

Open questions

  • jiff vs temporal_rs as the intrinsic backing (lean jiff for stability; both expose a TC39-Temporal-shaped API).
  • v1 surface scope — the full Temporal type set, or Instant/PlainDate/PlainDateTime/Duration/ZonedDateTime first and TimeZone/non-ISO calendars later.
  • Relation to the bitemporal substrate (tx_from/tx_to, as_of N) — those extents should be std::datetime instants; confirm the wiring.
  • Whether TimeInterval stays a std::datetime value type, with the reified-individual form a separate ontology-layer concept (D2/D5).

RFD 0048 — The test atom: in-language unit tests, and why a test is substrate

  • State: accepted — partially implemented (v1 shipped in #804; deferred assertion forms remain)
  • Depends on: RFD 0015 (mutate body surface — a test body is a mutate body plus assert), RFD 0025 (check discharge — the closest relative, and the contrast that defines a test), RFD 0020 (runtime engine — Engine::evaluate, the one read path a deductive-plane assert uses), RFD 0042 (self-validating .oxbin — the trust argument for carrying tests in the artifact), the scenario harness (#764 — the integration-test sibling).
  • Prior art: the §19 walking-example test "…" { … } sketch (a pre-implementation surface, superseded here); the scenario-harness expect vocabulary (oxc-driver/src/harness.rs); Rust’s #[test] / #[cfg(test)] (the source-discovery model this RFD weighs and rejects for Argon).

Question

Argon needs in-language tests: a way to write, inside a package, a unit test of what its rules, fns, and mutations actually do. Two questions follow. (1) Surface: what does a test look like and what does it assert? (2) Carrier: is a test a substrate concern — an elaborated declaration persisted in the .oxbin axiom-event log and mechanized in the Lean carrier taxonomy, like mutate/query/check — or a tooling concern, discovered from source by ox test and never persisted? The carrier question is the load-bearing one, because it decides whether the Lean mechanizes anything about tests at all.

Context

The test keyword has been reserved since the early grammar, parsing-but-silently-swallowing its body — the last entry on the v0.2.1 loudness burn-down (#342). The scenario harness (#764, ox run-scenario over scenarios/*.toml) shipped as the integration-test surface and deliberately reserved the tests/ directory for the in-language test atom, the unit-test surface. The §19 walking-example sketched a test form (imperative let/insert + assert) that never parsed.

The construct shipped in #804. During review one architectural question was deferred to this RFD: putting a test in the substrate means adding AxiomKind::TestDecl, which — because AxiomKind is @[language_interface]-mechanized — forces a testDecl variant into the Lean. AGENTS.md says “the Lean does not cover … tooling.” So either tests are not tooling, or the carrier choice is wrong. This RFD settles it.

Decision

A test is substrate, at the carrier / data-shape layer. The runner is tooling. Precisely:

  • The test declaration is an elaborated Core IR artifact, carried in the axiom-event log as AxiomKind::TestDecl with body TestDeclBody { name: String, operations: Vec<Operation> } — structurally identical to MutationDeclBody. The body is the same Operation Core IR a mutate lowers to, plus the test-only assertion ops: Operation::Assert { condition, rendered } (value/boolean), Operation::AssertDerivable { predicate, args, negated, rendered } (derivability), and Operation::AssertRejects { ops, expected_code, rendered } (negative enforcement — the block’s own lowered Operation stream, run isolated). The Lean carries testDecl in the AxiomKind taxonomy (Argon/Storage/AxiomKind.lean).
  • The runner is tooling, in Rust and prose only: ox test, the per-test fresh-store orchestration, the PASS/FAIL/ERROR classification and reporting, --filter, the non-zero exit. The Lean mechanizes none of this, and Operation::Assert’s arity is not in the drift gate (Operation is not a @[language_interface] inductive — same as every other mutate op).

The surface (shipped):

test "active leases are returned for tenant" {
    let alice = insert Person { name: "Alice", age: 30 };
    let unit  = insert Property { address: "1 Main", sqft: 700 };
    let l = sign_lease(alice, unit, 2500, 365);
    assert alice.active_leases() == [l];
}

A test "<string-name>" { … } is a named imperative block run top-to-bottom against a fresh store. The body is the mutate-body statement set (RFD 0015) interleaved with assert <bool-expr>; — no separate fixture block. assert mirrors require, with one difference: a failed require aborts the body; a failed assert records a pass/fail outcome and execution continues. An assert whose condition names a derived predicate / pub query / nav-method is evaluated against the reasoner’s materialized extent (via Store::query_derive, the one read path), so a test asserts what the rules derive, not just what was stored. assert is test-only: a stray assert in a mutate/fn body refuses at build time with OE1318.

The assertion vocabulary completes its negative-enforcement half (shipped): alongside the positive assert <bool-expr> and assert [not] derivable F(args), the rejection form assert rejects [( Pkg::Code )] { <mutate-body-stmts> } asserts that a write block is refused by a write-path guard. Argon is a constraint language — checks, where/iff invariants, group axioms — and without this form the test atom could assert what a model accepts but not what it refuses, which is the more important property of a constraint. The block runs against an isolated copy of the test world (committing nothing back) and the result is classified: a genuine write-path guard rejection (a where-invariant OE0668, a check delta-guard, a group axiom, an endpoint refusal) PASSes — matching a pinned Pkg::Code when given, a different code FAILing as the wrong reason; an accepted write FAILs; and a non-guard error (a typo, an unbound reference, a type error) ERRORs loudly. The guard-vs-non-guard line is load-bearing: only a genuine constraint refusal satisfies rejects, so a broken test can never masquerade as a passing rejection test. rejects, like derivable, is a contextual keyword (leading position after assert).

Rationale

The carrier ruling is validatable on four independent grounds; each was checked against the code and the Lean.

  1. AxiomKind is the event-log taxonomy, not the five-atom set — and tests join an existing precedent. The five atoms (meta-calculus, constructs, rule, trait, macro) are the vocabulary-introduction primitives and are fixed. AxiomKind is broader: it already carries ruleDecl, queryDecl, mutationDecl, computeDecl, and bridgeDecl — none of which is one of the five atoms. testDecl sits in exactly that company. So calling it “the test atom” is colloquial naming of a declaration form, not a claim of a sixth substrate atom. (Verifiable: Argon/Storage/AxiomKind.lean, oxc-protocol/src/storage.rs.)

  2. A test body is Core IR, which the Lean scope already covers. TestDeclBody.operations is Vec<Operation> — the identical elaborated IR a mutate lowers to. AGENTS.md’s mechanization scope lists “Core IR: elaborated intermediate representation; surface-to-IR lowering preservation” and the Storage event log. A test, once elaborated, is a member of both. Carrying it is consistent with what is already mechanized; excluding it would be the special case requiring justification.

  3. The carrier/runner split is exactly what “the Lean does not cover tooling” means. The Lean testDecl is a tag in the carrier enum; it carries no test semantics — there is no mechanized pass/fail relation, no ox test model, no isolation theorem. The tooling (the runner and its reporting) lives in Rust and prose, untouched by the Lean. So the scope line holds verbatim: the event-log taxonomy (Storage) is substrate and mechanized; the runner is tooling and is not. Adding testDecl to the taxonomy no more “mechanizes tooling” than mutationDecl does.

  4. Trust and reproducibility (RFD 0042). The .oxbin is the package’s complete elaborated form, and it is self-validating: it re-checks its own invariants at load. A test carried in the artifact runs against the exact elaborated state that ox check/ox build validated — there is no second elaboration that could drift from the checked one. Source-discovery would re-parse and re-elaborate test bodies independently of the artifact, opening precisely that drift. Carrying tests is the choice that preserves “the artifact is the package.”

Alternatives

  • Source-discovery (the Rust #[cfg(test)] model): rejected. ox test would parse tests/*.ar, elaborate test bodies on demand, and run them, persisting nothing. It is appealing on the intuition that “tests aren’t knowledge,” but it (a) reintroduces a second elaboration path that can drift from the checked artifact (against RFD 0042), (b) makes a test the only package-level declaration not carried in the artifact, splitting the declaration model for no semantic gain, and (c) buys nothing the carrier model lacks — tests are already inert at query/serve time, so persisting them costs nothing at runtime.
  • A sixth atom: rejected, and a category error. A test introduces no vocabulary and no new substrate primitive; it is a declaration form whose body is existing Core IR. The five atoms are fixed (see ground 1).
  • Stripping tests from every artifact: not now. Whether a release artifact should omit its tests (as a Rust release binary omits #[cfg(test)]) is a build-profile question, not a carrier question — it does not bear on whether tests are substrate. Deferred (see open questions).

Consequences

  • AxiomKind::TestDecl is in the .oxbin event log and the Lean carrier taxonomy (the variant count is now 26). Tests are inert at query/serve time — only ox test enumerates them.
  • A test is not a check. A check is a standing universal obligation that fires a diagnostic over whatever world is loaded and is observer-only (it populates no IDB); a test is an existential example that constructs a known world and asserts a specific expected outcome. Check-firing is one thing a test can assert — directly, via assert rejects [( Code )] { <write> }, which asserts the write is refused by a guard (and on the exact code when pinned) — but the two are duals, not the same construct.
  • Isolation is fresh-store-per-test (the scenario-harness discipline), so tests are order-independent; within a test, writes are read-your-writes over the committed + deductive state.
  • A published artifact currently carries its tests. This is acceptable (they are inert) and is the trust-preserving default; a future release profile may strip them.

Open questions

  • Shared / parameterized fixtures across tests — the one deferred assertion-surface item (the fixture / expect block forms; refuses loudly until built, never silently accepted). The dedicated derivability forms assert derivable F / assert not derivable F and the three-valued outcomes (an INCONCLUSIVE result distinct from FAIL when a derivability assert is evaluated over an open-world relation, where absence is can, not not — the same fail-closed discipline RFD 0045 forced for the write side) are now built (§17.14): present ⇒ PASS/FAIL, absent under CWA ⇒ FAIL/PASS, absent under OWA ⇒ a loud INCONCLUSIVE that exits ox test non-zero. The negative-enforcement form assert rejects [( Pkg::Code )] { … } is also now built (§17.14): a write block is run isolated and a write-path guard rejection PASSes (on the pinned code when given), an accepted write FAILs, and a non-guard error ERRORs loudly — so the assertion vocabulary covers both what a model accepts and what it refuses.
  • Release-profile stripping of tests from a distributed artifact (a build-profile lever, orthogonal to the carrier ruling).
  • An assertion-soundness theorem in Lean (optional research): that a passing deductive-plane assert implies the asserted derivation holds at the fixpoint. The runner stays tooling regardless; this would only mechanize the meaning of the carried Assert op, adjacent to Reasoning/Checks.lean.

RFD 0049 — Error-tolerant diagnostics: recovery, source-faithful expansion, and the What/Where/Why/Fix model

  • State: discussion
  • Depends on: RFD 0037 (declarative macros + carrier-based hygiene — the expansion pipeline this preserves source through), RFD 0025 (check discharge — the user-check diagnostic channel), RFD 0027 (the #[order]/meta-property decorators whose mis-binding exposed the gap), the editor experience epic (#723 — the LSP surfaces these renderers feed), the teaching-diagnostics arc (#705/#727 — the What/Where/Why/Fix model this completes).
  • Prior art: orca-mvp’s compiler/LSP — granular parser recovery (error_here/error_and_bump/err_and_recover + a vocabulary-agnostic IDENT IDENT sync point, oxc/src/cst/parser.rs), diagnostic attribution maps that never surface synthesized forms (oxc/src/elaborate/diagnostics.rs), a shipped LSP quickfix system (lsp/src/convert.rs, snippet_resolver.rs), and partial-analysis caching that survives parse errors (lsp/src/analysis.rs). rustc/rust-analyzer (rowan ERROR nodes, structured Suggestions feeding both rendered help and code actions), Roslyn (red-green trees, skipped-token/missing-node recovery), forward source maps (TS→JS, Sass→CSS). The vault: Projectional Editing Taxonomy (Hazel — “a program with holes is never a parse-error/type-error wall”) and decision D-012 (the OE/OW/OI severity-prefix scheme).

Question

How should Argon’s compiler, runtime, and language server behave on broken or incomplete models so that diagnostics genuinely guide the modeler — and what invariant prevents the class of failures where an error is rendered against text the user never wrote?

Context

A real bug surfaced the gap. This input (note the missing ; after Top):

use mlt::*;
pub category Top
#[order(2)]
pub category Kind <: Top

fails with OE0001: unexpected token BANG at module level and renders synthesized post-expansion sourcepub category Toporder! { (2) pub category Kind <: Top } — at line numbers that do not exist in the file. The #[order(2)] decorator was re-serialized as a module-level macro-bang. The reported cause (order!/MLT) is wrong; the actual fault is a missing terminator on the preceding declaration, and the rendering shows text no one wrote.

This is not a wording problem (those are addressed by the citation scrub and the source-frame work that already landed). It is three structural defects in the layers beneath the message:

  1. Expansion destroys source fidelity. oxc-workspace/src/expand.rs expands one invocation per round by format!-building a synthetic string ("{name}! {{ {args} {body} }}"), replacing the module text, and re-parsing. After any expansion, every span is an offset into synthetic text; a downstream error renders that synthetic text at phantom line numbers. The renderer already distinguishes the pieces it emits (Piece::Sub — a metavariable substitution carrying the user’s argument tokens — vs Piece::Lit/Tok — macro-body tokens), but that origin information is discarded the moment the pieces are joined into a flat string.

  2. The grammar is ambiguous at declaration boundaries. A bodyless concept declaration (pub category X <: Y, no { … } body and no = union) is accepted with an optional terminator (p.eat(SEMI)). With no ;, the parser cannot tell the declaration ended before the next item, so a following #[…] attribute is silently mis-attached, and the only diagnostic — generic “unexpected token” — never says what was expected.

  3. Broken input is a wall. Recovery is coarse (skip-to-item-start); there are no per-construct “expected X” diagnostics, no machine-applicable fixes, and the LSP’s structural features (completion, document-symbol, references — all recently shipped) collapse on a file that does not fully parse rather than degrading over the recoverable region.

The substrate to do this correctly already exists or is proven. orca-mvp shipped the whole error-tolerant spine — granular recovery, attribution maps that assert every diagnostic resolves to real source, a one-diagnostic→many-renderers model with quickfixes, and partial-analysis caching. The current parser is rowan-based (lossless green trees with ERROR nodes are natural). The expansion pipeline expands one contiguous invocation per round, which means a forward provenance map is exactly composable. The vault’s north star is Hazel’s principle that a program with holes is never a wall; for a textual language the achievable, complete form of that is an error-tolerant spine: lossless parse → recovery → faithful provenance → partial analysis → actionable fixes.

Decision

The load-bearing invariant, from which everything else follows:

Every diagnostic span resolves to a location the user actually wrote, and the user is never shown text they did not write. A diagnostic span that cannot resolve to real source is a loud compiler bug — an internal-error diagnostic — never a phantom rendered at the user.

The architecture is five layers. Each is built as a complete vertical slice; none is optional or stubbed.

D1 — Lossless, error-tolerant parsing with granular recovery

The parser produces a lossless rowan green tree for any input, with ERROR nodes wrapping unparseable spans and explicit MISSING markers where a required token is absent. Recovery is per-construct, not module-level: each construct emits a specific expected-token diagnostic (“expected ; to terminate category Top”, “expected } to close this body”) and resynchronizes at the nearest construct boundary, including the vocabulary-agnostic IDENT IDENT concept-declaration sync point (so recovery does not hard-code domain metatype names — a concept can be introduced by any in-scope pub metatype). This recovers orca-mvp’s error_here / error_and_bump / err_and_recover discipline. Parsing always continues past an error; the rest of the file yields a usable tree.

D2 — The terminator rule (the grammar is made unambiguous)

A declaration is terminated by exactly one of: its body’s closing }, the end of its = union, or — when it has neither — a required ;. This is precisely Rust’s item-termination rule (struct Foo; requires ;; struct Foo { … } self-terminates), and it is the project’s stated Rust-aesthetic default. p.eat(SEMI) becomes p.expect(SEMI) with recovery: a missing terminator on a bodyless declaration emits “expected ; to terminate <decl>” with an add-; fix and continues. This removes the ambiguity that silently mis-attaches a trailing attribute — the defect was the optional terminator, and an ambiguous grammar is a defect, not a stylistic choice.

D3 — Source-faithful expansion via a forward, piece-granular provenance map

Expansion preserves source provenance through a forward-built provenance map, not by reverse-engineering offsets and not by rebuilding the pipeline around per-token SyntaxContext. As each expansion round renders, it records, for every output range, its origin:

  • a Sub piece → the invocation argument’s real source range (the user’s text);
  • a Lit/Tok piece → the macro definition-site range (with the macro identity);
  • a synthesized wrapper (e.g. the order! { … } an attribute rewrite produces) → the attribute/invocation site.

Because each round replaces a single contiguous source range, per-round maps compose: round N’s map composes through round N−1’s, so any final-text offset resolves transitively to either a real user-source location or “synthesized inside the expansion of M, invoked at S”.

Diagnostic resolution walks this map: an error on copied-through user code lands on the user’s real declaration; an error on macro-body output lands at the invocation site with a secondary “in expansion of M, defined here” label. The synthesized macro-bang text becomes structurally unrenderable to the user. The orca-mvp attribution-map assert is adopted: if resolution finds no real-source anchor, that is an internal-error diagnostic.

Hygiene is unaffected. Argon’s carrier/oracle hygiene (RFD 0037) resolves names at render time; the provenance map resolves spans for diagnostics. They are orthogonal — there is no hygiene case that forces per-token provenance — so the provenance map is added alongside the existing hygiene oracle, which is left intact.

D4 — One structured diagnostic, What/Where/Why/Fix

A single Diagnostic value, structured rather than prose:

  • What — the headline (citation-free; guarded).
  • Where — a primary span resolved through D3, plus secondary labels (the prior declaration, a macro definition site, the conflicting decl).
  • Why — the justification chain: the axiom-trace provenance chain for semantic checks, the grammar expectation for syntax errors.
  • FixVec<Suggestion>, each a span + replacement text, machine-applicable.

Every renderer consumes this one value: terminal (miette), LSP, ox explain, the mdBook appendix, the InfoView inspector. This completes the #727 model.

D5 — Quickfixes and code actions, from the same Suggestions

The Suggestions of D4 are the LSP code actions — one source, two surfaces (the rendered “help:” line and the editor quickfix are never two implementations). This recovers orca-mvp’s shipped set (add missing pub, add else, disambiguate glob) plus the cases this RFD creates (add ;, add a use for an unimported vocabulary classifier — the OE0605 cliff). A “did you mean” producer (bounded edit-distance over in-scope names) emits Suggestions for unresolved paths and metatypes.

D6 — LSP graceful degradation over the recoverable region

A salsa-cached partial analysis — symbol table, reference index, resolved names over the non-ERROR parts of the CST — that survives parse errors (orca-mvp’s persisted AnalysisResult). Hover, completion, document-symbol, semantic tokens, and references all operate over the parts that parsed, even when other parts are broken. A missing ; never blanks the whole server; it produces a precise diagnostic and the rest of the file stays live.

D7 — The invariant is enforced, not aspirational

Two guards keep the spine from eroding: (a) a test asserting that a diagnostic produced over expanded input resolves to a real source span (never synthesized text) — the D3 attribution-map assert, exercised on the motivating bug; (b) the LSP degradation is covered by tests that feed a deliberately broken file and assert hover/completion/symbols still answer over the recoverable region.

Rationale

The defects are architectural, so the fixes are too. D1+D2 make a syntax error a precise, recoverable event instead of a silent mis-parse. D3 is the keystone: the worst confusion (“what is order! { } and why is that line number wrong?”) is a fidelity violation, and once provenance is preserved that entire class is gone. D4+D5 make every diagnostic teach (Why) and act (Fix) from one structured source. D6 makes the editor useful precisely when the model is broken — which is most of the time during authoring.

The piece-granular forward map (D3) is chosen over a per-token SyntaxContext/ExpnId rebuild because it is the correct fit for Argon’s actual architecture, not because it is simpler. rustc unifies provenance and hygiene in SyntaxContext because its hygiene is per-identifier scope-sets; Argon’s hygiene is carrier/oracle-based and works at the name level (RFD 0037), so a per-token model would rewrite a working subsystem to buy provenance it can get exactly from the map. The current renderer already computes piece origin (Sub vs Lit/Tok); the map records what is already known. Forward source maps are the standard, exact technique for “spans must survive a text-rewriting transform” (TS→JS, Sass→CSS) — and being built forward, with full structural knowledge of what each expansion replaced, they are exact, not the fragile reverse-offset-guessing they are sometimes confused with.

Alternatives

  • Per-token SyntaxContext/ExpnId, tree-to-tree expansion (the rustc model). Strictly more powerful (per-identifier hygiene + provenance unified) but requires rebuilding the expansion pipeline, lexer token, and parser integration, and supplants Argon’s working carrier hygiene. Rejected: it buys no correctness over D3 for Argon’s hygiene model, at large cost. If a future hygiene requirement ever needs per-identifier scope-sets (macro-defines-macro with capture across expansions the carrier cannot express), D3’s map is forward-compatible — the ExpnId is the same content-derived expansion_id already minted.
  • Reverse offset remapping (recover origin by diffing expanded vs original text post hoc). Rejected: lossy and fragile under nesting and substitution; cannot distinguish user-argument tokens from macro-body tokens. D3 records origin forward at render time instead.
  • Layout / automatic-semicolon-insertion instead of D2’s required ;. Rejected: ASI has well-known footguns (Go, JavaScript) and trades one ambiguity for a subtler one; the required terminator is unambiguous and matches the Rust default.
  • Leave expansion text-based, fix only the message wording. Rejected: it cannot satisfy the invariant — any error over expanded input still renders synthetic text.

Consequences

  • Macro/attribute expansion gains a provenance map threaded through the per-round driver; the per-round text pipeline (a correct nesting design) is retained, hygiene is untouched.
  • The Diagnostic type is restructured once to carry structured What/Where/Why/Fix; all existing renderers (frames, axiom-trace, ox explain, the citation-clean catalog) plug into it. Recently-shipped LSP features (completion, symbols, references) gain graceful degradation.
  • ; becomes required after a bodyless declaration. Existing corpora with bodyless declarations missing terminators will get a precise, auto-fixable diagnostic; a one-time ox fmt/quickfix sweep adds the terminators. This is a surface change and is recorded as such.
  • Two regression guards (D7) make the source-fidelity invariant and the LSP degradation permanent.

Open questions

None blocking. The expansion_id already minted for hygiene is reused as the provenance map’s expansion identity, so no new identity scheme is needed. The build order is dependency-driven (D1/D2 → D3 → D4 → D5 → D6); each lands as a complete slice.

RFD 0050 — Documentation architecture: three books, correctness by construction, and a verified authoring pipeline

  • State: discussion
  • Depends on: the reference manual (Part I of the book) and its crash course; RFD 0049 (error-tolerant diagnostics — the What/Where/Why/Fix model and ox explain corpus the diagnostic docs draw on); RFD 0044 (packages and the registry — the example corpus is a set of real packages); the @[language_interface] drift gate (spec/lean/Argon/Interface.lean), whose discipline the documentation-freshness gate mirrors; the editor-experience work (the LSP shares the diagnostic corpus these renderers feed).
  • Prior art: Rust’s three-artifact split — The Rust Programming Language, The Rust Reference, Rust by Example — plus the rustc error index (rustc --explain); the Diátaxis documentation framework (tutorial / how-to / reference / explanation as four distinct reader-needs); mdbook’s {{#include}} transclusion; the Oxide Computer RFD process this series already follows; literate-specification precedents that cite a mechanization alongside prose.

Question

How should Argon’s documentation be structured, authored, and kept correct as the language moves — given that the reference manual systematically drifts from the implementation today, that the substrate is mechanized in Lean while the reasoner is implemented Rust-first, and that “what runs today” cannot be read reliably from prose or even from code comments?

Context

The drift is structural, not a matter of diligence. The reference manual asks one artifact, in one voice, to do three different jobs — teach a newcomer, specify the language normatively, and record design history — and to track two different timelines at once: the language as designed (which changes by deliberate decision, rarely) and the language as implemented (which changes every merge). An inline status badge or a “refuses today” sentence in normative prose is therefore a time-bomb with a one-merge fuse.

This is measurable, not hypothetical. A fact-check of the prose against the tree at main found shipped capabilities described as unbuilt: the temporal value library (real, jiff-backed) and the in-language test declaration (real, with its own ox test runner) were both still called “future” in committed prose. And reading the implementation’s own code comments and reserved markers *under-*reported the language: well-founded semantics for cyclic negation and the defeasible-rule surviving extent both run today, yet are easy to read as “not built.” The lesson is sharp and load-bearing for everything below: implementation status is unreliable read from prose, and unreliable read from code comments. The only trustworthy signals are an example that compiles and runs in CI, the deliberately-honest “what runs today” sections of the crash course, and commit history.

Three further facts shape the design:

  • The layers move at different speeds. For the surface and substrate semantics, the book runs ahead of the Lean and the Lean ahead of the Rust. The one inversion is the reasoner: well-founded semantics, the join/optimizer engine, incremental maintenance, and defeasible evaluation are implemented in Rust ahead of their Lean mechanization. Documentation must make this legible without misleading a reader about what is proven versus what runs.
  • The crash course already demonstrates the target. It teaches the meta-calculus before any vocabulary, frames a foundational ontology as an ordinary package rather than a language feature, and states plainly what is specified versus what runs. The drift lives in the numbered reference chapters, not here. The crash course is the seed, not a thing to replace.
  • The language is unfamiliar and dual-purpose. A declared (not built-in) classifying vocabulary, four-valued Truth4, per-concept world assumptions, a seven-tier cost ladder, defeasibility, and standpoints are not what a reader arrives expecting from OWL, SQL, or Prolog. Argon is also both a language and a database, with a modeler audience and a data-systems audience. The documentation has to install a correct mental model, not just list features.

Decision

One principle is load-bearing, and the rest follows from it:

The artifact most at risk of being wrong carries the fewest independently-falsifiable claims. Narrative teaches by pointing — at examples that compile in CI, and at a reference whose claims are mechanically anchored — rather than by restating facts it could get wrong. Risk is inverted on purpose: the highest-variance prose is made the lowest-risk by construction.

D1 — Three user-facing books, one information architecture

Not three silos. One system, layered by who owns truth, each book answering one reader-question (the Diátaxis split, adapted):

  • Argon by Example — the verified substrate (“show me it working”). Real .ar packages, compiled and run in CI. This is the only place code lives. Both other books transclude from it, so a broken example is a failed build, not a stale snippet. Indexed two ways: by concept and by task (“how do I model a role / a temporal fact / a defeasible exception”).
  • The Argon Reference — the normative surface (“what is the exact rule”). Terse and complete. Each section carries a Lean-provenance link (existence-checked in CI); grammar sections are generated from grammar.toml; code is transcluded, never pasted; the diagnostic appendix is the error index, single-sourced with ox explain <CODE>. It describes the language as it is — no inline status badges (see D2).
  • The Argon Book — the teaching narrative (“how do I think about this”). It grows from the crash course, teaches substrate-first, and makes minimal original factual claims: it motivates, sequences, and explains, but every code sample is a transclusion and every precise rule is a link to the Reference. It is correct by construction.
  • No published implementation-status surface. The language is done enough that “what is implemented today” is no longer a question the documentation must answer — so the published status surfaces (inline chapter badges, the feature-status page, and a generated coverage grid) are retired. The three books describe the language as it is. The anti-drift value those surfaces carried — catching when the reference falls behind the language — is preserved as an internal CI check (cargo xtask check-coverage) over a feature registry (coverage-features.toml): every feature names its CI-checkable signals (a runnable example, a diagnostic code in the generated catalog, a Lean file) and the check fails the build if any cited signal no longer exists. That is the anti-drift mechanism, not a reader-facing grid. The live, CI-verified evidence of what runs is Argon by Example — every package there compiles and runs in CI.
  • A single glossary: one definition per term (metaxis, metatype, metarel, refinement, standpoint, tier, defeasible, the value/ontology boundary), authored once and transcluded into every book.
  • One cross-link grammar, so the books read as one system: learn it → Book §; exact rule → Reference §; see it run → By Example #; why this design → RFD; is it proven → Lean.

RFDs and the Lean are referenced archives, not part of any reading path. The AGENTS.md intent nodes remain contributor-facing and separate from the three user books.

D3 — The Book spine

The expanded crash-course arc — a modeler’s workflow, not an enumeration of atoms (the atom-by-atom organization belongs to the Reference):

  1. Orientation — what Argon is and why (a typed knowledge graph, a rule engine, and a bitemporal store in one); install; a fast end-to-end taste.
  2. Modeling a domain — the meta-calculus, substrate-first (declare your own metatype/metaxis/metarel, plus the reflective intrinsics); the value/ontology boundary; data versus concepts; <: (specialization) versus : (instance-of); first-class relations; refinement (iff versus where). Examples are neutral; a foundational ontology appears only as one bounded worked example.
  3. Reasoning — the five rule modes; derivation and recursion; the stratified fixpoint and well-founded semantics; the write path (mutate); queries.
  4. Truth under incompleteness — Truth4; world assumptions (closed by default, the per-concept open-world opt-in); defeasibility; standpoints and federation.
  5. Confidence — the decidability ladder (why a model terminates and what it costs) and checks-and-diagnostics as the trust surface (ox explain, the justification “why”).
  6. Building real systems — packages and the registry; traits; macros; tests; the runtime and serving; the bitemporal store; generated SDKs.

D4 — The anti-drift machinery

  • A feature registry (coverage-features.toml) checked by cargo xtask check-coverage: every feature’s cited signals (example, diagnostic, Lean file) must exist, so the reference cannot silently fall behind the language (D2). The check is internal — it does not publish a status surface.
  • Example transclusion (mdbook {{#include}} with named anchors), so no book contains a code snippet that is not a region of a compiling package.
  • A source-commit drift-check: each generated/anchored claim pins the commit it was verified against; a check flags cited sources that moved.
  • A documentation-freshness gate that mirrors the @[language_interface] drift gate: a feature PR that flips an RFD state to shipped, changes a drift-checked carrier, adds a diagnostic code, or adds an example prompts the matching doc touch. Soft warning first, hardenable.
  • Grammar generation of the Reference’s syntax sections from grammar.toml.
  • The diagnostic corpus single-sourced into both ox explain and the Reference error index.

D5 — The verified authoring pipeline

Per section, a pipeline (not a single pass): structure (charter: what it covers, which examples it owns, dependencies) → content ledger (every claim paired with a source — a Lean module/theorem, an oxc location, an RFD, a diagnostic code, or an example id; every code sample a real package or a flagged gap) → adversarial verify (the compiler for code claims, an existence-check for Lean links, an agent told to refute for prose semantics) → cross-section reconcile (resolve contradictions, build the glossary, confirm coverage) → constrained authoring (prose written only from the verified ledger; code only by transclusion) → review (voice, then a technical re-check that prose still matches the ledger and examples still pass).

Three properties make it sound rather than merely orderly:

  • Ground-truth order and a blocklist. Authority runs Lean (substrate semantics) → oxc source (surface, runtime, diagnostics) → reference prose. Explicitly not authoritative: stale editor grammars, superseded surface versions, and any external note. Where reference prose and code disagree, code wins; where the Lean covers the substrate, the Lean wins.
  • Verification is mechanical wherever possible. Code claims are checked by the compiler in CI; grammar is generated; status is a CI signal; Lean links are existence-checked. The agent’s judgment is the fallback for prose semantics only — the smallest surface that must rely on it.
  • The drift/gap ledger is a first-class output. A claim that cannot be verified becomes one of three things: a documentation fix, a spec-or-code drift ticket, or a language gap that needs a decision. The documentation build doubles as a correctness audit of the language.

The pipeline is layered by book (By Example green first, then the Reference, then the Book) and is designed for steady state: the freshness gate, drift-check, and CI examples run for the life of the project; the initial authoring is run number one, not a finished project.

D6 — The plan

  • Wave 0 — the anti-drift tooling (coverage registry, transclusion, drift-check, freshness gate, shared-glossary infrastructure, Lean-link check) plus the small, already-identified prose drift-fixes.
  • Wave 0.5 — a tracer bullet: one concept taken through the entire system end-to-end (a CI-verified example → a Reference section with provenance → a Book passage transcluding it → its feature-registry entry → its freshness hook), to validate every seam before fan-out.
  • Wave 1 — Argon by Example (the verified substrate).
  • Wave 2 — the Reference (normative, status-free, literate).
  • Wave 3 — the Argon Book (narrative, transcluding and linking).
  • Steady state — the gates run continuously; new features ship with their doc touch, example, and feature-registry entry by construction.

Rationale

The correctness-by-construction inversion is the whole design. Documentation rots because prose asserts facts that later change; the fix is to let prose assert as little as possible and point at things that are checked. Examples are checked by compilation; the Reference is anchored to the Lean and to generated grammar; status is a CI signal. The narrative book — the artifact a model is most likely to hallucinate — ends up carrying almost no standalone claims, so it cannot drift in the ways that matter.

Three books rather than one because the three reader-questions are genuinely different and have different cadences: a learner wants a motivated path, a practitioner wants a precise rule, and either wants to see code run. Folding them into one artifact is exactly the conflation that produced today’s drift. Splitting them lets each take the drift-control that fits — transcluded-and-compiled for examples, anchored-and-generated for the reference, point-don’t-restate for the book.

No published status surface, and that is forced. The Context shows status is wrong when read from prose and wrong when read from code comments; any hand-maintained surface is a smaller copy that drifts the same way. With the language done enough that “what is implemented today” is no longer a question the documentation must answer, the right move is to publish no status surface at all and let the books describe the language as it is. The anti-drift value is kept where it belongs — as an internal CI check (check-coverage) over signals that are already checked: a green example, a present diagnostic, an existing Lean symbol. The check fails loudly if a cited signal vanishes, so the reference cannot quietly fall behind, without any reader-facing grid to drift.

Lean-provenance in the Reference earns its place twice: it is a trust asset unique to a language with a mechanized substrate (“this rule is proven sound; here is the theorem”), and it is a drift anchor (a CI check that the cited symbol exists). It must be honest about the reasoner inversion: where Rust leads the Lean, the provenance says so rather than implying a proof that is still owed.

The plan is sequenced by dependency and by risk. Tooling first because everything stands on it. A tracer bullet before fan-out because the integration seams — transclusion anchors, the provenance link format, status derivation — are where a docs system of this size will actually break, and proving them on one concept is far cheaper than discovering them across the whole corpus.

Alternatives considered

  • One mdbook with clearer parts. Lower effort, but the audience-and-timeline conflation that causes the drift survives. Rejected.
  • Keep inline status badges, auto-generated from the registry. Preserves at-a-glance reading, but it re-introduces status into the normative prose surface. Rejected: keeping the spec timeless means publishing no status at all (the decision retired even the generated grid).
  • A hand-maintained coverage ledger. This is a smaller version of the old hand-maintained STATUS.md feature matrix and drifts the same way. Rejected in favor of CI-derived status.
  • A single big-bang reference rewrite. The reference is largely sound; the drift engine is the problem, not the prose. A rewrite would re-create the drift the day it shipped. Rejected in favor of decoupling status, anchoring claims, and fixing the localized offenders.
  • RFDs and Lean inline in the reading path. Rejected: they are referenced archives. Inlining design history and proof into a learner’s or practitioner’s path is the conflation again.

Consequences

  • New build tooling and CI gates (registry, transclusion preprocessor, drift-check, freshness gate, Lean-link check).
  • The Reference loses inline status badges and publishes no status surface; the anti-drift signal lives in the internal check-coverage check instead; RFDs leave the Reference reading path; the diagnostic corpus becomes a shared single source feeding both ox explain and the error index.
  • A standing obligation: a feature PR touches its example, its registry signal, and (when relevant) its doc section — enforced softly at first.
  • A positive side effect: the documentation build surfaces language drift as tickets, making the docs a continuous correctness audit.
  • Migration is evolution, not rewrite — the crash course seeds the Book, the numbered chapters become the Reference once decoupled and anchored, and the existing example packages become the By Example substrate once re-verified against main.

Open questions

  • The tracer-bullet concept for Wave 0.5 — first-class relations (exercises concepts, relations, and Lean-provenance together, and is the framing most in need of being publicly precise) versus refinement (iff/where).
  • Whether the freshness gate ships soft-warning or hard-fail, and on which triggers.
  • The Book’s per-chapter granularity, and whether a task/idiom cookbook is part of Argon by Example or a separate surface.
  • Hosting layout: three books under one site with a shared theme and a portal landing page.
  • How the value/ontology boundary is taught — as its own early chapter, or woven through Modeling.

RFD 0051 — oxfmt: a canonical, idempotent source formatter

  • State: discussion
  • Depends on: the shared lossless rowan CST (oxc-parser, oxc-syntax); the [[notation]] table and the dual-notation surface (§2.4.1); the [fmt] notation-policy cascade (RFD 0013 toolchain, #762); the example corpus (RFD 0044 / RFD 0050), which the formatter is exercised against.
  • Prior art: the pretty-printing lineage — Oppen, Prettyprinting (1980); Hughes, The Design of a Pretty-Printing Library (1995); Wadler, A Prettier Printer (2003); Lindig, Strictly Pretty (2000); Bernardy, A Pretty But Not Greedy Printer (2017). Production formatters built on that algebra — gofmt, Black, Prettier, dprint, Biome, nixfmt, Dhall, and Lean’s printer. The normal-form framing — Eberhart, On the Relationship Between Parsing and Pretty-Printing (2012); Clarke, Liepelt & Orchard, Scrap Your Reprinter (2017), on why layout-preservation and canonicalization conflict; Black’s assert_equivalent AST-safety check.

Question

What should Argon’s ox fmt / oxfmt be? The shipped v1 normalized whitespace only — it stripped trailing space and capped blank lines but could not re-indent, reflow, or impose a canonical layout, so the promise in the toolchain chapter that “spacing and layout are the Argon convention” had no implementation behind it.

Context

A formatter is the normal-form function for layout-equivalence on programs. Define s₁ ∼ s₂ iff parse(s₁) = parse(s₂); then fmt = print ∘ parse sends each text to the canonical representative of its class. Two properties follow from that framing rather than being engineered:

  • Idempotence (fmt(fmt(s)) = fmt(s)) is forced — the normal form of a normal form is itself.
  • Canonical form is defined by the printer, not discovered. There is no platonic “right” layout; the break and spacing rules are the definition.

Three facts about the codebase shape the design. The parser already produces a lossless rowan CST in which trivia (whitespace, the four comment kinds) are ordinary tokens — so losslessness is structural, with no parse-time attachment to get wrong. The grammar is uniform: every composite node is a delimited comma-list, a brace-delimited item/statement body, a bare comma-list, or an inline token sequence — so the lowering is a generic element walk plus a few family handlers, not ~100 bespoke rules. And the surface is newline-insensitive, so layout carries no meaning to preserve beyond the author’s blank-line paragraphing.

Decision

Build the formatter as a normalizer over a Wadler/Oppen combinator document IR, fed from the shared CST, emitting a single canonical layout with near-zero configuration, behind a layered correctness guard. Concretely:

  1. Document IR (the one irreversible choice). Each node lowers to a Doc of text / line / softline / hardline / nest / group / concatenation; a renderer resolves each group to flat-or-broken by whether the flat form fits the target width. A naive transcription of Wadler’s lazy algorithm is exponential in a strict language, so the renderer is the strict (Lindig) reformulation: a single work-stack with bounded look-ahead and pre-propagated break flags (a group containing a forced break never probes flat), keeping it linear.

  2. Greedy selector. Each group is resolved locally (flat if it fits, else broken). Greedy is space-suboptimal on a minority of constructs; an optimal cost-based evaluator can be swapped in behind the same IR per-construct later, against the real corpus — that is not an architectural fork.

  3. Near-zero configuration. The only layout knob is the target line width. Everything else is fiat (the gofmt/Black/Dhall posture, matching Argon’s single-canonical-form stance). Notation direction (preserve / unicode / ascii) is policy, resolved through the existing ox.toml cascade, not layout.

  4. Canonicalize, do not preserve. A single tool cannot both preserve the author’s layout and produce an idempotent canonical form — the two obey conflicting lens laws (Clarke et al. 2017). oxfmt chooses canonicalization: it discards input layout and imposes the canonical one. The one author signal it keeps is blank lines between items, collapsed to at most one (gofmt semantics). A layout-preserving reprinter — for refactoring tools and LSP code actions — is a separate tool with distinct laws and is out of scope here.

  5. Trivia stays in the CST. Comments are read from the tree and placed by line ownership (own-line comments lead, same-line comments trail); the /// / //! / // / /* */ distinctions are preserved and comment interiors are never reflowed. Post-parse attachment-to-AST-nodes — gofmt’s self-described “biggest mistake” — is avoided.

  6. Macro and quote {} bodies are opaque. A macro body is parsed twice — by the host grammar (where layout is dead) and by the macro engine (where token adjacency, repetition spacing like $($x:tt),*, and trailing commas can be load-bearing). Host-grammar token-equivalence therefore does not imply the macro is unchanged, so oxfmt renders macro definitions and quote {} subtrees verbatim and formats only the surrounding code (the rustfmt-conservative stance). This keeps the guard below sound — the formatter never alters bytes it cannot prove inert — with the matching consequence that a macro-bearing file is not layout-invariant (its body layout is preserved), exactly as blank-line paragraphing is.

  7. A meaning-preservation safety guard (the maximal-correctness contract). Every rewrite is verified before it is returned: the candidate must re-parse cleanly, its significant-token sequence (kind + text, modulo trailing commas, which are layout in Argon’s grammar) must equal the input’s, and its comment multiset must match. On any failure the formatter emits the input unchanged — it is, by construction, incapable of changing what a file means. This is stronger than an AST-shape comparison because the lossless tree lets the guard check comment preservation directly.

  8. Location. In oxc-fmt, over the shared oxc lexer/parser/CST — no second grammar (the Nix cautionary tale). ox fmt [--check] <paths> and the oxfmt binary share one path; --check is exactly the “already a normal form?” predicate.

The canonical-forms layering (why this ships now)

Settling a canonical surface form (clause ordering, dual-form spellings, the forall/exists surface, predicate-sublanguage unification) refines by merging classes, giving a chain ∼₀ ⊆ ∼₁ ⊆ ⋯. The formatter is well-defined at every stage, and each later decision lands as one additional confluent pass that only shrinks the already-canonical set — it never invalidates prior output. So the layout floor ships now on settled syntax, and the unsettled canonical-form decisions are neither blocked by nor forced by the formatter; each becomes a small pass with a built-in acceptance test (idempotence + token/comment equivalence on the corpus) when its decision lands.

Correctness

The layered net, each rung catching what the one below misses:

  1. Parse-preservation — the output re-parses cleanly (cheapest gate).
  2. Token-and-comment equivalence — the safety guard above, enforced on every format.
  3. Idempotencefmt(fmt(s)) = fmt(s) byte-exact, over the whole corpus and under fuzzing.
  4. Layout-invariance (modulo blank lines) — re-spacing a file’s token stream (mangling indentation and intra-line spacing, preserving paragraph breaks) yields identical canonical bytes across the corpus: input layout does not leak into output.
  5. Totality / no-panic fuzzing — arbitrary-string and token-salad property tests confirm the lowering and renderer never crash and every produced output is a fixed point.

What is out of scope

The layout-preserving reprinter (refactoring / range edits); incremental and range formatting for the LSP; an optimal cost-based evaluator beyond the constructs that demonstrably need it; and forcing the unsettled canonical-surface decisions, which the layering above defers to their own rulings. A code style guide (spec/reference/code-styleguide.md) describes the canonical layout the implementation produces — derived from the formatter, not the other way round.

RFD 0052 — Deployment topologies, the connection abstraction, and host-language parity

  • State: discussion

Question

Argon is a language backed by a database. A model is rarely a standalone artifact — it is embedded into systems written in general-purpose languages, or deployed as its own service those systems call. How is Argon deployed and consumed across three independent choices — where the database runs (embedded in a host process vs. a standalone process), how a host reaches it (a native call, a wire protocol, or a generated SDK over either), and which host language drives it (Rust and TypeScript, co-equal) — and where exactly does the embedded/standalone line fall for concurrency?

This RFD does not define the serving API (RFD 0014), the in-process runtime contract (RFD 0020), persistence swap and connectors (RFD 0036), or the derivation surfaces (RFD 0046). It sits above them and fixes the consumption model they compose into.

Context

Most databases ship an object-relational mapper because there are two models that do not agree: the store’s data model and query language on one side, the host’s object model on the other. The ORM is the impedance-matching middleman — leaky, drift-prone, two sources of truth kept in sync by hand.

Argon has one model. Schema, type system, constraints, queries, and reasoning are all declared in Argon under a single type discipline. So a host-language binding is not a mapping between two models; it is a faithful typed projection of the one model into the host language, drift-gated the way the Lean↔Rust interface is. The generated client is thin and total: it exposes the model’s declared query / mutate / derive / compute surface (and the ad-hoc surface of RFD 0033) as typed host functions, and the host types are the schema. The schema-migration drift an ORM fights does not arise — the host’s types are regenerated from the model and gated on change.

Two facts follow and frame everything below:

  • The generated SDK is one access mechanism, not the data model. It must be orthogonal to where the database runs.
  • Argon must be deployable as a database, not only embeddable as a library. Embedded (linked into a host process), sidecar (a process beside the host), and standalone (its own networked, durable service) are all first-class.

Consumers include ordinary Rust and TypeScript services, the Tide TypeScript workflow runtime, and — by construction — a future Rust durable-execution library. None is privileged; each reaches Argon through the same surface.

Decision

D1 — Two orthogonal axes: topology × access

TopologyWhere it runsAccessAnalogue
Embeddedruntime linked into the host process; local storagenative call (Rust crate / TS napi) or SDK over an embedded handleSQLite, DuckDB
SidecarArgon process beside the host on one nodewire (/v1) or SDK over a local clientlocal Postgres
StandaloneArgon as its own durable, networked servicewire or SDK over a remote clientmanaged Postgres

The generated SDK sits on top of the access column: it wraps the native handle in the embedded case and the wire client in the sidecar/standalone cases. A host writes to the SDK and chooses a topology at deploy time by which handle it constructs.

D2 — One connection abstraction

There is a single Connection abstraction whose surface is the OxbinRuntime semantics of RFD 0020, with two implementations: Embedded (links the runtime, local storage) and Remote (a /v1 client, capability-gated). ox gen emits code written against Connection, so host code — model::queries::all_staff(conn) — is identical whether conn is in-process or a network client. This one indirection is what makes “use Argon any of these ways” a fact rather than a slogan.

D3 — Host-language parity

ox gen --target rust and ox gen --target ts are co-equal deliverables. Both project the same model from the same source, expose equivalent typed surfaces, and are both in-process-capable and remote-capable. Both must emit the model’s rich result shapesTruth4, standpoint-tagged, set-valued, ordinal results — idiomatically (a Rust enum; a TS discriminated union), never a scalarized flattening. In-process mechanics differ by host: Rust links the runtime crate; TypeScript uses a napi bridge (D7); HTTP is the universal floor for both.

D4 — The concurrency contract is set by topology

This is the line between an embedded library and a production database, and it is deliberate:

  • Embedded ⇒ single-process ownership. Durable embedded storage is owned by exactly one process: in-memory for tests, a single-owner file backend otherwise. There is one writer. Embedding does not promise multi-process shared-file access; a host that needs concurrent multi-writer access has, by that need, chosen the sidecar or standalone topology.
  • Standalone ⇒ the serve layer is the single logical writer, and must handle concurrent multi-caller load gracefully and performantly. A standalone Argon is a world-class production database server: connection handling and admission control, snapshot-isolated concurrent reads over the bitemporal log, serialized promotion of writes (one logical writer; no silent merge), backpressure, and fair scheduling. The detailed design of this layer is the significant engineering effort this RFD opens (see Consequences); this RFD fixes the contract, not the mechanism.

D5 — Access is provided; durability layers on top

Argon provides access: the connection, the generated SDK, and the serve layer. A workflow runtime provides durability: journaling, deterministic replay, and fork-scoped writes, layered over a Connection. Determinism is the workflow wrapper’s concern — pin a transaction-time read point, journal the reads, scope writes to a fork — and works over any connection regardless of topology. Argon stays workflow-agnostic; Tide is the TypeScript implementation of this wrapper; a Rust durable-execution library would be another. Workflow semantics are never baked into the runtime.

D6 — Semantic transparency across transports

Embedded and remote differ only in latency and in the capability boundary — never in expressivity or in the shape of what comes back. as_of(vt, tt) means the same; rich results survive the wire (CBOR) identically to in-process. The /v1 protocol and the in-process trait are two encodings of one semantics. Capabilities are enforced at the network edge — generic writes denied, only the declared (and ad-hoc-permitted) surface dispatches — and are not imposed on the in-process, in-trust embedded caller. The SDK surface is identical across topologies; the remote path additionally enforces capabilities.

D7 — Tide extraction and the JS bridge

Tide is extracted to its own repository, structured like Argon, with its orca-mvp couplings (world-state, kernel-storage) severed to traits so its core depends on no orca-mvp component and runs non-ontology workflows. The dependency direction is tide → argon; the Argon binding lives on the Tide side; Argon remains ignorant of workflows. The JavaScript↔runtime in-process bridge (napi, later optionally WASM) is an Argon component — it is about embedding Argon in JavaScript, not about workflows — and Tide reuses it for its ops, so standalone Bun/Node services get the same in-process embedding a Tide workflow gets.

Rationale

The orthogonality in D1/D2 is the whole point of removing the ORM. Because there is one model, the typed projection is faithful and thin, so it can be a facade over any transport without re-introducing a second model. Collapsing topology and access — making the SDK mean “remote” or making “embedded” mean “no SDK” — would rebuild the middleman it eliminated.

D3’s parity is a requirement, not a courtesy: Argon is consumed at least as much from Rust as from TypeScript, and today only the TypeScript projection exists (because Tide needed it). Rust consumption is currently “link the runtime and hand-wire it,” which is below parity. The rich-shape clause is where the no-silent-scalarization discipline lives — a contested or set-valued result that the embedded path returns intact and the remote path or the codegen flattens would make D2/D6 a lie.

D4 is the production hinge. Embedded single-ownership keeps the SQLite/DuckDB contract honest and cheap. Standalone multi-caller concurrency is the price of being a real database, and it is where the hard engineering is; naming it as a contract now prevents an embedded-shaped design from being quietly assumed to scale to a server.

D5 keeps the runtime clean and reusable. Journaling and replay are properties of orchestration, not of data access; pushing them into Argon would couple the database to one workflow system and bar the plain (non-durable) Rust or TS consumer. The determinism design composes with D6: a tx-pinned as_of read is reproducible whether the connection is embedded or remote.

D7’s direction is the standard foundation rule: a foundation does not depend on its consumers. Locating the napi bridge in Argon, not Tide, follows from what it is — Argon-in-JavaScript — and lets one bridge serve both standalone JS services and Tide ops.

Alternatives

  • A meta-build-system wrapping Cargo and npm/Bun. Rejected. Integration follows the protobuf/protoc precedent: the model is the neutral source (like a .proto), ox gen is the generator, and the host’s native build drives codegen (build.rs for Rust; a prepare step for TS). The moment the toolchain owns the host build it becomes a framework and stops being portable — the ODE failure mode.
  • SDK-only; no native embedding. Rejected. Argon is a database; embedded in-process operation (SQLite/DuckDB-shaped) is a primary topology, not a remote-only convenience.
  • Multi-writer embedded shared-file access. Rejected for v1. Concurrency control across processes sharing one on-disk store is the standalone topology’s problem; embedded promises single-owner.
  • A dedicated binary wire protocol now. Deferred. /v1 HTTP is the one wire protocol initially; a binary/streaming protocol can later sit behind the same Remote handle without changing host code.
  • A third storage time-axis for law enactment/effective dates. Rejected as a storage concern. The store stays two-axis (valid-time, transaction-time); rule version and effective date are ordinary bitemporal facts interpreted by the reasoning layer (consistent with RFD 0036’s “richer temporal structure as payload”).

Consequences

  • ox gen --target rust is a new first-class deliverable, co-equal with the TypeScript target: typed concept types, typed query/mutate/derive wrappers over Connection, the CBOR codec, and a generated /v1 client.
  • A JavaScript↔runtime napi bridge becomes an Argon component, shared with Tide’s ops.
  • The standalone serve concurrency layer requires real engineering — admission control, snapshot-isolated concurrent reads, serialized promotion, backpressure, scheduling — and is the next deep design effort this RFD opens, deferred behind the portable-substrate phases and tracked in issue #978. It extends RFD 0014’s surface with a concurrency-and-load contract.
  • Tide is extracted to its own repository with orca-mvp couplings reduced to traits; the Argon binding and ox-tide plugin shim live Tide-side.
  • ox grows cargo-style plugin discovery (ox <name>ox-<name> on PATH), so ox tide run is convenience and tide run standalone always works.
  • Host integration follows the native build of each language; ox.toml remains the consumer-agnostic model manifest and gains no host-codegen configuration (that lives host-side).

Open questions

  • TypeScript in-process: napi-first or WASM-first? Leaning napi-first (Bun and Node both support it; shares code with Tide ops), WASM later for edge/browser. HTTP is the floor regardless.
  • ox gen --target rust: build.rs/OUT_DIR or a published crate? Leaning build.rs (the prost/tonic model, no per-version crate churn), with “emit a crate” as a flag when several hosts share one model.
  • One wire protocol or two? /v1 HTTP now; a binary protocol only if the hot path demands it, behind the same Remote handle.
  • Embedded durable concurrency: what does single-owner mean concretely for the file backend — advisory lock, lockfile, exclusive open?
  • Standalone concurrency model specifics: how reads achieve snapshot isolation over the bitemporal log; how promotion serializes against concurrent readers; the admission/backpressure policy under load.
  • Does oxup distribute Tide as a component (matching-version pinning, the rustup-toolchain model), or does Tide ship a standalone installer first?

RFD 0053 — The standalone concurrent engine: transactor, MVCC reads, and IVM-in-lockstep

  • State: discussion
  • Depends on: RFD 0052 (deployment topologies — sets the contract this RFD mechanizes), RFD 0014 (serving surface this extends), RFD 0020 (runtime engine / Engine::evaluate), RFD 0018 (DBSP reasoner / DRed maintenance), RFD 0036 (heterogeneous stores — frozen-foreign federation, the P1 durability spine), RFD 0035 (composable pipeline / persisted read-model), RFD 0025 (mutation delta-guard atomicity), RFD 0046 (derived delta as a commit by-product)
  • Tracks: issue #978
  • Prior art: Datomic’s transactor / single-writer + MVCC-snapshot reads over an immutable log (Hickey); PostgreSQL MVCC (Stonebraker, Ports & Grittner SSI); group commit (DeWitt et al. 1984; ARIES, Mohan et al.); DBSP incremental view maintenance (Budiu et al. 2023) and DRed (Gupta–Mumick–Subrahmanian 1993); well-founded semantics (Van Gelder–Ross–Schlipf 1991); content-addressed immutable storage (Merkle; Git); the tail-latency / fair-scheduling case for admission control (Dean & Barroso, “The Tail at Scale”)

Question

RFD 0052 fixes the contract for the standalone topology: a standalone Argon is a production database server whose serve layer is the single logical writer, serving concurrent multi-caller load with snapshot-isolated reads, serialized promotion, admission control, backpressure, and fair scheduling. It deliberately defers the mechanism to this RFD.

What is that mechanism? Concretely: how does one logical writer allocate monotonic transaction time and commit durably without blocking readers; how do readers obtain a consistent snapshot over the append-only bitemporal log without blocking the writer; how does the incremental view maintainer advance derived views in lockstep with each commit so that a query is consistent with the transaction time it reads; and how do admission control, crash recovery, and the federated/heterogeneous backing compose with all of the above — without disturbing the embedded single-owner path RFD 0052 preserves.

This RFD does not redesign the reasoner (RFD 0018/0020), the federation soundness gates (RFD 0036), or the connection/SDK surface (RFD 0052). It composes them into a concurrent server.

Context

What already exists

A substantial fraction of the contract is already built; this RFD must not re-design it. The as-built state:

  • Monotonic transaction time exists and is correct. next_tx_nanos lives in the storage backend behind a write lock; every append assigns tx_from from it and ratchets it forward, never backward (oxc-storage-mem). Store::current_tx_nanos() returns the largest assigned tt. The clock is already a strictly monotonic, single-threaded-by-construction allocator.
  • MVCC-shaped reads exist. RuntimeReadPoint/RuntimeAsOf carry Now | AtTt(tt) | AtVt(vt) | At{vt,tt}; query_extent_at filters the append-only log by bitemporal visibility (tx_from ≤ tt < tx_to, valid-time window). Paginated reads already pin a concrete tt into the cursor so a multi-page walk sees one fixed snapshot (oxc-serve R-M17/#259). Reads never take the mutation lock today.
  • A single logical writer per scope exists. The serve layer serializes mutation dispatch per (tenant, fork) scope under a per-scope async mutex (mutation_locks, #271/#224); distinct scopes run in parallel. The mem backend additionally serializes every write behind the ServiceState RwLock.
  • Atomic commit with a delta-guard exists. A mutation buffers its whole body, pre-validates against a discardable overlay store, runs the RFD 0025 check delta-guard (violations(post) ⊆ violations(pre)), and only then flushes to the committed store — all-or-nothing (run_operations/flush_mutation_buffer).
  • The IVM maintainer exists and is wired per-commit. IncrementalMaintainer holds persistent join arrangements and a DRedc retraction structure; maintain_after_commit applies a per-commit (retracts, then asserts) delta, advancing a write-generation watermark on success and dropping the cache to rebuild otherwise (#437/#444). Monotone asserts take the incremental path; non-monotone asserts and ineligible modules (iff, navigation-from, new individuals) fall back to a correct full recompute. Engine::evaluate is the single evaluation path; WFS handles recursion-through-negation per stratum.
  • A durable, content-addressed log exists. oxc-connection::open_durable over FileKvStore: event bodies are immutable content-addressed segments (filename = BLAKE3, tamper-evident by read-back), and one mutable per-scope manifest lists the segment ids in commit order plus a watermark. The commit point is write-temp → fsync → atomic rename — torn writes are impossible. Recovery replays the manifest-ordered scan into a fresh store; tx-times are carried on each event so pinned reads reproduce post-restart.
  • Crash-safe, conflict-free federation exists. Foreign relations are read-only by construction (the ForeignRelation SPI has no write method) and frozen once into the catalog before the synchronous fixpoint (RFD 0036 D11). The per-placement LCWA world gates (OE0901 NAF-over-OWA, OE0904 recursion-through-source) fire at build time. The only cross-store write admitted is the P3 idempotent content-put, which crosses no entity boundary.
  • Admission primitives partly exist. Per-request wall-clock timeout, a reasoner budget checked at fixpoint round boundaries, body-size and result-row caps with loud refusals. Tower middleware is in place.

What is missing — the gap this RFD closes

The as-built engine is correct under concurrency but tuned for the embedded and warm-cache serve cases, not for sustained multi-caller write load on a standalone server. The specific gaps:

  1. No durable group commit. Each append_batch is a solitary write-temp → fsync → rename. Under N concurrent writers to one scope this is N serial fsyncs on the critical path — the classic group-commit bottleneck (DeWitt 1984). Throughput is fsync-bound.
  2. No pipelined / asynchronous commit submission. A writer blocks on its own durable commit before the next can be admitted; there is no decoupling of append from durable-acknowledge from visible-advance.
  3. The visible-tt advance is implicit, not a published commit boundary. Readers resolve Now to whatever current_tx_nanos() happens to be; there is no explicit “last durably-committed and IVM-maintained tt” watermark that a reader pins to get a snapshot guaranteed consistent with maintained derived views.
  4. No global admission control, per-tenant fair scheduling, or backpressure. The only queue point is the per-scope lock; a hot tenant or a flood of requests has nothing throttling it but timeouts. There is no concurrency semaphore, no fair queue, no load-shed.
  5. As-of-past reads of derived views are unsolved. Current-tt derived reads hit the maintained model; a derived read at a past tt has no answer short of full recompute. #978 names this the one genuinely open data-structure question.
  6. The IVM maintainer’s state is not checkpointed. Recovery rebuilds arrangements from a full event-log replay + recompute; for a large store this is an unbounded cold-start.

These are production-hardening, not correctness, gaps — which is why RFD 0052 could defer them. This RFD designs the mechanism, sequenced so each phase is independently valuable and the embedded path is never disturbed.

Decision

The standalone engine is a single-logical-writer transactor fused with an MVCC reader plane and an IVM maintainer running in lockstep with the commit stream — Datomic’s concurrency model (one transactor, immutable log, snapshot reads) with the derived layer maintained incrementally by the DBSP/DRed maintainer as the commit tt advances. The design is a sequence of decisions over the as-built substrate, not a rewrite.

D1 — The transactor is the one logical writer; its commit is a three-phase published boundary

A scope’s writes pass through a single logical writer (the existing per-scope serialization, generalized). A commit is three explicit, separable phases:

  1. Append — assign the next monotonic tt, write the event(s) into the in-memory store and the in-memory commit buffer. Cheap; under the scope lock.
  2. Durable-acknowledge — the event bodies (already content-addressed segments) and the scope manifest reach disk and fsync returns. This is the durability point and the group-commit batching point (D4).
  3. Advance-visible-tt — publish the new commit tt into a per-scope visible watermark (a single atomic, monotonic value). Only after durable-acknowledge does the watermark advance, so a reader that pins the watermark reads only durably-committed state.

The ordering is append → durable-commit → advance-visible. A crash between append and durable-acknowledge loses the uncommitted tail (correct: it was never acknowledged); a crash between durable-acknowledge and advance is recovered by reading the manifest watermark on restart (the manifest is the source of truth, the in-memory visible watermark is a cache of it). Read-your-writes within a transaction is already provided by the within-body overlay (RFD 0015) and the buffer-then-commit discipline; this decision adds no new RYW mechanism, it makes the cross-transaction visibility boundary explicit.

D2 — Reads are MVCC snapshots pinned to the visible watermark; readers and the writer never block each other

A read resolves its read point once, at admission:

  • AtTt(tt) / At{vt,tt} — an explicit historical snapshot; read it directly.
  • Now — resolved to the scope’s current visible watermark (the last durably-committed, IVM-maintained tt), and that concrete tt is what the read uses (and what a paginated cursor pins, extending the existing R-M17 mechanism from “the store’s current_tx_nanos” to “the published visible watermark”).

Because the log is append-only and events are never mutated in place, a snapshot at tt is simply the set of events visible at tt — no read locks, no undo segments, no vacuum. A reader holds no lock the writer needs, and the writer appends new events (with strictly larger tt) that the reader’s pinned snapshot does not see. This is the as-built posture (reads never take the mutation lock); D2 formalizes it as visibility against the published watermark rather than against a racing current_tx_nanos(). The visibility rule is unchanged: an event is visible at (vt, tt) iff tx_from ≤ tt < (tx_to or +∞) and the valid-time window contains vt.

A long-running reader does not retain old versions at cost: the immutable log already keeps all versions, so MVCC here is free of the version-store / vacuum machinery a mutable-page database needs. The only retention concern is forget (physical erasure, capability-gated); a forget is a writer event like any other and a snapshot pinned before it still observes the data only if the segments survive — forget semantics (GC of erased segments vs. snapshot retention) are an open question (Q4).

D3 — IVM runs in lockstep with the commit stream

The maintainer advances in step with the visible watermark: when a commit’s durable-acknowledge completes, the maintainer applies that commit’s (retract, then assert) delta (the as-built apply_commit_delta), and only then is the visible watermark advanced to that tt. The invariant this buys:

A read pinned at the visible watermark sees a derived model consistent with exactly the committed base facts at that tt. Derived views never lag or lead the base facts a query reads.

This is the lockstep contract. It composes with the existing recompute-vs-incremental choice unchanged: monotone, delta-simple commits maintain incrementally (cost ∝ frontier); non-monotone or ineligible commits trigger a bounded recompute of the affected strata (Engine::evaluate per stratum, WFS for recursion-through-negation) before the watermark advances. Maintenance is therefore on the commit critical path — which is acceptable because (a) the incremental path is microseconds at 100K facts (#437), and (b) the group-commit batch (D4) amortizes a single maintenance pass over a batch of base deltas where the program admits it. A commit whose maintenance fails drops the cache and forces the next read to rebuild — the watermark still advances (base facts are committed and durable), and the rebuild is a read-side cost, not a write-side stall.

As-of-past derived reads are resolved (Q1): checkpoint-and-replay — restore the nearest checkpoint of the derived read-model and replay forward to the requested tt, rather than keeping versioned arrangements (a bounded history of arrangement deltas keyed by tt). The maintainer keeps no per-tt arrangement history on the hot path; a past derived view is recomputed from the base log + nearest prior checkpoint. Two binding constraints fall out of the resolution and are stated with it under Open questions (Q1): checkpoint cadence is configurable, not assumed-rare, and replay must use the law as-of-then (the rules must be enactment-time-bitemporal — a Phase-5 prerequisite).

D4 — Group commit batches durable acknowledgement; submission is pipelined

The transactor decouples the three phases of D1 so that durable acknowledgement is batched and submission is pipelined:

  • Group commit. Concurrent transactions to a scope (or, with a per-scope manifest, across scopes sharing one durable backend) that have appended are coalesced into one durable batch: their event segments are written, then one manifest rewrite + fsync acknowledges all of them. This is the standard group-commit amortization (DeWitt 1984; ARIES) — the per-transaction fsync cost falls to (fsync latency) / (batch size). The batch boundary is a short time/size window; a transaction waits at most one window for its durable-acknowledge.
  • Pipelined submission. Append (phase 1) for the next transaction proceeds while the previous batch is in durable-acknowledge (phase 2); the transactor does not serialize the cheap append behind the expensive fsync. Transaction tt order is the append order (monotonic), and the visible watermark advances in that order as batches acknowledge — so pipelining never reorders commits or exposes a gap. A batch that fails durable-acknowledge fails all its members atomically (none advance the watermark); the in-memory appended tail is rolled back to the last durable tt.

The existing single-shot append_batch (already atomic write-temp → fsync → rename) is the degenerate batch-of-one; group commit generalizes it. The content-addressed segment write is idempotent (re-putting identical bytes is a no-op), so a batch retried after a partial failure is safe.

D5 — Admission control, fair scheduling, backpressure, timeouts

The server gains an explicit admission layer in front of dispatch:

  • Concurrency semaphore. A bounded global (and optionally per-scope) permit pool caps in-flight requests; CPU-bound reasoning runs under block_in_place so a permit maps to bounded compute, not an idle await. Exhaustion is backpressure, not unbounded queueing.
  • Per-tenant fair scheduling. Admission is a fair queue keyed by tenant (weighted round-robin / deficit round-robin), so one tenant’s burst cannot starve others — the multi-tenant analogue of fair CPU scheduling, and the direct mechanism for RFD 0052’s “fair scheduling” clause. Writes additionally fold into their scope’s commit batch (D4), which is itself a fairness point.
  • Backpressure. When the semaphore or a queue is saturated, the server sheds load loudly with a structured 503/retry-after envelope (the same loud-refusal discipline as the existing result-cap and timeout) — never a silent truncation, never an unbounded queue that converts overload into latency collapse (Dean & Barroso).
  • Timeouts. The existing per-request wall-clock deadline and reasoner round-boundary budget remain the upper bound; admission adds an enqueue deadline so a request that cannot get a permit within its budget fails fast rather than occupying queue depth.

Defaults are conservative and operator-tunable via the existing OperabilityLimits, extended with semaphore size, per-tenant weights, and queue-depth caps.

D6 — Crash recovery reconstructs state and the maintainer from the log

Recovery is the as-built replay, made explicit and checkpoint-accelerated:

  1. Open the durable store; read each scope’s manifest (watermark + ordered segment ids).
  2. Verify integrity on read-back: each segment’s content hashes to its filename; a mismatch is a loud refusal (corruption is never silently tolerated). This is the tamper-evident property — content-addressing is the integrity check; there is no separate prev-hash chain, and #978’s “hash-chain on append” is satisfied by the content-addressed manifest (the manifest is an ordered list of content ids, so the manifest’s own content id is a Merkle commitment to the whole log prefix). Whether to add an explicit running prev-hash for tamper-evidence-of-ordering (vs. tamper-evidence-of-content, which content-addressing already gives) is Q5.
  3. Replay the manifest-ordered events into a fresh store, preserving each event’s tt (so pinned historical reads reproduce).
  4. Reconstruct the maintainer. Today: a cold rebuild via Engine::evaluate over the replayed EDB. With a maintainer checkpoint (the RFD 0035 D7 / 0036 D9 persisted read-model segment — a content-addressed columnar payload keyed by (module_fingerprint, storage_gen)), recovery seeds the maintained model from the checkpoint and replays only the suffix of commits after the checkpoint’s tt. The checkpoint is declined on any key mismatch (schema change, divergent watermark) and recovery falls back to full rebuild — never a stale model.
  5. Publish the visible watermark = the manifest watermark; the server admits traffic.

Durability and integrity are the manifest’s atomic rename + content-addressing; no additional WAL is introduced (the event log is the WAL — append-only, the recovery source of truth).

D7 — Federation composes by construction; the transactor writes only Argon’s own log

The federated / heterogeneous backing composes with the transactor and MVCC without new transaction machinery, because RFD 0036 already constrains writes:

  • Foreign stores are never write targets. The transactor writes only Argon’s own append-only log. There is no cross-store write transaction and no 2PC (RFD 0036 D2). A federated read joins frozen foreign EDB snapshots with Argon’s own facts; the freeze happens once per query before the fixpoint, so a federated read is itself a consistent snapshot (the Argon side at the pinned tt, the foreign side at its single fetch).
  • The per-placement LCWA world gates are upstream of the transactor. OE0901 / OE0904 fire at build time; the transactor inherits a program already proven sound for its foreign placements. The runtime per-placement world map (merged at module load) governs NAF resolution identically under concurrency — the transactor introduces no new world-assumption surface.
  • P1 persistence-swap is the transactor’s durable backend, not a foreign store. When Argon’s own log lives in DynamoDB / FoundationDB / RocksDB (RFD 0036 D9), the commit spine is the same three phases (D1) with durable-acknowledge being the Datomic-shaped single linearizable CAS on the root/watermark pointing at an immutable content-addressed manifest. The bulk segment store needs only eventual consistency (immutable data); only the root CAS must be linearizable. Group commit (D4) batches into one manifest + one CAS. The FileKvStore reference backend is the local instance of this contract.
  • The P3 content-put (the one admissible cross-store write) is idempotent and entity-local; it is a native reference event in the log (transactional, single-entity) plus an out-of-transaction content put — it does not widen the transactor’s contract.

D8 — Embedded stays single-owner; standalone is strictly additive

The embedded path (RFD 0052 D4) is unchanged: one process owns the store, one writer, the Connection is single-threaded-by-contract. Standalone adds the transactor / admission / group-commit / lockstep machinery in the serve layer and the durable backend, over the same Store, Engine::evaluate, maintainer, and event-log substrate. The Connection surface (RFD 0052 D2/D6) is preserved bit-for-bit: as_of(vt, tt) means the same, rich results survive identically, and a host cannot observe whether conn is embedded or a /v1 client — except in latency and the capability boundary. Semantic transparency is the gate: the differential strategy (below) proves embedded and standalone agree fact-for-fact.

Rationale

Why Datomic’s model and not a mutable-page MVCC (Postgres-style). Argon’s store is already an immutable, append-only bitemporal log with content-addressed segments. That is precisely the substrate Datomic chose, and it makes MVCC nearly free: a snapshot is a tt cutoff, there is no version store to garbage-collect, no undo log, no vacuum, and historical reads are first-class rather than bolted on. A mutable-page design would throw away the bitemporal log’s central property. The single logical writer is not a scaling compromise — it is what makes write-write conflicts impossible by construction (every commit gets a fresh monotonic tt; there is no lost update to detect) and what lets the IVM maintain a single coherent derived model. Read scaling is unbounded (lock-free snapshots); write scaling is one logical writer per scope, amortized by group commit — the same trade Datomic ships to production.

Why lockstep IVM rather than asynchronous materialized views. A query in Argon evaluates against the derived model (RFD 0046 D1: decisions are queries over materialized derivations). If derived views lagged the base facts asynchronously, a read-your-writes-then-query sequence could see its own base write but not the derivation it triggers — a correctness hazard, not just a staleness annoyance. Binding the watermark advance to maintenance completion makes the derived model part of the snapshot. The cost — maintenance on the commit path — is bounded by the incremental maintainer’s frontier-proportional cost and amortized by group-commit batching, and falls back to a read-side rebuild when incremental maintenance is ineligible, so the write path never blocks on a from-scratch fixpoint.

Why group commit and pipelining are the throughput levers. With one logical writer, the durable fsync is the serial bottleneck. Group commit converts per-transaction fsync cost into per-batch cost — the single highest-leverage change for write throughput, and the one every serious log-structured database ships. Pipelining keeps the cheap append off the fsync critical path. Neither changes the commit order or the visibility contract; they change only when the fsync is paid.

Why admission control is loud and fair, not silent and FIFO. A standalone database under overload must shed load predictably (loud 503, fast-fail on enqueue deadline) rather than absorb it into unbounded latency — the tail-at-scale failure mode. Per-tenant fairness is a first-class requirement of a multi-tenant server (RFD 0052), not an add-on; a single fair queue at admission is simpler and more robust than per-subsystem throttling.

Why checkpoint-and-replay for as-of-past derived reads (Q1 resolved). It keeps the steady-state maintainer data structures exactly as #437/#444 shipped them (no versioned-arrangement memory overhead on the hot path), matches the immutable-log grain (replay a suffix from a checkpoint), and reuses the already-designed persisted read-model segment as the checkpoint. It is reversible: a specific hot historical relation can be selectively versioned later if profiling demands it, but the maintainer’s core cannot cheaply be un-versioned once versioned arrangements are wired into the hot path — so the cheaper-to-reverse choice leads. And the replayed historical state is re-derived from auditable lineage rather than read out of a stored snapshot, which makes defensibility a property of provenance (the replay reconstructs the derivation, not just the answer) — the right grain for the audit/defensibility domain this engine serves. Versioned arrangements are strictly more memory and complicate the hot path for a workload (heavy historical derived analytics) that is not yet shown to dominate; they remain a selectively-applicable, measured opt-in.

Two sharpenings bind the resolution. First, checkpoint cadence is configurable, not assumed-rare: historical-derived reconstruction (“what did the system derive as of the original filing date?”) is a first-class but bursty operation in the audit/defensibility domain, so the cadence must be tunable to keep replay distance short (fast historical reads) without paying versioned arrangements’ permanent hot-path memory — the operator trades checkpoint storage for replay latency per workload. Second, replay must use the law as-of-then: a past-tt derived reconstruction must replay the rules as they were at that tt, or it applies current rules to past facts — wrong law. The rules must therefore carry an enactment-time axis (be bitemporal), which is a hard prerequisite for the Phase-5 as-of-past-derived path. RP-004 already gives the data plane a bitemporal axis (bitemporal iof), but rules today are compiled into the Module and are not enactment-time-versioned; closing that gap is on the Phase-5 critical path (#1019).

Alternatives

  • Multi-writer with conflict detection (SSI / OCC). Rejected. The immutable log + single-tt allocator makes write-write conflicts impossible by construction; introducing concurrent writers would require reintroducing conflict detection, abort/retry, and a serialization-anomaly theory (write skew) that the single logical writer eliminates for free. The per-scope writer already gives cross-scope write parallelism, which is the real multi-tenant scaling axis.
  • Asynchronous (eventually-consistent) materialized views. Rejected for the default path: it breaks read-your-writes-into-derivations (Rationale). A bounded-staleness derived read could be offered as an explicit opt-in for analytics that tolerate lag, but it is not the default and not in v1.
  • A separate write-ahead log distinct from the event log. Rejected. The event log is already append-only and is the recovery source of truth; a second WAL would duplicate it. The manifest’s atomic rename is the commit point; content-addressing is the integrity check.
  • No group commit; rely on fast NVMe fsync. Rejected. Even on fast storage, per-transaction fsync caps single-scope write throughput at (1 / fsync latency); group commit is the difference between hundreds and tens-of-thousands of commits/sec and is mandatory for the “world-class production database” contract.
  • Versioned arrangements as the default for as-of-past derived reads. Rejected as the default (Q1 resolved → checkpoint-and-replay). More memory on the hot path and not cheaply reversible; retained only as a selectively-applicable, measured opt-in for a historical-derived-analytics workload shown to dominate.
  • Sharded / partitioned writers within a scope. Out of scope. A scope is the consistency boundary; sharding a scope reintroduces cross-shard consistency. Scale across scopes (tenants/forks), not within one.

Phased implementation sequencing

Each phase is independently valuable, independently testable, and lands without regressing the embedded path. The first phases are correctness-and-clarity refactors over the as-built code; the throughput and historical-read phases build on them.

  • Phase 0 — Publish the visible watermark (D1/D2). Make the commit boundary explicit: a per-scope atomic visible watermark advanced after durable-acknowledge, and Now reads resolved against it (not raw current_tx_nanos()). Mostly a clarification of as-built behavior; the win is a precise, testable visibility contract and the hook every later phase needs. Proven by: a snapshot-isolation conformance suite (a reader pinned at W never observes a commit at tt > W; a reader’s Now is stable across the read even under concurrent writes).

  • Phase 1 — Lockstep IVM contract (D3). Bind the watermark advance to maintenance completion; assert the lockstep invariant explicitly. The maintenance call already runs per-commit (maintain_after_commit); Phase 1 makes the ordering (maintain → advance) a contract and adds the read-your-writes-into-derivations test. Proven by: a differential test — for every commit, a query at the new watermark equals Engine::evaluate over the committed base facts at that tt (the assert_maintains_against_oracle harness extended to the watermark boundary).

  • Phase 2 — Group commit + pipelined submission (D4). Coalesce concurrent appends into one durable batch (one manifest rewrite + fsync per batch); pipeline append ahead of durable-acknowledge. Generalizes the as-built single-shot append_batch. Proven by: (a) a crash-injection test (kill between append and durable-acknowledge → tail lost cleanly; kill between durable-acknowledge and advance → recovered from manifest), proving the visibility/durability ordering survives batching; (b) a throughput benchmark showing commits/sec scaling with batch size; (c) the snapshot suite from Phase 0 still green under batched commits.

  • Phase 3 — Admission, fairness, backpressure (D5). A concurrency semaphore, a per-tenant fair-scheduling admission queue, loud 503 load-shed, and an enqueue deadline, layered as tower middleware over dispatch. Proven by: a load test demonstrating (a) bounded in-flight concurrency, (b) a hot tenant not starving a cold one (fair-share latency), (c) loud shed (not silent truncation, not unbounded queue) at saturation, (d) fast-fail on enqueue-deadline.

  • Phase 4 — Checkpoint-accelerated recovery (D6). Persist the maintainer’s read-model checkpoint (the RFD 0035 D7 / 0036 D9 segment) and seed recovery from it, replaying only the post-checkpoint suffix; verify segment integrity on read-back. Proven by: a recovery test asserting post-restart state (base + derived) is byte-identical to a full replay, with checkpoint seeding measurably faster; a corruption-injection test proving a hash mismatch is a loud refusal and a key mismatch falls back to full rebuild.

  • Phase 5 — As-of-past derived reads (D3 / Q1). Implement checkpoint-and-replay for derived reads at a past tt (recompute the derived model at tt from base log + nearest prior checkpoint). Versioned arrangements remain an opt-in deferred behind a measured need. Proven by: a differential test — an as-of-past derived read equals Engine::evaluate over the base facts visible at that tt; a benchmark establishing the recompute cost envelope (the input that would justify versioned arrangements).

  • Phase 6 — P1 durable-backend transactor (D7). Generalize the commit spine over a remote durable backend (the single-linearizable-CAS-on-root contract), so the transactor runs against DynamoDB / FoundationDB / RocksDB with group commit batching into one manifest + one CAS. Proven by: the full conformance + crash suite run against a remote backend conformance harness; semantic transparency (Phase-0..5 suites green) regardless of backend.

The portable-substrate phases (RFD 0052: Connection keystone, Rust/TS codegen, napi bridge, Tide extraction) are unblocked throughout — they ride the embedded path and the existing /v1 serve, and this RFD’s phases harden the standalone serve beneath them without changing their surface.

Differential and conformance strategy

The governing proof is semantic transparency: the standalone engine returns, for every operation, exactly what the embedded engine returns — same facts, same rich shapes, same as_of semantics — differing only in latency and the capability boundary. Concretely:

  • The maintainer oracle (already shipped). FullRecomputeMaintainer is the differential oracle: every incremental maintenance is proven equal to Engine::evaluate from scratch (assert_maintains_against_oracle). Phase 1 and Phase 5 extend this oracle to the watermark boundary and to past tt respectively — a maintained/replayed derived read must equal the from-scratch model at that tt.
  • Snapshot-isolation conformance suite (Phase 0). Property tests over interleaved readers and the writer: monotonic visibility, no torn reads, stable Now within a read, no reader-writer blocking.
  • Crash-injection suite (Phases 2, 4). Kill the process at each commit-phase boundary; assert the recovered state matches the durable prefix exactly and the visibility ordering holds.
  • Load/fairness suite (Phase 3). Measured bounded concurrency, per-tenant fair-share, loud shed, fast-fail.
  • Embedded↔standalone equivalence harness. Run a corpus of programs and operation sequences through both the embedded Connection and the /v1 standalone serve; assert fact-for-fact and shape-for-shape equality (the RFD 0052 D6 transparency contract made executable).
  • No cargo test; cargo nextest run -j 4 per house rules. Each phase merges only with its suite green.

Consequences

  • oxc-serve / oxc-connection gain a transactor module: the published per-scope visible watermark, the three-phase commit, group-commit batching, and pipelined submission — over the existing per-scope serialization and append_batch.
  • An admission layer (semaphore + per-tenant fair queue + backpressure + enqueue deadline) is added as tower middleware, extending OperabilityLimits.
  • The durable backend (oxc-storage-durable) gains group-commit batching — multiple transactions’ segments + one manifest rewrite + one fsync — generalizing the single-shot path. The P1 remote-backend contract (single linearizable CAS on root) is the same spine.
  • The maintainer (oxc-reasoning) is unchanged in its hot-path data structures; recovery gains a checkpoint seed (the persisted read-model segment), and an as-of-past derived read path (checkpoint-and-replay) is added. Versioned arrangements remain an unimplemented, measured opt-in.
  • Recovery is checkpoint-accelerated and integrity-verified on read-back; the event log remains the single recovery source of truth (no separate WAL).
  • Federation is undisturbed: foreign stores stay read-only, the freeze-once rule and the per-placement LCWA gates are upstream of the transactor, and P1 swap is just the transactor’s durable backend.
  • The embedded path is untouched; the Connection and /v1 surfaces are preserved; semantic transparency is gated by the equivalence harness.
  • Risk — maintenance on the commit critical path. Incremental maintenance is microseconds (#437), but a non-monotone or ineligible commit triggers a recompute. Mitigation: the recompute is bounded to affected strata, group commit amortizes a batch’s base deltas into one maintenance pass where eligible, and an ineligible commit advances the watermark and defers the rebuild to the read side. The risk to watch is a workload that is both write-heavy and non-monotone-ineligible; Phase 1’s benchmarks must characterize it.
  • Risk — fairness under heterogeneous request cost. Reasoning cost varies wildly by program; a fair request-count queue can be unfair in CPU. Mitigation: the reasoner budget bounds per-request cost; cost-aware fair scheduling (deficit by measured compute) is a Phase-3 refinement if request-count fairness proves insufficient.

Open questions

  • Q1 — As-of-past derived reads: checkpoint-and-replay vs. versioned arrangements. RESOLVED → (a) checkpoint-and-replay. Restore the nearest checkpoint of the derived read-model and replay forward to the requested tt; the maintainer keeps no per-tt arrangement history on the hot path. Rationale (full form under Rationale): it honors the substrate’s derive-don’t-store + compaction-as-replay grain; it is reversible (a specific hot relation can be selectively versioned later, but the maintainer’s core cannot be cheaply un-versioned — the cheaper-to-reverse choice leads); and replayed state is re-derived from auditable lineage, making defensibility a property of provenance. Versioned arrangements remain a selectively-applicable, measured opt-in for a workload not yet shown to dominate. Two binding sharpenings:
    • Configurable checkpoint cadence (not assumed-rare). Historical-derived reconstruction is a first-class but bursty audit/defensibility operation; cadence is operator-tunable so replay distance stays short (fast historical reads) without paying versioned arrangements’ permanent hot-path memory.
    • Replay uses the law as-of-then — a Phase-5 prerequisite. A past-tt reconstruction must replay the rules as they were at that tt (else it applies current rules to past facts — wrong law), so the rules must be enactment-time-bitemporal. RP-004 gives the data plane a bitemporal axis, but rules are compiled into the Module today and are not enactment-time-versioned. Closing this is on the Phase-5 critical path and is tracked separately (#1019); Phase 0 does not touch it.
  • Q2 — Group-commit batch policy. Time-window vs. size-threshold vs. adaptive (Nagle-style), and whether the batch coalesces across scopes sharing one durable backend (more amortization, but couples scope commit latencies) or stays per-scope (simpler isolation). Recommendation: per-scope, adaptive window, revisit cross-scope batching if fsync amortization is insufficient.
  • Q3 — Fairness granularity and cost model. Per-tenant only, or per-(tenant, principal)? Request-count fair share, or cost-aware (measured compute) fair share? Recommendation: per-tenant request-count in Phase 3; escalate to cost-aware only if measured unfairness warrants.
  • Q4 — forget under MVCC. A capability-gated physical erasure conflicts with snapshot retention: a reader pinned before a forget expects to see the data, but forget exists to erase it. Does forget win immediately (erase segments, breaking older snapshots — the data-deletion intent) or lazily (erase only once no live snapshot pins it)? Recommendation: forget wins immediately for the intent (it is a compliance operation), with older pinned snapshots observing a tombstone, not the erased payload — but this needs a decision against the bitemporal semantics.
  • Q5 — Explicit ordering hash-chain vs. content-addressing alone. Content-addressing makes each event tamper-evident by content and the manifest a Merkle commitment to the prefix. Is an additional running prev-hash (tamper-evidence of ordering, Git-commit-style) worth the per-append cost, or does the content-addressed ordered manifest already satisfy #978’s “tamper-evident hash-chain”? Recommendation: the manifest suffices for v1; add an explicit chain only if an audit requirement demands per-event ordering proofs independent of the manifest.
  • Q6 — Visible-watermark scope vs. global. Is the visible watermark strictly per-scope (clean isolation, but a cross-scope query has no single consistent tt) or is there a global monotonic commit clock across scopes that a cross-scope read can pin? Recommendation: per-scope is the consistency boundary; a cross-scope read pins each scope’s watermark independently and is consistent per-scope, not globally — unless a use case demands global snapshot isolation across scopes, which would argue for a global commit clock.

RFD 0054 — Set-valued, characterization-relative composition over standpoints

  • State: discussion

Question

A record holds more than one rival way of characterizing the same underlying facts, and the domain genuinely permits more than one (a transaction read as a sale vs. a financing; a party read as an owner vs. a conduit). The two readings are not two values on a shared axis — they are outputs of two different deterministic theories over the same brute facts: one yields a recognized gain, the other a loan balance and an interest schedule. The honest computation carries every rival characterization through derivation independently and hands back the set of characterization-indexed results — never a single reconciled answer, because collapsing to one silently relocates the choice onto the machine.

How does Argon represent and evaluate this — holding a set of rival characterizations un-collapsed, deriving each independently, and reading a sound qualitative floor across them — and exactly which part is already in the substrate, which is a small addition, and which genuinely needs a model engine?

This RFD does not redefine the standpoint surface (§11), the federation semantics (Standpoint/Federation.lean), the decidability ladder (§10), or the world-assumption gates (RFD 0036). It sits on top of them and fixes the composition primitive and its evaluation strategy.

Summary of the answer

A characterization is a standpoint — not by analogy but by identity: a standpoint is a context carrying its own facts and rules, related by a <: lattice, with each standpoint internally consistent (strict default) and disagreement preserved across standpoints. That is exactly “within a fixed frame the collapse is determinate fact-finding; across frames, hold the set.”

The substrate already does most of the work. The deterministic core needs one small addition (a non-collapsing, standpoint-indexed read), and the modal boundary read is ordinary stratified Datalog for the decidable fragment. A model engine (tableau over std::kripke) is genuinely required, but only for the intractable / infinite / unstratified fragment, and a refusal gate holds its place soundly until it lands.

Verified ground truth

Every claim below is read from source; the design rests on these.

  1. Federation does not collapse rival values. Store::query_derive_federated keys the AFT info-join on the full tuple (encode_tuple), so distinct per-characterization values are distinct tuples that each survive as Is. What federation discards is the standpoint index (it folds over (_standpoint, rows)). Truth4::Both arises only for the identical tuple asserted positively in one standpoint and negatively in another (Federation.lean::federate_eq_both_iff; AFTEquivalence.lean T3: both ⟺ no consistent global K3-section).

  2. Standpoints are the characterization carrier. A standpoint scopes facts and derive rules (§11). Each standpoint is internally K3-consistent under the default strict consistency policy; cross-standpoint disagreement is preserved at federation regardless of policy (Consistency.lean). The <: lattice is an enforced DAG (OE1211), so the cross-standpoint fold always stratifies stratum-by-stratum (Stratification.lean, Vennekens-Gilis-Denecker 2006). The per-standpoint evaluation context is materialized today by materialize_predicates_for_standpoint (DEFAULT layer ∪ stamped-s facts; the runtime side of Visibility.scoped_view_eq_default_union_own).

  3. Modal over the standpoint frame is finite ∀/∃, and compiles to Datalog. box w P = ∀w'. accessible w w' → P w'; the standpoint frame is S4, worlds = standpoints, accessibility = the <: reflexive-transitive closure (Reasoning/Modal.lean). Gómez-Álvarez, Rudolph & Straß (KR 2023) prove Standpoint-EL+ compiles to ~30 pure Datalog rules (PTIME), worlds reified as standpoint IDs, accessibility as explicit transitive closure, with box via stratified negation or count-equality. The <: ancestor closure is already precomputed (standpoint_lattice.rs::ancestors, global-control.standpoint_ancestor_sets).

  4. The intractable fragment is sharp. Adding empty standpoints (NP-hard), rigid roles (coNP-hard), or nominals (EXPTIME-hard) each independently breaks PTIME (IJCAI 2023); modal⊗temporal cross-nesting is already refused (OE0712). These, plus the rigidity frame over infinite configuration worlds and unstratified modal recursion, are the fragment that needs a model engine.

  5. There is no single “modal engine” — evaluation is a compile-time strategy choice (translation to Datalog vs. tableau-on-demand). The decidability ladder places modal as its own tier (cost modal+FOL) that dispatches: static discharge, else an external reasoner over std::kripke (Decidability/Modal.lean).

  6. Two gaps are live in the implementation. (a) AtomIR::Modal{op,inner} is frame-blind and is stripped to inner at compile (compile/rule.rs), while being classified Tier::Modal and admitted — so a box/diamond program builds, runs, and is confidently wrong for anything but the rigid-default case (a live silent-wrong). (b) std::kripke is not built (no package), and there is no standpoint-frame discharge in the Lean (only the rigidity-frame fixed discharge in StaticDischarge.lean).

Design

D1 — The reification surface: the across binder

The across clause on a query gains an optional binder:

federation-clause ::= 'across' (Ident 'in')? '[' standpoint-ref (',' …)* ']'
  • across [S₁, S₂] (no binder) — federate: the existing AFT info-join. Preserved unchanged; it is the right tool for genuine same-proposition disagreement detection (the Both read).
  • across s in [S₁, S₂] (binder) — enumerate: bind s to each standpoint in turn, evaluate the body in standpoint s’s view, and project s as a first-class Standpoint-typed column. The result is the rival-set, indexed by characterization — held un-collapsed.

The binder/no-binder distinction is the enumerate/federate switch: local, syntactic, no new keyword. The runtime already computes the per-standpoint contributions (materialize_predicates_for_standpoint per sid, then the dispatcher); enumerate keeps the sid as a result column instead of folding it away. This is the product/relational Kripke-frame strategy (“add a world variable to every predicate”) realized at the query boundary.

A Standpoint value is a reflective name (the sid: NameRef the runtime already carries), in the family of the reflective TypeRef (RFD 0027 / the reflective-name surface). It is an ordinary value: downstream rules join on it.

D2 — The boundary read: qualitative □ / ◇, never counted

With the standpoint reified as a column on a relation R(s, …), the characterization-relative reads are ordinary, already-wired aggregates:

  • (necessity / sound floor)forall s in frames: R(s, …) — survives under every admissible characterization. forall lowers to count-equality (count{s : P} == count{s : frame}), which is a boolean, not a ratio.
  • (possibility / contestable frontier)exists s in frames: R(s, …).
  • qualitative worst-case◇(¬survives): is there an admissible frame under which it fails (the adversary’s move). Wired via NAF + exists.

The discipline is structural: the natural encoding produces a qualitative verdict, never a measure. A count of frames (count{surviving}/count{frames} → a probability over worlds) is an off-path, deliberate construction — the forbidden scalarization, and not what / produce. The sound floor is a meet computed as a read that leaves the held set intact; it is never the AFT info-join (which would replace the set with one tag). The numeric min/ argmin over a value codomain is not a substrate read: it is the optimizer’s move on the held set, human-invoked, and lives above the language.

Soundness of -as-forall over the frame set rests on the frame set being closed and complete — which it is: the across set together with its <: ancestors is fully known (the closed-world-over-a-closed-set condition the federation gates already enforce, §6.9 / RFD 0036).

D3 — Standpoint-frame box / diamond translation

For box/diamond appearing in a rule body over the standpoint frame (frame root recovered from the enclosing standpoint block via the lowering context, or the across set for a query — see D7), lowering translates to stratified Datalog over the reified relations and the standpoint accessibility relation reach:

diamond_s P(x̄)  ≡  ∃ s'. reach(s, s') ∧ P(s', x̄)
box_s     P(x̄)  ≡  ¬ ∃ s'. reach(s, s') ∧ ¬ P(s', x̄)      (stratified NAF)

reach(s, s') is the <: reflexive-transitive closure (the precomputed ancestor sets), exposed as a reasoning relation. P(s', x̄) is P reified with the world column — materialized per standpoint and tagged (the product reification). Tiers: closure (the <: closure) + recursive (the stratified aggregate). This is the KR-2023 reduction; the box-as-NAF form is sound because the frame set is closed (D2).

D4 — The dispatch and the silent-wrong fix

box/diamond lowering becomes frame-aware, replacing the unconditional strip:

AtomFrame / targetAction
box/diamond(x : T), T fixed/rigidrigidity (classification)discharge to x : T (the existing sound reduction, now principled) — StaticDischarge.box_fixed_discharge
box/diamond(P), explicit standpoint frame, decidablestandpointtranslate (D3)
box(¬(x:T)), dynamic/anti-rigid rigidity frame, empty standpoints, rigid roles, nominals, unstratified modal, modal⊗temporalneeds tableaurefuse — new OE11xx ModalNotEvaluated, mirroring the bridge OE1102 discipline

This closes the live silent-wrong: a program that the engine cannot soundly evaluate refuses loudly rather than silently stripping to rigid-default.

D5 — Determinacy-mode is a user enum

The tripartite determinacy-mode {indeterminate-by-law | unknown-by-data | low-confidence} is a flat user-level enum column on the per-characterization facts, carried through derivation and dispatched on by the D2 aggregates (e.g. □(survives ∧ mode ≠ indeterminate-by-law)). It is not a truth-value algebra: Truth4 is not extended, and the determinacy-mode is orthogonal to both Truth4 (cross-standpoint agreement) and well-founded-undefined (derivability). Argon’s enum values (struct/enum value carriers) already model it; this is near-commodity and needs no substrate change.

D6 — The model engine (held by the refusal gate)

The non-translatable fragment (D4 “refuse” row) is served by a tableau model engine over a built std::kripke carrier (World, Entity, iof, accessible, current). This is the genuine tier:modal backend. It is the one large build; the OE11xx refusal makes deferring it sound (loud refusal, never silent wrong). It is not “purely mechanical mechanization of well-understood math,” so it carries its own design and (when built) its own Lean obligations — it is out of scope for the Lean deferral that covers D3’s finite-frame reduction.

What does not change

  • Truth4 stays four-valued. Both is genuine same-proposition contradiction (= no consistent global section), orthogonal to rival values.
  • Federation default stays Paraconsistent (no-binder across). Enumerate is the new, additive, lossless peer; federation remains the lossy opt-in for disagreement detection.
  • The <: DAG, the consistency policies, the world-assumption gates are untouched.

Decidability placement

  • D1 enumerate + D2 reads + the decidable D3 translation: tier:closure (<: closure) + tier:recursive (stratified aggregates / NAF). Executable on the existing semi-naive engine.
  • D4 “refuse” / D6: tier:modal. Refused until the engine lands.

Implementation plan (dependency-sorted)

  • Phase A — the unblocking core. D1 binder (grammar + lowering + the enumerate readout keeping sid) + the Standpoint-typed column + D2 reads as ordinary forall/exists. End-to-end testable; delivers hold-the-rival-set + the qualitative floor with no engine.
  • Phase B — soundness + mid-body. D4 frame-aware dispatch (discharge / refuse, killing the silent-wrong) + D3 standpoint-frame translation (reach relation + the box/diamond lowering). Requires D7 below.
  • Phase C — the escape hatch. D6 tableau engine + std::kripke package.

D7 — Prerequisites surfaced by verification

  • AtomIR::Modal frame recovery: determine the frame at lowering (where ctx.standpoint_id / the across set / “is inner a classifier atom” are available) and emit translated/​discharged ordinary AtomIR, so AtomIR::Modal survives only for the tableau-bound case. This avoids a wire/IR shape change.
  • Query lowering order: thread the across set into the lowering context before body lowering (today it is extracted after). Derives already carry the enclosing standpoint via ctx.standpoint_id.
  • Expose reach(s, s') (the precomputed <: ancestor closure) as a reasoning relation for D3.

Lean note

Per a one-time, explicitly-authorized deferral (this primitive is blocking a dependent project and D3’s reduction is the proven KR-2023 finite-frame result, not novel mathematics): the soundness proof of the D3 standpoint-frame discharge is deferred. The data-shape mirror that the oxc-protocol drift gate enforces is not deferred — any IR change stays in lockstep with the Lean @[language_interface] inductives. D6 (the model engine) is out of this deferral.

References

  • Verified sources: spec/lean/Argon/Reasoning/{Modal,StaticDischarge}.lean, spec/lean/Argon/Decidability/Modal.lean, spec/lean/Argon/Standpoint/{Federation,Consistency,AFTEquivalence,Stratification}.lean, compiler/crates/oxc-runtime/src/lib.rs (query_derive_federated, materialize_predicates_for_standpoint), compiler/crates/oxc-oxbin/src/sections/standpoint_lattice.rs, spec/reference/src/semantics/decidability.md, spec/reference/src/semantics/standpoints-and-federation.md.
  • Gómez-Álvarez & Rudolph (2021), Standpoint Logic (JELIA): polytime reduction to OWL 2 by reification.
  • Gómez-Álvarez, Rudolph & Straß (2023), A Datalog reasoner for Standpoint-EL+ (KR): ~30 Datalog rules, PTIME; intractability boundaries (IJCAI 2023).
  • Vennekens, Gilis & Denecker (2006), Splitting an Operator: stratified fixpoint over a DAG of strata.
  • Belnap (1977); Denecker, Marek, Truszczyński (2000), Approximation Fixpoint Theory.

RFD 0055 — The conformance corpus: a standing golden differential-correctness gate

  • State: accepted
  • Depends on: RFD 0048 (the test atom — the in-language golden vehicle), RFD 0050 (documentation architecture / “every package compiles and runs in CI”), RFD 0018 / RFD 0020 (the reasoner and Engine::evaluate the corpus exercises), RFD 0005 (refined collections / field projection — the keystone bug’s home), RFD 0025 (mutation delta-guard — the write-path goldens)
  • Tracks: issues #1024, #1025, #1026; guards #1014, #1015, #991, #1001, #1005, #996
  • Prior art: differential testing (McKeeman 1998); metamorphic testing (Chen et al.); golden / characterization tests (Feathers); SQL logic tests (SQLite’s sqllogictest); the project’s own Lean batched oracle (engine_agrees_with_batched_lean_oracle) and pipeline differential audit (pipeline_differential_audit_over_corpus)

Question

A 2026-06 maturity audit and a real-ontology dogfood found roughly nine silent-wrong engine bugs — wrong answer, no error — that 3,200+ unit tests, the Lean differential oracle, and the pipeline differential audit all passed over. The field-projection P0 (#1014) is the archetype: a field read aliasing across two co-classifying concepts with same-named fields, whose regression test had been #[ignore]d and whose shape no oracle covered.

Why did every oracle miss them, and what gate closes the class?

Context

Every existing correctness oracle attaches at or below Engine::evaluate:

  • the Lean batched oracle and the golden replay vectors check the reasoner over already-elaborated programs;
  • the pipeline differential audit checks direct ≡ pipeline ≡ frozen — three evaluation routes over the same compiled rules;
  • the FullRecomputeMaintainer IVM oracle checks incremental maintenance against from-scratch recompute.

The silent-wrong bugs live in the front half of the pipeline — elaboration (value-fn inlining #991/#1001/#1005), lowering and materialization (field-projection concept-keying #1014, defined-concept extent ordering #1015), the mutate write path (#992, #996), and serialization (#993–#995). A self-differential oracle is structurally incapable of catching a front-end defect: when a lowering bug corrupts the input, every downstream route inherits it and they agree — on the wrong answer. The pipeline audit passes the #1014 buggy state for exactly this reason.

The gap was therefore not oracle existence but corpus breadth at the surface, checked against a truth the oracles cannot derive: the program’s intended meaning.

Decision

Add a standing conformance corpus of small, clean, documented ontologies whose expected results are hand-pinned and checked end-to-end through the real ox CLI, gated in CI. Golden (human-anchored) is the primary method; the existing oracles remain the floor underneath for the reasoning core and the optimizer/IVM paths. This is the front-half layer they cannot reach.

It lives Argon-side (Tide depends on Argon, not the reverse — the gate must stand alone), in a new top-level conformance/ directory, separate from examples/: examples/ is the teaching corpus gated on compilation (ox check); the conformance corpus is adversarial, gated on golden output, and enforces a rule that would be wrong for teaching examples — no entry without a golden.

Design

  • Tree. conformance/corpus/<entry>/ — an ordinary ox package (ox.toml, root.ar, model modules) plus its golden: a tests/ directory (the test atom) and/or a demo.toml scenario with expect assertions. A README.md carries provenance (guards: / feature: / method:). Refusal fixtures carry a refuses.txt listing the OE#### code(s) the build must be refused with.
  • Gate. compiler/crates/oxc-driver/tests/conformance_corpus_gate.rs — a cargo nextest test that discovers every ox.toml under corpus/ and shells out to CARGO_BIN_EXE_ox (always source HEAD, never a stale installed toolchain). Positive entries must ox build then pass ox test and ox run-scenario; refusal fixtures must be refused by ox check --codes with each pinned code.
  • Three guarantees beyond the examples gate. Correctness not just compilation; no entry without a golden (the #[ignore]d-test failure mode, prevented structurally); refusals pinned by code.
  • Methodology. “Correct” is established per entry by a hand-specified golden encoding the program’s intended answer — so a front-end bug that corrupts every route still fails against the human-known truth. Where an oracle is independent (the reasoning fragment), it remains a second check.

The keystone multi_classification_fields is the acceptance witness for #1014: on a toolchain without the fix both version projections return both type-confused rows and the equals golden fails (got 2, expected 1); with the fix, green.

This RFD also records the stratify.rs fix it carries: the refusal Display strings for AggregateCycle (OE1317), NegatedWfsRelation (OE1365), and NafCycle (OE1309) now embed their code prefix, so ox check --codes names them — the eval-time variants already did; the stratify-path ones silently dropped the code the corpus pins (#1025).

Alternatives considered

  • Tide-side workflow corpus. Rejected: Argon’s correctness gate must not depend on a downstream repo. The wire/serialization bugs (#993–#995) live in Argon crates (oxc-connection, oxc-gen) and are checkable Argon-side.
  • Extend the Lean fuzzer instead. Rejected as the primary fix: the fuzzer generates post-elaboration ProgramFeature rules, bypassing the surface fn/lowering/mutate paths where the bugs live. It remains the right tool for the reasoning fragment.
  • Fold into examples/. Rejected: conflates teaching with adversarial regression and makes the no-golden rule impossible to enforce (teaching examples legitimately lack goldens).

Open follow-ups

  • #1024 — write-side enum/option value literals (None, Some(x), enum constants) error at runtime despite building clean; blocks the real absent-optional write entry (optional_iteration guards the nearest expressible contract meanwhile).
  • #1026 — the scenario harness drops the Truth4 verdict from federated rows, so expect cannot assert it; standpoint_federation pins membership + scoped views meanwhile.
  • The corpus may later sweep examples/ goldens (their demo.toml/tests are not currently run wholesale in CI) and register reasoning-evaluable entries into the pipeline differential audit for the free route-equivalence check.

RFD 0056 — Bitemporal rules: rule validity over time

Naming. The substrate concept is domain-neutral: a rule is a bitemporal fact, and its valid-time is the interval over which it is in effect — exactly as a fact has a valid-time. The language surface and engine use neutral names only (at #DATE# on a rule head; rule_vt; query_derive_at; “the rules in effect at a point”). The legal-informatics vocabulary in the motivation and citations below — enactment-time, abrogation, annulment, “the law as it stood” — names the motivating domain (audit / defensibility / statutory reasoning) and the prior art (Governatori, LegalRuleML), not any Argon construct. The feature works identically for contract terms, pricing schedules, policy versions, or configuration regimes; none of those are “law.”

  • State: accepted
  • Depends on: RFD 0053 (the standalone concurrent engine — Phase 5 as-of-past derived reads is the first consumer), RFD 0047 (the temporal value library / bitemporal substrate this extends to the rule plane), RFD 0028 (defeasibility — rule strengths, proof tags, the superiority/defeat graph this must compose with), RFD 0017 (refinement where/iff — definitional scope, the adjacent scoped-redefinition arc), RFD 0020 (runtime engine / Engine::evaluate), RFD 0018 (DBSP reasoner)
  • Tracks: issue #1019
  • Prior art: Governatori & Rotolo, “Changing Legal Systems: Abrogation and Annulment. Part II: Temporalised Defeasible Logic” (DEON 2008) — the canonical four-temporal-slot proof-tag and the abrogation/annulment dichotomy; Governatori, Palmirani, Riveret, Rotolo & Sartor, “Back to the Future: Variants of Temporal Defeasible Logic for Modelling Norm Modifications” (ICAIL 2007) — the second (repository/viewpoint) temporal axis; Governatori, “Logics for Legal Dynamics” (in Araszkiewicz & Płeszka eds., 2015) — the two-dimension reduction (in-force vs. viewpoint); Cristani, Olivieri & Rotolo (ICAIL 2017) — the six-cell norm-change operator family; LegalRuleML (Athan, Governatori, Palmirani, Paschke & Wyner, OASIS 2013) — efficacy / enforcement / validity / assertion temporal dimensions; Akoma Ntoso / LegalDocML — point-in-time consolidation of legislation; bitemporal databases (Snodgrass–Ahn 1985; Jensen–Snodgrass 1999; SQL:2011 system-versioned tables); Lawsky, “A Logic for Statutes” (2017) — defeasible, scoped statutory reasoning

Question

RFD 0053 Phase 5 reconstructs derived facts at a past transaction time tt by checkpoint-and-replay (Q1). That reconstruction is only correct if it replays the rules in force at that tt — the law as it then stood. Replay current rules over past facts and the system answers a counterfactual no auditor asked for: today’s law applied retroactively.

The data plane is already bitemporal: a fact carries a valid-time interval (when it holds in the world) and a transaction-time interval (when the system recorded it), and a read pins a (vt, tt) point (AsOf, event_visible_at). Rules are not. They compile flat into the Modulecompiled_rules: BTreeMap<String, Vec<CompiledRule>> keyed by head name — and Module::compiled_rules() returns all of them with no temporal predicate. Rule selection is timeless.

What temporal structure does a rule need so that “what did the system derive as of the original filing date, under the law as it then stood?” is a first-class, validated question — and how does that structure compose with defeasibility, scoped redefinition, the decidability tier ladder, and the Phase 0/1/4 commit-and-checkpoint machinery Phase 5 reuses?

Context

How many temporal dimensions a legal norm carries, and what they are, is not an open design question — the legal-informatics literature settled it, and the answers converge:

  • Governatori 2015 reduces a norm to two temporal dimensions: when the norm is in force (efficacy) and when the norm exists in the normative system from a given viewpoint (the repository/transaction axis).
  • LegalRuleML names four — efficacy, enforcement, validity, assertion — which collapse onto bitemporal at the substrate: efficacy = valid-time; validity (repealed/annulled status) + assertion (enactment origin) = transaction-time; and enforcement (the statute-of-limitations window in which an authority may act) is an application-layer concern, not a substrate axis.
  • Governatori–Rotolo 2008 tags a defeasible conclusion ±∂^{t_d}@{t_r} l_{t_l} with four temporal slots, of which three are stored coordinates and one is the query instant: literal valid-time t_l, rule time-of-force t_v (the rule’s valid-time), repository time t_r (transaction-time), and derivation time t_d (the “now” at which the inference is drawn — provenance, not a selection axis).

The load-bearing consequence: “enactment-time” is the rule’s valid-time (its efficacy / in-force window), not its transaction-time. A statute enacted in 2025 but effective from 2020 has rule-valid-time [2020, …) and rule-transaction-time [2025-commit, …). Modelling enactment as transaction-time makes retroactive legislation — the motivating audit case — inexpressible.

What already exists

  • The data plane is bitemporal. Every AxiomEvent carries a BitemporalExtent { vt_start, vt_end, tx_from, tx_to } (oxc-protocol), and this rides on every axiom kind — including the rule-declaration kinds (RuleDecl, QueryDecl, …). Rule events already have the storage shape; nothing reads their valid-time.
  • The visibility predicate is general. event_visible_at(event, vt, tt) (oxc-runtime) filters any event by (vt, tt); event_visible_at_valid_time is the valid-time half (vt_start > vt ⇒ out, vt_end ≤ vt ⇒ out).
  • Defeasibility is built. Rules carry rule_strength (strict/defeasible/defeater), label, is_default, defeats edges, and defeat_strategy (RuleDeclBody); the superiority/defeat graph is resolved and acyclicity-checked at elaboration, and compiled three-stratum (Governatori). The temporal-defeat preservation theorem (temporal_defeat_complete_preserves) and the abrogation default are already established in the RP-004 exploration.
  • The commit boundary is explicit. Phase 0 publishes a per-scope visible watermark advanced after maintenance; Phase 1 binds maintain → advance as a contract (“a read pinned at the published watermark observes a derived model consistent with exactly the committed base facts at that tt”); Phase 4 checkpoints the maintainer’s read-model, keyed (module_fingerprint, storage_gen).

What is missing

The rule’s valid-time has no semantics and no consumer. Module::compiled_rules() is a timeless flatten. There is no way to select the rule-set in force at a target point, so the Phase-5 replay path cannot replay the law as it then stood.

Decision

A rule is bitemporal exactly as a fact is. Give the rule-event’s already-present valid-time the efficacy / in-force semantics, and make rule selection at evaluation a function of the read-point instead of a constant. This extends the shipped data-plane bitemporal model to the rule plane; it invents no new axis and adds no parallel catalog.

1. Representation

Rules remain bitemporal axiom events in the one event log. On a RuleDecl event:

  • extent.vt_* is the efficacy window — when the law is in force. Enactment with effective date D sets vt_start = D (retroactive enactment is vt_start < tx_from, the existing data-plane “retroactive correction” shape). Prospective repeal / sunset sets vt_end forward.
  • extent.tx_* is the viewpoint — when the rule text was recorded or corrected — already populated by the commit path.

No versioned rule-set keyed by enactment time: the log is the version axis, and a keyed structure would only re-pose retroactive enactment, which is precisely bitemporality.

The single behavioural change is the selection seam. Module::compiled_rules() (the timeless flatten) gains a read-point-parameterised sibling that runs the existing event_visible_at over rule events:

compiled_rules_at(read) = { rule | event_visible_at(rule_event, read.law_vt, read.tt) }

Engine::evaluate(&[CompiledRule], …) is unchanged — it already takes a borrowed rule slice, so only the caller’s selection changes. The reasoner core and the tier classifier stay temporally oblivious.

2. The time axis (the crux): three coordinates, one default coupling

A read names a point in a bitemporal rule plane and a bitemporal fact plane that share one transaction-time. Per Governatori’s two-dimension reduction and bitemporal-database orthodoxy (transaction-time is a property of the repository, singular), the viewpoint is not split per plane. The selection coordinates are three:

  • fact-VT — which facts held in the world (exists today: AsOf vt);
  • rule-VT — which rules were in effect (new);
  • viewpoint-TT — which recorded state of the repository (facts and rule transcriptions) we believe (exists today: AsOf tt).

The derivation instant t_d (Governatori’s fourth slot) is the replay/query “now”; it is recorded in the derived tuple’s provenance (proof_tag channel), not a read knob.

Default coupling. AsOf stays the (fact-VT, viewpoint-TT) point, wire-compatible; a read adds one optional coordinate, rule_vt: Option<Time>, defaulting to fact-VT. So:

  • as_of vt ⇒ facts valid at vt, under the law in force at vt — the headline audit query, free, no extra surface;
  • Now ⇒ current facts, current law (the conservativity case);
  • explicit rule_vt unlocks the counterfactual quadrant — old facts under today’s law (fact-VT past, rule-VT = now), which is also where retroactive annulment lives.

There is no separate “law-TT” to default: a correction to the transcribed statute text is a viewpoint advance of the one repository, visible at tt ≥ the correction exactly as a fact correction is. The decoupling auditors need — old law vs. current law over old facts — is a valid-time decoupling (rule-VT ≠ fact-VT), not a second transaction-time.

3. Composition

Defeasibility / priority. The superiority/defeat graph stays static, file-local, acyclicity-checked at elaboration. Enactment-time does not touch the graph; it filters which rules are in force at the read-point, inducing a subgraph. A defeat edge whose target is not in force is inert; acyclicity of the whole graph implies acyclicity of every subgraph, so the build-time check still suffices — no new decidability burden, and superiority need not become dynamic. The defeat algebra (team defeat, ambiguity blocking, defeated-defeaters) is time-invariant; only its inputs are time-indexed (mechanized: defeat_preserved_under_full_inForce). The default is abrogation — forward-only: a defeater (or repeal) effective at t₂ closes a warrant from t₂ forward; warrants before t₂ stand. Lex posterior (“later rule wins”) is expressed as enactment-VT ordering plus explicit defeat edges — not time-stamped derivable preferences (Olivieri 2025), which would force superiority dynamic and is out of scope.

Annulment (retroactive invalidation, ab initio) is the separate operation: a transaction-time retraction (forget) across viewpoints with dependency cascade. It escalates to tier:fol and is out of scope for #1019 — the Phase-5 audit story is abrogation-complete. It is a tracked follow-on requiring its own conservativity (AnnulmentExtraction).

Scoped redefinition (definitional scope / Lawsky). Scope and enactment-time are orthogonal and resolve at different stages: scope guards resolve at elaboration (which definition applies in which region); enactment-time resolves at evaluation (which region-scoped rule is in force at the read-point). A rule may carry both. Resolve scope first, filter by read-point second; they never unify.

Lockstep / checkpoint. Phase 1’s invariant extends from “consistent with the committed base facts at that tt” to “…under exactly the rules in force at that read-point.” The Phase-4 checkpoint key folds in the active-rule-set digest at the checkpoint’s read-point, so a checkpoint is never replayed across a law change (a repeal between checkpoint and target invalidates reuse exactly as a schema change does). Phase-5 replay: restore the nearest checkpoint whose read-point matches → replay the base-log suffix → at each step derive with compiled_rules_at(read).

4. Decidability and cost

Tier is a structural property of each rule (RuleDeclBody.main_tier, pre-classified at enactment), independent of enactment time. Selecting a subset of rules can only hold-or-lower the module tier (max over a subset ≤ max over all), so an as-of-past read never escalates cost above the present module’s, and replay over a subset is ≤ replay over the full set. Selection is O(rules) filtering against event_visible_at, memoizable per distinct read-point. Enactment-VT is meta-level (which rules exist) and orthogonal to the object-level temporal sub-tier (since/until inside rule bodies); a rule’s efficacy window must not be conflated with a metric-temporal interval in its body.

5. Mechanization plan (Lean → reference → RFD → code)

Per the substrate workflow and the discipline of mechanizing a novel soundness frame in scratch-Lean first, with a differential oracle gating the implementation.

Conservativity property (central theorem and differential gate). Present-law evaluation is bit-identical to today’s flat behaviour:

when every rule is in force at the read-point, compiled_rules_at(read) returns the full rule-set, so evaluation equals Engine::evaluate over the flat compiled_rules().

Supporting properties. Forward-only enactment: a rule effective from t₂ changes no active set, hence no derivation, at any read-point < t₂ (abrogation; no dependency closure needed). Defeasibility composition: on the in-force subgraph the gated warrant coincides with the ungated warrant.

A core-only scratch-Lean sketch of all three is established (the bitemporal RP-004 exploration): activeAt_eq_all_of_all_inForce / eval_eq_flat_of_all_inForce (conservativity), enactment_forward_only / eval_invariant_forward_only, and defeat_preserved_under_full_inForce — the last proved through the canonical temporal_defeat_complete_preserves. Zero sorry, zero axiom.

Phases, with RFD 0053 Phase 5 as the first consumer:

  • A — scratch-Lean (done, never-merged): bitemporal rule selection + the conservativity and forward-only theorems + defeasibility composition.
  • B — reference: the AsOf/as-of-semantics chapter gains the rule_vt coordinate and the default coupling; a rule-efficacy section in the temporal substrate.
  • C — RFD: this document.
  • D — code: efficacy semantics on the rule-event valid-time in oxc-instantiate; compiled_rules_at(read) in oxc-runtime reusing event_visible_at; the rule_vt coordinate threaded through the serve/query path; the active-rule digest folded into the Phase-4 checkpoint key; Phase-5 replay selecting per read-point. Differential oracle = the Phase-1 lockstep oracle extended along the in-force axis: an as-of-past derived read equals from-scratch Engine::evaluate over the base facts and rules visible at that read-point.

Resolved questions

These were posed as open and are resolved by the prior-art convergence above:

  • Enactment = valid-time, not transaction-time. Retroactive legislation forces it; the issue text (#1019) is corrected accordingly.
  • One shared viewpoint, not a per-plane transaction-time. The audit decoupling is valid-time (rule-VT ≠ fact-VT); there is no separate law-TT to default.
  • Default coupling is rule-VT ← fact-VT.
  • #1019 scope is abrogation (efficacy valid-time windows, tier:recursive); annulment is a tier:fol follow-on.

Open questions

  • OQ1 — Checkpoint forcing. Should a rule-efficacy or rule-transcription change force a Phase-4 checkpoint, bounding replay distance to within one law regime? A correctness-neutral cadence choice (RFD 0053 Q1 makes cadence operator-tunable); leaning yes.
  • OQ2 — Surface spelling. Resolved (Phase B). A rule’s validity is authored with the existing fact at #DATE# qualifier on the rule head (derive H(x) at #DATE# :- …); the read-side coordinate is ReadPoint { data: AsOf, rule_vt: Option<DateTime> } with None tracking the data point’s valid time, and the in-process API is query_derive_at(.., rule_vt). Specified in the reference manual’s Rule validity over time (temporal substrate) and AsOf semantics sections.
  • OQ3 — Annulment follow-on shape. The tier:fol retroactive path and its conservativity (AnnulmentExtraction) are deferred; file as a tracked follow-on when Phase 5 lands.

RFD 0057 — Argon as a served platform: the operational host

  • State: discussion
  • Depends on: RFD 0052 (deployment topologies — fixes embedded-vs-standalone and the connection abstraction this RFD operationalizes), RFD 0053 (the standalone concurrent engine — the transactor / MVCC / IVM-in-lockstep core this RFD wraps in a deployable shell), RFD 0014 (the /v1 serving surface), RFD 0036 (heterogeneous stores — the durable backends a deployment configures), RFD 0025 (the check delta-guard and the cross-version schema-change gate)
  • Tracks: issue #978 (standalone serving)
  • Prior art: OpenTelemetry’s tracing/OTel bridge and OTLP export (vendor-neutral telemetry); the loopback-bind + co-located auth-proxy pattern (the “sidecar gateway” / service-mesh trust boundary, Envoy / Istio); graceful drain on SIGTERM (axum::serve(...).with_graceful_shutdown, the twelve-factor “disposability” rule, Wiggins); layered configuration precedence (flags > env > file > defaults — the Viper / figment / Kubernetes-config lineage); atomic hot-swap of a versioned artifact under live traffic (blue-green / zero-downtime config reload); readiness/liveness probes (Kubernetes pod lifecycle, but kept orchestrator-agnostic here)

Question

RFD 0053 builds the serving core: a single-logical-writer transactor, MVCC snapshot reads over the bitemporal log, group commit, admission control, and an IVM maintainer running in lockstep with the commit stream — Phases 0–4 and 6 are merged and wired into oxc-serve. That core is a correct, concurrent, durable database engine. It is not yet a platform: a thing an operator can deploy, secure, observe, configure, reload, and run as a long-lived service against an orchestrator.

What is the operational shell around the engine? Concretely: what is the trust boundary a standalone Argon presents to a network, and who terminates authentication; how is the running server observed (traces and metrics) without bolting a second telemetry stack onto it; how does it start, drain, reload a changed model, and stop cleanly under an orchestrator’s lifecycle signals; how is it configured across a file, environment, and flags without leaking secrets; and what does Argon ship so a deployment is reproducible — a container, a service unit, a runbook — without prescribing one infrastructure?

This RFD is explicitly not engine semantics (RFD 0053 owns the transactor, MVCC, IVM, and admission mechanism) and not authentication (the gateway owns it — see D1). It is the host: the operational contract that turns the engine into a deployable platform. It builds directly on RFD 0052’s topology axes and RFD 0053’s engine.

Context

What already exists

The platform is not built from nothing; a meaningful operational floor already ships in oxc-serve, and this RFD must name it as built rather than re-decide it:

  • Loopback bind is already enforced. serve() refuses any non-loopback bind address up front (if !config.host.is_loopback() { return Err(ServeError::NonLoopbackBind(...)) }). The runtime will not listen on a routable interface; this is a hard precondition, not a default.
  • Trusted context arrives in headers. Dispatch reads x-tenant-id, x-principal-id, x-standpoint-id, and x-fork-id to resolve the (tenant, fork, principal, standpoint) scope of a request. These are scope signals consumed downstream of the network edge.
  • tracing is already emitted. serve() installs a fmt subscriber (a no-op if the host already installed a global subscriber, so it is safe from any embedding), filtered by RUST_LOG, defaulting to info. Per-request structured logging (log_request: method, path, x-tenant-id / x-fork-id, wall-clock duration, outcome) already flows through tracing.
  • Hot-reload primitives exist. RuntimeService::reload_if_changed() re-loads a changed .oxbin, validates it, and performs the swap; spawn_watch_task() watches the artifact. The additive-schema vs. type-change gate is enforced (ARGON_ACCEPT_SCHEMA_CHANGE opts into a cross-version change against a live A-box), with tests for both the accept-additive and reject-type-change paths.
  • Admission and operability limits are configured via OperabilityLimits (RFD 0053 Phase 3): the concurrency semaphore, the per-tenant fair queue, the enqueue deadline, body-size and result-row caps, the per-request deadline, and the reasoner budget all read from one struct.
  • ServeConfig / StorageMode / AdhocPolicy already carry the bind address, the storage backend selection, and the ad-hoc policy.

What is missing — the gap this RFD closes

The pieces above are an operational floor, assembled incrementally for the serve core. They are not yet a coherent platform contract. The gaps:

  1. The trust model is implicit. Loopback bind is enforced, and trusted context arrives in headers, but nothing states the deployment shape that makes those two facts safe together: who terminates authentication, who sets the trusted headers, and the rule that client-supplied trusted headers must never be honored. Without that contract stated, an operator could expose the runtime directly or pass client headers through — both unsound.
  2. Observability is logs-only. tracing is emitted, but there is no exported, vendor-neutral telemetry — no distributed traces spanning a request’s dispatch/reason/persist/maintain phases, no metrics (latencies, throughput, check-violations, store growth, IVM rebuild frequency, budget hits) an operator can scrape or ship to an OTLP collector.
  3. Lifecycle is not graceful. The server has no SIGTERM drain: an orchestrator’s stop signal terminates in-flight requests rather than draining them. Hot-reload primitives exist but are not stated as a platform contract (validate → atomic swap → no dropped in-flight, under the schema-change gate).
  4. Configuration is partial and ad-hoc. Settings are spread across ServeConfig, OperabilityLimits, environment variables (RUST_LOG, ARGON_ACCEPT_SCHEMA_CHANGE), and CLI flags, with no single layered file, no stated precedence, and no secret-handling rule.
  5. There is no deployment packaging. No reference container, no service unit, no runbook. An operator deploying Argon today reverse-engineers the topology from code.

These are operational-shell gaps, not engine gaps — which is exactly why RFD 0053 could leave them to this RFD.

Decision

A standalone Argon is deployed as a loopback-only engine behind a co-located authentication gateway, observed through OpenTelemetry over the existing tracing bridge, with a graceful lifecycle (drained shutdown, atomic hot-reload), layered configuration (flags > env > file > defaults, secrets by reference), and an infra-agnostic reference packaging (container, service unit, runbook). Each decision is the operational shell over the RFD 0053 engine; none changes engine semantics.

D1 — Deployment topology: a loopback engine behind a co-located auth gateway

This is the platform’s defining shape. It is the only network-facing posture a standalone Argon presents, and it is what makes the engine’s existing loopback bind and header-borne context sound:

  • The runtime binds loopback only, by design. is_loopback() is enforced in oxc-serve (already built). The runtime is network-isolated: it never listens on a routable interface, so it is unreachable from anything but a process on the same host (or in the same network namespace / pod).
  • A co-located auth gateway terminates authentication. A separate process on the same host — mTLS, OIDC, or API-key, the operator’s choice — authenticates the caller and proxies the request to the loopback runtime. The gateway is where credentials are verified; the runtime never sees a raw credential and runs no auth.
  • The gateway overwrites the trusted context headers. After authenticating, the gateway sets x-tenant-id, x-principal-id, and x-standpoint-id (and x-fork-id) from the authenticated identity, overwriting whatever the client sent. These headers are never passed through from clients: a client-supplied x-tenant-id is overwritten, not honored. The runtime trusts these headers precisely because they cannot reach it except through the gateway that just set them (the loopback isolation is what enforces “except through the gateway”).

The trust chain is therefore: network → gateway (authenticate, set trusted headers, strip client-supplied ones) → loopback → runtime (trusts the headers, runs no auth). The runtime’s loopback bind is not a development convenience that gets relaxed in production — it is the production trust boundary. Authentication is out of scope for this RFD (it is the gateway’s job and the gateway’s design); what this RFD fixes is the shape that makes the runtime’s existing posture safe.

D2 — Observability: OpenTelemetry via the tracing bridge

The runtime already emits tracing. Observability rides that bridge rather than introducing a second instrumentation stack:

  • Traces. A tracing→OTel layer exports distributed traces. Each request is a root span; the handler-internal phases are child spans — dispatch, reason, persist, maintain — so a slow request is attributable to a phase, and a trace crossing the gateway (D1) joins the gateway’s span via propagated context.
  • Metrics. The same bridge exports metrics: request latency by path and tenant, mutation throughput, check-violations, store growth, IVM-rebuild frequency (the RFD 0053 D3 recompute-vs-incremental signal), and reasoner-budget hits (the RFD 0053 D5 admission signal). These are the operator’s window into the engine’s two cost surfaces — the commit critical path and the admission layer.
  • Vendor-neutral OTLP, configurable endpoint. Export is OTLP to an operator-configured collector endpoint (D4); off by default, on when an endpoint is configured. No vendor SDK is linked.

This is chosen over Prometheus-direct (a /metrics scrape endpoint) deliberately: one bridge gives both traces and metrics and stays vendor-neutral, where a Prometheus endpoint gives metrics only and a separate trace exporter would still be needed. An operator who wants Prometheus runs an OTLP→Prometheus collector — the runtime is not coupled to either.

D3 — Lifecycle: graceful shutdown and atomic hot-reload

The server is a long-lived service under an orchestrator’s lifecycle, so it must start, drain, reload, and stop on the orchestrator’s terms:

  • Graceful shutdown on SIGTERM. axum::serve(...).with_graceful_shutdown(...) wires the stop: on SIGTERM the listener stops accepting new connections, in-flight requests drain (bounded by the per-request deadline already in OperabilityLimits, so drain is finite), and the process exits clean. A request in flight at SIGTERM is finished or deadline-cut, never severed mid-commit — which composes with the RFD 0053 transactor’s atomic commit (a commit either reaches its durable-acknowledge or is rolled back to the last durable tt).
  • Hot-reload of a changed .oxbin. On a watch event or an explicit reload signal/endpoint, the server validates the new artifact and performs an atomic Module swap with no dropped in-flight requests — new requests bind the new module, in-flight requests complete against the one they started on. This makes the existing reload_if_changed() / spawn_watch_task() primitives a platform contract. The cross-version schema-change gate still applies: an additive schema reloads freely; a type-incompatible change against a live A-box is refused unless ARGON_ACCEPT_SCHEMA_CHANGE opts in (RFD 0025 / RFD 0053’s recovery key-mismatch discipline). Reload never silently accepts a model that would invalidate persisted state.

D4 — Configuration: a layered TOML file, env overrides, CLI flags

A platform is configured, not hard-coded. The configuration is a single TOML file layered with environment overrides and CLI flags:

  • Precedence: flags > env > file > defaults. A CLI flag wins over an environment variable, which wins over a file entry, which wins over the built-in default. This is the standard layered-config precedence (the Viper / Kubernetes lineage).
  • Coverage. The file covers: the oxbin path; the storage backend + its durable path or database-url (RFD 0036); the bind address (loopback, D1); the [placement] federation map (RFD 0036); OperabilityLimits (semaphore size, per-tenant weights, queue-depth and enqueue-deadline, body/row caps, per-request deadline, reasoner budget — RFD 0053 D5); admission/fairness tuning; the OTel endpoint (D2); and the ad-hoc policy (AdhocPolicy, RFD 0033). It is the union of ServeConfig, OperabilityLimits, and the existing env knobs, given one home.
  • Secrets by reference, never inline. The database-url and any other secret are supplied by environment variable or a secret-file reference (a path the runtime reads at startup), never written inline in the TOML. A secret in the config file is a configuration error to be lint-warned, not silently accepted. This keeps the file checkable into source control and the secret in the orchestrator’s secret store.

D5 — Deployment packaging: reference artifacts, not opinionated manifests

Argon ships what makes a deployment reproducible without prescribing an infrastructure:

  • A reference container. A Dockerfile building the runtime image, with a documented entrypoint that reads the D4 configuration, wires the D2 OTel endpoint, and exposes the readiness/liveness probes the orchestrator wires.
  • A systemd unit reference. A reference .service unit for a bare-host / VM deployment — the non-orchestrated case — with the same configuration and lifecycle contract (D3’s SIGTERM drain maps directly onto systemd’s stop).
  • A deployment runbook. Prose covering: the D1 topology (loopback engine + co-located gateway, header overwrite), readiness/liveness wiring to the orchestrator (a ready probe gating traffic until the model is loaded and recovery is complete, a live probe detecting a wedged process), the schema-change gate (how a model upgrade is rolled out under D3), and secret handling (D4).

The packaging is infra-agnostic: it is the contract plus reference artifacts, not opinionated Kubernetes manifests. An operator deploys the reference container into their own orchestrator, or runs the systemd unit on a VM; Argon does not ship a Helm chart or an operator and does not assume Kubernetes. The contract (loopback + gateway, OTLP, graceful lifecycle, layered config, probes) is what is normative; the container and unit are references of it.

Rationale

Why loopback-plus-gateway rather than auth in the runtime. Authentication is a fast-moving, deployment-specific concern (mTLS here, OIDC there, an API key for a script) with a large attack surface and a different release cadence than a database engine. Folding it into the runtime would couple the engine to one auth scheme, widen its trust surface, and make every auth change an engine release. The loopback-plus-gateway split is the service-mesh trust-boundary pattern: the engine trusts its local network namespace, the gateway owns identity, and the two compose without the engine knowing how identity was established. The runtime’s existing loopback enforcement is already half of this; D1 names the other half (the gateway, the header overwrite) so the existing posture is sound rather than accidental. The header-overwrite rule is the load-bearing invariant: trusted context is trusted because it can only have come from the gateway, which the loopback isolation guarantees.

Why one OTel bridge rather than Prometheus-direct. The runtime already speaks tracing; OTel rides that one bridge to export both traces and metrics, vendor-neutrally, with one configuration surface (an OTLP endpoint). Prometheus-direct gives metrics only — a separate trace exporter would still be needed, and the operator would configure two stacks. Vendor-neutral OTLP means an operator who wants Prometheus, or Datadog, or Honeycomb, runs the appropriate collector and the runtime is unchanged. The phase-level spans (dispatch/reason/persist/maintain) are chosen because they map exactly onto the RFD 0053 commit pipeline, so a trace localizes a slow request to a mechanism the engine RFD already names.

Why graceful lifecycle is a contract, not a nicety. A standalone database under an orchestrator is restarted routinely — on deploy, on scale, on node drain. A SIGTERM that severs in-flight requests turns every routine restart into a burst of client errors and, worse, a request cut mid-commit relies entirely on the transactor’s atomicity to not corrupt state. Draining (bounded by the existing per-request deadline, so drain terminates) makes restart invisible to callers. Atomic hot-reload under the schema-change gate makes a model upgrade a non-event the same way — validate, swap, no dropped requests, refuse a change that would invalidate persisted state. Both are the twelve-factor “disposability” rule applied to a database.

Why layered config with secrets by reference. A single file makes a deployment reviewable and reproducible; the flags > env > file > defaults precedence is the universal expectation (it lets an orchestrator override a file entry via env, and an operator override both via a flag for a one-off). Secrets by reference keeps the file safe to commit and the secret in the orchestrator’s secret store — the failure mode of an inline secret in a checked-in config is too common to leave to discipline.

Why reference artifacts, not manifests. Shipping a Dockerfile, a systemd unit, and a runbook makes a deployment reproducible and documents the contract concretely. Shipping Kubernetes manifests (or a Helm chart, or an operator) would (a) assume an infrastructure Argon has no business assuming, (b) bind Argon to Kubernetes’s release cadence and API churn, and (c) re-create the framework-lock failure mode RFD 0052 rejected for the build system. The contract is normative; the artifacts are reference instances an operator adapts.

Alternatives

  • Authentication in the runtime. Rejected. Couples the engine to one auth scheme, widens its trust surface, and ties auth changes to engine releases. The gateway split is the standard service-mesh boundary and keeps the engine’s loopback posture sound. (This RFD does not design the gateway; it only fixes the topology the gateway plugs into.)
  • Expose the runtime directly on a routable interface with built-in auth. Rejected — it is the same coupling as above plus the loss of the network-isolation guarantee that makes header-borne trusted context safe. The is_loopback() enforcement exists precisely to forbid this.
  • Passing client-supplied trusted headers through. Rejected as unsound: a client could assert any tenant or principal. The gateway must overwrite, not merge, the trusted headers. This is stated as an invariant in D1, not an option.
  • Prometheus-direct (/metrics scrape) instead of OTLP. Rejected as the primary path: metrics only, no traces, and a second telemetry stack for traces. OTLP via the existing tracing bridge gives both vendor-neutrally; a Prometheus consumer runs an OTLP→Prometheus collector.
  • A bespoke metrics/trace format. Rejected. OpenTelemetry is the vendor-neutral standard; a bespoke format would force every operator to write an adapter.
  • Kill-on-SIGTERM (no drain). Rejected. Turns routine restarts into client-error bursts and leans entirely on commit atomicity to avoid corruption. Bounded drain is finite (per-request deadline) and makes restart invisible.
  • Process restart for a model change (no hot-reload). Rejected as the only path. Hot-reload with an atomic swap and no dropped requests is the zero-downtime upgrade; a restart is the fallback, not the norm. The schema-change gate guards both.
  • Environment-only or flags-only configuration. Rejected. A standalone server’s configuration surface (storage, placement, limits, OTel, ad-hoc policy) is too large for env-only to stay reviewable; a single layered file with env/flag overrides is the reproducible-and-overridable middle.
  • Inline secrets in the config file. Rejected — the file is meant to be reviewable and committable; secrets belong in the orchestrator’s secret store, referenced by env or path.
  • Shipping Kubernetes manifests / a Helm chart / an operator. Rejected. Assumes an infrastructure, binds Argon to Kubernetes’s cadence, and re-creates the framework-lock failure mode. Reference container + systemd unit + runbook is infra-agnostic.

Consequences

  • oxc-serve gains the platform shell: a graceful-shutdown signal handler (with_graceful_shutdown wired to SIGTERM), a layered configuration loader (D4: flags > env > file > defaults, secrets by reference), and an OTel export layer over the existing tracing subscriber (D2). The hot-reload primitives (reload_if_changed / spawn_watch_task) are promoted to a stated platform contract with the schema-change gate (D3).
  • The loopback enforcement and header-borne context are re-stated as the trust contract (D1) — the runtime’s existing is_loopback() refusal and x-tenant-id / x-principal-id / x-standpoint-id consumption are now the documented engine half of the gateway topology, with the client-header-overwrite rule named as an invariant the gateway must uphold.
  • Telemetry becomes operator-visible: phase-level spans (dispatch/reason/persist/maintain) and the engine’s cost-surface metrics (latency by path+tenant, mutation throughput, check-violations, store growth, IVM-rebuild frequency, reasoner-budget hits) export over OTLP to a configured collector; off when unconfigured.
  • A reference deployment ships: a Dockerfile + documented entrypoint, a systemd unit reference, and a runbook (topology, readiness/liveness, schema-change rollout, secrets). Infra-agnostic — no Kubernetes manifests.
  • Authentication is explicitly out of scope and lives in the gateway; the runtime runs no auth and trusts the gateway-set headers under the loopback guarantee.
  • The engine (RFD 0053) and its semantics are untouched. This RFD adds no transactor, MVCC, IVM, or admission mechanism; it wraps the existing ones in an operational shell. The embedded path (RFD 0052 D4) is likewise untouched — the platform shell is a standalone-serve concern.
  • Risk — the gateway is trusted absolutely. The runtime trusts gateway-set headers with no further check, so a misconfigured gateway (passing client headers through, or binding the runtime non-loopback) breaks the trust model. Mitigation: the runtime’s is_loopback() refusal is a hard precondition the runtime enforces unilaterally; the runbook (D5) makes the header-overwrite rule explicit; and the topology is the documented, only-supported shape.
  • Risk — observability overhead on the commit path. Per-phase spans on the hot commit path add instrumentation cost. Mitigation: OTel export is off when no endpoint is configured, sampling is operator-tunable, and the phase spans are coarse (four phases, not per-operation).

Open questions

  • Q1 — Reload signal vs. endpoint vs. watch. D3 admits all three triggers (file-watch, an explicit signal, a reload endpoint). Which are first-class for v1? Recommendation: file-watch (already built) + an explicit admin reload endpoint behind the gateway; reserve a signal (SIGHUP) as a convenience.
  • Q2 — Readiness probe semantics under recovery. A ready probe must gate traffic until the model is loaded and RFD 0053 recovery (checkpoint-seed + suffix replay) has completed. Does readiness also wait for the IVM maintainer to reach the recovered watermark, or admit reads at the base-fact watermark and let derived reads rebuild on demand? Recommendation: ready at the recovered watermark (derived reads consistent on first traffic), with a configurable “ready-early” for fast restart.
  • Q3 — Per-tenant trace/metric cardinality. Metrics keyed by tenant (D2) can explode cardinality at high tenant counts. Top-N + an “other” bucket, or operator-configured tenant allow-list for per-tenant breakdown? Recommendation: aggregate by default, per-tenant breakdown opt-in via config.
  • Q4 — Config hot-reload vs. oxbin hot-reload. D3 hot-reloads the model; should a subset of the D4 configuration (limits, OTel endpoint, fairness weights) also be hot-reloadable without restart, or is config restart-only? Recommendation: oxbin and OperabilityLimits hot-reloadable; bind address and storage backend restart-only (they define the process).
  • Q5 — Gateway reference implementation. Does Argon ship a reference gateway (a thin proxy demonstrating the header-overwrite contract) alongside the reference container, or only document the contract? Recommendation: document the contract in the runbook for v1; a reference gateway is a candidate follow-on if operators ask for one — but it must never become the gateway (that would re-import the auth coupling D1 rejects).
  • Q6 — Multi-region / HA. Out of scope here (a non-goal). The single-logical-writer model (RFD 0053) is per-scope; cross-region replication, failover, and read replicas are a future RFD over this platform shell, not part of it.

RFD 0058 — The Tableau Model Engine (Modal Escape Hatch)

Status

Proposed. Slice 1 is the explicit-single-frame K3 modal tableau, built as a new tier executor. Slice 2 — the fibred composition of the Kripke frame over standpoints (this document’s §“Slice 2”) — is built. Slice 3 is scoped here as a roadmap but is NOT built; the refusal it will eventually lift stays a loud refusal (OE1104).

Summary

Argon’s reasoner is a tier-classified, Z-set, semi-naive Datalog engine. The modal surface (box/diamond over a Kripke frame, §11.2) is the one place the Datalog engine cannot reach: a box(P) is a universal over an accessibility relation, a diamond(P) an existential, and while the standpoint frame is translated to stratified Datalog (translate_standpoint_modals, §D3), the explicit Kripke frame — worlds and an accessible relation the modeler declares via std::kripke — has no evaluator. Today every such modal is refused with OE1104 (the held escape hatch).

This RFD introduces a tableau model engine as a new TierExecutor advertising Tier::Modal. A tableau is the standard decision procedure for modal/description logics: it builds a candidate Kripke model and checks satisfiability by saturating expansion rules until a clash or an open model. It is the right machinery for the modal escape hatch precisely because it can do what fixpoint Datalog cannot — quantify over the frame structurally.

Because a tableau is the sensitive piece of a financial-grade substrate — a wrong answer here is silently catastrophic — the engine is gated by a differential oracle against both the existing Datalog executor (on the decidable overlap) and the mechanized ModalFrame semantics, before it goes anywhere near production.

The escape hatch as a tier executor (not a second path)

The load-bearing invariant of the reasoner is that all evaluation flows through Engine::evaluate, dispatched to a TierExecutor by tier. The tableau is a tier executor, not a parallel evaluation path:

        AtomIR-shaped rules
               ↓ instantiate
   translate_standpoint_modals
     ├─ standpoint frame → stratified Datalog (recursive tier)
     ├─ explicit Kripke frame → AtomIR::Modal preserved      ← slice 1
     └─ everything else → OE1104 (narrowed)
               ↓ compile
   CompiledAtom::Modal  (new variant)                        ← slice 1
               ↓ Engine::evaluate dispatch by tier
   KripkeExecutor : TierExecutor (Tier::Modal)               ← slice 1
               ↓
   Z-set RelationCatalog (BTreeMap<Tuple, Weight>)

The executor reads the RelationCatalog — the Datalog-materialized base facts (accessible, the per-world classification of the inner predicate) seed the tableau. This is the Konclude pay-as-you-go move: saturation first, tableau only for the modal goals. The executor writes its derived modal facts back into the same catalog. There is no second store, no second engine — the executor is one more backend behind the same dispatch surface.

Slice 1 — scope (held precisely)

A box/diamond modal tableau over a single explicit Kripke frame, K3-valued, strict consistency, one frame. Concretely:

  • Frame: the modeler’s std::kripke Worlds and accessible relation — explicit facts in the catalog. Not the standpoint reach-closure (that is the already-built §D3 Datalog translation), not the rigidity/classification frame.
  • Operators: box(P) and diamond(P) where P is a single positive predicate atom whose evaluation is per-world.
  • Value algebra: K3 — Truth4 constrained to {Is, Not, Can}.
  • Consistency: strict. A proposition forced to both Is and Not at one world is a clash. Can (unknown, open-world) is an open branch, not a clash.

Slice 1 does not do, and these keep refusing with a tighter OE1104:

  • The fibred composition of the Kripke frame over standpoints (slice 2 — now built; see §“Slice 2”).
  • Rigid roles, nominals (slice 3).
  • The classification/rigidity frame (slice 3+).

Semantics (authoritative; cites the mechanization)

Over a Kripke frame with accessibility R (spec/lean/Argon/Reasoning/Modal.lean, spec/lean/Argon/Standpoint/ModalFrame.lean):

  • box(w, P) = ∀ w'. R(w, w') → P(w') — necessity is the intersection over accessible worlds (Modal.lean box).
  • diamond(w, P) = ∃ w'. R(w, w') ∧ P(w') — possibility is the union (Modal.lean diamond).
  • Duality: diamond w P ↔ ¬ box w (¬P) (diamond_iff_not_box_not, proven).

Slice 1’s R is the user’s explicit accessible facts; the abstract KripkeFrame of Modal.lean is instantiated with those facts directly (not the reach reflexive-transitive closure ModalFrame.lean builds for standpoints).

Dead-end worlds (the faithful K(m) semantics). A world with no accessible successor is a dead-end. box(w, P) = ∀ w'. R(w, w') → P(w') is vacuously true there (the universal over an empty set), and diamond(w, P) = ∃ w'. … is vacuously false (the existential over an empty set). This is not a design choice but the standard semantics of base multi-modal logic K(m): by the DL↔modal correspondence (Schild, “A correspondence theory for terminological logics,” IJCAI 1991), the description logic ALC is a notational variant of K(m), in which ∀r.C (= box) is vacuously true at an element with no r-successors — exactly standard DL -rule behavior. It is also what Modal.lean’s box/diamond compute. The executor therefore anchors a box rule over every frame world — a dead-end world’s vacuously-true box derives the head — and a diamond rule derives nothing at a dead-end. (Anchoring only over worlds-with-successors would silently drop a dead-end’s vacuously-true box — an under-derivation, not the faithful semantics.)

The frame split. This dead-end case is specific to slice 1’s generic frame. Slice 1’s explicit accessible frame is base K: arbitrary, dead-ends allowed, hence vacuous box. The standpoint frame (ModalFrame.lean’s reflexive-transitive reach closure) is S4 — reflexive, hence serial — so every world reaches at least itself and there are NO dead-ends; the vacuous-box case cannot arise there. The two frames are distinct modal logics over distinct accessibility relations, and slice 1 is the K one.

Value algebra — why K3 is correct for one frame

spec/lean/Argon/Standpoint/Consistency.lean mechanizes the strict consistency policy and proves (append_strict_inK3) that under the default strict policy every cell stays in the K3 fragment {Is, Not, Can}. The information-determined-inconsistent value Both arises only at cross-standpoint federation (append_strict_fails_iff: a strict append fails exactly when it would create a (T, F) conflict, and that conflict is the cross-source case). Slice 1 is a single frame under strict consistency, so by this invariant every label is in K3. The carrier is Truth4 (oxc-protocol/src/storage.rs), with info_join the AFT bilattice operation; slice 1 uses only the K3-closed sub-table (Both never produced).

  • Clash = a proposition forced to both Is and Not at one world — the K3 inconsistency strict rejects.
  • Can (open-world unknown) = an open branch, not a clash. This is the open-world modal trap: box(P) holds only if P is Is at every accessible world; a Can/unknown world does NOT satisfy box (unknown ≠ true).

DL ≅ modal correspondence (why a role tableau ports)

Schild 1991: a tableau over an accessibility role is exactly a modal tableau — ∃accessible.P = diamond P, ∀accessible.P = box P. This is why the description-logic role-tableau machinery (studied from a separate read-only DL reasoner as an architectural reference) ports to Argon’s modal frame, re-skinned to Argon’s Truth4 label shape. No DL-reasoner code is copied; the pattern is ported.

Machinery (ported architecture, re-skinned)

The portable, proven tableau machinery — adapted, never copied:

  • Trail-based zero-copy undo + dependency-directed backjumping. Every mutation (a label cell set, an edge added, a node created) pushes a trail entry; undo replays the trail in reverse to a mark. Backjumping computes the earliest branch the current clash depends on and unwinds to it, skipping irrelevant choice points. This is the dominant performance win over cloning state per branch.
  • The expansion loop with dirty-node tracking and pre-computed rule data. Only nodes whose label changed are re-processed; the rule that fires on each label cell is classified once. The /-rules of DL become the diamond/box-rules over accessible.
  • Blocking. A single explicit frame is finite (explicit accessible facts), so slice 1 terminates by the finite frame. Subset/equality blocking is still implemented for the general/cyclic case so a cyclic accessible relation terminates. The blocking comparison is K3-label info-order dominance, not set inclusion: a node is blocked by an ancestor whose label dominates it in the information order.
  • Clash detection. Adapted to the K3 Is ∧ Not clash at a single world.

The DL-specific data shapes (concept-label bitvectors, SROIQ role algebra, absorption/NNF/EL pre-passes) are NOT ported — Argon’s label is Truth4-per- proposition over worlds.

Routing — narrowing the refusal, not dropping it

Today two sites refuse modals:

  1. translate_standpoint_modals (instantiate) refuses an unframed modal with OE1104 (InstantiateError::ModalNotEvaluated).
  2. The rule compiler (compile_atom) refuses a surviving AtomIR::Modal with OE1104 (RuleCompileError::ModalNotEvaluated) as a backstop.

Slice 1 inserts the explicit-Kripke-frame recognition: when the modal’s inner predicate is evaluated over a frame backed by std::kripke’s accessible, AtomIR::Modal is preserved (not translated, not refused), compiled to a new CompiledAtom::Modal { op, inner } variant, and handled by the KripkeExecutor. Every other shape — standpoint frame (translated as before), no frame / rigidity, rigid roles, nominals — KEEPS refusing OE1104. The refusal is narrowed to exactly slice 1’s complement, never silently accepted.

CompiledAtom is internal to oxc-reasoning and is NOT mirrored in any @[language_interface] Lean inductive, so adding CompiledAtom::Modal does not touch the Lean↔Rust drift gate. (AtomIR::Modal and ModalOp are already mirrored and unchanged.)

Soundness floor (the hard gate)

  1. Differential oracle. The KripkeExecutor’s result MUST agree with (a) the semi-naive Datalog executor on any program in the decidable overlap, and (b) the ModalFrame semantics — box(P) = the intersection (∀ over accessible), diamond(P) = the union (∃). A differential test runs over an explicit frame where box ≠ diamond (a proposition true at some accessible worlds, false or unknown at others) so a swap or collapse of the two operators fails it.
  2. Termination. Tested on a cyclic accessible relation — blocking must fire. A non-terminating tableau is a failure.
  3. K3 clash soundness / open-world box. box(P) holds only if P is Is at every accessible world; a Can/unknown accessible world must NOT satisfy box. Tested explicitly (the classic open-world modal trap).
  4. Scope refusal. Standpoint-frame, rigid-role, nominal, and classification- frame modals still refuse OE1104 — tested that slice 1 did not silently accept them.

Slice 2 — the Kripke frame fibred over standpoints

Slice 2 lifts the slice-1 single-frame modal tableau to the fibred composition of Argon’s two modal frames: it evaluates the explicit-Kripke box/diamond (slice 1’s tableau) independently per standpoint, then info-joins the per-standpoint K3 results across standpoints (the already-shipped FDE federation, §11 / Federation.lean). The four-valued Both arises exactly when two standpoints disagree on a modal fact — genuine perspectival disagreement — and nowhere else.

Why fibred, not a product (decidability)

A genuine product of two S4 modalities (S4×S4) is undecidable. Slice 2 deliberately avoids that boundary. The composition is a fibration: the Kripke accessibility relation is evaluated entirely within a single standpoint’s catalog (slice 1, unchanged), and the standpoint dimension enters only after the per-standpoint modal answer is fixed, as a flat info-join over the per-standpoint results. This is the fusion (independent join) of the two logics, not their product — decidable by the Wolter transfer theorem for fusions (Wolter, “Fusions of modal logics revisited,” 1998) composed with finite-frame model-checking of each per-standpoint Kripke frame (every explicit accessible frame is finite; slice 1 already terminates on it). The two accessibility relations never interleave: a standpoint boundary is never crossed inside a box/diamond expansion, and a Kripke world-step is never taken across standpoints. That isolation is the decidability argument and the soundness argument both.

Scope (held precisely)

  • In: the explicit std::kripke frame (slice 1’s box/diamond over accessible) fibred over a query’s across [...] standpoint set (or a standpoint {} block). Per-standpoint K3 result, info-joined across standpoints under the query’s FederationPolicy (paraconsistent: Both surfaces; strict: K3-projected at the boundary).
  • Out (still refused / not coupled): the classification / mutation- reachability frame (slice 3); rigid roles and nominals (slice 3); a genuine product or cross-nested box_standpoint(box_kripke(P)) (refused — see §“Cross-nesting guardrail”); a standpoint-frame modal and a Kripke modal composed in one operator (the cross-family coupling, refused).

The soundness crux — per-standpoint K3, then federate (Can vs Not)

For federation to manufacture the correct Both on genuine disagreement and never a false Both from mere ignorance, each standpoint’s modal result must reach federation as its faithful K3 valueIs / Not / Can — distinguishing determined-false from open-world-undetermined:

  • federate(Is, Not) = Both — genuine perspectival disagreement (one standpoint says the modal necessarily holds, another says it is determined-false). Correct.
  • federate(Is, Can) = Is — one standpoint confirms the modal; another simply does not know (its frame has an undetermined successor, no determined-false). This must not become Both. (Federation.lean federate_eq_both_iff: the result is .both iff some source said .both or two sources genuinely disagree — hasIs ∧ hasNot. A Can contributes to neither hasIs nor hasNot, so it cannot induce Both.)

The trap. Slice 1’s decide/decide_box/decide_diamond and its denotational oracle collapse Can → Not: decide_box returns Not both when an accessible successor is determined-false and when a successor is merely Can (open-world unknown). This collapse is correct for the single frame: slice 1’s derivation boundary is fail-closed (only Is derives the head, so Not and Can are indistinguishable there — both yield “no derivation”). But it is catastrophic at federation: a standpoint whose box(P) is genuinely Can (an accessible world is unknown, none determined-false), reported as Not, would federate with another standpoint’s Is to a false Both — a silent-wrong manufactured from ignorance.

Resolution (the linchpin — flagged for review). Slice 2 introduces a separate federation-outcome function that returns the faithful K3 value, WITHOUT changing slice-1’s single-frame projection. For box(P) at world w over a standpoint’s frame, scanning the accessible successors:

  • Is iff every accessible successor has P = Is (necessity confirmed);
  • Not iff some accessible successor has P = Not (a determined-false witness defeats necessity);
  • Can otherwise — no successor is Not, but some successor is Can (necessity is open-world undetermined: it could still go either way);
  • vacuously Is at a dead-end world (∀ over no successors), per the K(m) semantics slice 1 already pins.

The ordering matters: a Not successor dominates a Can successor — box is Not if any successor is determined-false, regardless of how many others are Can, because determined-false already defeats necessity. For diamond(P) at w (dual):

  • Is iff some accessible successor has P = Is (a witness);
  • Not iff every accessible successor has P = Not (every possibility foreclosed);
  • Can otherwise — no Is witness, but some successor is Can (a witness could still emerge);
  • vacuously Not at a dead-end (∃ over no successors).

This is the standard Kleene K3 reading of box = ⋀ / diamond = ⋁ over the three-valued successor labels (Is = ⊤, Not = ⊥, Can = the middle), with the dead-end conventions slice 1 fixes. It is computed directly from the frame (KripkeFrame::prop_at, which already returns the faithful Is/Not/Can), so it needs no change to the tableau procedure or its oracle. The per-standpoint federatable value is exactly this K3 value; slice-1’s fail-closed decide and its denotational oracle are untouched (slice 1’s single-frame contract is unchanged and its differential gate still holds).

A frame whose seed forces Is ∧ Not at one world is a clash (slice-1 strict-consistency rejection); that standpoint contributes nothing for the clashing cell (it does not contribute a determined value, i.e. it is treated as Can for that cell at federation — the strict-consistency reject is local to the one frame and must not poison the federation with a phantom determined value).

Routing — per-standpoint loop then federate

Two new pieces, both narrowing existing seams (no second engine, no second store):

  1. Frame-aware dispatch fix (closes a latent silent-wrong). translate_standpoint_modals (instantiate) previously checked is_explicit_kripke_modal only inside its roots.is_empty() branch — so a Kripke modal appearing inside an across [...] query or a standpoint {} block (roots non-empty) was mis-routed into the standpoint-frame Datalog translation (synth_standpoint_modal), silently producing a wrong extent instead of preserving AtomIR::Modal for the tableau. The fix checks is_explicit_kripke_modal first, independent of roots: a Kripke modal is preserved as AtomIR::Modal (→ tableau) in every standpoint context. A non-Kripke modal with no frame still refuses OE1104; a non-Kripke modal with a standpoint frame still translates as before.

  2. Per-standpoint query-routing loop. When a federated query carries a non-empty across set and a Kripke CompiledAtom::Modal, the runtime, for each standpoint s in the across set, materializes s’s per-standpoint catalog (materialize_predicates_for_standpoint(s) — DEFAULT ∪ standpoint-own), runs the slice-1 KripkeExecutor over that per-standpoint catalog, and reads the faithful K3 federatable value per head tuple. The contributions are then info-joined by the already-shipped query_derive_federated under the query’s FederationPolicy. The federation for a modal head bypasses the plain (in_pos, in_neg) classify_source_rows path — that path can only ever produce Is/Can for a modal head (the executor’s fail-closed projection writes Is-only), so it cannot carry the Not federation needs.

Cross-nesting guardrail (non-negotiable, from the decidability theory)

Cross-nesting of the two modal families — box_standpoint(box_kripke(P)) and its converse — is the coupling the undecidability boundary lives at; only additive (fibred / fusion) composition is sound and decidable. This is already refused by the existing nested-modal gate (lower_modal/atom_lower.rs): the gate refuses any modal whose inner atom_contains_modal, uniformly, before frame recognition runs — so box(box(…)), diamond(box(…)), and the cross-family box(box_kripke(…)) are all rejected with the modal refusal at lowering time. The cross-family case is a special case of that uniform refusal; no extension is required. Slice 2 pins it with a dedicated cross-family test so a future relaxation of the gate cannot silently open the undecidable door.

Soundness floor (slice 2)

  1. Differential oracle (fibred). Extend slice-1’s denotational oracle to the fibred case: run the per-standpoint tableau, take each standpoint’s faithful K3 federatable value, info-join them, and compare against a denotational fibred oracle — box/diamond evaluated per standpoint over each standpoint’s frame (the K3 reading above), info-joined. The test ranges over frames where standpoints disagree (→ Both), agree (→ K3), and where one standpoint is Can/unknown while another is Is (→ Is, not Both). The Can-vs-Not distinction is the central oracle-covered property.
  2. Mis-routing closed. A Kripke modal in an across / standpoint context routes to the tableau, not the standpoint-frame translation (tested).
  3. Cross-nesting refused. box_standpoint(box_kripke(P)) (and converse) are refused (tested).
  4. Soundness factoring. Slice-2 soundness = slice-1 tableau soundness (the per-standpoint K3 value is the slice-1 frame semantics, distinguishing the Can it previously projected away) × Federation.lean federate_eq_both_iff (federation is Both iff genuine disagreement) × a fibred-isolation argument (a standpoint boundary is never crossed inside a modal expansion, a Kripke step is never taken across standpoints — so the two frames compose additively, not as a product). Faithful to ModalFrame.lean / Modal.lean / Federation.lean / Consistency.lean.

Roadmap (NOT built here)

  • Slice 3 — rigid roles, nominals, the classification/rigidity frame. The rigidity frame (worlds = configurations an individual’s classification holds in), rigid roles, and nominals (singleton concepts). The full §11.2 static-discharge table.

Each slice lifts a strictly smaller piece of the OE1104 refusal; until a slice ships, its complement keeps refusing.

Module placement

The tableau lives as a tableau module inside oxc-reasoning (oxc-reasoning/src/tableau/), alongside the other executors. It is not a new top-level crate: the executor is one more backend behind TierExecutor, it shares the catalog and Truth4 carrier, and a new top-level directory is not warranted for slice 1.

RFD 0059 — The drift gate as a typed interface manifest

  • State: accepted
  • Depends on: RFD 0055 (the conformance corpus — the “Lean emits → Rust replays → CI freshness-gates” loop this reuses), the @[language_interface] contract (spec/lean/Argon/Interface.leancompiler/crates/oxc-protocol/)
  • Tracks: issue #1038
  • Prior art: the project’s own conformance-vector loop (EmitVectors.leancompiler/tests/lean-vectors/*.jsonconformance_replay.rs, CI-freshness-gated in check.yml); schema-from-source generation (protobuf descriptors, cargo-public-api); the general principle that a contract checker should read the elaborated artifact, not re-parse the surface text.

Question

@[language_interface] is the load-bearing Lean↔Rust contract: inductives tagged with it carry the language’s data shapes, and the Rust mirror enums in oxc-protocol must align by name and arity. CI enforces this through compiler/crates/oxc-protocol/tests/drift.rs. Is that enforcement sound, or is it fragile in a way that can let real drift through silently?

Context

drift.rs establishes the Lean side of the contract by textually scanning every .lean file under spec/lean/Argon/: it greps for @[language_interface], then for the next inductive line, then parses constructor lines for names and counts arrows for arity. Three properties of that scanner are load-bearing and, on inspection, fragile:

  1. It keys on the unqualified inductive name (the token after inductive) and resolves duplicates with .or_insert — first-in-filesystem-walk-order wins. read_dir order is not deterministic across machines.
  2. There is more than one tagged inductive with the same short name. RuleMode is @[language_interface]-tagged in both Syntax/Rule.lean and Storage/AxiomBody.lean. They currently have identical variants, so the mirror binds correctly by luck, not by construction — the moment the two diverge, the gate binds to whichever the walk reaches first and can report green on a drifted type. (Term and AtomIR also have same-named twins in Reasoning/EvalProgram.lean; those are saved today only because the twins are untagged.)
  3. Coverage is partial and silent. ~88 inductives carry the tag; only ~40 appear in declared_mirrors(). A tagged type with no mirror entry drifts entirely undetected — only a coarse “≥ 25 discovered” sanity floor and the Declgrammar.toml check guard the remainder.

On top of these, the scanner is regex-grade parsing of a real grammar (one-line | a | b | c, :-detection, breaks on def/theorem/@[), so it can mis-read multi-line constructor signatures or attribute-then-comment-then-inductive sequences.

None of this is hypothetical drift today — the gate is green. But it is one rename away from a silent false-pass on a CI-enforced correctness contract, which is the exact “silent-accept at an un-RFD’d seam” failure class the 2026-06 system audit found to dominate.

Decision

Stop deriving the Lean side of the contract by re-parsing surface text. Have the Lean elaborator emit a typed interface manifest — every @[language_interface] declaration with its fully-qualified name, kind, and constructor names + arities — and have the Rust gate diff its mirror table against that manifest. The manifest is the same shape of artifact as the conformance vectors: Lean computes it, it is committed, and CI freshness-gates it against a fresh emission.

This makes three things true by construction that the scanner only approximated:

  • Names are qualified, so same-short-name twins are distinguishable and the binding is never ambiguous.
  • The elaborator is the source of truth for constructors and arity, so there is no grammar to re-parse and no parsing bug to hide drift.
  • Coverage is enumerable: the gate sees every tagged declaration and can require each to be either mirrored or explicitly waived, turning “silently unchecked” into “explicitly listed.”

Design

  • Emitter. spec/lean/EmitInterface.lean + an emit-interface lean_exe. It folds over the environment, selects declarations tagged @[language_interface], and emits, sorted by qualified name for stable diffs:

    {
      "_comment": "GENERATED by `lake exe emit-interface` — do not edit by hand.",
      "schema": "argon.interface.v1",
      "decls": [
        { "name": "Argon.Substrate.Atom", "shortName": "Atom", "kind": "inductive",
          "ctors": [ { "name": "metaCalculus", "arity": 1 }, … ] },
        …
      ]
    }
    

    arity is the constructor’s field count (parameters excluded) — the same convention the Rust mirror table already uses (metaCalculus : MetaCalculusAtom → Atom ⇒ arity 1). Structures carry kind: "structure" and their single mk (waived from mirroring; included for completeness).

  • Committed artifact. compiler/tests/lean-vectors/interface-manifest.json, alongside the conformance vectors, never edited by hand.

  • Gate. drift.rs parses the manifest with serde instead of scanning .lean files. It then:

    1. Detects ambiguity: if two manifest decls share a shortName with different ctors, hard-error (the divergence hole). Identical twins (today’s RuleMode) are tolerated and bind unambiguously.
    2. Checks each mirror (declared_mirrors()) against the manifest decl of that name by constructor PascalCase-name and arity — the existing alignment logic, now over typed data.
    3. Enforces coverage: every inductive manifest decl is either mirrored or in an explicit UNMIRRORED_WAIVERS list with a one-line reason. A newly-tagged-but-unmirrored type fails the gate instead of passing unseen.
    4. Keeps the Declgrammar.toml variant check and a sanity floor, now exact against the manifest.
  • Freshness. CI re-emits and diffs, exactly as the conformance vectors are gated (check.yml): lake exe emit-interface piped to diff against the committed manifest, with a clear regen instruction on mismatch. The cargo test only consumes the committed file (it does not require lake), matching the established split.

Non-goals (sequenced separately)

  • Namespacing the root-level surface AST types (_root_.Expr/Pattern/Literal/RuleAtom/RuleMode/…). These shadow Lean.* under open Lean and are a latent footgun for any metaprogram in the tree (it is what broke the mech probes before the tool-side fix). The typed manifest emits qualified names regardless, so it composes with a later move into Argon.Syntax.* — but that move is a large mechanical rename across the canonical substrate, a merge-conflict hazard against the many in-flight branches, and no longer load-bearing for any tool. It is tracked as its own change, to be done in a quiet window after this manifest lands.
  • Rust-side introspection. The mirror table stays hand-maintained; Rust enums are not reflected at test time. The manifest secures the Lean side (where the parsing fragility lived); the Rust side remains a small, reviewed table.

Alternatives considered

  • Harden the text scanner in place (B1): make .or_insert a hard duplicate-name error and assert mirror-or-waiver, keeping textual parsing. Smaller, and it closes the two worst holes (ambiguous binding, silent coverage gap). Rejected as the primary because it leaves the grammar-reparsing brittleness and keeps two encodings of “what is tagged” (the scanner’s view and the elaborator’s truth) that can disagree. It is the acceptable fallback if the emitter proves disproportionate.
  • Do nothing — the gate is green. Rejected: green-by-luck on a contractual gate is the failure mode this RFD exists to remove.

RFD 0060 — The Lean substrate mechanization architecture

  • State: accepted
  • Depends on: RFD 0059 (the typed-interface manifest drift gate — the seam this RFD reorganizes around), the @[language_interface] contract (spec/lean/Argon/Interface.leancompiler/crates/oxc-protocol/)
  • Tracks: issue #1038 (the _root_ shadowing footgun), #1062 (dedup), #1063 (lowering preservation)
  • Prior art: the convergent structural conventions of mature mechanizations — CompCert (lib/ foundation never depends upward; deep-embedded source+IR), CakeML (functional big-step, in-logic bootstrap), Iris / iris-lean (directory = namespace, import hygiene via linters), Mathlib (namespace = path, one-concept-one-definition, @[deprecated] rename aliases, import-graph linters), seL4 (refinement reserved for a thin hand-written slice). Design-research evidence base: deep-vs-shallow embedding (Gibbons & Wu 2014), the intrinsic-typing cost scaling law (Reynolds 2000; Allais et al. 2018; Rouvoet et al. 2021), the spec↔impl trust-tier menu (Pnueli et al. 1998; Klein et al. 2009), lowering preservation as least-model agreement vs step-simulation (Leroy 2009), and the calibrated finding that naming→maintainability is convergent best practice but not a measured causal lever (Ringer et al. 2019; Baanen et al. 2025).

Question

The Lean 4 / Mathlib development under spec/lean/Argon/ is canonical for Argon’s substrate semantics. It was grown incrementally by agents without Lean tooling, and it is mathematically sound but architecturally unplanned. Two questions, settled here:

  1. What is the right internal architecture — module structure, namespacing, embedding choices, dedup — for a world-class substrate mechanization, and how do we migrate to it without re-proving anything?
  2. How should the CI / drift contract be coupled to the Lean during and after that re-architecture, given the contract was itself partly responsible for the mess?

Context

The verified current state (three independent firsthand audits + the design-research synthesis converge):

  • Sound where it matters, tangled where agents touched it. ~1,500 source declarations (kernel-elaborated ≈8,000) across 146 files, ~25.6k lines. Zero sorry and zero axiom in the build target; the strict proof-discipline gate (mech verify) passes; the development is faithful (mech check flags are structural false-positives — ¬-lemmas, empty/nil base cases).
  • Crown jewels to preserve verbatim: the Foundation/ bilattices (the K3 “no CompleteLattice on MetaValue” landmine is mechanized as a theorem, infoJoin_escapes_K3, not merely a comment) and the Reasoning/Datalog/ AFT proof spine. Mathlib reuse is already disciplined (fine-grained imports, no blanket import Mathlib).
  • The dominant defect is namespacing incoherence: 101 of 167 @[language_interface] types are at _root_; of 146 files, 57 use namespace Argon.*, 48 use none, 41 use a bare-generic namespace (Modal, State, Closure). Core types Atom, Term, Expr, Literal, Tier, Truth4 shadow Lean.*/Mathlib — the footgun that broke the mech probe (#1038).
  • The coupling caused the mess, and RFD 0059 already cut the cause. The original drift gate text-scanned .lean files keyed on the bare inductive name, so the convention grew up to declare contract types at _root_. RFD 0059’s manifest keys on qualified names emitted by the elaborator, so root-level placement is now load-bearing for nothing — vestigial.
  • The coupling is already loose. The lean CI job (full lib build + manifest/vector freshness) is push-only and non-blocking — it does not gate PRs or the merge queue. Only the committed-manifest-vs-Rust-table check (drift.rs, no lake needed) gates the queue, and the drift key is shortName = the last namespace component, so moving a type from _root_ into Argon.Syntax.* keeps its key stable; the gate survives namespacing. A reorg’s failure mode is therefore post-merge red on main (a stale committed manifest), not a queue block.
  • Two content holes, not just structural debt: CoreIR/Lowering.lean is a placeholder (lowerRule emits empty heads/bodies, deriveCategory hardwires .cat1) — the content of the preservation theorem is absent; and ARS/Spec.lean’s ars_contracts_hold reduces to ⟨trivial, trivial, trivial⟩ — a vacuous theorem, a faithfulness liability.

Decision

Re-architect the substrate in place, proof-preservingly to the target below. The skeleton (directory layering) is kept; the skin (namespacing) is normalized; the fused organ (the ABI contract) is separated from the proof body. No rewrite; no intrinsic-typing or refinement rebuild.

  1. Namespace = directory path under Argon.*. Every declaration lives under Argon.<Layer>[.<Sub>]; no domain type at _root_; no bare-generic namespace (namespace Modalnamespace Argon.Decidability.Modal). One concept gets exactly one definition. Stated honestly as convergent best practice + footgun removal (the _root_/Lean.* shadowing is a real correctness hazard), not as a proven maintainability cause.
  2. The import graph is guarded against growing entanglement, and the foundation stays pure — but the target is bounded entanglement, NOT a strict DAG. The directory graph is not a linear DAG, and for a reflective core it should not be (see “The layering” and “Delimited reflection is forced”): the meta-calculus’s upward edges are essential reflection, permanent by design. The enforceable invariant (the evidence-backed “keep the import graph minimal” lever, plus our KNOWN_UNMIRRORED-style baseline idiom) is: a committed baseline of the cross-directory edge set that distinguishes INTENTIONAL reflective edges from ACCIDENTAL ones, a lake-free CI guard (spec/lean/scripts/check_layering.py, runs on PRs + enforces in the merge queue) that fails on any new edge (no new entanglement) or any stale edge (an accidental cycle shrank — lock the win), and hard foundation-layer purity (Foundation/Interface rest only on Mathlib). The resting state is bounded entanglement; only the accidental edges are debt.
  3. The ABI is separated from the proofs by a data-from-proofs discipline (the seam). Contract-bearing @[language_interface] types live in thin data-only modules (declarations + deriving, no substantive theorems); their proofs and semantics live in importing modules. The in-Lean elaborator manifest (RFD 0059) stays the single source of truth; no non-Lean IDL (it would reopen the two-sources-of-truth hole 0059 closed). Restructuring the proof body then never touches the contract surface.
  4. Embedding: deep + extrinsic, everywhere. Surface AST and IR stay deep-embedded (lowering is the canonical non-compositional traversal). Typing stays extrinsic. Reject well-typed-by-construction / intrinsic IR typing (Argon’s subtyping + refinement + occurrence/flow typing + RuleIR’s global stratification is exactly the rich-equational-index regime where intrinsic typing’s transport + coherence tax dominates, with no Lean port of the Allais lineage even existing) and reject wholesale Rust↔Lean refinement (≈20py, does not amortize; seL4’s own reason for keeping hand-written code).
  5. Gate posture: migration-tolerant during the window, re-armed hard after. During the migration: keep the cheap silent-accept guards hard (manifest freshness, ambiguity/shape-twin detection); demote mirror-alignment to advisory; rely on the behavioral oracle (the conformance vectors / engine_agrees_with_batched_lean_oracle, the tier that actually carries spec↔impl correctness). After the seam is stable: re-arm all of drift.rs hard and pull the freshness re-emission into the merge-queue gate (it is currently only post-merge). Operational discipline meanwhile: because the Lean is under-gated, reorg PRs regenerate the committed manifest locally before merge.
  6. Close the two content holes (separately scoped, after the structural work). Implement CoreIR/Lowering as a real translation with an explicit match-relation, and prove preservation as least-model / denotational agreement (not CompCert step-simulation — Argon’s semantics is a fixpoint). Replace the vacuous ars_contracts_hold with the real contract, or, if Rust wiring genuinely blocks it, an honest tracked open obligation rather than a True-laundered theorem.

Design

The layering: foundation purity + bounded entanglement (NOT a strict DAG)

The coarse intended order:

L0  Foundation / Interface / Schema   bilattices, the tag attribute, ontology+signature data  → Mathlib only
L1  Syntax                            the surface AST (deep, extrinsic)
L2  Core (data)                       Substrate (atoms), MetaCalculus *data* (Axis), CoreIR (incl. Lowering)
L3  Semantics                         TypeSystem, Reasoning (Datalog/AFT), Standpoint, Locality, Decidability,
                                      and the meta-calculus's *reflective semantics* (see below)
L4  Contracts                         Storage, Runtime, BuildArtifact, Pipeline, ARS
    Oracle/  Scratch/                 globbed out of the lib — not in the layering

(Schema is foundational data, not a contract — Mathlib-only, depended on by Locality/TypeSystem — so it sits at L0.)

The enforced invariant is NOT a strict linear DAG. It is acyclicity-as-achievable + hard foundation purity + a frozen, minimal entanglement baseline — what check_layering.py enforces. This is deliberate, on two grounds: it is the invariant the maintainability evidence actually supports (the strict stratification is convergent convention, not measured cause), and — decisively — a strict per-directory DAG is the wrong ideal for Argon, because its core is a reflective meta-calculus.

The meta-calculus is an intentional two-tier reflective subsystem. Its data (Axis — the axis / metatype / metarel shapes) is low (depended on downward by Storage, Decidability, …). Its reflective semantics is high by necessity: IS/CAN/NOT classification is the reasoning fixpoint (IsCanNot builds on Reasoning.Fixpoint); TypeRef reflection is decided by TypeSystem.Subtyping (Reflect); the meta-property model is the storage catalog (AxisRelation). The meta-level is expressed in object-level vocabulary — that is what reflection means (Bowen–Kowalski amalgamation; 2LTT; Lean’s own cyclic-and-operationally-broken bootstrap). So the upward edges MetaCalculus → {Reasoning, TypeSystem, CoreIR, Storage} are essential and permanent, not debt — re-homing them to satisfy a DAG would scatter a defining atom across four directories and is forbidden (the baseline marks them INTENTIONAL).

The genuinely accidental cycles — the only ones to dissolve over time — are {CoreIR, Substrate} and the filing-neutral TypeSystem → Runtime (the MutationWrites bridge). The within-L3 mutual edges (Reasoning ↔ Standpoint ↔ Locality ↔ TypeSystem) are legal within-stratum, not cycles. So “cycle-breaking” is a small, optional tidy — not a phase the architecture is waiting on.

Delimited reflection is forced (the reflective tier)

Argon’s reflection is delimited: TypeRef is a handle into a closed, predicative catalog (no Type:Type); iof/specializes/extent are decidable predicates over reasoner-materialized catalog relations; runtime metaprogramming is a separate macro atom (re-checked), not open reflection. This is forced, not stylistic, by the two commitments that define Argon — characterized decidability and a sound Lean mechanization — confirmed from four independent directions:

  • Argon’s own decidability ladder: the metaorder tier (reflection / higher-order instantiation) is “bounded decidable; otherwise not.” Decidable ⟺ bounded is the ladder’s organizing principle.
  • Soundness: impredicative Type:Type is Girard-inconsistent (Hurkens) — it could not be modeled in the predicative Lean substrate at all.
  • Static reasoning: open, syntax-observing reflection is Wand-trivial (contextual equivalence collapses to α-congruence), destroying the equational theory the static-check plane and the content-addressed build depend on — hence the macro-atom separation.
  • Argon’s own domain (DL/KR): unbounded/expressive metamodeling (OWL-Full) is undecidable; the decidable path is a controlled base + stratification. The undecidability is not from the cross-level identity tie — Motik (2007) proves tied metamodeling decidable over a controlled base — so Argon does genuine contextual-tie, catalog-tier reasoning, strictly richer than OWL-punning / Java Class<?>.

So Argon sits at the richest uniformly-decidable point: closed predicative codes for the handle, first-class bounded multi-level iof/specializes for the reasoning, with higher-order orders pushed to stratified library theories (std::mlt, std::potency at tier:metaorder) that opt back into PTIME via a declared order bound. The only construct forfeited is unbounded orderless metamodeling (Type-of-itself) — which ontological modeling does not need and which is not even proven decidable. The bounded reflective edge set in the baseline is the formal expression of this delimitation.

Two obligations this carries (tracked separately, not in this RFD’s slices): the order bound must be a checked contract the tier classifier honors (with a cross-level-equality guard), not an inferred bound; and the order_bound(N) ⇒ N-stratified ⇒ decidable demotion is currently a sound argument, not a Lean proof.

Enforcement. The baseline guard (check_layering.py, lake-free, runs on PRs + enforces in the merge queue) freezes the cross-directory edge set and holds foundation purity, failing on new entanglement. The baseline distinguishes INTENTIONAL reflective edges (permanent) from ACCIDENTAL ones (may be shrunk). There is no “phase 2 converge-to-DAG” — bounded entanglement is the resting state.

The seam (data-from-proofs)

A module that declares @[language_interface] types is a data module: type declarations + deriving, no substantive theorems. Proofs about those types live in sibling semantics modules that import them. The @[language_interface] tag stays on the real inductive/structure (the emitter reads constructor data from .inductInfo/.ctorInfo, so it must tag the actual type, not an abbrev alias). Argon/Interface.lean remains the attribute definition and becomes the documented ABI index. Consequence: a proof-side re-architecture (renaming internal namespaces, splitting proof files, reordering) cannot move a contract type by accident, because contract types are quarantined in data modules.

Dedup targets (surgical — the raw count of “6 duplicates” overstates it)

  • RuleMode — genuinely identical (Syntax/Rule.lean + Storage/AxiomBody.lean, same 5 ctors). Unify to one canonical definition (Argon.Syntax.RuleMode), Storage imports it. (Conflict note: Storage/AxiomBody.lean is modified by feat/0.3-surface-operators-630-631; sequence after it lands.)
  • WorldAssumption ×3 — untagged, but divergent constructor order across Schema/ and the two TypeSystem/Soundness/ copies (a latent bug). Unify to one Argon.Schema.WorldAssumption.
  • FieldDecl, Path — intentional same-name different-layer twins, already drift-waived (KNOWN_DUPLICATE_SHORTNAMES). Keep distinct; namespacing makes the divergence legible. No merge.
  • Term, AtomIR — the second copy is the out-of-target oracle (Reasoning/EvalProgram.lean); namespace the oracle as Argon.Reasoning.Oracle.* so the divergence is named, not accidental. No merge.

Migration plan (proof-safe slices, in order)

  1. Layering guard (done in this RFD’s PR)check_layering.py + the committed 52-edge baseline, wired as a lake-free lean-layering CI job (runs on PRs, enforces in the merge queue). Freezes entanglement and foundation purity now; no code moved.
  2. Namespace normalization — move every declaration under Argon.<Layer>; kill _root_ and bare-generic namespaces; ensure no open Lean in the spine. Proof terms are untouched (rename + import churn only); the drift shortName key survives; regenerate the committed manifest in the same PR. This does not change cross-directory edges, so the layering baseline is unaffected. Sequence the MetaCalculus/* and Storage/AxiomBody.lean files around feat/0.3-surface-operators (do them last or after it merges).
  3. Optional tidy (NOT cycle-breaking the reflective tier) — the meta-calculus’s upward edges are intentional reflection and stay. The only accidental edges worth dissolving are {CoreIR, Substrate} and the filing-neutral TypeSystem → Runtime (MutationWrites, which could move to Runtime). Small and optional; the guard’s stale-edge check locks any win. Do not re-home MetaCalculus/{IsCanNot,Reflect,AxisRelation}.
  4. DedupRuleMode, WorldAssumption; namespace the oracle twins (Argon.Reasoning.Oracle.*). Coordinate on Storage/AxiomBody.lean.
  5. Re-arm the gate — pull freshness into the queue gate; restore mirror-alignment to hard.
  6. Content holes (Option C, separate effort) — lowering match-relation (#1063); de-vacuify ARS.

Each slice: own worktree off origin/main; lake build + mech verify + the drift tests + manifest freshness green before merge; commit as you go.

Alternatives considered

  • Intrinsic / well-typed-by-construction IR, or seL4-style refinement against the Rust engine. Rejected on documented cost-scaling grounds (transport + coherence tax for rich indices; refinement does not amortize). Argon’s extrinsic-deep choice already matches CompCert/CakeML.
  • A non-Lean IDL for the contract. Rejected — reintroduces the two-sources-of-truth hazard RFD 0059 eliminated; the contract belongs in the prover, since the Lean leads on substrate semantics.
  • Formally decouple / rip the gate to fully advisory during the redesign. Rejected as unnecessary: the gate is already PR-/queue-non-blocking and survives namespacing, so migration-tolerant posture suffices while keeping the cheap silent-accept guards.
  • Do nothing (keep the organic structure). Rejected: the _root_ shadowing is an active footgun, and “architected” means the layering is a checked invariant, not prose that drifts.

Non-goals

  • The reasoner-semantics catch-up (the Rust-leads-the-Lean reasoner gap, issue #134) is out of scope; this RFD is about structure and the contract, not closing semantic gaps beyond the two named content holes.
  • Naming/namespacing is adopted as cheap anticipatory hygiene + footgun removal; this RFD makes no claim that it causes lower maintenance cost (the evidence does not support that claim). The evidence-backed maintainability levers — acyclic/minimal import graph, one-concept-one-definition, interface-hiding, proof-robustness (structural tactics, no auto-generated-name dependence), a deprecation path before foundation-type changes — are folded into the slices above and the layering DAG.

RFD 0061 — A single VERSION file is the source of truth for the toolchain version

  • State: accepted
  • Depends on: RFD 0032 (version-coupled VS Code extension — the .vsix carries the toolchain version), the on-demand-tag release model (RELEASING.md)
  • Tracks: the release-version arc (supersedes the stored-version-of-record machinery)
  • Prior art: a checked-in VERSION file read at build time (the Linux-kernel / many-C-project pattern — the version lives in the tree, not the VCS metadata); SemVer pre-release / build-metadata grammar (X.Y.Z-dev, +g<sha>); Cargo’s build-script cargo:rustc-env mechanism. This RFD supersedes the git-tag-derived design (git describe, the Go/setuptools-scm/vergen pattern) that an earlier revision adopted.

Question

Argon is Sharpe-internal and never published to crates.io; consumers pin git revs. The toolchain version was once stored as a version of record in eight places — compiler/Cargo.toml [workspace.package].version, oxup/Cargo.toml, the VS Code package.json, two Cargo.locks, and three editor stamps — kept in lockstep by cargo xtask bump / cargo xtask check-versions and a post-tag bump PR. That stored version bought nothing (nothing is published to a registry that reads the field; internal path-deps ignore it) and cost a recurring post-tag bump PR that starved main, a lockstep gate that could drift, and a “bump, PR, merge, then tag” release dance.

The first fix collapsed all of that into the git tag: git describe derived the version at build time, so cutting a release was just git tag && git push. That keeps the single-source win but makes every build git-dependentgit describe needs a .git with reachable tags. A source tarball, a shallow CI checkout, an in-tree copy without .git, or any build off the VCS metadata then can’t resolve the version and falls back to a 0.0.0 placeholder; the version is also not human-readable from the tree (you must run git to know it).

So: where should the single source of truth live so a build is git-independent — resolvable from a plain tarball, reproducible, and human-readable — while keeping the “no lockstep, no bump PR” win?

Decision

A single VERSION file at the repo root is the source of truth for the toolchain version. It holds the bare current dev line (e.g. 0.3.4) on one line. Every binary reads it at compile time via include_str! — no git, no git describe, no fetched tags — and surfaces the composed version through --version and the LSP serverInfo.version. The git tag is cut from VERSION; the only “bump” is an atomic advance of that file after a cut.

Why a file, not the tag: the version must resolve with no .git at all (tarball, shallow checkout, in-tree copy). A checked-in file is git-independent, reproducible from the source alone, and human-readable (cat VERSION). The tag still exists — it is what stable points at — but it is downstream of the file, not the source.

The derivation (build.rs)

Each binary crate (oxc-driver — the ox/oxc/ox-lsp/oxfmt bins — and oxup), plus the two library crates that surface the version on the wire (oxc-lsp, oxc-serve), runs a build.rs that reads VERSION via include_str! and emits cargo:rustc-env=ARGON_VERSION:

buildcomposed ARGON_VERSION
dev (default), git reachable<base>-dev+g<shortsha>
dev (default), no git at all<base>-dev
stable channel signalbare <base>
ARGON_VERSION set directlythat value, verbatim

where <base> is the trimmed VERSION contents. The core version always resolves from VERSION — the +g<shortsha> is pure build metadata, attached only when git happens to be reachable and never required. A build with .git absent or unreadable still produces <base>-dev; it never errors and never falls back to a placeholder (the file is always present in the tree).

Channel selection is by explicit signal, never inference: ARGON_RELEASE_CHANNEL=stable (set by the release stable build) selects the bare <base>; a non-empty ARGON_VERSION baked directly wins outright (the release pipeline bakes the fully-resolved label — bare X.Y.Z for stable, X.Y.Z-dev.<ts> / X.Y.Z-nightly.<date> for dev/nightly). Local / dev / no-signal builds get the -dev form. build.rs also emits ARGON_VERSION_STRING — the human --version banner composing the version, the build sha, and a dev/toolchain note (the #234 provenance string).

build.rs emits cargo:rerun-if-changed on the repo-root VERSION (located by walking up from CARGO_MANIFEST_DIR, so it is correct for every includer crate’s depth) and rerun-if-env-changed on ARGON_VERSION / ARGON_RELEASE_CHANNEL / ARGON_TOOLCHAIN_VERSION / ARGON_BUILD_SHA. A light rerun-if-changed on the resolved HEAD ref is kept only because the optional +g<sha> metadata moves with it; absent git it is simply skipped.

The shared body lives in compiler/version_build.rs, include!d by the three compiler-workspace crates; oxup keeps an identical copy (it is a deliberately standalone workspace and does not cross-include).

The placeholders (never edited)

  • compiler/Cargo.toml [workspace.package].version and oxup/Cargo.toml [package].version are 0.0.0, forever. Cargo requires the field; nothing reads it. (Normal dev never edits VERSION either — only the release cut’s atomic advance does.)
  • The three editor stamps (editors/nvim/lua/argon/version.lua, editors/emacs/argon-version.el, editors/vscode/src/argon-version.ts) are 0.0.0-dev placeholders. The .vsix is stamped from ARGON_VERSION at package time (release.yml build-vsix); a plain checkout keeps the placeholder. The editor version-skew nudge (#675) treats a 0.0.0 / -dev stamp as “unstamped” and suppresses the warning.
  • The VS Code package.json marketplace version is stamped from ARGON_VERSION at package time.

What stays deleted

  • cargo xtask bump and cargo xtask check-versions (the entire compiler/xtask/src/version.rs).
  • The check-versions step in check.yml’s drift job.
  • The post-tag bump PR machinery in release-cut.yml (the bump-PR step).

The cut procedure + the atomic advance

Cutting a stable release (release-cut.yml, workflow_dispatch from main) is:

  1. gate — enforce zero open P0-blocker issues (RELEASING.md);
  2. taggit tag "v$(cat VERSION)" at the green main tip and push it (which fires release.yml’s stable path; the stable build reports the bare X.Y.Z). An explicit version input escalates to a minor/major (must be >= the file line);
  3. atomic advance — bump the patch (X.Y.ZX.Y.(Z+1)), write it back to VERSION, and fast-forward push it to main directly as the bypass bot (a protect-main bypass actor — no PR, no merge queue, no starvation). The push is rebase-retried in case main moved (it is the only writer during a cut). This single commit is the entire bump — there is no lockstep gate and nothing else to advance.

A dev build off the freshly-advanced main then reports X.Y.(Z+1)-dev automatically — newer than the just-cut release, not yet the next one.

Out of scope

The .oxbin / 4-axis artifact-schema versioning and the ARGON_ACCEPT_SCHEMA_CHANGE gate are a separate artifact axis, unrelated to the toolchain semver, and are untouched.

RFD 0062 — CI tiers + affected-scoping: main green by construction

  • State: accepted
  • Depends on: cargo xtask affected (the affected-set foundation; the reverse-dependency closure with a conservative broad fallback), the on-demand-tag release model (RELEASING.md), RFD 0061 (one VERSION file — no version-lockstep gate to run), RFD 0060 (the Lean layering guard, a pure-text check that gates every tier while the heavy lean job does not)
  • Tracks: the CI-redesign arc (this RFD is the workflow rewrite that the affected-detection tool was built for)
  • Prior art: affected-set / target-determination in monorepo CI (Bazel’s --output=targets change-impact, Nx affected, Turborepo’s --filter + dependency graph) — monorepo good, monobuild bad; the merge-queue “test the would-be-merge commit” model (GitHub merge queue, Bors, Zuul’s speculative gating); the single-aggregate required check pattern that lets matrix + conditional jobs gate a queue at all.

Question

check.yml is the CI everyone depends on and the sole gate on the merge queue (ci-gate is the only required status check; ALLGREEN grouping, squash, batches of ≤5). It grew a 3-tier structure expressed as scattered if: github.event_name == … conditions on each job, and it ran the whole workspace for every code change. Two problems compounded:

  1. The tiers were implicit. Whether a job gated the queue, ran only post-merge, or was informational on a PR was encoded in a raw github.event_name comparison buried in each if:. Reading “which jobs run for a merge_group?” meant tracing every job. A miswired tier was invisible.
  2. Every gate was a monobuild. A one-line change to a leaf connector rebuilt and retested the entire workspace in the queue. That made the gate slow, which is the pressure that pushes checks out of the queue (where they can no longer red main) purely for speed — the wrong reason to weaken a gate.

So: how do we make the merge gate both strong (it runs everything that can red main and is affordable) and fast (it does not monobuild), with the tier of every check self-documenting?

Decision

Principle: main is green by construction

The merge gate — the queue — runs everything that can red main and is affordable. The post-merge tier holds only a small, documented, deliberate accepted-risk set: checks that are slow, rarely combination-sensitive, and whose recovery is a fast revert. Nothing else may sit post-merge. Affected-scoping is what makes the queue both strong and fast: it is the lever that lets a gate stay strong without monobuilding, so “too slow” stops being a reason to drop a check out of the gate.

Three explicit tiers

Tier membership is a named output of the setup job (is_pr / is_queue / is_postmerge, derived once from github.event_name), and every job’s if: reads those named outputs — never raw github.event_name. A job’s tier is then self-documenting at the if: line, and a miswiring is a one-line diff, not an archaeology dig.

TierEventWhat runsci-gate
1 — PRpull_requestaffected build + clippy + fmt + deny + drift; oxup (if affected); book (if affected); the always-on text guards (lean-layering, rfd-file-count, flake, fmt)informational
2 — queuemerge_groupTier 1 + affected debug compiler-test + compiler-test-release-gate (release-tests affected ∩ merge-critical) + release-binary-smoke (non-nix, libduckdb-free release link) + macos-build-smoke (compile-only)enforces
3 — push / post-mergepush to mainTier 2 + full lean + full macOS compiler-test + full-workspace compiler-test-releaseenforces (loud; does not block)

Tier 1 gives a PR fast, cached, scoped feedback (build + lint), not the slow test suite — the slow suite runs once, in the queue. ci-gate is informational on a PR so authors can enqueue without waiting on it; the enforcing run is the queue (and push).

Affected-scoping

The setup job runs cargo xtask affected for the event’s diff range — a PR uses three-dot (merge-base...head, the PR’s own changes), a queued group uses two-dot (base..head, exactly the group’s commits); the asymmetry is deliberate and preserved — and exposes broad, package_filter (a ready -p X -p Y …), and the category flags (lean / protocol / oxup / book / scripts). The scoped Rust jobs run package_filter when broad != true, else the full --workspace. oxup / book / lean and the script smokes gate on their category flag (or broad).

The tool is conservative by construction: under-including is a correctness bug (a regression slips the gate and reds main); over-including only wastes CI minutes. So every uncertain path resolves to more work — any infra / shared / unmapped path forces broad=true (the full workspace), and an empty diff is treated as broad. A derived rust output (broad || non-empty closure) gates the scoped compiler jobs, so a Lean-only or docs-only change — broad=false with an empty closure — correctly skips them rather than running cargo build with no -p (which would silently build only the default member).

Merge-critical crates

A curated set, defined once (the workflow MERGE_CRITICAL_CRATES env): oxc-instantiate oxc-serve oxc-runtime oxc-connection oxc-storage-pg — codegen, the concurrency/engine core, serving, and pg wire-divergence, the crates most prone to release-mode or cross-environment divergence. The queue’s compiler-test-release-gate runs nextest --release over (affected ∩ merge-critical); an empty intersection makes the job a no-op (it still reports success, so ci-gate stays green). This catches a release-only codegen miscompile in a divergence-prone crate in the queue (the class that bit oxc_instantiate::tier_classify::classify_body, commit 1892c0b, which debug-only CI hid for weeks) without release-testing the whole workspace in the gate. The set is curated, not derived — widen it when a new crate proves release-sensitive.

The documented accepted-risk set (Tier 3)

These run post-merge only and can red main. The bet, made explicitly: gating them in the queue is too slow, they are rarely combination-sensitive, and main recovery is a fast revert. ci-gate enforces on push, so a Tier-3 failure is loud (red main) — it just does not block the merge that introduced it.

  • Full leanlake build + the vector/manifest freshness gates + the batched-Lean conformance differential. Slow, OS-independent, rarely combination-sensitive. (Its pure-text companion, the RFD-0060 lean-layering guard, does gate every tier.)
  • Full macOS compiler-test — the macOS test leg. The queue keeps a compile-only macos-build-smoke so a Darwin build break still gates; only the macOS test execution is post-merge.
  • Full-workspace compiler-test-release — the complete release-mode sweep over every crate, both OSes. The queue gates the merge-critical subset; this catches a release-only bug in a non-merge-critical crate.

The system-dependency guard, deliberately non-nix

release-binary-smoke builds cargo build --release -p oxc-driver on a plain, non-nix runner with no system libduckdb (DuckDB is opt-in; the release binaries link none). A regression that makes oxc-driver need a system lib at link time passes every nix job — the dev shell supplies the lib — yet breaks the real release build (this is exactly what bit the toolchain before DuckDB was gated off by default). The nix dev shell is load-bearing but its flakiness is a known risk, so this guard does not deepen the nix reliance: it reproduces the release link outside the dev shell, which is the only place the bug is visible.

Factoring: the rust-test-env composite action

The heavy cargo nextest jobs share a long setup — free-disk, cargo caches, nix install, sccache, a Linux-only Postgres container (no job-level services: block, which would break the macOS matrix leg), the .lake cache restore + mathlib fetch + the serial eval-batch pre-build (so the conformance tests don’t race two lake builds under -j 4). That setup is factored into .github/actions/rust-test-env, so the test jobs stay thin.

Keep

  • ci-gate is the sole required check + its self-guard: a step that asserts ci-gate.needs covers every job in the workflow, so a job added later but forgotten in needs fails ci-gate loudly instead of silently becoming non-gating. The self-guard’s coverage is updated for the new jobs.
  • The composite caches (cargo-cache, free-disk, sccache-setup) and the affected-tool’s conservatism.

Consequences

  • A PR’s feedback is scoped and fast; the slow suite runs once, in the queue.
  • The queue gate is strong (debug tests + the merge-critical release subset + the non-nix link + the macOS compile) yet scoped, so a leaf change does not monobuild.
  • main can still go red — but only from the explicitly-listed Tier-3 set, and loudly. Recovery is a revert, by design.
  • Reading any job’s tier is a one-line if:. A miswired tier or a forgotten ci-gate wiring is caught by the self-guard, not by a red main.

RFD 0063 — The bitemporal read-point: the snapshot at now for derived reads

  • State: accepted
  • Depends on: RFD 0047 (the temporal value library / bitemporal substrate — BitemporalExtent, event_visible_at), RFD 0056 (rule validity — a rule’s valid-time and the rule_vt selector), RFD 0053 (the standalone concurrent engine — the published visible watermark as transaction-time-now, the Phase-4 read-model checkpoint, the Phase-6 embedded↔standalone equivalence harness), RFD 0020 (runtime engine / Engine::evaluate)
  • Tracks: issue #1098
  • Prior art: Snodgrass & Ahn (1985) — the valid-time / transaction-time distinction; Jensen & Snodgrass (1999) — bitemporal data models and the snapshot (timeslice) operation; SQL:2011 — system-versioned + application-time-period tables, where a bare query reads the current snapshot and AS OF / BETWEEN … PERIOD FOR address the past; Argon RP-004 (the temporal substrate already in the reference) — defines the bare view as the snapshot VT_start ≤ now ≤ VT_end ∧ TT current.

Question

A read of derived facts must, like a read of base facts, name a point in both temporal dimensions: when the facts/rules hold in the world (valid time) and what the system believed when (transaction time). Today only one half of one dimension is honored. query_extent_at accepts a 4-variant read-point and filters base facts by (vt, tt), but query_derive accepts no read-point: it seeds its EDB from scan_live (current belief, every valid-time) and evaluates over all compiled rules. And RuntimeReadPoint::Now resolves transaction-time-now (the published watermark) but leaves valid-time unbounded — a “now” read returns current belief across all valid-times, not the snapshot at now.

The consequence is a silent-wrong (#1098): a future-dated fact or rule (… at #2030#) participates in a live read today, and an expired one keeps participating, with no error and no way for a bare derived read to ask for the snapshot. What read-point must derived reads carry, what does now mean on each axis, and how does a now-snapshot stay consistent with the Phase-4 read-model cache when wall-clock time crosses a validity boundary with no write?

Context

  • The data plane is bitemporal; the read model is not, on the valid-time axis. Every AxiomEvent carries BitemporalExtent { vt_start, vt_end, tx_from, tx_to }, and event_visible_at(event, vt, tt) filters by both. query_extent_at(vt, tt) uses it. But the derived path (query_deriveseed_base_catalogEngine::evaluate) never threads a read-point: it scans scan_live (transaction-time-now, all valid-times) and uses every rule.
  • Transaction-time-now is already deterministic. resolve_now_tt() returns the published visible watermark (RFD 0053 Phase 0) — a committed value, not a clock read. Reads pinned to it reproduce identically on replay.
  • Valid-time-now has no source in the runtime. Valid-time now is the wall clock. The write path captures it per call (SystemTime, RFD 0047 / the embeddable Connection), but no read does. The runtime itself is clock-free, which is exactly what the Phase-6 equivalence harness and Phase-4 checkpoint replay rely on.
  • Rule validity already has its selector. RFD 0056 shipped compiled_rules_at(rule_vt) / clause_valid_at — the rule-dimension of the read-point — but only on the explicit query_derive_at API; the default ignores it. It is the rule half of the read-point this RFD completes.

Decision

A read names a bitemporal read-point (vt, tt, rule_vt), uniform across base-fact extents, derived facts, and reflection reads. The runtime read-point is always concrete — the wall clock never enters the runtime.

1. The point and its default

  • tt — transaction time: which beliefs. Default now = the published visible watermark (resolve_now_tt()).
  • vt — valid time: when facts/rules hold. Default now = the wall clock captured once at the request boundary.
  • rule_vt — which rules are in effect. Defaults to vt (RFD 0056 coupling), overridable for counterfactuals (current rules over past facts).

A bare read is the snapshot at now: (vt = wall-clock-now, tt = watermark, rule_vt = vt). It filters base facts by event_visible_at(vt, tt) and selects rules by compiled_rules_at(rule_vt). Reading the past pins vt/tt/rule_vt to chosen instants.

2. The clock lives at the boundary; the runtime is pure

RuntimeReadPoint::Now is resolved at the request boundary (oxc-serve, the embeddable Connection, the ox driver) to a concrete (vt, tt)vt from the boundary’s wall clock, tt from the watermark — before the runtime is called. The runtime evaluates against a concrete point and reads no clock. So a recorded read-point replays byte-identically: the Phase-6 equivalence harness and Phase-4 checkpoint recovery stay deterministic, because now was frozen into a value at the edge, exactly as the write path freezes its per-call valid-time.

3. Derived reads thread the point

query_derive / query_derive_async / query_dispatch and their _core carry the resolved (vt, tt, rule_vt). The discipline is uniform: every read of stored state the seed catalog makes is taken at the read-point, not at current/all-times — so the derived EDB is the snapshot’s EDB. That means not only the fact and relation-tuple events (event_visible_at(vt, tt) over a tx-aware scan that re-admits facts live-at-tt, the same predicate the extent path uses), but also the per-clause property field-values (individual_fields_at(vt, tt)), the refinement (iff) predicate inputs, the navigation-view iof-element membership, and the per-standpoint / enumerate-rival materializers. Rule selection routes through compiled_rules_at(rule_vt) (the RFD 0056 machinery), composing with #1034 standpoint scoping. Reads that are genuinely point-independent — the module schema (TBox), the reflection relations (which read back the already-point-filtered extents), prefetched foreign rows — are left at current/all-times by construction. The standing invariant: a derived read and a base-fact (query_extent_at) read at the same point agree, on both axes — pinned by a differential-oracle test per input class (facts, rules, refinement-fields, field projections, nav-view membership, past-belief retraction). The extent Now path is reconciled to the snapshot for consistency.

4. The cache carries a validity epoch

A snapshot-at-now read-model is valid only until the next validity boundary. The read-model cache and the Phase-4 PersistedReadModel gain a validity epoch: alongside (module_fingerprint, storage_gen) they record valid_until = the least vt_start/vt_end boundary strictly after the build vt across the facts and rules the model used. A cached/checkpointed model is served only if the read’s vt < valid_until; otherwise it is rebuilt. Crossing a boundary by wall-clock alone — with no write, no storage_gen change — therefore never serves a stale model. This is the RFD 0056 §D “active-rule/fact digest in the checkpoint key” obligation, made precise as an interval guard. The interval-rebuild is the correct first form; the engine-native end-state is to treat a boundary crossing as an incremental EDB delta the DBSP maintainer absorbs (temporal-delta IVM) rather than a rebuild — recorded below, gated on mature maintainer retraction.

5. The read-point is a type, not a convention

The runtime’s read surface takes a ReadPoint whose axes are non-defaultable named choicesValidTime { All, At(t) }, BeliefTime { Latest, At(t) }, RuleValidTime { All, TrackValidTime, At(t) } — with named constructors (all_valid_times(), snapshot_now(vt), snapshot(vt, tt), as_of(...)). There is no Option-shaped “unspecified” to leave unset: All is a deliberate, greppable choice, not an absence. A serving boundary therefore cannot silently fall back to all-valid-times — it must construct a ReadPoint, and every boundary does so through one shared resolver (resolve_read_point(as_of, wall_clock_vt)), the single place that captures the wall clock and couples rule_vt to vt. The guard is structural: a new serving surface won’t compile against the read API without choosing a point.

6. One dispatch, every read-point

Derived and pub query-body reads route through the body-aware derive path (query_derive_at / the dispatch invocation) at every read-point, not only at Now. Before this RFD the non-Now branches (AtVt/AtTt/At) did bare concept-extent projection and ignored query bodies — correct only because derived reads never carried a non-Now point. The snapshot makes every read carry a concrete point, so the body-aware path must serve all of them; extent/concept reads keep the extent path (the discriminator is whether the target has a body/derivation).

Rationale

  • Both axes are fundamental. A data-systems substrate that records when things hold and when they were believed must read on both axes; honoring only transaction time is half a model. RP-004 already specifies the snapshot-at-now bare view — the code simply never implemented the valid-time half for reads.
  • Symmetry with the data plane. A base-fact read at Now and a derived read at Now must agree on which facts exist; threading the same event_visible_at(vt, tt) into the EDB seed is what makes them agree.
  • Determinism is preserved by construction. Freezing vt at the boundary keeps the runtime clock-free, so nothing downstream (incremental maintenance, checkpoint replay, the equivalence harness) becomes time-dependent in a way that breaks reproducibility.
  • The epoch guard is the minimal correct cache invariant. The only way a now-snapshot model goes stale without a write is a wall-clock boundary crossing; an interval valid_until is exactly the guard that detects it, and degenerates to “never expires” when no rule/fact is dated (the conservativity case).

Alternatives

  • Keep Now = all-valid-times; valid-time only via explicit AtVt. Rejected: it makes the bare read not a snapshot, contradicts RP-004, and leaves the silent-wrong (#1098) live. “Opt-in correctness” is the half-measure this RFD removes.
  • Read the wall clock inside the runtime. Rejected: it makes evaluation, incremental maintenance, and checkpoint replay time-dependent, breaking the Phase-6 determinism contract. The clock must be frozen at the edge.
  • A parallel “as-of-derived” API distinct from the live path. Rejected: it would fork two evaluation paths; the read-point is one concept with now as its default, not a separate mode.
  • Option<i64> valid-time on the read API instead of a typed ReadPoint. Rejected: None conflates “deliberately all-valid-times” with “unspecified,” so a boundary that forgets to resolve now silently serves all-valid-times — a latent silent-wrong with no compile-time catch. The named-variant type makes “all” a deliberate, greppable choice (§5).
  • Event-driven valid-time-now (a logical clock advanced by tick events). Tempting — it would make reads fully deterministic and let the existing storage_gen cache key catch boundary crossings for free. Rejected: valid-time tracks the world’s clock, which advances continuously regardless of system commits; a fact valid [2020, 2030) must cease at real-time 2030 with or without a commit. An event-driven VT clock freezes between commits and the fact would not expire on time. (Transaction-time is event-driven — the watermark — which is why the two axes have genuinely different clocks.)
  • Temporal-delta IVM as the cache (now) vs. interval-rebuild. The engine-native ideal is to absorb a boundary crossing as an incremental EDB delta (the facts/rules whose windows open/close) in the DBSP maintainer. Deferred, not rejected: it requires mature maintainer retraction (the open IVM piece). The valid_until interval-rebuild (§4) is fully correct and rebuilds only at boundaries (never for undated modules), so it is the right first form, with temporal-delta the recorded end-state.

Consequences

  • Behavior change across every read. A bare read becomes the true snapshot: future-dated and expired facts/rules no longer appear at Now. Tests/examples that relied on “all-valid-times” are updated to the snapshot semantics (or to an explicit as-of).
  • The boundaries capture now. oxc-serve, the embeddable Connection, and the ox driver resolve Now to a concrete (vt, tt); in-process/test callers inject a fixed vt for reproducibility.
  • Rust leads the Lean here (engine/read semantics; AGENTS.md). The bitemporal read-point over BiState is a Lean catch-up follow-on.
  • query_derive_at(rule_vt) (RFD 0056) is subsumed as the rule-axis of the unified read-point.

Open questions

  • OQ1 — Reflection reads. meta/extent/$iof reflection relations are materialized into the catalog; they inherit the snapshot EDB, but whether a reflection read ever wants a distinct rule_vt is deferred until a use appears.
  • OQ2 — Boundary vt skew. A served request freezes vt at ingress; a long-running federated read spanning external scans uses that single frozen vt for all legs (the federation freeze is already single-shot). No per-leg clock; revisit only if a federated leg needs its own valid-time.

RFD 0064 — Compiler source decomposition: add-only hotspots, a per-file ceiling, content-free codegen merges

  • State: accepted — implemented (this PR)
  • Depends on: RFD 0035 (the composable execution pipeline — the crate seams this decomposition respects), RFD 0059 (the typed interface manifest / @[language_interface] drift gate, preserved verbatim), RFD 0062 (CI tiers + affected-scoping — the gate this change runs clean against), RFD 0014 / RFD 0057 (the serve/connection orphan-rule split this change keeps intact)
  • Prior art: Conway’s law and the file-as-coordination-unit (Parnas 1972 — information hiding as the decomposition criterion: cut along change axes, not along nouns); git’s textual 3-way merge and the merge attribute / custom merge-driver mechanism (gitattributes(5)); the directory-of-fragments pattern for conflict-free concurrent extension (conf.d, /etc/*.d, cargo’s per-file module tree); sccache content-addressed compilation caching.

Question

The compiler is one acyclic crate DAG with thin leaf hubs and no god-crate — re-crating buys nothing. But a measurement campaign over the commit history found the friction is intra-crate: a handful of god-files dominate every multi-agent cost axis at once. oxc-runtime/src/lib.rs (~31.5K LOC, 204 commits over 39 days), oxc-instantiate/src/lower.rs (~19.1K LOC, 166 commits), and oxc-syntax/grammar.toml (156 commits) are simultaneously the top edit-churn files, the top re-read files (runtime 142 reads, lower 131 — an agent must re-scan the whole file to find its edit site), and the top cross-session code-collision surfaces. Concurrent feature work serializes on these files: two agents adding two unrelated declaration forms both edit lower.rs, both edit grammar.toml, both edit lib.rs, and P(collision) climbs with concurrency. What is the smallest structural change that removes the contention without re-crating, without churning the build, and without rotting the genuinely-serial cores by scattering them?

Context

  • The crate graph is already clean. The dependency DAG is acyclic, leaf hubs are thin, and the orphan-rule-driven serve/connection split (transport-agnostic core under the HTTP layer) is intentional and correct. The lever is file decomposition inside crates, not crate topology. Re-crating would pay linker and orphan-rule costs for a problem that lives one level down.
  • The cost is co-edit collision, not file size per se. A 6K-LOC file that one workstream owns is cheap; a 3K-LOC file that five workstreams all append to is expensive. The campaign measured co-change — which edit sites land in the same file across concurrent sessions — and the god-files are exactly the high-co-change, low-cohesion ones: a grammar entry, a lowering arm, and a runtime command are independent units stapled into one file by history, not by coupling.
  • Some cores are genuinely serial and must stay whole. The §6.9 typed-slot gate cluster, the single write/derive evaluation core, and Module::load are a tightly-coupled spine: splitting them would force every change to touch N files instead of one and would invite drift between halves that must move together. Their co-location is an invariant, not debt — RFD 0063’s read-point discipline and the single Engine::evaluate path both depend on it.
  • Generated artifacts conflict even when their inputs do not. grammar.toml feeds a codegen step (cargo xtask gen) that packs discriminants by file-order index into generated.rs mirrors across four crates plus reference appendices and editor grammars. Two branches that each add a diagnostic touch disjoint logical entries but both regenerate the same packed output, so git reports a textual conflict in a file whose correct resolution is never a hunk merge (a textual merge interleaves variants and corrupts every downstream index).

Decision

Decompose the measured hotspots into add-only surfaces, hold every file under a ~3,000-LOC ceiling except the one irreducibly-serial core, and make generated-artifact merges content-free. Decompose only along measured low-co-change seams; leave the serial cores monolithic.

1. A ~3,000-LOC per-file ceiling, with one principled exception

No source file exceeds ~3,000 LOC. The sole exception is oxc-runtime/src/lib.rs at its irreducible ~6.4K floor: it holds the §6.9 typed-slot gate cluster, the single write/derive core, and Module::load — a spine whose parts change together and whose splitting would raise per-change file count and invite half-drift. The ceiling is a contention bound, not an aesthetic one; the exception is where contention is already low (one workstream owns the spine) and cohesion is high.

2. Hotspots become directory-of-files add-only surfaces

Each god-file becomes a directory whose entries are disjoint by construction, so concurrent feature-adds touch disjoint files:

  • grammar.d/ — one file per grammar/diagnostic entry (442 entries), replacing the monolithic grammar.toml. Adding a diagnostic or node adds a file; it does not edit a shared list.
  • lower/<kind>lower.rs splits into one module per declaration kind (concept, rel, fact, rule_ref, mutate, query, standpoint, defeat, meta, …) plus a thin mod.rs dispatch, and atom_lower/<kind> likewise (aggregate, quantifier, modal_temporal, cst, …). Adding a lowering arm adds a module.
  • commands/ — the driver’s subcommands (query, lint, package, constructs, test_harness) become per-command files.
  • Per-concern impl Store siblings — the runtime’s non-spine surface splits into cohesive sibling modules each carrying one concern’s impl Store (store_init, store_read, query, rules, refinement, standpoint, federation, read_model, persist, classify, defeasible, …). Store/Module fields stay put in lib.rs; child-module privacy carries the access the split needs, so no field is widened to pub to satisfy a sibling.

Default ownership is disjoint single-writer: a feature-add lands in its own new file, not a shared edit site, so two concurrent adds do not collide.

3. Generated artifacts merge by regeneration, not by text

A merge=regenerate git attribute on every codegen product (the four generated.rs mirrors, the reference appendices, the editor grammars, the examples indices) binds a custom merge driver (scripts/regenerate-merge-driver.sh). On a conflict in a generated file the driver discards both sides’ text and re-runs codegen from the already-merged grammar.d/ source-of-truth, adopting the output verbatim. Conflicts in generated files become content-free: the source fragments merged cleanly (disjoint files), and the product is a pure function of them.

4. sccache shares compilation across worktrees

RUSTC_WRAPPER=sccache is wired in the dev shell against a per-user content-addressed cache ($HOME/.cache/sccache, CARGO_INCREMENTAL=0 as sccache requires), so the rebuild cost a decomposition could add — more, smaller compilation units across more concurrent worktrees — is absorbed by cache hits on unchanged units shared across every agent’s worktree.

Rationale

The decomposition criterion is Parnas’s, applied to the unit that actually serializes multi-agent work: the file. Cut along the axis of change (one entry, one arm, one command, one concern per file) and concurrent feature-adds become disjoint writes that never meet in a 3-way merge. Cut along the axis of nouns (one file per data type) and you scatter a serial spine across files that must move together — which is why the runtime spine is the exception, not the rule. The ceiling makes the bound legible and gate-enforceable; the single exception makes the anti-rot principle explicit so the ceiling is not later read as license to shred the spine. Generated-artifact regeneration closes the last collision channel: once sources are disjoint, the products must not reintroduce a shared edit site, and a textual merge of index-packed codegen is never correct anyway.

Alternatives

  • Re-crate the god-files into new crates. Rejected: the DAG is already clean; the contention is intra-crate. New crates pay orphan-rule and link costs and re-route the public surface for a problem that file-splitting solves directly.
  • Split the runtime spine too, to honor the ceiling uniformly. Rejected: it is the genuinely-serial core. Splitting raises per-change file count, invites drift between halves that must move together, and breaks the §6.9 gate co-location and single-Engine::evaluate invariants. A uniform ceiling here would optimize a number, not the contention.
  • Keep grammar.toml monolithic and resolve codegen conflicts by hand. Rejected: the per-entry merge is the whole point; a hand-merge of index-packed generated.rs corrupts discriminants. grammar.d/ + the regenerate driver removes both the source and the product collision.
  • Leave it; rely on rebase discipline. Rejected: the campaign measured P(collision) rising with concurrency on these exact files. Discipline does not scale with agent count; structure does.

Consequences

  • A feature-add (new declaration form, diagnostic, command, runtime concern) lands as a new file, not a shared edit — concurrent adds no longer collide, re-read cost drops to the relevant fragment, and ownership is disjoint single-writer by default.
  • The ~3,000-LOC ceiling is a standing structural invariant with exactly one documented exception (oxc-runtime/src/lib.rs); a future file that crosses it without that justification is a regression.
  • Generated-file merge conflicts are content-free; the source-of-truth is grammar.d/, and the committed generated.rs mirrors are 1:1 products of it (the RFD 0059 drift gate still asserts the manifest is in sync).
  • Cross-worktree compilation is shared via sccache, bounding the rebuild cost of finer-grained units.

Invariants preserved

The decomposition is structure-only; every semantic contract is held:

  • §6.9 typed-slot gate cluster stays co-located in runtime/lib.rs.
  • Single Engine::evaluate path; single-sourced dispatch_*_core.
  • The orphan-rule serve/connection split (transport-agnostic core under HTTP).
  • The @[language_interface] drift gate (RFD 0059) — manifest + mirrors unchanged.
  • EXPAND-first phase order.
  • The /v1 wire-byte contract.
  • .oxbin / event-id byte identity.

Verification

The moves are purely mechanical and were proven so:

  • Per-crate test counts identical at every commit (no test added, dropped, or skipped by a move).
  • instantiate_file_inner output md5-identical pre/post (the lowering split changes file layout, not bytes emitted).
  • Reasoning Lean-oracle + differential tests green.
  • Final gate: cargo fmt, cargo clippy -D warnings, the full test suite (3,602 tests), and check-drift all clean; the full workspace compiles.

Open questions

  • A [[core_ir_variant]] codegen scaffold (future RFD). Adding a declaration form is still a cross-crate ritual — the hidden check↔instantiate coupling means a new form touches both phases in parallel. An estimated ~60–70% of that work is mechanical (the parse-arm ↔ check-arm ↔ lower-arm correspondence) and could be collapsed into generated, add-only code driven by a single [[core_ir_variant]] registry entry, the same way grammar.d/ now drives the diagnostic/node surface. That is a larger change with its own design and its own drift-gate implications; it is deferred to a separate RFD. This RFD removes the file-collision cost of the ritual; it does not remove the cross-phase cost.

RFD 0065 — The runtime⟷engine seam: oxc-value + the QueryEngine facade

  • State: accepted
  • Depends on: RFD 0020 (the unified runtime data engine — the data system and the inference engine are the same system, the invariant this seam preserves), RFD 0021 (the as-built reasoner engine — Engine::evaluate, the CompiledRule executed form, the RelationCatalog public-API seam this facade extends), RFD 0035 (the composable operator-tree pipeline — the logical::lower → optimizer → PhysicalOp path whose live consumer this RFD records), RFD 0036 (heterogeneous stores — RFD 0035’s forcing consumer, the federation path that revived the tree), RFD 0064 (compiler source decomposition — the file-level seams this crate-level seam is orthogonal to)
  • Prior art: Parnas (1972) — information hiding: name a module by the stable contract it exports, not by the internals a client happens to reach; the leaf-crate / facade-trait idiom (a shared low vocabulary crate + a narrow trait over a thick implementation crate) as the standard Rust cut for breaking a thick crate’s accidental coupling without splitting its cohesive core; CQRS read-model framing (RFD 0020 D10) for why the value vocabulary is a shared asset, not the reasoner’s private one.

Question

A measured analysis of Argon’s crate graph found the layers cleanly separable — the DAG is acyclic, leaf hubs are thin (RFD 0064) — except one seam: reasoner ⟷ runtime, which is fused two ways, and the fusion is what blocks disjoint engine/runtime ownership (one owner evolving the inference engine, another the data-system runtime, against a stable contract rather than against each other’s internals).

The fusion has two distinct faces, both firsthand-measured against the tree:

  1. The runtime is built on the reasoner’s value model. The store, write path, and read model name oxc_reasoning::compile::Value and its cluster at 299 sites in oxc-runtime alone (Value, Tuple, the CBOR/numeric codec — encode_tuple/decode_tuple/enum_*_cbor_bytes/struct_*_cbor_bytes/rational_to_value/decimal_str_to_rational/the *_CBOR_TAG constants — and the civil/tz/wall temporal helpers, ids::InternalId). This is the shared runtime value vocabulary, but it physically lives inside the reasoner’s compile module, so the runtime cannot name a value without depending on the whole engine.

  2. The store reaches around the Engine into its internals. On the store/persist/read-model path the runtime names ~32 engine-internal symbols across logical/physical/maintainer/executor/optimizer/runtime (e.g. maintainer::{FactDelta, ProjectionMaintainer, FrozenMaintainer, IncrementalMaintainer}, physical::{PhysicalOp::Materialize, materialize_into_catalog}, executor::temporal::interval::{Interval, coalesce}, logical::lower::{evaluate_via_physical, try_evaluate_via_physical, …, pipeline_compiled_rules}, runtime::relation::Relation). The runtime couples to how the engine is built, not to a contract it exports.

The unified-system design (RFD 0020: “the data system and the inference engine are the same system”) is deliberate and must be preservedEngine::evaluate is the single fused evaluation entry, and splitting it into a separate database product plus a separate engine product would discard the whole RFD 0020 thesis. So: what is the smallest cut that makes engine and runtime disjointly ownable without splitting the unified design — i.e., gives each side a stable contract to evolve behind, while the fused evaluation path stays one path?

Context

  • The crate DAG is already acyclic; the fusion is symbol-level, not topological. The runtime depends on the reasoner crate (correct — RFD 0020), but it reaches into the reasoner’s private surface rather than a contract. The lever is two surgical seams inside the existing dependency, not a re-crating of the engine.
  • oxc-protocol is a confirmed leaf. Its only dependencies are blake3, ciborium, semver, serde, serde_json (and optional sqlx) — zero intra-workspace oxc-* deps. A new value crate depending on oxc-protocol + numerics (num-bigint/num-rational/num-traits, ciborium, jiff) introduces no cycle.
  • The value cluster is mis-located, not engine-coupled. Value is a pure-data enum (Bool/Int/Date/Individual/Internal/Name/Null/Symbol/Cbor); Tuple, SymbolCompare, the CBOR/numeric codec, and the civil/tz/wall calendar helpers (delegating to jiff) have no dependency on join algorithms, the optimizer, or the fixpoint loop. They sit in compile for historical reasons — they were authored where they were first needed.
  • Engine::evaluate is genuinely fused and must stay so. It reorders each body (SIP), dispatches to the executor, and is the sole entry behind PhysicalOp::Recurse (the fused Datalog-fixpoint operator). This RFD does not unbundle it; it makes the runtime name it through a facade instead of reaching past it.
  • FORK-2 (the operator-tree live/dead question) had to be settled before cutting the facade, because the facade’s boundary line (what stays public, what goes private) depends on which parts of logical/physical/optimizer are on the production path. The resolution is recorded below as a first-class decision.

Decision

Two surgical seams make engine and runtime disjointly ownable while keeping the unified system one system. The runtime and the engine couple only through {oxc-value, QueryEngine}.

CUT-A — oxc-value: the shared value vocabulary as a leaf crate

Extract the value cluster out of oxc-reasoning::compile (and the crate root) into a new leaf crate oxc-value, depending only on oxc-protocol (the confirmed leaf) + numerics. It carries:

  • Value — the pure-data enum, no impl that reaches the engine; the data vocabulary, nothing else.
  • Tuple, SymbolCompare, the CBOR/numeric codec (encode_tuple/decode_tuple, the enum_*/struct_*/ref_blob_*/instant_*/plain_time_* CBOR (de)serializers, rational_to_value/rational_to_wire_string/real_wire_str_to_rational/decimal_str_to_rational, the *_CBOR_TAG constants).
  • The civil/tz/wall temporal helpers (parse_date/render_date/parse_duration/render_duration/epoch_day_to_nanos/is_temporal, render_instant, …).
  • ids::InternalId — the identifier the value layer and the engine share.

oxc-reasoning and oxc-runtime both depend on oxc-value; the reasoner re-exports the moved names at their old paths so the cut is a re-export, not a rename churn. The 299 runtime sites continue to compile; their meaning moves from “reaching into the engine” to “naming the shared vocabulary.”

CUT-B — QueryEngine: a facade trait over the engine internals

Introduce a QueryEngine facade trait (~7 methods + 1 associated type, ProjectionMaintainer) over oxc-reasoning’s logical/physical/maintainer so the runtime store names a stable API instead of engine internals. The boundary line is drawn by FORK-2 (below):

  • Moves down to the shared low crate (so both sides name them without engine coupling): Interval, Bound, coalesce, FactDelta, ProjectionMaintainer (the associated type).
  • Stays public on the facade (the contract the runtime is allowed to name): CompiledRule, Term, AtomIR, RelationCatalog (extending the RFD 0021 RelationCatalog public-API seam).
  • Becomes private to the engine (no longer nameable by the runtime): LogicalPlan::Apply and ApplyGate (the store is their sole external consumer → pub(crate)). Note: the materialize unit is physical::MaterializedRelation (there is no MaterializedLeaf type); it is shared by the federation/pipeline API and stays public — privatizing it is out of this seam’s scope. The runtime nonetheless names no engine internal on the analytical-freeze path once the fused freeze_apply (build→lower→materialize) method exists.

Engine::evaluate stays the single fused evaluation entry behind PhysicalOp::Recurse. The facade is a re-export + wrapper boundary, not a re-architecture: it renames what the runtime is allowed to see, it does not change how evaluation runs.

The established invariant

The runtime and the engine couple ONLY through {oxc-value, QueryEngine}. Any new runtime reach into a logical/physical/optimizer/executor internal is a regression against this seam; the contract is the two named surfaces, and the fused Engine::evaluate path stays one path behind them.

FORK-2 resolution — the operator tree is LIVE on the production query path

The boundary in CUT-B depends on a fact that two prior RFDs record as the opposite of what is now true, so it is settled here as a first-class decision.

The LogicalPlan/optimizer/physical operator-tree is live on the production query path, not dead scaffolding:

  • oxc-runtime’s federation path (federate_translate.rs) calls oxc_reasoning::logical::lower::{try_evaluate_via_physical, try_evaluate_with_frozen_stratified_naf, try_evaluate_with_frozen_lower_stratum}; checks.rs calls evaluate_via_physical; strict_cache.rs and the rule tests call pipeline_compiled_rules.
  • That path runs lower_rule/lower_bodyOptimizerPipeline::with_default_passes() (logical/lower.rs) → PhysicalOp::{Materialize, Recurse} (physical/mod.rs), where Recurse is the fused semi-naive fixpoint and Materialize is the frozen-EDB seam.

This is exactly the inversion RFD 0021 D1 said to perform “when a consumer demands the tree”, revived by RFD 0035’s federation consumer (RFD 0036) precisely as designed. Therefore:

  • RFD 0021 D1’s “dead/reserved, not wired” prose is now STALE — the operator tree it deferred is on the production path.
  • RFD 0035’s Context (“two halves that don’t meet”; LogicalPlan “orphaned … never instantiated by any front-end”) is now STALE — its own decision has landed.

The genuinely-dead residue is narrow and was confirmed firsthand: runtime/operators.rs (the Z-set operator vocabulary, zero forward-path callers), the unproduced LogicalPlan::{Distinct, Recurse} variants (constructed only in optimizer rebuild-on-match arms, never originated by lower_body), and the docstring-only SLG/DBSP/SMTExecutor stubs. That residue is being deleted separately — it is not part of this seam, and its removal is what makes the facade’s “private” set genuinely the live tree only.

Rationale

  • Two seams, not one split. The fusion has two independent faces (a mis-located shared vocabulary; a store reaching past the engine), so it takes two independent cuts. Neither cut touches the fused evaluation path — they relocate a vocabulary and rename a surface. That is why disjoint ownership is achievable without contradicting RFD 0020.
  • oxc-value is a relocation, not an extraction of behaviour. The cluster is pure data + codec + calendar helpers with no engine dependency; it is the runtime’s value vocabulary that happens to live in the reasoner. Moving it to a leaf crate both sides depend on is the information-hiding cut: name the value by its own crate, not by the engine that first defined it.
  • QueryEngine names the contract, not the construction. The runtime needs to drive evaluation and maintain the read model; it does not need to know the operator tree exists. A ~7-method facade gives it the verbs (CompiledRule, RelationCatalog, the maintainer associated type) while LogicalPlan/ApplyGate/MaterializedLeaf become the engine’s private business — so the engine owner can refactor the tree freely behind the facade.
  • Settling FORK-2 is load-bearing for the cut. The facade’s public/private line is the live/dead line of the operator tree. Drawing it on stale “the tree is dead” prose would have wrongly privatized a live path or wrongly exposed dead residue; the firsthand measurement is what makes the boundary correct.

Alternatives considered

  • Split the unified system into separate DB + engine products. Rejected: directly contradicts RFD 0020 — the data system is the inference engine. Disjoint ownership is the goal, not disjoint products; two owners against one stable contract is the win, two products is the loss.
  • Wrap Engine::evaluate instead of cutting at the logical::lower seam. Rejected: a thin wrapper over the single evaluation entry re-exposes the very logical/physical internals the cut is meant to hide (the runtime would still name them to build the call). The facade must sit at the contract the runtime consumes, not at the fused entry it ultimately reaches.
  • Keep the fusion; rely on discipline not to reach past the engine. Rejected: the fusion is exactly what blocks disjoint ownership — the owner’s goal. Discipline does not give a second owner a contract to evolve behind; structure does.
  • Cut only oxc-value (CUT-A) and leave the store reaching into engine internals. Rejected: it fixes the value face but leaves the ~32-internal store reach, so the engine owner still cannot refactor logical/physical/maintainer without breaking the runtime. Both faces must be cut for disjoint ownership.

Consequences

  • Disjoint ownership becomes possible. The engine owner evolves logical/physical/optimizer/executor behind QueryEngine; the runtime owner evolves the store/persist/read-model path against {oxc-value, QueryEngine}. The fused Engine::evaluate path stays one path — the unified design is intact.
  • The coupling surface is now exactly two named crates/traits, and the established invariant makes any new runtime reach into an engine internal a legible regression.
  • oxc-value is a new leaf crate depending only on oxc-protocol + numerics; the DAG stays acyclic (no cycle, confirmed against the leaf check).
  • The narrow dead residue (runtime/operators.rs, unproduced LogicalPlan::{Distinct, Recurse}, the SLG/DBSP/SMT docstring stubs) is removed separately, so the facade’s “private” set is the live tree only.
  • Sequencing is fixed: CUT-A strictly before CUT-B — CUT-B’s moved types (Interval/Bound/coalesce/FactDelta/ProjectionMaintainer) land in oxc-value, the crate CUT-A creates. CUT-B cannot move them down until the low crate exists.

Invariants preserved

  • RFD 0020 unified-system design: the data system and the inference engine remain one system; Engine::evaluate stays the single fused evaluation entry.
  • RFD 0021’s RelationCatalog public-API seam — extended onto the facade, not replaced.
  • RFD 0035’s operator-tree pipeline — now recorded as live; its public/private split is the facade’s boundary line.
  • The acyclic crate DAG (RFD 0064) — oxc-value is a leaf addition, no cycle.

Open questions

  • The exact QueryEngine method set. ~7 methods + the ProjectionMaintainer associated type is the measured shape; the final signatures are settled in the implementing PR against the live call sites (the facade must cover every verb the store currently reaches an internal for, and no more).
  • Whether the read-model maintainer surface (FrozenMaintainer/IncrementalMaintainer) belongs on the facade or stays an oxc-value trait the engine implements. ProjectionMaintainer as an associated type points at the latter; the maintainer constructors the runtime names today (FrozenMaintainer::new, IncrementalMaintainer::new) are the boundary case to settle when CUT-B lands.

RFD 0066 — Agent integration: the Argon MCP server

  • State: discussion
  • Depends on: RFD 0014 (§19 runtime serving — the connection/serve split this parallels: a transport-agnostic dispatch core under a thin transport layer), RFD 0033 (the ad-hoc query and mutation surface — the type-checked-or-refused body this exposes as argon_ad_hoc_query), RFD 0036 (heterogeneous and specialized stores — the Connection::Remote federation path the remote profile rides), RFD 0046 (derivation serving surfaces — the structured ProofTree/explain model this consumes, and the why-not boundary it marks open), RFD 0052 (deployment topologies and the connection abstraction — the embedded vs. standalone split this server’s two profiles inherit)
  • Prior art: the Model Context Protocol (MCP) and the official Rust SDK (rmcp) — the tool/resource/prompt vocabulary and the stdio transport this server speaks; the Language Server Protocol (ox lsp) as the editor-frontend precedent — a second frontend over the same compiler internals, shipped with the toolchain, editor-agnostic; CQRS read-model framing (RFD 0020 D10) for why the runtime read surface is a shared asset a third frontend may serve without owning the engine.

Question

Argon’s compiler and runtime expose no live integration surface for a coding agent. ox gen --target ts-skill emits a static, per-ontology SKILL.md + SDK reference for writing TypeScript against a generated SDK — explicitly a code-generation artifact, not a runtime client. ox lsp gives an editor-agent the check loop, and nothing more. The live compiler+runtime agent lane is empty: an agent cannot reach the runtime’s structured provenance, bitemporal as_of, first-class n-ary relations, forks, or standpoints, and cannot close the authoring loop (edit → check → why → build → reload → query → explain) against a running knowledge base.

What is the right integration surface for a coding agent, and where does it sit relative to the existing frontends? Specifically: does it ride the existing transport-agnostic dispatch core (sibling to oxc-serve’s HTTP layer) or duplicate runtime logic; what does it expose and what does it withhold; and how does it keep source, artifact, and the served module coherent so an agent never reasons over a stale or wrongly-typed knowledge base?

Context

  • The dispatch core is already transport-agnostic. oxc-connection’s RuntimeService is the state holder oxc-serve wraps; the dispatch_*_core entry points and explain_fork_fact_proof are framing-neutral. oxc-serve is a thin axum /v1 layer over them (RFD 0014). A coding-agent surface is a third frontend over the same core — sibling to HTTP, not a new runtime.
  • RuntimeService carries the coherence machinery the bare Connection does not. It holds reload_if_changed/spawn_watch_task (hot-swap the active module on artifact change; loud-refuse an invalidating schema change against a live ABox, never silently re-type), the OperabilityLimits governor, per-scope mutation locks, cursor pagination over query rows, and the fork lifecycle. The bare embedded Connection (the Tide / RFD 0052 embedder path) has no reload — building on it would reintroduce source/artifact incoherence and duplicate reload logic.
  • The governor already enforces the loud-gate philosophy at runtime. OperabilityLimits enforces a request timeout, a reasoner budget checked at every fixpoint round boundary (so CPU-bound work terminates), and a row cap that refuses loudly rather than truncating. AdmissionControl (multi-tenant concurrency fairness) is a separate, orthogonal concern — inert for a single stdio client.
  • Cursor pagination over query rows already exists (DispatchRequest.page, R-M17/#259); only the Connection::query convenience omits it. Riding the full DispatchRequest through RuntimeService gets paging for free.
  • The coherence primitives already exist. oxc-oxbin::content_hash (SHA-256 over canonical CBOR, §18.6), the loaded module_hash, and the lockfile hashes are the substrate for a precise staleness model — none of it needs to be invented.
  • Structured provenance already exists for data facts. explain_fact returns a structured ProofTree; a clean non-derivation is reported as derived:false, root:None. The check-violation why is, by contrast, currently prose (render_violation_why). Real why-not provenance does not exist (RFD 0046 marks it open).
  • The diagnostic catalog is single-source and already teaches. ox explain OE#### emits extended explanations from grammar.d; the same catalog feeds CLI and LSP. The loud-gate codes (OE13xx, OE1317) are the actionable surface an agent most needs guidance on.

Decision

Specify oxc-mcp / ox mcp: an MCP (Model Context Protocol) server that exposes the authoring loop (check/build/why) and the runtime (bitemporal query, structured provenance, forks, standpoints) to agents, as a third frontend over the existing dispatch core — sibling to oxc-serve’s HTTP layer, parallel to ox lsp.

D1 — Runtime-provenance-led, one closed loop

The headline value is the runtime: structured ProofTree provenance (explain_fact), bitemporal as_of, first-class n-ary relations, forks, standpoints — none of which a coding agent can otherwise reach. The authoring loop (check/why/build) is the on-ramp, not the headlineox lsp already gives an editor-agent the check loop. The product is the closed loop: edit source → check → why → build → reload → query → explain_fact → iterate. The design is organized around making that loop coherent and legible, not around re-exposing the editor’s check surface.

D2 — The runtime backend is RuntimeService, not the bare Connection; the server is a third frontend

oxc-mcp’s tool handlers are to RuntimeService what oxc-serve’s HTTP handlers are: thin framing over dispatch_*_core + explain_fork_fact_proof, minus axum, plus the authoring tools. The backend is RuntimeService precisely because it carries reload_if_changed/spawn_watch_task, the governor, per-scope mutation locks, cursor pagination, and the fork lifecycle. The bare Connection has no reload — using it would reintroduce source/artifact incoherence and duplicate reload logic. One McpError(ConnectionError) newtype (to satisfy the orphan rule), symmetric to serve’s ApiError.

D3 — Two profiles, one tool surface

  • Local = RuntimeService (in-memory storage, watching target/<pkg>.oxbin) + oxc-workspace — the full authoring and runtime surface, coherent by construction; what a coding agent uses.
  • Remote = a /v1 HTTP client against a deployed ox runtime serve (the Connection::Remote impl, RFD 0052 D2) — runtime read/introspect only (you do not author against a deployed knowledge base), gaining admission + crash isolation from the separate process.

These are profiles, not maturity levels — the tool surface is one surface; a profile determines which tools are live. The agent learns which surface is available via argon_status.

D4 — The coherence invariant (the spine)

Source, artifact, and served module are kept coherent by construction, never guessed:

  • argon_build writes the .oxbin, then explicitly calls reload_if_changed before returning (deterministic — it does not rely on the watch debounce); the watch task is a backstop.
  • Every runtime result is stamped with the serving module_hash.
  • argon_status exposes {loaded_module_hash, on_disk_oxbin_hash, source_dirty, fork, as_of, limits, capabilities} so staleness is observable, never inferred.
  • An invalidating schema swap against a live ABox refuses loudly — structured schema_incompatible / needs_rebuild — never a crash, never a silent re-type.

The primitives (content_hash, module_hash, lockfile hashes) already exist; D4 composes them into a stated invariant.

(As-built: the dirty check is a content-hash of the build inputs — a blake3 digest over every *.ar under the project PLUS ox.toml / ox.lock (the prelude / dependency surface that changes the artifact without touching an .ar), compared to the digest captured at the last build. It is unsound-safe: an unreadable input fails TOWARD dirty, never silently clean. The first cut’s mtime proxy was racy and blind to ox.toml/lockfile edits — replaced. argon_build verifies active_module_hash == built_hash after reload rather than trusting reload_if_changed silently. A dirty runtime tool auto-builds (DirtyPolicy), or refuses with a structured DIRTY_WORKSPACE / NO_ARTIFACT result naming the next step.)

D5 — Governor: reuse, do not rebuild

The MCP server configures the existing OperabilityLimits (a tighter row cap to protect agent context) and adds per-call cancellation. It does not import AdmissionControl — multi-tenant concurrency fairness is inert and unneeded for a single stdio client. The row cap refuses loudly over the limit (the loud-gate philosophy applied to result size); it never truncates.

D6 — Pagination: ride the full DispatchRequest

Cursor pagination over query rows already exists (DispatchRequest.page, R-M17/#259); only the Connection::query convenience omits it. Riding RuntimeService + the full DispatchRequest gets paging for free. Over-cap unpaged reads refuse loudly — never truncate.

D7 — Error model: domain outcomes are successful results

Domain outcomes — check found violations, an ad-hoc body is ill-typed, a query is empty, a fact is not derived — are successful tool results (isError:false) carrying structured payloads. McpError is reserved for “couldn’t run.” Surfaced payloads carry stable, machine-readable fields: stale_artifact, needs_build, limit_exceeded, capability_required, diagnostic_codes, retryable.

(As-built: the split is exactly this — McpError is a newtype over oxc_connection::ConnectionError (orphan rule), mapping the core’s status / code / message / details onto rmcp’s ErrorData; everything else returns Ok(CallToolResult) with a structured payload (needs_build, stale_artifact, diagnostic_codes, limit_exceeded, truncated, …). A row-cap overflow (ARGON_RUNTIME_RESULT_TOO_LARGE), a derive of an underivable head (ARGON_RUNTIME_DERIVE_FAILED), an ad-hoc type error, and a check delta-guard rejection (ARGON_RUNTIME_CHECK_VIOLATION) are all domain outcomes, not errors. capability_required / retryable are reserved for the gated fork / Remote surfaces.)

D8 — ProofTree folding

A recursive derivation branches exponentially, so explain_fact returns a bounded projection — the root plus the first N inference levels, each unexpanded subtree carrying a stable node-id — plus an expand_proof(node_id) tool for depth on demand. Never naive full serialization.

D9 — Teaching lives in the diagnostic catalog, not in the server

Enrich the loud-gate codes (OE13xx, OE1317) with actionable, fix-class remediation in the single-source catalog (grammar.d) → CLI (ox explain), LSP, and MCP all benefit; the argon_check payload inlines them. The loud-gate sequence (an agent will not infer it from tool descriptions alone) is encoded in the server instructions field and in the argon_authoring_loop / argon_diagnose_absence prompts.

D10 — Write model: ephemeral fork first-class, promotion built correctly

The ephemeral fork (create → mutate test facts → query/derive/explain → auto-abort, never promotes) is read-shaped, safe, and first-class — the hypothetical-reasoning / rule-testing primitive. Promote-to-main is built correctly, not omitted: fork_diff summarizes in domain terms; promotion is capability-gated per-call with an unmistakable state indicator, carrying conflict behavior + provenance. The fork machinery already exists in RuntimeService.

D11 — why unification

explain_fact already returns a structured ProofTree for data facts; the check-violation why is currently prose (render_violation_why). Unify the check-violation why to the same structured trace shape (violated rule/constraint id, involved declarations, premise facts, spans) plus a prose rendering — one provenance model, two entry points. This is a real build item in oxc-runtime’s check-explanation path, not an MCP veneer.

D12 — Negative provenance: an honest boundary

“Why did X not derive / why is this query empty” is where ontology authors live, and real why-not provenance does not exist (RFD 0046 marks it open/out-of-scope; explain returns a clean derived:false, root:None). Do not fake it. explain_fact reports derived:false distinctly (a membership answer, not an error); argon_diagnose_absence guides the decomposition with existing tools (confirm the rule is evaluable, derive each body predicate, explain each expected premise, check as_of/standpoint). Genuine why-not is the top runtime frontier (a reasoner feature, RFD 0046), outside this RFD’s remit.

D13 — Packaging

oxc-mcp is a crate (an rmcp server; a Backend trait with Local/Remote impls; an actor/mailbox serializing the non-Sync state with per-call cancellation; the coherence layer). ox mcp is a subcommand in oxc-driver mirroring ox lsp, shipped with the toolchain via oxup, editor-agnostic. A Claude Code plugin bundle (a manifest registering ox mcp + the prompts as a skill) is the reference distribution. The rmcp (official Rust MCP SDK) API must be verified at implementation time.

Tool, resource, and prompt surface

Names are snake_case, no dots (MCP-tool-name convention). This section is reconciled to the as-built oxc-mcp surface (the embedded stdio profile); where the implementation refined the original enumeration the delta is stated inline. The decisions D1–D13 stand; only the surface census moves.

Authoring (loud gate):

  • argon_check — the full check pipeline over SOURCE; structured diagnostics ({code, severity, message, range, fix?}). Runs even when no artifact is loaded (the repair on-ramp); binds the same full_package_diagnostics the LSP runs. (Fix-class remediation is whatever the single-source catalog carries per D9 — the payload inlines the catalog codes, it does not author its own.)
  • argon_build — compile to .oxbin behind the loud gate, atomically write, reload, and verify active_module_hash == built_hash; returns {ok, moduleHash, wroteOxbin, gateFailures, message}. A gate failure is a domain outcome, not an error.
  • argon_status — the staleness probe: loaded vs on-disk module hash, sourceDirty, artifactLoaded, the paths.
  • argon_test — test-mode build; reports whether the test-bearing artifact compiled (the loud test gate).

Runtime read (full Truth4 envelope):

  • argon_query — a declared query; bitemporal as_of; cursor-paged; returns the FULL four-valued (Truth4) envelope (per-row truth, hidden K3 counts). Federation is driven by the query’s declared across [...] clause — there is no per-call across override (it is not on the lifted dispatch core, so exposing one would be a false affordance).
  • argon_ad_hoc_query — an RFD 0033 body (source string), type-checked against the loaded module or refused; full Truth4.
  • argon_derive — materialize a derived extent; full Truth4 envelope, optional per-tuple proof tags.
  • argon_scenario_run — materialize the whole derived model once and project every head in the FULL Truth4 envelope ({projection, factsDerived, facts:[{head, tuple, truth4, hidden}], hidden}). A scenario materializes only definite-Is facts (foreign connectors hard-refused; across / enumerate heads held separately; defeat planes 2-valued), so each fact carries truth4:"Is" and hidden is all-zero — the tag makes “all-Is by construction” legible rather than leaving the agent unable to tell it from an envelope-stripped read. projectK3:true renders the bare {head, tuple}. (As-built rename of the RFD’s argon_snapshot: the tool runs the scenario’s full derivation, not a paged store dump — the name now says what it does. Deliberately a tool, not a standing resource, as the RFD intended. The full-Truth4 envelope is single-sourced in the MCP renderer over the shared core derived_snapshot_value — the /v1/snapshot flat shape is unchanged.)
  • argon_explain_fact — the full structural AND/OR ProofTree for one fact (live read-point only); maxDepth folds the tree. A not-derived fact returns derived:false plus a whyNot negative-provenance tree — an honest membership answer (D12), not a synthesized why-not.
  • argon_expand_proof — re-reconstruct a fact’s proof at a larger maxDepth to drill past a folded marker (D8). (As-built: folding is by depth, not by per-node id — maxDepth is the stable handle; same reconstruction as explain_fact.)
  • argon_checks_current — evaluate the module’s checks against current state (optionally one by name); returns the violation set + clean.
  • argon_why — glass-box runtime check violations: evaluate the scope’s checks and return the firings. (As-built: the D11 unification — a single structured check-violation provenance shape in oxc-runtime, prose + structured trace — is NOT what shipped here. argon_why returns the structured firing set the runtime already produces; the deeper D11 build item in oxc-runtime is a follow-on, see “Gated / follow-on”.)

Write (full check-guard pipeline):

  • argon_mutate — a declared mutation; returns the receipt, minted entities, and the RFD 0046 D2 derived delta ({added, removed} per head, when includeDerivedDelta is set) + observe-channel diagnostics. A check delta-guard rejection (ARGON_RUNTIME_CHECK_VIOLATION) is a structured domain refusal — atomic, nothing persisted — not an error.
  • argon_ad_hoc_mutate — an RFD 0033 mutate body (source string), type-checked then executed and persisted through the SAME RFD 0025 check delta-guard pipeline as a declared mutation.
  • argon_batch — an ordered array of declared mutations executed ALL-OR-NOTHING in ONE per-scope critical section (step N sees steps 0..N), persisted in one transaction. A mid-batch check-guard violation rolls back the WHOLE batch — nothing persisted — surfaced as a structured domain refusal carrying the failing step’s 0-based batchStep. Rides RuntimeService::run_batch (lifted by #1205), with the step projection single-sourced in oxc_connection::render_batch so the /v1/batch and MCP surfaces cannot drift.

Compute (pull-plane evaluation):

  • argon_compute — evaluate a declared compute fn over the scope’s committed state (optional bitemporal as_of); returns {value, emissions, moduleHash}. A top-level payloadless enum constant renders {tag} (RFD 0027 D5). Rides RuntimeService::run_compute (lifted by #1205), with the value projection single-sourced in oxc_connection::render_compute (the same /v1 compute render, byte-for-byte).

Discovery / introspection:

  • argon_schema_lookup — look up a concept / relation / query / mutation / check by qualified path or short name, or the whole schema index, from the loaded module (scoped — never an unfiltered whole-TBox dump). (As-built: this folds in the RFD’s separate argon_list_queries + argon_describe_query — one name-or-index lookup over the schema covers list-all and describe-one.)
  • argon_epistemics — the per-construct epistemic profile: decidability tiers + standpoint lattice from the loaded artifact, construct filters to one name. (As-built addition: the tool-shaped read over the tier/standpoint surfaces the RFD only exposed as resources.)

The complete as-built tool census is eighteen: argon_check, argon_build, argon_status, argon_query, argon_derive, argon_mutate, argon_batch, argon_compute, argon_ad_hoc_query, argon_ad_hoc_mutate, argon_explain_fact, argon_expand_proof, argon_why, argon_checks_current, argon_scenario_run, argon_test, argon_epistemics, argon_schema_lookup.

Resources (stable, addressable; routed through the ArgonRuntimeHandle trait, not a local file read, so a future Remote impl serves them identically):

  • argon://diagnostics/catalog — the whole OE#### catalog (every code, severity, description, long-form explanation, reserved flag); the agent’s authority on what a code MEANS, sourced from the same OxcDiagnosticCode the compiler emits.
  • argon://diagnostics/{code} — a template: one code’s entry.
  • argon://schema/index — the loaded module’s schema index. (As-built: a single scoped index resource, not the RFD’s argon://schema/{path} per-path template — per-construct lookup is the argon_schema_lookup tool’s job; the resource is the index.)
  • argon://standpoints — the standpoint lattice (artifact standpoint-lattice section, §D.7).
  • argon://tiers — the decidability tier table (artifact tier-table section, §D.8).
  • argon://module — the active artifact’s load status + module hash. (As-built rename of the RFD’s argon://module “manifest + hashes”.)

Five resources + one template. As the RFD intended: not current-diagnostics (that is argon_check output), not the derived snapshot (that is argon_scenario_run).

Prompts (investigation playbooks; each written so the agent drives the full epistemic state, not the K3 projection, and respects the loud-gate build loop):

  • argon_investigate_fact — is this fact derived, and why / why-not? Drives argon_statusargon_explain_factargon_expand_proof, reading every row’s truth4.
  • argon_diagnose_absence — the counter-abduction loop: a query is empty / a fact is missing; inspect the hidden Can/Both counts first, then derive, explain a representative missing tuple, and propose the smallest base-fact change as a type-checked argon_ad_hoc_mutate for a human to confirm.
  • argon_audit_mutation — what would this write change (the D2 derived delta) and does it cross any check? Drives argon_mutate with includeDerivedDelta and argon_why on a refusal.

(As-built delta: three prompts — the RFD named argon_authoring_loop + argon_diagnose_absence. argon_authoring_loop is subsumed by the server instructions field, which states the loud-gate sequence verbatim; the three shipped prompts are the runtime-investigation playbooks D1 leads with — fact-provenance, absence-diagnosis, write-audit.)

Gated / follow-on (NOT in the as-built surface)

  • Remote (/v1 client) profile + streamable-HTTP transport. The backend is the ArgonRuntimeHandle trait precisely so a Remote impl slots in without reworking a handler, but it is NOT built — it is gated on the open coordination-model decision (how an agent’s scope/principal binds to a remote deployment, an Open question below). The as-built ships the embedded stdio profile only; D3’s two-profile design stands, one profile is live.
  • Fork lifecycle (argon_fork_create / _mutate / _query / _derive / _explain / _diff / _abort / _promote, D10). Forks are NOT on the transport-neutral RuntimeService surface the #1205 dispatch lift exposed — fork orchestration lives on the embedded oxc_connection::Connection and as serve-private /v1 handlers (exactly as query dispatch did before #1205). Wiring forks here would mean reimplementing that orchestration; instead it is a follow-on needing a fork-orchestration lift (analogous to #1205) plus the coordination-model decision. D10’s write model stands as design; the live surface is the declared/ad-hoc argon_mutate write path.
  • Structured check-violation why in oxc-runtime (D11). argon_why ships glass-boxing the runtime’s existing firing set; the deeper D11 build item — one unified structured trace shape (prose + structured) shared by explain_fact and the check-violation why — is a real oxc-runtime item, still to land.
  • Claude Code plugin bundle (D13). ox mcp ships with the toolchain; the reference plugin manifest registering ox mcp + the prompts as a skill is a follow-on distribution artifact.

Concurrency and isolation

(As-built reconciliation: the #1205 dispatch lift made RuntimeService Clone + Send + Sync, so the planned actor/mailbox is unnecessary — the backend holds the service directly and the rmcp handler tasks call the lifted run_query / run_mutation / run_compute / run_batch orchestration against it; the service is internally synchronized. Read-only runtime ops never hold the ServiceState lock across the CPU-bound reasoner: they take a brief guard only to deep_clone the scope store, drop it, and reason against the clone — the snapshot-then-reason discipline the serve foreign-read path uses. Only the ad-hoc mutate path holds the write guard, serialized per scope. The single-stdio-client profile means there is no second agent session to race the backend; the project-root lockfile / co-resident-LSP-watch interaction folds into the still-open watch question below. Panic isolation at the handler boundary remains the intent for the eventual Remote/HTTP profile.)

Rationale

  • A third frontend, not a new runtime. The dispatch core is already transport-agnostic (RFD 0014); the only honest place for an agent surface is beside the HTTP layer, over the same dispatch_*_core. Anything else duplicates runtime logic and invites drift.
  • RuntimeService, because coherence is the hard part. The reason to ride RuntimeService rather than the bare Connection is exactly D4: reload, the governor, mutation locks, paging, forks. Building on the embedder path would mean re-implementing reload — and the first re-implementation that drifts is a silent stale-KB bug, the worst failure mode for an agent that trusts what it queries.
  • The runtime is the value the agent cannot otherwise reach. An editor-agent already has the check loop via LSP. What it does not have is structured provenance, bitemporal reads, n-ary relations, forks, and standpoints. Leading with the runtime (D1) is what makes this surface worth building rather than a re-skin of ox lsp.
  • Honesty over a faked feature. Why-not provenance is where authors live, and it does not exist (D12). The disciplined move is to report derived:false precisely and guide the decomposition with the tools that do exist — not to synthesize a plausible-looking explanation the substrate cannot back. Genuine why-not is named as the top runtime frontier, not quietly skipped.
  • Reuse the loud-gate everywhere. The governor, the catalog, the ProofTree model, the fork machinery, paging — all exist. The server configures and frames them (D5, D6, D8, D9, D10); the only genuinely new build items are the framing layer, the coherence stamping (D4), and the structured check-violation why (D11).

Alternatives considered

  • Build on the bare embedded Connection (the Tide / RFD 0052 path). Rejected: it has no reload, so the server would either re-implement reload_if_changed (duplicate logic, the drift risk above) or serve a stale module. RuntimeService already owns coherence.
  • Author-against-remote (a uniform read+write surface across both profiles). Rejected: you do not author against a deployed knowledge base. The remote profile is read/introspect only (D3); authoring is inherently local where source, build, and reload are co-located.
  • Maturity levels (a v1 read-only surface, write later) rather than profiles. Rejected: the full surface above is committed scope. Local vs. remote is a capability distinction (what is live where), not a phasing of ambition; the fork write model (D10) is part of the design, not a deferred tier.
  • Fake why-not provenance (synthesize a “why empty” explanation). Rejected on honesty grounds (D12): the substrate cannot back it (RFD 0046 open). A confident wrong explanation is worse than a precise derived:false plus a guided decomposition.
  • Naive full ProofTree serialization. Rejected: a recursive derivation branches exponentially and would blow the agent’s context. Bounded projection + lazy expand_proof (D8) is the only viable shape.
  • Teach the loud-gate inside the server (tool descriptions, hardcoded prose). Rejected: teaching belongs in the single-source catalog (D9) so CLI, LSP, and MCP share it; duplicating it in the server is a second source that drifts.

Consequences

  • The agent lane is filled by a frontend, not a fork of the runtime. oxc-mcp couples to RuntimeService + dispatch_*_core exactly as oxc-serve does; the runtime stays one runtime.
  • A new committed build item lands in oxc-runtime: the structured check-violation why (D11) unifies the two provenance entry points — it is not MCP-only.
  • The diagnostic catalog gains fix-class remediation on the loud-gate codes (D9), improving ox explain and LSP, not only MCP.
  • ox mcp ships with the toolchain via oxup, editor-agnostic, with a Claude Code plugin bundle as the reference distribution.
  • Why-not provenance is named as the top runtime frontier (D12), scoped out of this RFD and to RFD 0046 — a legible boundary, not a silent gap.
  • This RFD is a design record. State discussion: it fixes the design rationale, not the implementation. The embedded stdio profile (increments 1–5, the read/write/introspect surface above) is built — oxc-mcp / ox mcp ship the eighteen-tool surface, five resources + one template, and three prompts reconciled above; the Remote profile, the live fork lifecycle (increments 6–7), the D11 structured check-violation why, and the Claude Code plugin bundle are the gated follow-ons. The build order below is the committed delivery shape; the surface section is reconciled to what is live.

Build order (delivery increments — the full surface above is committed scope, not a v1 subset)

  1. oxc-mcp skeleton + Backend (Local = RuntimeService + workspace) + actor + McpError + argon_status / coherence stamping.
  2. Authoring tools + catalog fix-class enrichment + inlined remediation + server instructions/prompts.
  3. Runtime read tools with paging + governor config + cancellation.
  4. explain_fact + ProofTree folding + expand_proof; structured check-violation why in oxc-runtime.
  5. Discovery tools + schema/catalog resource templates.
  6. Fork model (ephemeral + capability-gated promote with diff-summary).
  7. Remote backend profile + ox mcp subcommand + Claude Code plugin bundle.

Open questions

  • The exact rmcp API. The official Rust MCP SDK surface (server construction, tool/resource/prompt registration, the stdio transport, cancellation) must be verified against the SDK at implementation time; the decisions above are framing-level and SDK-version-independent.
  • The capability mechanism for argon_fork_promote. Per-call capability gating (D10) needs a concrete carrier — an explicit confirmation argument, a session capability grant, or a config-time enable — settled when increment 6 lands.
  • The row-cap default for the agent profile (D5). “Tighter than serve’s, to protect agent context” needs a measured default against real agent context budgets.
  • Where the project-root lockfile lives and how it interacts with ox lsp’s own watch. Two frontends (LSP + MCP) may watch the same target/<pkg>.oxbin; the lockfile (concurrency / isolation §) must not deadlock a co-resident editor session.

This design was hardened by a multi-model adversarial review — decorrelated critique across heterogeneous providers — which shaped the decisions above (notably the RuntimeService-not-Connection backend, the coherence invariant, and the honest why-not boundary).

RFD 0067 — The temporal literal surface, carrier reach, and the four-valued comparison frontier

Naming. The substrate concept is domain-neutral. This RFD settles how temporal values are written (literals), how far the carriers reach (range × precision), and how temporal comparison behaves under four-valued open-world semantics. The legal effective-dating and agent/robot examples below name motivating domains, not constructs — the design serves both on one substrate.

  • State: discussion
  • Revises: RFD 0047 (temporal value library — adds the Instant/ZonedDateTime literal forms and the inner-shape kind rule; drops the mandatory-Z datetime literal), RFD 0056 (bitemporal rules — valid-time is nanosecond, not day-granular; out-of-range valid-time must refuse, not saturate), RFD 0063 — both files sharing that number (a pre-existing numbering collision, README “no reuse” notwithstanding): 0063-bitemporal-read-point (nanosecond valid-time / snapshot precision) and 0063-datalogmtl-temporal-engine (the metric timeline runs at a declared resolution decoupled from snapshot precision)
  • Relates to: RFD 0024 (Allen as a library — unchanged), RFD 0016 (numeric tower — the out-of-range-literal loud-refusal precedent, OE1373), RFD 0007 (missing-value semantics — NA vs unknown)
  • Tracks: issue #1285 (the parked #…Z# carrier decision this resolves); the two firsthand-found defects it surfaced — #1318 (mutation-body insert constants escape the static loud-gate) and #1319 (epoch_day_to_nanos silently saturates out-of-range valid-time)
  • Prior art: ISO 8601-1/-2 (EDTF) and RFC 3339 (date/time/datetime/offset grammar); RFC 9557 / IXDTF (the [Zone] bracket annotation and Z=“UTC known, local offset unknown”); TC39 Temporal and Rust jiff (per-type parse — “you pick the type, the suffix only validates”; the Instant/Plain*/Zoned split); XSD 1.1 Part 2 (xsd:dateTime timezone-optional partial order with the ±14h incomparable window; xsd:dateTimeStamp timezone-required total order); Belnap–Dunn FDE / Fitting bilattices (the four-valued codomain); Wałęga, Cuenca Grau, Kaminski et al., “DatalogMTL over the integers” (KR 2020 / TPLP 2023 — the ℤ-restores-decidability vs ℚ-undecidability result); Snodgrass bitemporal / SQL:2011; pandas datetime64[ns] (the i64-nanosecond epoch window this design currently inherits); Elixir sigils and SQL typed literals (the surveyed authored-literal families); the Argon temporal-representation research campaign (78 agents, .local/research/datetime-literal/{recon-map,deep-map,SYNTHESIS}.md + 37 vault concept notes) — the decidability ledger, incomparability lattice, and refutation ledger; Gabbay–Kurucz–Wolter–Zakharyaschev, Many-Dimensional Modal Logics (the Σ¹₁ two-metric-timeline result); Rivieccio–Jung–Jansana (twist structures / four-valued modal logic); Kolaitis–Vardi (Datalog strong monotonicity — the four-valued-= obstruction); Denecker–Marek–Truszczyński (AFT) and Małuszyński–Szałas 4QL (four-valued Datalog is PTIME on a finite lattice); Pollaci 2026 (three-valued WFS-DatalogMTL over ℤ — the adjacent occupied cell)

Question

Issue #1285 asked a narrow question: does a #YYYY-MM-DDTHH:MM:SSZ# literal denote a zoned Instant or a wall-clock PlainDateTime? The token def and book advertise the trailing Z; the value-layer validator refuses it. The literal is broken in every position.

Widened to the real design: what is the coherent temporal literal surface for Argon’s seven value carriers; how far do those carriers reach (does one substrate serve legal effective-dating at day precision and agent/robot state at nanosecond precision — and what about astronomical timescales); and how does temporal comparison behave under Argon’s four-valued, open-world (Truth4) semantics without breaking the integer-timeline decidability the reasoner depends on?

Context

The value model (settled — RFD 0047)

Seven runtime carriers (oxc-value/src/value.rs), integer- or canonical-string-backed, jiff-validated:

CarrierRepresentationKind
Datei64 days since epochcivil, day
PlainTime (surface Time)i64 nanosecond-of-daycivil wall-clock, no date/zone
PlainDateTime (surface DateTime)canonical jiff civil stringcivil wall-clock date+time, no zone
Instanti128 nanoseconds since epochabsolute UTC point, zone-free
ZonedDateTimecanonical jiff Zoned string (…-04:00[America/New_York])Instant + IANA zone; the offset is baked into the string
TimeZonevalidated IANA idreference datum
Durationi64 whole daysexact, day-granular

The civil carriers are jiff-backed and span ±9999 years (proleptic Gregorian). Instant uses i128 precisely because a nanosecond count over that span overflows i64.

The current literal surface (broken)

Only two literal tokens exist: #YYYY-MM-DD#Date, and #…T…# → civil DateTime (decided purely by the presence of a T byte, oxc-lexer). The advertised #…Z# form is loud-refused at value conversion (oxc-value/src/wall.rs:146-168 rejects any trailing Z/offset/[Zone], because DateTime is zone-free). Instant, ZonedDateTime, TimeZone, Time are constructible only through string-parsing builtin functions; Duration only through <int>.days/.weeks sugar. So of seven carriers, five have no literal, and the one datetime literal advertises a form the substrate forbids — the #1285 incoherence.

The reasoning substrate (settled, firsthand-validated)

  • One integer (ℤ) metric timeline, and it is the fact valid-time axis. The Lean models time as Nat (spec/lean/Argon/Reasoning/Temporal.lean — “Book §6.10 specifies ℤ”); the reasoner’s interval points are i64 in the timeline’s granularity unit (“days, matching Value::Date”, oxc-value/src/interval.rs:10-13). Rational/real (dense) time is the undecidability cliff (Wałęga TPLP 2023); discreteness — not day-granularity — is what buys decidability.
  • Transaction-time is a non-metric frozen selector. All six DatalogMTL operators quantify over valid-time only; tx and rule_vt are single i64 snapshot coordinates (oxc-runtime/src/read_model.rs:131-143). The two-metric-timeline product (LTL×LTL) is Σ¹₁-undecidable over ℤ; Argon avoids it by construction.
  • Truth4 is Belnap–Dunn FDE. Its negation is De Morgan and involutive — kernel-checked, axiom-free (spec/lean/Argon/Foundation/Truth4.lean:256-309: neg_neg, neg_truthMeet, neg_truthJoin).
  • The bitemporal axes are stored at nanosecond resolution (read_model.rs: vt/tx are epoch-nanos), but the surface caps valid-time at #date# (oxc-instantiate/src/lower/fact_parse.rs:614at requires a #YYYY-MM-DD# literal), and the metric materializer floor-projects vt onto the day timeline before reasoning (oxc-runtime/src/federate_translate.rs:271-303).

Two defects found while validating this design (firsthand)

  1. The static loud-gate covers rules, not mutation-body constants. #…Z#, a calendar duration (1.months), and even a type-mismatched constant (plain_date_time(…) into a Date field) pass ox check and ox build in insert/struct-field positions and refuse only at mutation execution. The gate (validate_runtime_evaluable_rules) validates rule bodies; insert constants escape it. General — the type-mismatch case is non-temporal.
  2. Out-of-range valid-time silently saturates. epoch_day_to_nanos is epoch_days.saturating_mul(NANOS_PER_DAY) (oxc-value/src/civil.rs:241-243). Any valid-time beyond the i64-nanosecond window (1677-09-21 … 2262-04-11) clamps to the boundary. Round-trip-proven: at #5000-01-01# and at #9999-01-01# collapse to the same instant (as_of #5000# and as_of #9999# return identical rows) — a silent coercion, the exact class the loud-gate forbids and that OE1373 already refuses for integer literals.

Decision

1. One delimiter; the inner ISO shape totally determines the carrier

Keep #…# as the sole temporal-literal delimiter. It is collision-free against #[attribute] (single-byte lexer dispatch: #+digit → temporal, #[ → attribute) and is already a leading, static kind marker — the only property the loud gate forces (the kind must never ride on a trailing payload byte that could silently flip it). The inner ISO-8601/RFC-9557 shape determines the carrier, as a total, static, refusing function κ : SurfaceForm → Carrier + Diagnostic:

LiteralCarrier
#YYYY-MM-DD#Date
#HH:MM:SS[.fff]#Time
#…T…# (no zone marker)DateTime (civil, zoneless)
#…T…Z# or #…T…±HH:MM#Instant (absolute UTC)
#…T…±HH:MM[IANA/Zone]#ZonedDateTime

Duration stays a unit-suffixed quantity (30.days), not a #…# literal — it is a length, not a point.

This resolves #1285: #…Z# is the Instant literal (which is what a trailing Z = UTC always meant), not a refused pseudo-DateTime. Every reasoning-relevant carrier becomes a first-class literal reachable in compile-time positions. This is kind-determination, not suffix-inference: each shape maps to exactly one carrier, decided statically at parse; a malformed or ambiguous inner form is loud-refused. It is the loud-gate reframed as a proof obligation — κ total + deterministic is the formal content of “never silently coerce.”

2. Nanosecond reach; valid-time is not day-granular

The bitemporal axes are nanosecond. at <…> and as_of <…> accept an Instant literal (sub-day), not only #date#. Snapshot selection (“the fact valid at instant t”) is a granularity-free integer comparison — so one substrate serves agents/robots at nanosecond precision and legal effective-dating at day precision, differing only in which literal precision the modeler writes. The #date# form is a convenience (midnight-UTC), not the resolution.

3. Out-of-range valid-time refuses; the loud gate covers insert constants

  • Replace the epoch_day_to_nanos saturation with a loud refusal when a valid-time’s epoch-nanos exceeds the axis (following the OE1373 out-of-range-literal precedent). Never silently clamp.
  • Extend the static loud gate to validate statically-knowable mutation-body constants#…Z#, calendar durations, type-mismatches must refuse at ox check/ox build, not at execution.

4. Carrier reach and astronomical time

  • Civil carriers reach ±9999 years (Gregorian, jiff) — covering all recorded human history and any agent/robot in the present ± centuries. The nanosecond bitemporal axis currently spans the i64-nanosecond window 1677-09-21 … 2262-04-11 (the pandas datetime64[ns] range).
  • Astronomical/geological time is a modeled domain on the substrate, not a stretch of the civil carriers. The reasoning core (a discrete integer timeline + interval/Allen/metric algebra) is scale-agnostic and decidable at any discrete tick, so geological spans are reasoned over at a coarse tick as an ordinary ordered domain. The civil Date/Instant carriers are not extended to cover them: the Gregorian calendar is physically inapplicable at Myr/Gyr scale, and precision × range is bit-bounded. This is the ontology-neutral posture — model the domain, don’t weld it into the built-in carrier.

5. Reasoning-core dispositions (validated; recorded as decided)

  • as_of axis dispatch stays lexical: as_of <int> = transaction-time, as_of <#date#|instant> = valid-time. Transaction-time remains a non-metric selector (the only decidable bitemporal regime).
  • Calendar month/year shifts are anchored Date → Date functions (add_months/add_years), never metric operators or durations. A non-uniform (anchor-dependent) metric step has no decidability result; calendar-relative durations are loud-refused (OE1337). The metric timeline’s step is fixed/uniform by construction (interval.rs).
  • NA (non-applicable) is a schema/type concern, not a fifth truth value. It is expressed by field: T? (StructurallyOptional → a positive ⟨.is, none⟩, Lean-proven in TypeSystem/Soundness/FieldAccess.lean), [0..0] cardinality, or conditional impls. Can stays reserved for existing-but-unknown. Truth4 stays four values (drift-gate-locked).
  • Cross-time identity: = stays rigid, two-valued, congruent over system-allocated IndividualId (a four-valued or counterpart = provably breaks the Datalog join engine). Identity-mutation (legal merger = fusion, spinoff = fission) — which the current surface cannot express and which Argon’s legal/tax-restructuring domain requires — is closed by an explicit, first-class, std-neutral successorOf/mergedInto succession relation carrying valid-time, joined explicitly by the modeler, never the engine =.
  • Replay across oxup update: stored values read identically by construction — Instant is an integer; ZonedDateTime is an offset-baked, self-describing string (reading and ordering need no tzdb). The bundled tzdb/leap version pins at the build-artifact closure (make it observable — stamp jiff_tzdb::VERSION into artifact provenance). A zone-aware re-derivation (arithmetic producing a new zoned value across a revised DST rule) pins its version to the recorded read-point; discharged when zoned reasoning is surfaced.

6. Declared-resolution metric timeline (decided — resolves the former open question)

The metric-operator tick is a package-declared resolution, decoupled from the granularity-free snapshot precision. Decidability rests on discreteness, not on the tick equalling a day; the tick is a positive-integer coarsening of the same ℤ timeline (strictly fewer points, never crossing into density), so ultimate-periodicity and the finite-materialization argument transfer verbatim.

  • Surface: a package-global [temporal.metric] resolution manifest setting (resolution = "week", or "<N><unit>" like 2weeks) — not per-rule or per-module. The metric timeline is one axis shared by every rule and fact interval; a per-rule tick would let two rules quantize the same fact set to incompatible grids — the silent scale-mismatch the loud gate forbids. It mirrors default_world (a package-global evaluation cut, stamped into the artifact’s GlobalControl, threaded to the runtime). It lives under a [temporal] umbrella, not a bare top-level [metric]: metric alone is overloaded (it reads as a measurement/units/observability knob, especially beside the Real/Decimal/Money value tower, and only disambiguates for a reader who already knows the modal-vs-metric DatalogMTL split — the one who least needs it); and it is not a bare [temporal], which over-claims — temporal is the umbrella sub-tier and metric is one operator family inside it (beside the modal box/diamond family; the classifier’s additive-composition rule is stated in exactly those terms, OE0712). [temporal.metric] keeps the precise word scoped to the metric timeline, has the correct containment direction (temporal ⊇ metric), and leaves a growable sibling namespace ([temporal.modal], [temporal.snapshot], axis width, …). ([metric.temporal] was rejected — it inverts the hierarchy and returns the overloaded word to the owning position; a flat metric_timeline_resolution = is the acceptable no-nesting fallback.)
  • Semantics: one MetricResolution (ns-per-tick) feeds BOTH fact valid-time projection and metric-window bound scaling, so the fact grid and the window unit can never disagree. Snapshot reads stay full-nanosecond (they never consult the tick). A metric bound authored in whole days maps onto the tick by integer division; a non-tick-multiple bound loud-refuses (since[3 days] at a week tick → 3/7 → refuse, never rounds to 0/1). Fact intervals euclidean-floor onto the tick grid (inclusive-lo → containing tick; exclusive-hi → (nanos−1) floored, so a tick-aligned end excludes its whole tick and an empty window stays empty). The default is DAY — an undeclared package is byte-identical to the pre-RFD behaviour.
  • Bounds are authored in days (via .days/.weeks/bare int), the tick coarsens that authored unit; this preserves the meaning of existing day-authored programs (the alternative — tick-native bounds where since[3] = 3 ticks — was rejected as it silently reinterprets existing box_minus[1,3]). Sub-day fact ticks are admitted (still ℤ); authored sub-day bounds (.hours) are out of v1 scope (a day-authored bound at a sub-day tick refuses).
  • An unrecognized [temporal.metric] resolution value warns (OW1240) and falls back to DAY, per the RFD 0030 §D6 manifest-honesty convention (unrecognized manifest values are non-fatal + treated-as-absent, for forward-compatibility — an older toolchain must not hard-die on a future unit spelling). This is deliberately not a hard refusal: the mis-default yields internally-consistent day-tick results plus a surfaced warning, not corrupt/dropped data, so the loud-gate’s silent-wrong-value concern does not bite; the manifest-forward-compat convention wins.
  • The tick coarsens the metric operators only; the stored i64-nanosecond valid-time axis is untouched. Composing a very-coarse tick with an i128 axis widening (for deep-range bitemporality) is deferred to the separate axis-width open question below.

Rationale

  • Inner-shape κ over #…#, not a typed prefix or suffix-inference. The loud gate forbids silent coercion, not static kind-determination; a total κ over the inner shape is loud-gate-compatible, keeps a single delimiter, honors the carriers’ own ISO/RFC-9557 semantics (Z=UTC=Instant; [Zone]=Zoned), and needs no new lexer facility. Suffix-inference — a bare Z contextually flipping a value’s kind — is the footgun the loud gate exists to ban; κ is not that (one shape, one carrier, statically). The requirement that forced Instant into the literal set (sub-day valid-time for agents/robots) is met most economically by the inner shape.
  • Nanosecond is decidability-safe. The ℤ-vs-ℚ cliff is a density property; nanoseconds are integers. “Day-granular” was a legal-domain default, not a substrate requirement; Argon’s general mandate (agents, robots, fast-changing state) requires sub-day and pays nothing for it.
  • Saturation → refusal, insert-gate coverage: silent clamp and silently-mistyped inserts both violate the loud-gate invariant (“a build that drops or mis-stores a value yields wrong answers”). OE1373 is the precedent.
  • Every §5 disposition is forced by ground truth, not chosen: kernel-checked Lean (neg, FieldAccess), structural locks (drift gate, interval.rs fixed step, IndividualId), and runtime proofs (OE1337; the identity diagonal; the offset-baked round-trip).

Alternatives considered

  • Typed-prefix literals (date"…", instant"…"). The cross-provider design consensus (three independent model families). Rejected: #…# already satisfies the only forced constraint, inner-shape κ covers all carriers, and a typed prefix is a new lexer facility (Argon has no string-prefix lexing today) that buys nothing forced. Retained as the runner-up: if the inner-shape lexing of zone/offset forms proves problematic, the typed prefix (CamelCase, type-name-aligned) is the fallback.
  • Suffix-inference (ISO/Temporal-from-string). Rejected: a keystroke silently changing the carrier kind is the Python-aware/naive and OWL partial-order footgun.
  • Day-granular valid-time (keep it). Rejected: domain-biased, fails the agent/robot mandate, and unnecessary (sub-day is decidability-safe).
  • Widen Date/Instant to astronomical scale. Rejected: Gregorian is physically inapplicable at Myr/Gyr, and precision × range is bit-bounded; astronomical time is a modeled domain.
  • Native four-valued =, a fifth NA value, a standalone Offset carrier. Rejected respectively: breaks the join engine; is a schema-layer applicability concern; an offset-only string is losslessly absorbed by Instant and ZonedDateTime correctly requires a named zone.

Consequences

Immediate, forced fixes (shippable, #1285’s real scope):

  • Grammar/lexer/parser: recognize the zone/offset inner shapes; route #…Z#/#…±HH:MM#Instant, #…[Zone]#ZonedDateTime; keep #YYYY-MM-DD#/#…T…#.
  • oxc-instantiate fact_parse: accept an Instant in at/as_of; refuse out-of-range valid-time (replace saturating_mul).
  • Extend the static loud gate to insert-body constants.
  • Drift fixes: the tree-sitter grammar (grammar.js still requires Z), the lit.toml DATETIME desc, the book (lexical-structure.md, stdlib.md drop the mandatory Z), and the DST-gap-invalid canonical example in value.rs.
  • Directed diagnostics (e.g. a typed code for a malformed instant literal that names the carrier).

Builds (scheduled):

  • Surface the Instant/ZonedDateTime/TimeZone type names (std::datetime, per RFD 0047) — with the read-point tzdb-version pin, not as a hollow surface.
  • The successorOf succession package (parallelizable — a sibling work stream).
  • Observable tzdb-version artifact stamp; the declared-resolution metric timeline (decouple metric-op tick from snapshot precision).

Lean/book: revise the Lean dateTime grammar (drop mandatory-Z; inner-shape κ); reconcile the book.

Migration: small and mostly additive — Date/DateTime literals are unchanged; Instant/Zoned literals are new; the #…Z# uses in the tree are three lexer/parser/instantiate tests (oxc-lexer/src/lib.rs, oxc-parser/tests/fact_arg_literals.rs, oxc-instantiate/tests/fact_arg_literal_set.rs) and a research doc — no std/, examples/, or production use.

The research campaign’s decision surface (SYNTHESIS §7) lists six commitments “only the owner can make”; §5 above records all six as resolved against Argon ground truth — Truth4 negation is De Morgan-involutive (kernel-checked); transaction-time is a non-metric selector; metric operators range over one timeline; identity is surface-rigid = over perdurantist storage slices; the tzdb/leap version lives at the build-artifact closure (with a read-point pin for zone-aware re-derivation); calendar month/year units are anchored functions / EDB, never metric operators. That leaves Tier-1 (four-valued metric decidability, Open Questions below) as the sole research obligation, and it gates claiming — not shipping — the four-valued lift.

Open questions

  • The one genuine research obligation — four-valued metric decidability. Cross-kind temporal comparison is structurally four-valued: a zoneless DateTime vs an absolute Instant, an offset-unknown value, or two values across the XSD ±14h window are genuinely incomparable, which is a Can verdict a total order cannot produce. Truth4’s kernel-checked De Morgan involution satisfies the quasi-Boolean→classical reduction precondition — but no published result establishes the decidability of a Belnap/four-valued metric reasoner over ℤ. The 2-valued DatalogMTL decision procedure rests on three pillars — least-fixpoint over {true, ¬derived}, consistency ≡ ⊥-never-derived, and ultimate-periodicity over the 2-valued canonical model — and a designated, non-explosive Both attacks all three; whether the ℚ-undecidability lower bound even survives paraconsistency is open in every direction. The 2-valued-plus-refusal comparison ships sound today; moving to native Can/Both temporal comparison requires a Lean proof campaign (scratch-first) before it can be claimed decidable. This warrants its own RFD and a mechanization campaign; it gates claiming, not shipping, the four-valued lift.

    The concrete first proof step (from the research campaign’s decidability ledger): show the twist-structure “two coupled Boolean copies” reduction (Rivieccio–Jung–Jansana) factors the metric T_Π canonical-model / ultimate-periodicity ruler argument coordinate-wise — established for qualitative modal box/diamond, open for the metric ℤ ruler. Note four-valuedness is not itself the cost (Fitting/AFT: complexity-neutral over a finite interlaced bilattice, and 4QL keeps plain four-valued Datalog PTIME); the specific threat is the designated non-explosive Both against pillar (b) (consistency ≡ ⊥-never-derived) and pillar (c) (2-valued periodicity). Two adjacent sub-obligations ride along: a mechanized totality proof of the multi-carrier kind function κ (no published multi-carrier kind-inferred-from-body grammar has one — a new result), and ω-admissibility of the Allen layer over discrete ℤ (proven only over the dense line). The value-half of κ is itself four-valued at construction: a DST gap is a no-Is-witness, a fold is Both — today collapsed to a strategy parameter, which the loud-gate should instead surface.

  • Bitemporal axis width. The i64-nanosecond axis gives the 1677–2262 window; widening to i128-nanos (matching the Instant value carrier’s ±9999yr @ ns) if fine-grained deep-range bitemporality is wanted — a knob, priority-second to the saturation refusal.

  • Metric-timeline declared resolution. Decoupling the metric-operator tick (cost scales with tick count) from the granularity-free snapshot precision. Resolved — see Decision §6 ([temporal.metric] resolution package-global setting; one MetricResolution feeds fact projection + bound scaling; DAY default).

  • Explicit as_of/as_recorded axis naming. Only if authored fine-grained transaction-time instants become a need (transaction-time is a system watermark, rarely hand-written).

  • Incomparability composition. Whether the distinct incomparability causes (offset-unknown, calendar-vs-fixed duration, cross-kind, causal concurrency) collapse into one Truth4 algebra or require a product/bilattice of per-cause comparison relations — an open algebraic question in formal KR.

RFD 0068 — Native four-valued temporal comparison: decidability of a paraconsistent metric reasoner over ℤ

Scope. RFD 0067 §5 settled the temporal value dispositions and flagged one — native four-valued temporal comparison — as the sole open research obligation, gating claiming (not shipping) the four-valued lift. This RFD funds that campaign, records the mechanized result that discharges its load-bearing question, and fixes the binding implementation constraint the mechanization forces.

  • State: discussion (campaign funded)
  • Extends: RFD 0067 (temporal literal surface — Open-Question #1, the four-valued comparison frontier), which ships 2-valued-plus-refusal comparison today.
  • Relates to: RFD 0007 (missing-value semantics — Can vs NA), RFD 0024 (Allen library — the ω-admissibility sub-obligation), RFD 0060 (Lean substrate architecture — where the eventual merged proof lands), the @[language_interface] drift gate (Truth4 four-values lock).
  • Tracks: the four-valued-metric-decidability obligation (RFD 0067 Open-Question #1).
  • Prior art: Belnap–Dunn FDE (the four-valued codomain); Rivieccio–Jung–Jansana (twist structures / four-valued modal logic — the coordinate-wise reduction); Wałęga–Cuenca Grau–Kaminski et al., “DatalogMTL over the integers” (KR 2020 / TPLP 2023 — the ℤ-restores-decidability result and the per-coordinate ruler bound this imports); Wałęga–Zawidzki–Wang–Grau (AAAI 2023 — DatalogMTL saturation, the multi-page 2-valued periodicity argument); Pollaci 2026 (three-valued WFS DatalogMTL over ℤ — the adjacent occupied cell); Denecker–Marek–Truszczyński (AFT); Fitting (interlaced bilattices); Małuszyński–Szałas 4QL (four-valued Datalog PTIME on a finite lattice — four-valuedness is not the cost); Gabbay–Kurucz–Wolter–Zakharyaschev (the Σ¹₁ two-metric bound — why one timeline); Kolaitis–Vardi (the four-valued-= obstruction — why identity stays rigid while comparison goes four-valued).

Question

Argon’s comparison verdicts are four-valued (Truth4 = Belnap–Dunn FDE): Is / Not / Can / Both. Temporal comparison is structurally four-valued — a zoneless DateTime vs an absolute Instant, an offset-unknown value, or two values inside the XSD ±14h window are genuinely incomparable (a Can no total order can produce); standpoint disagreement over a temporal fact is Both. RFD 0067 ships the interim: 2-valued comparison plus loud-refusal of the incomparable cases. Going native — letting temporal comparison return Can/Both into the metric reasoner — raises the question that gates the claim:

Does a designated, non-explosive Both (paraconsistency) break the integer-timeline decidability that 2-valued DatalogMTL over ℤ enjoys — and if not, what does soundness of the four-valued lift require of the metric operators?

Context

Why this is not free

2-valued DatalogMTL over ℤ is decidable (Wałęga et al.) on three pillars: (a) a least fixpoint over derived atoms, (b) consistency ≡ ⊥-never-derived, (c) ultimate periodicity of the canonical model (the “ruler” bound), reducing entailment to a bounded check. Dense (ℚ/ℝ) time is undecidable (RFD 0067 §Context). A designated, non-explosive Both — the whole point of a paraconsistent codomain — attacks all three: (b) is literally “⊥ (Both) may be derived and is not explosive,” and (c) is stated over the 2-valued model. A priori it was open in every direction whether the four-valued lift stays decidable, or whether the dense-time lower bound even survives paraconsistency.

What is not the cost

Four-valuedness per se is complexity-neutral: Fitting/AFT over a finite interlaced bilattice, and 4QL keeps plain four-valued Datalog PTIME. The threat is specific — the interaction of the designated Both with the metric fixpoint and the periodicity argument, not the extra truth values.

The substrate this must respect (RFD 0067, kernel-checked)

One ℤ metric timeline = the valid-time axis; transaction-time a non-metric selector; Truth4 negation De Morgan + involutive (Foundation/Truth4.lean); the least-fixpoint lattice is Approx = Belnap FOUR = 2 ⊠ 2ᵒᵈ (Reasoning/Datalog/AFT.lean), never MetaValue (which has no complete lattice — is ⊕ not = both escapes K3). The four-valued reasoner is AFT over the twist/Approx space; the campaign’s question is whether that space’s metric closure is periodic.

Decision

1. Fund native four-valued temporal comparison as the target; the loud gate extends to the reasoner

Commit to native Can/Both temporal comparison as the intended surface — genuine incomparability returns Can, standpoint conflict returns Both, both flowing into the metric reasoner — replacing the interim 2-valued-plus-refusal once the decidability obligation is mechanically discharged. The loud-gate discipline extends from the compiler to the reasoner: the four-valued metric lift is not claimed decidable, and does not ship, on an unproven pillar. A scratch-first Lean campaign (never-merged until sorry-free and lead-signed-off) is the gate.

2. The load-bearing question is answered: the paraconsistent Both does NOT break periodicity (mechanized)

Established in the campaign scratch (Truth4MetricDatalog.lean), each result firsthand #print axioms-verified kernel-clean ([propext, Classical.choice, Quot.sound], no sorryAx):

  • coupling_preserves_periodicity — the campaign’s central open question: if each of the two twist coordinates is ultimately periodic, then the joint four-valued canonical model is (product period π₁·π₂). The designated Both inherits periodicity as a bit-pattern of the two coordinates (belnapAt_congr); it introduces no new aperiodicity. This is the coordinate-wise twist reduction (Rivieccio–Jung–Jansana), established for qualitative box/diamond, here carried to the metric ℤ ruler.
  • canonicalModel_isLeastFixpoint — the four-valued canonical model is a well-defined least fixpoint over Approx. This is pillar (b) reformed for paraconsistency: well-definedness with Both present, not ⊥-absence.
  • both_feeds_metric — a Both verdict at one time point propagates through a nonzero metric shift (the non-vacuity witness; see §3).
  • The reduction chain canonicalModel_ultimatelyPeriodic_of_twoValuedentails_periodic carries decidability from per-coordinate periodicity to bounded entailment.

Consequence. Decidability of four-valued metric DatalogMTL over ℤ reduces to (a) the published 2-valued per-coordinate ruler bound (Wałęga et al. — imported, TwoValuedRulerBound), and (b) the effective finite-domain check (entails_decidable). Neither is a new mathematical obstruction; both are named, remaining sorrys in the scratch. No place where the paraconsistent Both obstructs periodicity was found — the a-priori threat is refuted.

3. Binding implementation constraint: metric operators propagate the full Belnap pair

The mechanization forces a soundness constraint on the reasoner (both_feeds_metric, kernel-clean): a metric temporal operator (//since/until and the bounded variants) must propagate the full Belnap pair — both the positive-evidence coordinate and the negative-evidence coordinate — not just positive derivations. A metric operator that forwards only positive evidence drops the Both/Can content and is unsound under the four-valued lift. This is binding on any oxc-reasoning implementation of four-valued metric operators, and is a review checkpoint on any four-valued-lift PR.

4. The decidable fragment requires shift-invariance (faithfulness correction)

The periodicity statement is false over an arbitrary ground metric program: a program with facts at square times (0, 1, 4, 9, …) is finite-predicate, well-typed, and has no ultimately-periodic model (unbounded gaps) — firsthand-verified in the scratch (sqProg). Ultimate periodicity — hence decidability — holds for shift-invariant finite metric programs (FiniteMetricProgram.shiftInv): the ℤ-orbit of finitely many rule schemas, which is exactly what a rule program (as opposed to an infinite fact set) is. The decidable fragment is the shift-invariant one; the statement was corrected to carry this hypothesis before it was proved, not after. This bounds the claim and is the honest statement.

Rationale

  • Decidability was the only thing gating the claim; it is now essentially in hand. RFD 0067 §Consequences reduced the owner-decisions to five-resolved + this one research obligation. The mechanized coupling result removes the genuine unknown (does paraconsistency break periodicity — no); what remains is importing a published theorem and wiring an effective procedure — both proof-engineering, not open mathematics.
  • Scratch-first, lead-signed-off, never-weakened (the Lean operating model). The statement was strengthened to faithfulness (shift-invariance) before proving; non-vacuity was discharged first (both_feeds_metric and the bothAtZero witnesses show the four-valued content is not degenerate); the axiom hygiene was verified firsthand (#print axioms), not taken on the prover’s word. The two remaining sorrys are named import points, not scattered gaps.
  • The Belnap-pair constraint is not a style choice. both_feeds_metric shows a positive-only metric operator drops the four-valued content; §3 is the mechanized content of “the four-valued lift is real,” not aesthetic guidance.
  • Shift-invariance is where the dense-time analogue’s teeth actually are for a program. The undecidability cliff is density; the aperiodicity a ground fact-set can inject (square times) is a distinct failure the fragment restriction rules out. Naming it keeps the decidable-fragment claim honest rather than quietly true-only-for-rule-programs.

Alternatives considered

  • Keep 2-valued-plus-refusal permanently (never go native). Rejected as the target (kept as the shipping interim): refusing every genuinely-incomparable temporal comparison forces the modeler to pre-resolve incomparability the substrate exists to represent (Can), and discards standpoint conflict (Both) that the federation layer produces. The four-valued codomain exists precisely for these.
  • Three-valued WFS (K3, no Both). The adjacent occupied cell (Pollaci 2026). Rejected: drops paraconsistency — standpoint conflict collapses to undetermined (or, classically, to explosion); Argon’s federation semantics needs a designated non-explosive conflict value. (WFS-over-ℤ remains relevant prior art for the effective procedure entails_decidable.)
  • Re-prove the 2-valued ruler bound in-repo. Rejected: it is an established, multi-page published result; importing it (a cited axiom per the substrate’s citation rule, or a vendored mechanization) is correct. Re-deriving it is not this campaign’s contribution — the contribution is that the four-valued lift preserves it.
  • Native four-valued = (counterpart identity). Out of scope and separately rejected (RFD 0067 §5; Kolaitis–Vardi — a four-valued = breaks the join engine). Comparison is four-valued; identity stays rigid two-valued.

Consequences

  • Ships now (RFD 0067): 2-valued comparison + loud-refusal of incomparable cases. Unchanged.
  • The mechanization campaign (this RFD):
    • Scratch preserved at .local/research/datetime-literal/lean-scratch/Truth4MetricDatalog.lean (discharged content kernel-clean; 2 named sorrys).
    • Remaining to close before claiming decidable: import TwoValuedRulerBound (the published 2-valued periodicity — vendor or cite-axiom), and discharge entails_decidable (computable bound N, period π, Pred-enumeration → an effective procedure).
    • When sorry-free + lead-signed-off, promote from scratch into spec/lean/Argon/ (RFD 0060 architecture; under Reasoning/Datalog/ or Decidability/), where it becomes drift-gated substrate.
  • Binds the implementation: four-valued metric operators in oxc-reasoning must propagate the full Belnap pair (§3).
  • Then ships: native Can/Both temporal comparison, replacing the interim refusal, behind the proof.

Open questions

  • The two named imports (above): TwoValuedRulerBound, entails_decidable. Proof-engineering, not obstruction — but real work, and the effective procedure fixes the reasoner’s actual complexity bound.
  • Mechanized totality of the multi-carrier kind function κ (RFD 0067). κ is total/refusing by construction in Rust; no published multi-carrier “kind inferred from inner shape” grammar has a mechanized totality proof — a new (small) result, adjacent to this campaign.
  • ω-admissibility of the Allen layer over discrete ℤ. Allen interval relations are proven admissible over the dense line; the discrete-ℤ analogue (RFD 0024’s library over interval.rs’s fixed step) is unproven.
  • The value-half of comparison is itself four-valued at construction. A DST gap is a no-Is-witness (Can), a fold is Both — today collapsed to a strategy parameter (RFD 0067 §5). Surfacing them as genuine Truth4 verdicts is the write-side analogue of this read-side campaign.
  • Incomparability composition (carried from RFD 0067): whether the distinct incomparability causes (offset-unknown, cross-kind, calendar-vs-fixed duration, causal concurrency) collapse into one Truth4 algebra or need a product/bilattice of per-cause comparison relations.

RFD 0069 — The shared-live-KB write model: concurrent authoring, coexistence, and resolution

Scope. How many autonomous agents (plus humans and services) author concurrently against one shared, live Argon knowledge base, and how their contributions coexist, conflict, resolve, and coordinate. This RFD fixes the design — the layering, the write unit, the coordination boundary, and the resolution model — that the agent-integration surface (RFD 0066) forced into the open. It records why; it is not the spec. The load-bearing soundness claim is handed to a Lean mechanization (below), which gates implementation.

  • State: discussion (owner-ratified shape; surface syntax and the gating mechanization are open)
  • Relates to: RFD 0066 (agent integration — the surface that forced this), RFD 0052 / 0053 / 0057 (deployment topologies / the standalone concurrent engine / the served platform — the single-logical-writer substrate this builds on), RFD 0010 (strong negation / negative facts), RFD 0028 (defeasibility — the resolver), RFD 0045 (world assumptions — CWA default, #[world(open)]), RFD 0067 / 0068 (the four-valued lift), RFD 0014 (serving surface), RFD 0018 / 0021 (the incremental reasoner).
  • Supersedes (in part): the promote()-as-last-writer-wins fork-merge shipped on the /v1 + embedded fork path (retired by D8).

Question

The runtime is a single-logical-writer bitemporal store (RFD 0052 D4 / 0053). The agent surface (RFD 0066) is, as shipped, an embedded, single-client server — a deliberate placeholder. The near-term requirement is a shared live KB: many concurrent writers against one durable store, whose authored contributions must be able to disagree durably (Argon’s whole reason to exist is Truth4::Both, standpoints, and a defeated conclusion that stays queryable). What is the write model — the write unit, the merge, the resolution, the coordination boundary — that delivers this without destroying the disagreement the substrate is built to hold?

Context

  • The substrate already commits to coexistence machinery: FDE / Truth4 (Is/Not/Can/Both, Lean-mechanized, RFD 0067/0068), first-class standpoints (a <: lattice with an FDE information-join across per-source extents), Governatori defeasibility (#[defeats], +Δ/+∂/−Δ/−∂), CWA-by-default with per-concept #[world(open)] (RFD 0045), and a bitemporal append-only log with copy-on-write forks.
  • The current fork promote() is a last-writer-wins signed-net-delta append (embedded + serve-private /v1). It silently destroys exactly the coexistence the substrate exists to preserve.
  • This design is backed by a three-wave research campaign (a breadth map of 25 territories → a build-gating dive → a research-forward wave; ~500 verified sources). The campaign produced two structural results and a costed set of forks; the owner has ratified the shape recorded here. The campaign is a design input, not the spec — the external prior art it verified is cited inline.

Two structural results this design rests on

Result 1 — coexistence and resolution are two operators, not one (corroborated across six independent fields). A single merge operator cannot simultaneously be (i) a monotone information-order least-upper-bound — Both accumulates, order-independent, coordination-free — and (ii) a defeasible resolver — non-monotone, collapsing Both back to a definite value. The same wall was independently derived in six unrelated literatures: lattice theory (defeat moves a key down the info-order; an LUB cannot); differential-dataflow types (retraction needs an Abelian group, a join is only a monoid); judgment aggregation (majority is provably incompatible with a consensus postulate — Haret–Wallner–Woltran 2018); the CALM theorem (a non-monotone operation has no coordination-free implementation — Hellerstein–Alvaro 2020, an iff); conflict-of-laws doctrine (uniform symmetric cross-reference cannot terminate — the renvoi problem, In re Ross 1930 — law breaks symmetry with forum-priority, a defeats move, never a join); and abstract argumentation (the credulous∖skeptical meet is not set-intersection — Dung). The monotone information-join fragment has many homes; the defeasible resolver has none — no group-free algebra, no coordination theorem, no engine models it natively.

Result 2 — Both has no cheap home at any layer. Durable Both is not derivable in the knowledge-order least fixpoint (Fitting; it must be injected as an EDB annotation from the cross-standpoint join); it is forbidden by the provenance semiring (x·x̄ = 0); it is minimized away by record-linkage triage optima (Fellegi–Sunter); and it is collapsed by any scalar uncertainty surface. Carrying Both durably through the store → maintainer → aggregator → human surface is therefore the through-line build-not-find of the whole design.

Decision

D1 — Coexistence is reasoning-native, not storage-native. Disagreement lives in the reasoning layer (standpoints + Truth4 + provenance), not in the storage key. The write log stays a uniform monotone append; coexistence is a fold (the FDE information-join surfaces Both at projection/read time). This keeps the cheap commutative-monoid algebra and never hits the retraction-needs-a-group wall (Result 1). Storage-native coexistence (a third source axis in storage — demonstrated at scale by TARDiS/BT-tree/PRIMA) is real but drags per-type merge constructors, a two-valued store, and the group wall; it is rejected for v1. The reasoning-native bet is principled, not merely convenient: durable disagreement provably requires heterogeneous priors (Aumann), survives even a shared prior once higher-order common knowledge fails — the autonomous-agent case (Lederman), and is exactly what standpoint logic formalizes (Gómez-Álvarez–Rudolph).

D2 — Two operators, two layers. A monotone accumulate-Both merge (an information-order LUB; coordination-free, CRDT-clean, order-independent) is separate from a defeasible resolver that collapses Both to a definite value at read time, keeping the defeated conclusion queryable. Result 1 forbids one operator from being both. The merge is the write/fold path; the resolver is a read-time policy.

D3 — The write unit is the per-source (standpoint-scoped) extent — the Single-Writer carrier. Each source/agent owns a standpoint-scoped extent and is its sole writer. Sole-writer-per-extent is simultaneously contention-free (mechanical Single-Writer Principle) and monotone-append (semantic) — the two arguments coincide on one partition. Cross-source disagreement never rebases; it lands in different extents and meets as Both at the fold (D1).

D4 — The KB is a verb, not a noun. The shared object is the append-only write log plus per-standpoint projections; the cross-standpoint fold (the expensive coexistence computation) is materialized on demand, only where a cross-standpoint read is actually requested. The undecidable/coNP-hard certain-answer walls of eager global federation (Halevy) are thereby confined to the queries that demand them.

D5 — The tertiary cross-standpoint-designation layer (the resolver’s policy carrier). Argon gains a third, currently-unnamed rule layer, modeled on legal pluralism / conflict-of-laws: primary (facts) / secondary (rules) / tertiary = which standpoint’s view governs, relative to a forum. The tertiary layer is D2’s resolver expressed in the surface: forum-relative priority over standpoints, discharged by the existing defeasibility + priority machinery (RFD 0028) and Makinson–van der Torre input-output logic. This maps 1:1 onto standpoints + FDE + defeasibility + priority — the tightest external match to Argon’s whole committed shape, and it independently corroborates Result 1 (law resolves conflict by forum-priority, never by a symmetric join).

D6 — The coordination boundary is the monotone/non-monotone line (CALM). The monotone fragment {append a fact, OWA-assert, FDE information-join} is provably coordination-free — no serializer, no rebase. The non-monotone residue {retraction-semantics, #[defeats], CWA-negation-as-failure, uniqueness-of-a-value, exclusive-ownership} provably requires coordination. Uniqueness and exclusive-ownership are indivisible (escrow cannot remove them) and route through a per-scope linearizable frontier compare-and-swap; the rest is relocatable (escrow / sealing). Argon needs no self-built consensus — the single-logical-writer log means the only contested cell is the frontier watermark, served by one outsourced linearizable CAS.

D7 — Cross-writer trust needs an external decorrelated verifier, not internal vote-counting. With correlated writers (N clones of one base model are maximally-correlated voters), agreement is not evidence of correctness: majority correctness asymptotes to P(shared prior is right) < 1, not to truth (Dietrich–List), and more clones can lower competence (Kaniovski). The KB therefore never treats “N standpoints agree” as a correctness signal internally; trust is conferred by an external decorrelated verifier (a designated curator forum, or an out-of-family check) — the same decorrelation principle this track already applies in its cross-provider review gate.

D8 — Retire promote()-as-LWW. The flat-main last-writer-wins fork merge is withdrawn. It is neither a majority nor an arbitration operator (it is a recency-dictatorship), it voids every incremental-view-maintenance soundness theorem, and it is the anti-pattern four independent never-merge traditions reject. Its replacement is D1–D2: a standpoint-scoped monotone append-Both merge plus the read-time resolver.

Rationale

  • Reasoning-native keeps the cheap algebra and the substrate’s crown jewels. The append-only log is already monotone; the FDE join is already the coexistence operator; standpoints are already the per-source partition. D1–D5 wire what exists rather than building a storage-side merge engine against the grain (Datomic — which the serve layer resembles — is natively anti-Both).
  • The two-layer split is forced, not chosen. Result 1 is a structural wall independently corroborated across six fields; each rules out one operator being both accumulator and resolver. The corroboration is not itself the proof: the composition’s soundness for Argon is what the phase-0 Lean mechanization discharges — this RFD does not stand in for it.
  • The coordination cost is minimal and known. D6 confines coordination to the small indivisible residue; everything else fans out coordination-free.
  • Honesty over a faked signal. D7 refuses the seductive “agreements are answers” shortcut that correlation breaks.

Alternatives considered

  • Storage-native coexistence (a source/belief axis in storage; TARDiS-shaped). Rejected for v1: per-type merge constructors + a two-valued store + the retraction group wall. Real and fast, but against the grain of a Truth4 reasoning layer. (D1.)
  • One merge operator (a single CRDT-or-resolver). Rejected: Result 1 — the coexistence/resolution split (D2).
  • Flat-main last-writer-wins promote (the shipped path). Rejected: D8’s indictment.
  • Internal vote-counting for cross-writer trust. Rejected: correlated-writer non-independence (D7).
  • Eager global federation (fold all standpoints into one read model up front). Rejected: certain-answer undecidability/coNP-hardness (Halevy); D4 makes the fold lazy.

Consequences

  • Prerequisite, landed: the .oxbin standpoint-lattice + tier-table sections must be real (every decision is standpoint-based). Closed by the #1283 fix.
  • The through-line build-not-find: a Truth4-native engine at scale that carries Both through the fixpoint (4QL / Laurent-2021 are the sub-scale starting points); the federation information-join across source partitions; group-free four-valued incremental view maintenance (DBSP needs a group, Truth4 is a group-free bilattice → a semiring/semimodule IVM, unbuilt); a coexistence-preserving bitemporal merge; the tertiary designation layer (new surface); a source/standpoint-indexed Both human surface; and monotonicity verification over the bilattice.
  • A new surface layer (D5, tertiary designation) — new syntax + semantics, gated on the book + Lean.
  • Mechanization gates the build (framework-first). The load-bearing new soundness claim — that the two-layer composition (monotone append-Both merge + read-time defeasible resolver over standpoint-partitioned extents) is sound and that the reasoning-native fold behaves as specified under #[defeats] / CWA-negation / retraction — is mechanized in Lean before the implementation commits, with a differential oracle gating the engine. Result 1 already establishes the fold is not an ACI join once defeat enters (hence two layers); the mechanization discharges the composition.

Open questions

  • The tertiary-designation surface syntax (forum-relative priority over standpoints): how it is declared, and its interaction with the existing #[defeats] / priority plane (RFD 0028).
  • The correctness floor on the serve write path. Cross-process write serialization for the indivisible residue (uniqueness #271, delta-guard write-skew #224) and durable collision-free scope identity (#270/#285) — the per-scope frontier CAS D6 names — are enforced only in-process today. This must land before more than one writer shares a durable store.
  • The Both human/agent surface. Presenting source/standpoint-indexed disagreement (and a bounded triage of preserved plurality — the “bandwidth” problem) without a routing oracle that is worst exactly on the hard-contested items (Mozannar–Sontag).
  • Four-valued IVM at scale (group-free) and the exact resolver policy language.
  • The external-verifier mechanism (D7): whether the decorrelated verifier is a curator standpoint, an out-of-family check, or a human forum — and how its verdict is recorded.

Build order (phased; framework-first)

  1. Mechanize the two-layer composition soundness in Lean (scratch, never-merged until it holds); stand up the differential oracle. Gates 2+.
  2. The correctness floor — cross-process write serialization for the indivisible residue + durable scope identity (the D6 CAS). The substrate must be sound under real concurrency before writers share it.
  3. Standpoint-scoped write + monotone append-Both merge (D1/D3), replacing promote()-LWW (D8).
  4. The read-time defeasible resolver + the tertiary designation layer (D2/D5).
  5. The Truth4-native engine at scale + group-free four-valued IVM (the through-line).
  6. The Both agent/human surface + the external decorrelated verifier (D7).

Each phase is gated by a differential oracle against the mechanized model, not by the framework alone.

RFD 0070 — Derive-backed classification: the shape clause

1. Motivation

UFO roles and relational phases are condition-defined membership over other entities (participation in a relation, derived classification of related individuals). Argon’s inline iff { P } refinement is per-individual and local by construction (OE0660): it evaluates a predicate over self’s own field state + the static type graph, outside the reasoner fixpoint. It cannot express a relational instantiation condition. This is the documented OWL gap (gUFO: “OWL has no dynamic classification”; OntoUML→OWL reifies a TemporaryInstantiationSituation and emits SWRL for the instantiation condition). Native rule-derived membership closes it.

The ontology-fit analysis fixes the correspondence:

UFO metatypeinstantiation conditionArgon plane
phase (anti-rigid, sortal)intrinsic — over the individual’s own qualitiesinline iff { … } (local, decidable, outside the fixpoint)
role (anti-rigid, sortal)relational — over other entities / their derived membershipshape clause → concept-headed derive (rule-plane WFS/AFT fixpoint)

Rule of thumb: intrinsic-and-local → iff {}; relational OR fixpoint-dependent → shape clause. The mechanism follows the condition, not the metatype label. phase/role/relator/rigidity·sortality are package vocabulary (arg_ufo), never language surface (§3.4 no-ambient-vocabulary); the substrate provides only the two neutral mechanisms and never reads a UFO name.

Flagship (acceptance case 1, legally_capable): LegallyCapableNaturalPerson — a person is legally capable iff they hold a contractual capacity and are not under guardianship. Membership turns on other entities (held capacities) and a derived disqualification — outside OE0660 (verified: count{c in self.caps, c in ContractualCapacity} and a HasCap(self) call both refuse OE0660 today). Built + derived: extent = {alice} (bob excluded — non-contractual capacity; carol excluded — guardianship disqualifies).

2. Scope / non-goals

  • The inline iff local fragment is not widened. OE0660 stands; RFD 0017’s vacuous-invalidation containment is preserved. This RFD adds a declaration-level surface, not a widened expression grammar.
  • Federated shape membership is deferred (a dedicated later design). Shape membership over placed/foreign relations refuses today (the #1450 prototype’s foreign-connector refusal); this RFD does not lift that.
  • Bounded self-field aggregates in inline refinements (count over self nav-fields) are out of scope here: that was evaluator lag on the designed-in decidable fragment, fixed separately (#1501, landed via #1505).

3. Design — the shape clause

Declaration-level clause:

pub type LegallyCapableNaturalPerson <: NaturalPerson iff LegalCapacityShape(self);

<ShapePath> resolves to a module-local pub derive (the shape). It desugars to an ordinary concept-headed rule in the rule plane:

derive LegallyCapableNaturalPerson(m) :- LegalCapacityShape(m), NaturalPerson(m);

plus, per named witness, a projection derive. Keyword reuse, not plane merge: the compiler-enforced marker (body shape — an inline { … } block vs a shape-path Ident(…)) selects the plane. The locality difference stays semantic (§7). No new keyword is introduced: both planes share the one iff keyword, the plane selected by the compiler-enforced marker (the ratified framing, §7). (The exact lexing — one-token lookahead — and positional witness binding are #1450-derived surface specifics, prototype-only and implementation-pending; the semantic contract is the desugar target, not the surface bikeshed.)

The synthesized rule is single-source: it is the only rule with head C, and manual insert iof(x, C) is refused (OE0211, on main). A head-collision with another rule must also be refused; the prototype’s OE0678/OE0666 codes for that are #1450-only (NOT on main) — the diagnostic family is pending implementation, the requirement is not. This single-source discipline is what the semantics in §4 rests on.

4. Semantics — membership-as-IDB + the realization biconditional (mechanized)

A shape-defined concept C is an ordinary atom iof(x, C) of the reasoner’s well-founded / AFT fixpoint — uniform with every other ground atom. There is no separate classifier fixpoint and no hand-rolled alternating fixpoint (that is where the published hybrid-MKNF errors were: Knorr et al. 2011, corrected by Liu & You 2017/2021). Argon reuses its single AFT operator: the substrate’s stableRev on the product bilattice Approx = Set Atom × (Set Atom)ᵒᵈ, whose well-founded fixpoint wfFix P equals the Van Gelder model (wfTrue P, wfPossible P) — a theorem in the substrate (Argon.Reasoning.Datalog.AFT.wfFix_eq).

Realization biconditional (mechanized). Under the single-source discipline, for the synthesized rule C(m) :- Shape(m), Parent(m) and every individual x, membership realizes the surface promise in three-valued (Kleene) logic:

  • definitely-true region: iof(x,C) ∈ wfTrue P ↔ iof(x,Parent) ∈ wfTrue P ∧ Shape(x) ∈ wfTrue P;
  • not-false (Can) region: iof(x,C) ∈ wfPossible P ↔ iof(x,Parent) ∈ wfPossible P ∧ Shape(x) ∈ wfPossible P;
  • K3 value: wfModel P (iof(x,C)) = KleeneAnd (wfModel P (iof(x,Parent))) (wfModel P (Shape(x))).

These are proved over the joint operator on the existing Argon.Reasoning.Datalog substrate (statement ledger in Appendix B). Undefined membership surfaces as Can (RFD 0007); it is never fabricated into Is (RFD 0045-D1; collapse_conclusion_lacks_positive_evidence).

K3-non-corruption (the Both boundary). Within one store a shape-defined concept’s membership is single-source, so the well-founded model is consistent (wfTrue ⊆ wfPossible, substrate wfTrue_subset_wfPossible) and reads into K3 (Is/Can/Not) — never Both. A single-store realization “with Both” would be vacuous (the inconsistent-pair region is empty) and is refused as a statement. Both on shape membership arises only under federation (a default assertion + a standpoint refutation info-join to Both), which is the Liu-You inconsistent-pairs extension and is already carried by the two-layer composition (RFD 0069: L1 accumulate-Both / L2 resolve→K3, CompositionSoundness.lean). Federated shape membership is deferred (§2). This is the shape slice’s discharge of the “a Both leaking into the reasoner corrupts the fixpoint” landmine: the single-source guardrail is the non-corruption guard.

5. Guardrails (the floor — may tighten, not loosen)

  1. Per-individual groundedness on the shape head. Every classified variable is bound by a positive per-individual atom (the parent, or a typed shape-head param). An only-negatively-bound classified variable is not range-restricted → refuse OE1303 (acceptance case 4b refuse_ungrounded_head). Semantic content mechanized: a shape-defined individual is never even possibly a member unless already a possible member of the parent — membership derivation invents no individual (Appendix B L11; compile-time enforcement is the syntactic Compiled.CRule.Safe/Safe.head_determined).
  2. Aggregation stratified across classifier cycles. A shape whose body aggregates (forall/count) over the concept’s own extent has no well-founded fixpoint; the synthesized rule is folded into the build-time stratify set → refuse via the OE1317 extension (acceptance case 4a refuse_aggregation_cycle). The OE1317 refusal stands on the substrate (self-aggregating recursion has no well-founded fixpoint), not on the survey. As a design prior only: the design-record literature survey reports (medium confidence) that every deployed system stratifies aggregation w.r.t. the classifier cycle and the monotone carve-out is theory-only/unimplemented — corroborating, not load-bearing.
  3. Negation-through-classifier → Can + loud cycle diagnostic. Recursion through negation across classifiers is allowed (WFS gives it a model); membership surfaces Can. Argon additionally emits a loud cycle diagnostic — that loudness is Argon’s own silent-wrong-prevention norm (owned, §8), NOT inherited practice: Flora-2 merely surfaces undefined, it does not warn. We can allow what stratify-only systems forbid because Can is native to the envelope. Acceptance case 2 breach_fulfillment (asymmetric chain → definite; symmetric self-guard → Can) and case 5 cwa_owa_pair (Eligible/Blocked standoff → Can; ground breaker → definite).
  4. No existence invention in classifier heads. Membership derivation ≠ individual creation (guardrail 1 mechanization).

6. The #165 evaluation-unit constraint (BINDING)

Compositional/stratified evaluation of a program containing shape-defined concepts agrees with the whole-program well-founded model iff no well-founded-undefined (Can) membership atom crosses a split boundary into a position where the consumer projects Can→absent→Not. When a boundary atom is Can and the upper unit materializes only the lower unit’s definitely-true atoms as a completed EDB, the upper unit reads the Can atom as false and over-asserts (issue #165).

  • Disagreement is real (mechanized non-vacuity, Appendix B L8): for Plow = {p←¬q, q←¬p}, Pup = {r←¬p}, evaluating Pup over the materialized wfTrue(Plow) derives r as definitely-true, while the whole-program WFM leaves r undefined (Can). Stratified composition strictly over-asserts.
  • Agreement condition (assume-cited): compositional evaluation equals the whole-program WFM when every cross-boundary atom is two-valued in the whole-program WFM. This is the AFT-operator splitting theorem — Vennekens, Gilis & Denecker, Splitting an operator: algebraic modularity results for logics with fixpoint semantics, ACM TOCL (DOI 10.1145/1183278.1183284) — which applies directly because Argon’s wfFix is an AFT well-founded fixpoint; and, at the Datalog level, Ross, Modular stratification and magic sets for Datalog programs with negation, JACM 1994 (DOI 10.1145/195613.195646), whose modularly-stratified class (“makes every ground literal true or false”) is exactly the two-valued-boundary hypothesis.

Binding constraint. A Can-bearing shape concept and everything that (transitively, through the classifier) consumes it live in one WFS evaluation unit (one SCC-closure). Cross-unit materialization of derived membership is Can-faithful-or-refuse — never Can→absent. Two seams are bound:

  • The unified membership oracle (#1506) must carry the four-valued state (this is why the fix was one oracle, not a shape-only patch); a store-local evaluator refuses where membership is fixpoint-dependent rather than reading absence as Not.
  • The persisted-derived-read-model proposal (#1493) must be Can-faithful-or-refuse: materializing only wfTrue and letting a later read treat absence as Not re-creates #165 at the storage layer.

Perf. Defined-membership modules are IVM-ineligible (they already route to the slow rebuild path; strict_cache.rs treats any defined concept as delta-non-simple). Incremental WFS appears solved only in tabling form (Swift 2014); no set-oriented WFS-IVM is reported (design-record literature survey, self-flagged medium-confidence negative — not corroborated here). Working assumption: memoized recompute per undefined-bearing SCC. The #1493 proposal is the materialization complement, subject to the Can-faithful constraint above.

7. Decision 1 — construct count (DECIDED: two planes, one keyword, one marker)

Decision (ratified 2026-07-08): two evaluation planes, one keyword (iff), one compiler-enforced marker — a local decidable plane (iff { P }) and a fixpoint plane (iff ShapeName(self)), distinguished by a visible marker the compiler enforces, not two independent keywords. Strongest alternative (the field’s): uniform membership under WFS — Flora-2 / F-logic collapse the distinction, making every membership condition (including intrinsic per-individual conditions) an IDB predicate in one fixpoint, with no separate local fragment.

Argued against uniformity, from verified/mechanized sources:

  • K3-non-corruption — the realization theorem’s own basis. The mechanized single-source / OE0211 discipline (CwaOwa.lean; the design record’s paper realization argument) keeps classification K3-valued: a manually-asserted Is meeting a law-derived Not info-joins to Both, escaping K3 and corrupting the fixpoint. Two planes with separate sources (asserted where; derived iff/shape; no manual insert iof on a defined concept) preserve single-source. Uniform membership — one predicate both assertable and derivable — forfeits exactly the single-source guarantee that L7 and the realization theorem (§4) rest on. This is the strongest rebuttal, and it is mechanized, not stylistic.
  • Local decidability / polytime bound. Decidability/Fragment.lean + Complexity/Bounds.lean prove the local fragment bounded (d1EvalCost ≤ d1Size·(n+1)^depth, “locally determined by the root,” cost independent of N, evaluated at elaboration/write time). Uniform WFS membership makes every per-individual intrinsic check a global fixpoint atom, forfeiting the local bound and the write-time invariant enforcement (where invariants at flush, OE0668).
  • RFD 0017 containment. 0017’s vacuous-invalidation containment relies on the local fragment NOT coupling to the fixpoint (OE0660); its §Open flags “revisit if the fragment widens.” Uniform membership IS that widening — it forces transitive invalidation to be chased.
  • RFD 0060 delimited reflection is FORCED (decidability + sound mechanization); uniform (un-delimited) membership cuts against a committed decision.

Honest framing of the conclusion. These sources force two evaluation planes + a visible compiler-enforced marker — they do NOT by themselves force two surface keywords. The adopted syntax already reflects this: one keyword iff, the plane selected by the body shape. So the precise decision is two planes, one keyword, one marker, not “two forms.” The marker must be compiler-enforced and visible (OE0660 already refuses fixpoint-forms inside inline iff), so the plane a concept lives in — hence its cost class and whether its membership can be Can — is never silently changed by an edit.

True differentiator between the planes (correcting an earlier overclaim). It is NOT “inline iff never yields Can”: inline iff membership IS three-valued under #[world(open)] (RFD 0007; refinement.md). The differentiator is the SOURCE of Can — in the local plane it is open-world DATA ABSENCE (a missing field/relation under OWA); in the fixpoint plane it is FIXPOINT UNDEFINEDNESS (a well-founded-undefined SCC). Different provenance, different diagnostics, different perf.

(LOOM — production rules re-fused with classification, “death by complexity” — is at most an illustrative caution about hiding the plane boundary; a historical analogy, not evidence in this chain.)

8. Decision 2 — Can-membership dispatch on typed surfaces (DECIDED)

The #1450 prototype implements labeled-Can on all four read surfaces; its own design sketch says definite-only. These are not in conflict once the ENVELOPE is separated from the DECISION.

Decision (ratified 2026-07-08): the envelope is four-valued everywhere (never silently drop Can; hidden counts). Decisions requiring a definite type read definite-only. Two parts, distinct warrant:

  • THEOREM (sound): never promote CanIs. A surface requiring Is must not treat Can/Both as Is. Mechanized: cwaCollapse_is_iff (cwaCollapse t = .is ↔ t = .is) + collapse_conclusion_lacks_positive_evidence (CwaOwa.lean); RFD 0045-D1 (“derive is rejected as unsound, not declined”). The MKNF/Flora-2 undefined-tolerating-consumer restriction maps cleanly: a two-valued-requiring consumer restricts to the proven fragment.
  • POLICY (Argon house norm — owned, NOT a theorem): refuse Can LOUDLY rather than silently fail closed. The same mechanized chain that proves never-promote also models the silent K3 fail-closed reading: CwaOwa.member/memberOf return Bool, collapsing Can/Bothfalse with no signal — and that is mainstream practice (OWL open-world entailment, SHACL validation, SQL NULL 3-valued logic all fail closed silently). So silent-fail-closed is theorem-SANCTIONED; loudness is Argon’s silent-wrong-prevention norm layered on top. The alternative was weighed honestly — silent K3 fail-closed is simpler and matches mainstream tools — and the ratified choice is loud-refuse: a dropped Can is never invisible. The never-promote half is theorem; the loud-vs-silent half was the policy call, now decided.
SurfaceEnvelopeDecision on Can
query / derivelabeled-Can (+ hidden)read — no decision; projectK3:true gives the fail-closed view; whyNot needs CanNot
serve (/v1) / MCPlabeled-Can (+ hidden)read — no decision
dispatch (impl/method selection)consumes labeled envelopedefinite-only (theorem: never-promote); loud-refuse (ratified policy)
closed-world write gate (OE0258) / narrowingdefinite-only (theorem: never admit/narrow on Can); loud (ratified policy)
checks (Cat3 observers)report Canobserve, never fabricate

Caveat: #[world(open)] is a live attribute today, but OWA absence→Can relabeling for derived/shape membership is designed-not-wired (post-implementation). Acceptance case 5’s Can envelope is real today via ox derive; its per-world OWA relabeling of shape absence is not demonstrable pre-implementation (a non-member is currently absent, not Can, under #[world(open)]). Document per-case current behavior accordingly.

9. Migration from #1450 (evidence, never a merge base)

#1450 has the mechanism right (desugar to a concept-headed derive on the rule_head_concepts seam) but patches the classification↔catalog incoherence for the shape tag only, across 117 files, with no spec/Lean backing. Salvage by cherry-pick where this RFD agrees. The prerequisite membership-oracle unification is done — merged as #1506 (9f3bc30e2), #1500 closed: one membership oracle so classification_concepts/most_specific_types/dispatch/closed-world-gate/nav-seeding now see rule-head-concept membership that $meta/catalog/ox derive already served; the read-surface disagreement that had been live on main is fixed. The store-local evaluator refuses where membership is fixpoint-dependent.

10. Corrections folded in (Lean leads)

  • The design-record survey’s claim “the iff realization theorem is NOT mechanized (RP-007 §4.5)” is imprecise: Argon.TypeSystem.Realization.realize_iff (#166) already mechanizes the local-fragment realization at the abstract level (P : Ind → Prop opaque over the classification State). What remains open there is the value-environment-dependent local realization — and that IS correctly RP-007 §4.5. This RFD does not overturn that citation: RFD 0017 §61/§91 deliberately route the local iff realization theorem to the RP-007 §1.2-hazard-2 / §4.5 value-dependent sub-problem, and that routing stands.
  • What is genuinely distinct: the derive-backed realization (a shape body computed by the WFS/AFT fixpoint, not a value predicate over self’s fields) is a different open problem, homed in RP-008 (relational-extension / evaluation-level: “no RuleIR interpreter; Reasoning.State is meta-property only”). This RFD’s mechanization discharges it at the Datalog layer (Program Atom/wfFix), sidestepping RP-008’s missing-interpreter blocker for the classification-atom case. Net: RP-007 §4.5 = local value-dependent iff (RFD 0017’s, unchanged); RP-008 = derive-backed shape (this RFD).

Appendix A — acceptance corpus (verbatim motivating examples)

Held in the track design record at ratification; ships in-repo as the implementation’s acceptance suite (each case = ratified-pending target surface; each -runnable variant = the desugaring verified against the current toolchain):

  1. legally_capable — relational phase via shape; extent = {alice} (§1 flagship).
  2. breach_fulfillment — mutual-classifier recursion-through-negation; asymmetric→Is, symmetric loopCan.
  3. usc26_singletonspub fact statutory singletons + a downstream rule joining the shape-defined concept as a classification atom (the OE0226 TypeRef-parity regression class — must stay green). 4a. refuse_aggregation_cycle — OE1317 (aggregation through classifier cycle). 4b. refuse_ungrounded_head — OE1303 (ungrounded shape head — the genuine negative).
  4. cwa_owa_pair — CWA/OWA labeling; Eligible = {} Is + {pending} Can; §8 caveat applies.

Appendix B — scratch-mechanization statement ledger (never merged)

Verified against the built Argon lib (0 diagnostics, no sorry, no new axioms) at ratification; the scratch file is deliberately never merged — RP-008’s substrate mechanization re-homes what implementation needs. Status legend: PROVED-scratch / PROVED-substrate (cite) / ASSUME-cite / REFUSE.

#StatementStatus
L0wfFix P = (wfTrue P, wfPossible P) — realization is over the AFT joint operator on the product bilattice, not a hand-rolled alternating fixpointPROVED-substrate (AFT.wfFix_eq)
L1/L2WFS true-/possible-support lemmasPROVED-scratch
L3/L4single-source realization, is-/Can-regionPROVED-scratch
L5parent/body split = surface promise iof(x,C) ↔ iof(x,parent) ∧ shapeBodyPROVED-scratch
L6/S-K3K3 form wfModel C = KleeneAnd (wfModel parent) (wfModel shape)PROVED-scratch
L7single-store consistency wfTrue ⊆ wfPossible (no Both)PROVED-substrate (wfTrue_subset_wfPossible)
L8/S-165A#165 disagreement witness (compositional over-asserts)PROVED-scratch
L9/S-165Bagreement when boundary two-valuedASSUME-cite (Vennekens-Gilis-Denecker TOCL; Ross JACM 1994)
L10/S-NVCan/Is/Not witnesses (theorems non-vacuous)PROVED-scratch
L11/S-G1no existence inventionPROVED-scratch (+ Safe.head_determined cite)
single-store realization “with BothREFUSE — vacuous; Both is federation-only (RFD 0069), deferred
L12vacuity-refutation attestation clean on all PROVED theoremsattestation (run at packet close)

RFD 0071 — Agent-tooling distribution: the agents/ tree, the ~/.argon/.agents/ home, and oxup registration

  • State: committed (ratified 2026-07-09)
  • Provenance: transcribes owner-ratified decisions plus a precedent-traced survey of agent-ecosystem install conventions (per-host registration surfaces inspected empirically on live installs; installer precedents from shipped tools). A record of decided design, not a new decision.

Summary

Argon ships an MCP server (ox mcp) and a four-valued epistemic discipline for coding agents. This RFD records how that tooling is organized in-repo and installed onto a developer machine so that any supported agent host picks it up with no manual file copying and no repo accessoxup does it end to end.

Motivation

The initial per-host packages required a user to copy config + rules files by hand out of the repo. That is the wrong end state: users should not touch the repo, and installation must be idempotent, uninstallable, and upgrade-proof. The agent ecosystem has converged on conventions (a vendor-neutral skills home, per-host MCP config, the AGENTS.md rules standard) that a first-class installer should follow rather than reinvent.

Decisions

1. In-repo layout — top-level agents/

Agent tooling is host-neutral and lives at the top level in agents/, not under editors/ (which is for editor LSP clients). One package per host as peers — agents/{claude,codex,cursor,opencode,vscode}/ — plus the single source agents/argon-discipline.md. Claude Code is one host among peers, not special-cased outside the tree. Each host’s rules file is generated from the single source and drift-gated (cargo xtask gen / check-drift); no consumer is hand-mirrored.

2. Central artifact home — ~/.argon/.agents/

oxup owns ~/.argon/.agents/ as the canonical on-machine artifact home (skill body, rules texts, MCP fragments), regenerated wholesale on oxup update (the embedded-assets pattern already used for the Claude plugin). All host registrations point INTO this home, so an upgrade refreshes one place with zero re-registration.

The ecosystem’s vendor-neutral skills home is ~/.agents/skills/ (read natively by codex, cursor, opencode, and ~70 agents; symlink-by-default is the established install convention). oxup creates a single symlink ~/.agents/skills/argon -> ~/.argon/.agents/skills/argon. Claude Code does not yet read ~/.agents/skills and keeps its existing plugin path; it is not double-installed.

4. MCP registration — per host, host-native mechanism, PATH shim

The MCP command is always the PATH shim ox (ox mcp), never a versioned absolute path, so upgrades need no re-registration. Per host: use the host’s own registration CLI where it exists (codex codex mcp add; VS Code code --add-mcp when a genuine VS Code — not a fork — is detected); otherwise a parse-preserving merge of only Argon’s own key into the host’s config, preserving all foreign bytes (cursor ~/.cursor/mcp.json mcpServers.argon, which has no add CLI; opencode via its CLI or a parse-preserving JSONC merge, refusing loudly rather than corrupting a JSONC/symlinked config). Claude keeps the plugin’s own .mcp.json.

5. Rules — project vs global

Project mode appends a sentinel-delimited block to AGENTS.md (the de-facto cross-host standard: codex/cursor/opencode/Copilot all read it) plus the CLAUDE.md @AGENTS.md bridge for Claude (which reads only CLAUDE.md). Global mode is the default for a toolchain and prefers NO user-file writes — the discipline floor already rides the MCP tool descriptions + server instructions + the skill; if a global rules file is written at all, it is a sentinel block only, never a whole-file write (a whole-file write clobbers user content).

6. LSP per host — full parity, host limitations owned by the host

Feature parity per host is the requirement; no host is favored. Claude via its plugin .lsp.json; opencode via its config lsp.argon key (command:["ox","lsp"], extensions:[".ar"]) merged alongside MCP; Cursor and VS Code via the editor extension oxup already installs. codex has no LSP surface today — that is the host’s limitation, documented as such, with the MCP argon_check tool wired as the equivalent diagnostics channel; the cell is revisited as the host evolves.

7. Idempotency, receipts, uninstall

Plan→apply with merge-only-own-keys and foreign bytes preserved. A receipts file under ~/.argon/.agents/ records every touched host file so oxup agents unregister removes exactly those. Sentinel blocks for prose files; key-scoped merge for structured configs (never a sentinel inside JSON/TOML/JSONC). Pre-existing same-name user entries are respected. A non-interactive -y mode is provided.

Verification posture

Each cell is marked EMPIRICAL or DOCS-ONLY. Unverified lanes take the conservative verified path rather than blocking: VS Code --add-mcp fork semantics (DOCS — skip-with-message unless a genuine install is detected), opencode CLI write behavior (prefer parse-preserving merge / refuse loudly), and whether any Cursor surface reads ~/.cursor/rules/ (drop that target; rely on .cursor/rules/*.mdc project rules and AGENTS.md). These are tracked as open questions to close as installs become available, not gates on shipping.

Alternatives considered

A GUI one-click bundle (MCPB/.mcpb) and the remote MCP registry (server.json) are discovery/marketplace mechanisms, not local-install for a toolchain-owned stdio server; they do not replace per-host config writes. A single “write every host’s config” rewrite is rejected — it would clobber foreign content; merge-only-own-keys with receipts is the accepted practice across shipped installers (host-config mergers, the vendor-neutral skills installer, and the in-house Claude-plugin installer).

RFD 0072 — Persisted derived read-model: populating the .oxbin projection-cache section

  • State: discussion
  • Renumbered: originally allocated as 0070; renumbered to 0072 after main ratified the concurrently-allocated 0070 (derive-backed classification) and 0071 was claimed by the agent-tooling-distribution branch — the discussion-state RFD yields (the 0016 → 0017 precedent).
  • Opened: 2026-07-08
  • Decides: how a cold process — the CLI first among them — avoids re-deriving the entire reasoning model from an artifact that has not changed. Populates .oxbin section id 6 (projection-cache, reserved since the format’s first cut, inert ever since) with the post-fixpoint derived read-model (the serialized RelationCatalog) at build time, keyed on a derivation-input commitment (the already-shipped artifact_hash widened with the GlobalControl semantic fields, the mapping section, the accepted preamble axes, a derivation semantic epoch, and the payload schema version), consumed fail-closed by the strict read path — gated on self-built / explicitly-trusted provenance, foreign caches ignored loudly — with the module’s evaluation configuration restamped on load, and gated by the existing maintainer differential oracle plus new golden-anchored conformance entries. Amends the stale FORMAT.md §D.10 Cap’n Proto sketch to the shipped deterministic-CBOR codec (deterministic, not canonical — D1 states the distinction), with an explicit, justified artifact-tier deviation from RFD 0035 D7’s persisted-columnar runtime segment (D1). Section id 7 (arrangement-section) stays reserved; its specified OE1205 refusal is today defined but wired nowhere — making it real is an explicit obligation of this RFD (D1).
  • Built on: RFD 0018 (D4 CQRS event-log + persisted read model; D6 generation-driven invalidation; D7 arrangements), RFD 0021 (D7 “cross-query reuse needs the persisted read-model” — the open follow-on this RFD closes for the artifact tier), RFD 0033 (names the reserved-but-unpopulated projection cache as “a real forward arc”), RFD 0035 (D7 the columnar content-addressed segment + IVM maintainer is the primary read-model; D8 the maintainer proof obligation), RFD 0036 (D6 the mapping-artifact hash-pinned section pattern; D9 the runtime segment manifest), RFD 0053 (D6 the checkpoint-seeded maintainer, IncrementalMaintainer::from_model), RFD 0055 (the golden conformance corpus this must plug into), RFD 0062 (CI tiers the gates run in).
  • Grounded in: a measured baseline (2026-07-07, 96-core Linux dev box, release ox, perf stat -r 10 instruction counts ±0.00%, perf record -g call graphs) plus a four-track code recon with verified anchors. Numbers below are from that run; ratios, not absolutes, are the claim.

Question

ox query on a 23 KB example package retires 553 M instructions (~45 ms); the essential work — read ~60 KB, evaluate a handful of rules over a few hundred tuples, print rows — is on the order of 5 M. Every CLI invocation re-decodes the artifact, re-seeds a cold store, re-plans every rule, and re-runs the full stratified fixpoint, then exits and discards all of it (oxc-driver/src/lib.rs:1435-1442Store::new() with every cache field cold, oxc-runtime/src/write_gate.rs:46-64). The artifact did not change. Where should the derived state live so that work is proportional to the delta — and what is the invalidation and correctness story that lets a cache of reasoning results ship without reintroducing the silent-wrong-answer class (RFD 0055)?

Context

Measured cost structure. On examples/residential_lease_breach (the heaviest example cell): check 137 M / build 148 M / query 553 M instructions. The query call graph is ~40 %+ ciborium CBOR decode (Header::try_from 10.3 %, Decoder::pull ~19 % across monomorphizations, deserialize_struct + serde visitors ~9 %), ~15-20 % allocator traffic (malloc/cfree/realloc), ~5 % memcpy family — while the named reasoner/runtime code (evaluate_rule, reorder_body, stratify, build_views, …) is single digits. Decode-and-rederive dominates; reasoning does not. Eliminating translation alone caps at ~2.7× (Amdahl); the 10-100× headroom is in not re-deriving unchanged results. The engine’s own bench baseline quantifies the spring: full recompute tc_chains/100K ≈ 457 ms vs incremental_insert 37 µs — ~12,000× — and 4.5-5.7 s on the adversarial closure shapes (oxc-reasoning/benches/BASELINE.md).

What a cold read actually pays (verified anchors): read_oxbin slices sections (oxc-oxbin/src/reader.rs:65-105, GlobalControl is the only eager CBOR decode); Module::load eagerly decodes every event envelope + every decl body (oxc-runtime/src/module_load.rs:57,201-720), with a bytes→ciborium::Value→bytes→typed double round-trip per rule (helpers.rs:313-319, module_load.rs:497-504); Store::seed_from bulk-loads the log (store_init.rs:406-438); the first strict read misses model_cache and runs build_strict_maintainer = full EDB materialization + rule lowering/stratification/optimization + Engine::evaluate fixpoint (strict_cache.rs:138-169,486-498); every query body is re-lowered, re-stratified, re-optimized on every dispatch — no plan cache exists at any layer (federate_translate.rs:50-78, lib.rs:258-261). For a zero-mutation read of a v1-fragment module (undated, clock-free, non-temporal — D3) with no external providers registered (D4), all of §3’s bundle is a function of the artifact bytes plus the fixed evaluation configuration the D5 commitment carries (GlobalControl semantics, mapping, axes, evaluator epoch) — the qualified sense in which “the artifact determines the model” holds here.

The persistence machinery already exists — at the wrong tier for the CLI. The durable server path checkpoints the columnar read-model (ColumnarModel::to_bytes, serde-derived, oxc-reasoning/src/catalog/columnar.rs:33,159-171) into a content-addressed, integrity-checked segment (ReadModelSegment::seal, oxc-storage-durable/src/lib.rs:121-134) and warm-starts from it (oxc-runtime/src/persist.rs:179-249,275-346), installing a FrozenMaintainer placeholder whose first write drops the cache. RFD 0053 D6 added IncrementalMaintainer::from_model (maintainer/mod.rs:367-388): adopt an already-derived model without a fixpoint. The CLI touches none of this: Store::new() is pure in-memory, nothing is ever written back, and oxc-driver never calls write_oxbin.

The artifact slot exists and was engineered for exactly this. Section 6 projection-cache and section 7 arrangement-section are declared (oxc-oxbin/src/section.rs:42-44), carried as opaque Option<Vec<u8>> through reader/writer with automatic per-section BLAKE3 content hashes (writer.rs:172-177), mirrored in Lean as deliberately-abstract Option ByteArray (spec/lean/Argon/BuildArtifact/Oxbin.lean:65-66), and set to None by every producer (oxc-workspace/src/build.rs:602-603). GlobalControl.artifact_hash — BLAKE3 over symbol-table ∥ events ∥ standpoint-lattice ∥ tier-table — deliberately excludes the cache sections “so the hash stays stable across cache churn” (oxc-oxbin/src/content_hash.rs:41-64), proven by artifact_hash_ignores_cache_sections (writer.rs:348-365). The book already promises the exact consumer semantics: “Maintained projections; runtime re-saturates if absent” (spec/reference/src/execution/build/section-model.md:13) and blesses a DRedc-shaped section 6 beside a DBSP-shaped section 7 (versioning.md:18).

The spec sketch for the body is dead weight. FORMAT.md §D.10/§D.11 specify Cap’n Proto structs; no capnp dependency, no .capnp file, and no RFD deciding Cap’n Proto exist anywhere in the repo, and the sketch’s row-oriented Segment.rowBytes shape was superseded by RFD 0035 D7’s columnar decision. FORMAT.md self-describes as a mirror that “may lag the code” and demonstrably does (sections 10/11 undocumented; artifact_hash missing from its §D.4 key table). The newest shipped section — Mapping, id 11 (RFD 0036 D6) — is the live precedent for a compiled, content-addressed artifact section: self-contained module, deterministic CBOR via the crate codec, its own hash pins, a load-time drift refusal (OE1245) (oxc-oxbin/src/mapping.rs).

Why the artifact tier is the safe one. A 2026-07-07 adversarial test campaign against runtime-side decode-memo warm-starts (overlay/deep-clone seams, write_gate.rs:76-140) surfaced four distinct torn-snapshot interleavings (backend/memo snapshot gap; mid-rebind re-seed gap; durable-append tear; overlay rebind gap) — the stale-cache bug class in its natural habitat, live mutation. A build-time artifact section has none of those coordinates: ox build is a single writer over an immutable event set; the cache is written once, atomically, alongside the very bytes its validity key hashes.

Decision

D1 — Payload = the derived read-model as the serialized RelationCatalog, deterministic CBOR, in section 6. The projection-cache body carries the derived model of the built event log at the default scope as RelationCatalog::to_bytes — the codec that already exists, is documented deterministic and byte-round-tripping, and embeds the wire↔internal interning dictionary (catalog/mod.rs:434-460). Deterministic, not canonical, stated precisely: the codec serializes serde structures through ciborium in struct/BTreeMap order, which does not satisfy the repo’s canonical-CBOR key rule (encoded-key length, then lexical bytes — codec.rs:1-17; CatalogWireRef itself emits keys out of that order). Determinism is the property this design needs — identical models seal to identical bytes for the directory hash and reproducible builds — and canonicality is not: the payload is byte-hashed, never structurally re-hashed or key-order-compared. FORMAT.md §D.10 documents the section as deterministic-CBOR with exactly this note, so the format doc does not over-claim either. What the wire form does NOT carry, stated completely: it serializes only internal_to_wire + relations; decoding resets the world-assumption map and the metric resolution to defaults and per-tuple temporal intervals to empty (catalog/mod.rs:112,476-480). The first two are evaluation configuration fresh evaluation stamps from the module (catalog_seed.rs:20-25) — a cache-seeded catalog used without restoring them would evaluate open-world NAF under closed-world defaults, the exact silent-wrong class this design must exclude — so D4 mandates restamping the world map and metric resolution from the module immediately after decode, before any use; both inputs are inside the D5 commitment, so a module change that alters them invalidates the cache rather than being silently restamped over. Temporal intervals are model content, not configuration — they cannot be restamped — which is why D3 excludes metric-temporal programs from the v1 fragment outright: within the fragment the wire form loses nothing, so slice-2 from_model receives the full model and D6’s structural equality is well-defined.

Can-faithfulness (binding). RFD 0070 §6 binds this design by name: a well-founded-undefined (Can) membership atom must never cross a materialization boundary into a position where a consumer projects Can → absent → Not — “materializing only wfTrue and letting a later read treat absence as Not re-creates issue #165 at the storage layer.” The persisted catalog satisfies the constraint structurally: the engine’s well-founded bookkeeping rides inside relations as the $undefined:: / $assumed:: companion relations, so RelationCatalog::to_bytes persists the full three-valued state, the decoded catalog carries it back verbatim, and the serving view is built from that same catalog — a cache-served read reports exactly the Can extent a live evaluation reports, never a two-valued projection of it. The D6 differential arm pins whole-catalog equality (companions included) on every seeded corpus package, and D6 additionally requires an explicit round-trip test on a well-founded standoff, so the constraint is load-bearing in CI rather than an implementation accident.

The consumer rebuilds the columnar read view via to_columnar() on load (the cheap, decode-once direction); the catalog — not ColumnarModel — is persisted because ColumnarModel has no inverse conversion and IncrementalMaintainer::from_model — the eventual incremental upgrade — requires the full catalog. (That upgrade is deferred out of v1 entirely: from_model takes an EDB and a derived model whose exact relationship the caller must guarantee (maintainer/mod.rs:359-367), and the recovery code names re-interning a model against a reconstructed live EDB as a correctness hazard (persist.rs:328). The reconstruction contract — identical dictionary, EDB re-derived from module events, post-seed mutation differential coverage — is an Open Question, not a v1 promise; persisting the catalog now keeps the payload stable when it lands.) This is a deliberate, named deviation from RFD 0035 D7, whose persisted-columnar decision targets the runtime segment: at the artifact tier one payload must serve both slice-1 reads (via the rebuilt columnar view) and slice-2 from_model (which needs the catalog); persisting both doubles the section, and persisting only columnar makes from_model unreachable today. When a from-columnar maintainer seed exists, payload_version migrates the section to the columnar form — recorded as the reconciliation path, not left implicit. Wrapped in a self-contained payload type (mapping.rs Pattern B — own module in oxc-oxbin, not a sections/ row codec). No Cap’n Proto: the dependency was never decided, never vendored, and no consumer exists. FORMAT.md §D.10 is rewritten by this RFD to the shipped shape. §D.11 (section 7, DBSP Z-sets) stays reserved per versioning.md:18’s two-representations design — and this RFD wires the specified OE1205 refusal, which today is a defined-but-never-constructed diagnostic (error.rs:59-61; no validation layer examines arrangement_body): the loader gains a check that a present section-7 body raises OE1205, so the format’s stated MVP posture becomes enforced rather than aspirational.

D2 — Payload shape.

ProjectionCachePayload {
  payload_version: u32,            // independent of the four preamble axes; consumer gates on it
                                   //   AND commits it — the LAST commitment component (below)
  derivation_commitment: [u8; 32], // BLAKE3 over the COMPLETE derivation input (D5), encoded as a
                                   //   DOMAIN-SEPARATED, LENGTH-FRAMED tuple (length-framing per
                                   //   content_hash.rs; the domain tag is NEW here — the existing
                                   //   hash has no tag: a domain tag, then each component
                                   //   length-prefixed; Options framed with explicit presence
                                   //   tags — no bare `∥` concatenation, no ambiguity):
                                   //   artifact_hash
                                   //   ∥ the GlobalControl semantic fields, ENUMERATED — no
                                   //     ellipsis: default_world, metric_resolution_nanos,
                                   //     tzdb_version (adding a semantic field to GlobalControl
                                   //     REQUIRES adding it here — the same AGENTS.md review rule
                                   //     as the epoch)
                                   //   ∥ mapping_body-or-absent (placement worlds shift NAF
                                   //     semantics and are outside artifact_hash)
                                   //   ∥ ALL FOUR preamble axes (oxbin_format, core_ir,
                                   //     tier_ladder, runtime_contract) — the format axis is
                                   //     semantic too: a future additive section can reshape the
                                   //     producer's model while an older consumer still accepts
                                   //     the artifact (versioning.md:12's forward-minor
                                   //     acceptance is for EXECUTION; cache consumption requires
                                   //     exact format equality, gated in D4)
                                   //   ∥ DERIVATION_SEMANTIC_EPOCH — a new oxc-runtime constant
                                   //     (the crate that owns the WHOLE producer bundle:
                                   //     materialization + rule preparation + evaluation), bumped
                                   //     whenever any of those change semantics without a format
                                   //     axis bump. Lives above oxc-reasoning so a materialize/
                                   //     prepare change is inside the epoch, not beside it (D5).
                                   //   ∥ payload_version — framed LAST (the tuple evolves
                                   //     append-only). The version participates in what the
                                   //     consumer ACCEPTS: the producer commits the version it
                                   //     WRITES, the consumer the version it READS, so a
                                   //     wire-form bump invalidates by commitment as well as by
                                   //     the explicit D4 version gate — belt and gate, the same
                                   //     pattern as the format axis.
  seed_generation: (u64, u64),     // the post-seed storage_gen TUPLE (next event seq, event count) —
                                   //   the count alone is ABA-unsafe (append+forget returns to the
                                   //   same count; the tuple exists for this, strict_cache.rs:27)
  fragment_v1: {                   // build-time assertions of the v1-eligible fragment (D3); the
    dated: false,                  //   consumer re-derives each predicate from ITS module view and
    clock_reading: false,          //   cross-checks — a mismatch is an invalid payload, not a
    metric_temporal: false,        //   trusted claim (D4/D5)
  },
  scope: { tenant: DEFAULT, fork: DEFAULT, standpoint: default, read_point: Now },  // explicit, v1 fixed
  model: bytes,                    // RelationCatalog::to_bytes — dictionary + relations ONLY;
                                   //   world map + metric resolution are NOT in the wire form and
                                   //   are restamped from the module on load (D1/D4) — both are
                                   //   commitment inputs, so drift invalidates rather than restamps
}

Row counts and per-relation sizes are derivable from model; they are not duplicated. The section’s directory entry already carries BLAKE3 of the body (writer-automatic) — and D4 requires the consumer to actually verify it, which no reader does today for any section (reader.rs:65-105 slices without checking) — so the payload does not re-hash itself.

D3 — Producer: ox build, default on, with hard bounds and honest skips. After event assembly, build runs the same bundle the first cold read would run (materialize + pipeline_prepared_rules + Engine::evaluate, i.e. literally build_strict_maintainer over a store seeded from the built events) and serializes read_model()’s catalog into the section. Opt-out: ox build --no-derived-cache. Build skips emission (with an info-class diagnostic naming why) when any of the v1 fragment predicates fail, or the payload is over cap:

  • the module carries valid-time annotations (dated modules change model validity when the wall clock crosses an epoch with no storage change — the RFD 0063 invariant; v1 does not persist epochs, it declines them);
  • any rule reads the evaluation clock (today()/now: clock.rs:17 snapshots the host date per evaluation and eval.rs:1699 feeds it into rule bodies — a day-N model read on day N+1 would pass every D5 gate while carrying day-N clock-derived tuples; this is RFD 0063’s frozen-concrete-read-point contract applied to builds, and it also bounds the determinism claim below). Detection is static: the compiled rules name their builtin reads;
  • the program carries metric-temporal atoms (reuse the existing program_has_temporal predicate family): the catalog wire form excludes per-tuple temporal intervals (catalog/mod.rs:112; reset to empty on decode, catalog/mod.rs:476) and metric evaluation writes explicit intervals even for undated programs (temporal/executor.rs:420) — a temporal model cannot round-trip structurally, so v1 declines it rather than proving an interval normalization;
  • the encoded payload exceeds the v1 cap (min(64 MiB, 4 × events_body.len()), constants tunable by follow-up without an RFD). Build cost rises by exactly one cold-query fixpoint — work the artifact’s first consumer pays today anyway; amortization starts at the second read. Alongside emission, ox build records provenance for the D4 trust gate: it appends the BLAKE3 hash of the artifact file it wrote to the per-user trusted-artifacts ledger (D4). The record lives outside the artifact and outside the workspace, so it neither perturbs the artifact’s bytes (the reproducible-build property below is untouched) nor travels with a copied repository. Determinism, scoped honestly: within the v1 fragment (undated, clock-free, non-temporal) the model is a function of the event log together with the D5 commitment inputs — GlobalControl semantics, mapping, axes, epoch — all fixed at build time (the fixpoint is deterministic; clock reads are excluded by construction), and RelationCatalog::to_bytes is documented byte-deterministic (“an unchanged model always seals to the same content id”, catalog/mod.rs:440-446), so byte-identical inputs yield byte-identical sections — the reproducible-build property the rest of the artifact already has.

D4 — Consumer: the strict read path, fail-closed to recompute, with hard preconditions. ensure_strict_cache (strict_cache.rs:138-169) gains one arm ahead of build_strict_maintainer. The arm is taken only when all hold:

  • the store is in the pristine-seed state for this exact artifact. storage_gen equality alone is an invalidator within one store history, not a content identity: load_events accepts arbitrary events under their existing ids and its own contract permits an id rebind that leaves the (next_event_seq, event_count) pair unchanged (store_init.rs:410-438) — generation equality over different contents. The Store therefore gains an unforgeable pristine-seed marker: set exclusively by seed_from(module) (recording the seeding module’s artifact_hash), cleared permanently by every other ingest or mutation path (load_events, append_events_durable, append_events, retract_matching, any committed mutation — the clear sites are exactly the existing cache-reset chokepoints). The arm requires: marker present ∧ marker’s hash == this artifact’s artifact_hashstorage_gen == the payload’s seed_generation (the tuple now serving as a cheap cross-check, not the identity);
  • no foreign connector and no analytical provider is registered on the store: the strict maintainer otherwise materializes prefetched foreign rows and analytical output into the EDB (derive_read.rs:135-199, store_init.rs:105-174) that a build-time model cannot contain — serving the cache there would be a smaller, wrong closure. Registries-empty is the CLI/default shape; connector-bearing deployments fall through to today’s path untouched;
  • the module is inside the v1 fragment, re-derived on the consumer side (undated, clock-free, non-temporal — the D3 predicates) and consistent with the payload’s fragment_v1 assertions; a mismatch is an invalid payload;
  • for programs using zone-aware builtins (static detection, same discipline as the clock predicate): the consumer’s bundled tzdb version equals the artifact’s pinned tzdb_version (oxbin.rs:50 — the replay pin the artifact already carries). Zone-aware evaluation reads the consumer’s bundle (builtins.rs:247-288), so a newer bundle silently changes answers that the commitment — which hashes the pinned version on both sides — cannot see. This gate is mechanical, unlike the epoch discipline it complements; zone-free programs skip it;
  • the artifact’s oxbin_format_version equals the consumer’s own — forward-minor acceptance remains for artifact execution (versioning.md:12), but a newer format can carry additive semantic sections an older consumer cannot interpret, so cache consumption requires exact equality (the axis is also inside the commitment — belt and gate);
  • provenance admits it — the self-built trust gate, enforced by default. A cache section moves the artifact’s trust surface from auditable inputs (events — checkable by replay) to unauditable conclusions: a planted cache can serve rows the rules never derived, and nothing recomputes to notice. Consumption therefore defaults to self-built artifacts only, determined mechanically, never by path shape — workspace-path resolution is syntactic (it maps a package directory to an existing target/*.oxbin with no build, freshness, or producer-identity check, oxc-workspace/src/lib.rs:1581-1593), so it authenticates nothing and does not admit by itself. Self-built means one of:
    • same invocation: the artifact was built by this process (ox test / ox run-scenario building before reading; a build immediately consumed) — self-built by construction;
    • prior invocation: ox build appends the BLAKE3 hash of every artifact file it writes to a per-user trusted-artifacts ledger outside every workspace ($ARGON_TRUSTED_ARTIFACTS explicit path for tests/overrides, else $XDG_STATE_HOME/argon/trusted-artifacts, else $HOME/.local/state/argon/trusted-artifacts — the same dependency-free resolution the per-user config file already uses); a consumer hashes the artifact bytes it loaded and admits on ledger membership. The ledger lives outside the workspace deliberately: the threat is a cloned or written-into repository shipping a poisoned target/*.oxbin, and a co-located trust record would be forgeable by exactly the writer it gates. Entries are content hashes, not paths — a self-built artifact stays trusted if moved or copied within the user account; bytes built elsewhere are never trusted implicitly. If no ledger path resolves (no home directory), nothing is recorded and nothing admits — the fail-closed direction.
    • Environment variables are user authority — except when they point back into the workspace. The trust paths are env-resolvable by design (ARGON_TRUSTED_ARTIFACTS, XDG_STATE_HOME, ARGON_CONFIG, XDG_CONFIG_HOME, HOME), and repository-shipped machinery the user runs (a direnv .envrc, a Makefile, a .cargo/config.toml [env] table) can set them — so an env-resolved trust path that RESOLVES INSIDE the current workspace root would let a repository vouch for its own artifacts, re-opening the planted-cache hole through the environment. Such a grant is VOID and the refusal is LOUD (the same trust-refusal warning, naming the resolved path and the workspace root): a workspace-resident ledger is never read and the artifact classifies foreign; a workspace-resident per-user config loses its trust-derived-cache grant specifically, every other key of the file still applying. Containment is checked on the RESOLVED path (canonicalized, against the outermost ox.toml roots enclosing the invocation’s input and the working directory) regardless of which variable produced it, so redirecting XDG_STATE_HOME or HOME into the repository is the same refusal. Two honest limits: a repository carrying no ox.toml anywhere is not a workspace and anchors nothing (its artifacts already confer execution when operated on directly — the same trust posture as above); and a user whose home directory itself sits inside a workspace root will see every standing grant void, loudly — the loud direction, never the silent one. Trust can also be granted deliberately, when speed over the self-built guarantee is a chosen trade:
    • per load: --derived-cache trusts the named artifact’s cache for this invocation;
    • standing: trust-derived-cache = true under [cache] in the per-user config file ($ARGON_CONFIG, else $XDG_CONFIG_HOME/argon/config.toml, else $HOME/.config/argon/config.toml). The key is honored from the per-user config only — a workspace ox.toml must not self-authorize its own artifacts (a repository-controlled file granting trust to repository-controlled bytes would re-open the planted-cache hole). --no-derived-cache force-disables under everything above. A FOREIGN artifact’s present cache — admitted by none of the above — is ignored loudly: reads recompute from the event log (answers stay correct) and a warning-class diagnostic (OW1209, allocated via grammar.d/diagnostics/) names the ignored section and both opt-ins. Loud is the point: a trust refusal is a visible event, not a silent per-read performance cliff. Declines that are the user’s own request (--no-derived-cache) or a store-shape matter (non-pristine, registry-bearing, non-default scope) stay silent as before;
  • the section’s directory content_hash verifies against the body (accidental-corruption gate the reader currently never runs for any section), and the payload passes bounds enforced before materialization — mechanism, not aspiration: (1) the encoded-length gate (D3 cap) on the raw body; (2) a structural preflight over the payload bytes with the low-level CBOR decoder (ciborium-ll header walk — today only a transitive dependency via ciborium; the implementation declares it explicitly in oxc-oxbin’s Cargo.toml): validate declared dictionary / relation / per-relation row counts and cumulative declared byte lengths against fixed caps before any serde materialization allocates; only then RelationCatalog::from_bytes (catalog/mod.rs:460 today decodes generically — the preflight wraps it, it does not modify it);
  • every D5 validity gate passes. Then the arm seeds a ModelCache mirroring the durable warm path’s architecture precisely — stated precisely because the persist.rs:236-243 precedent is subtler than “frozen maintainer”: there the frozen placeholder wraps an empty catalog and reads are served from the separate columnar view. Here: decode the catalog, restamp the world-assumption map and metric resolution from the module (D1 — the wire form does not carry them; catalog_seed.rs:20-25 is the restamp source), build the columnar view from the restamped catalog as the serving surface, and install the frozen maintainer over the restamped catalog itself (not empty — it keeps the lockstep invariant meaningful and is the ready input for the deferred from_model upgrade). First ineligible write drops the cache and rebuilds exactly as today. (The from_model incremental upgrade is deferred out of v1 — see D1 and Open Questions; v1 is frozen-seed only.) Consumers, scoped honestly: every read that flows through ensure_strict_cacheox query, ox derive, ox test, ox run-scenario (decided here because D6’s golden gate runs exactly those commands; a cache the conformance gate never exercises would be an untested consumer). ox why is out of v1: converged_model_for_explain (explain.rs:846-889) deliberately re-materializes and re-evaluates for explanation fidelity and does not read this cache. An absent section is silent fall-through (the book’s “re-saturates if absent”). An invalid section (commitment mismatch, generation mismatch, hash mismatch, undecodable, over-bounds, version- or epoch-refused) is also fall-through-to-recompute — never served — but emits a new warning-class diagnostic (OE12xx, allocated via grammar.d/diagnostics/) so a corrupt cache is visible, not silent. Durable-backend (Postgres-backed) stores are out of v1 scope explicitly, and fail closed by construction: only the one-shot CLI read commands thread consumption consent (ox serve — the only --storage pg surface — never does), and the pristine-seed marker is set only by in-memory module seeding, so a pg-backed store never reaches this arm; the pg-backed seed watermark is untested and stays out of scope until a serve-side consumption design (Open Questions) provides its own provenance answer.

D5 — Validity = a complete derivation-input commitment, generation lockstep, and an explicit trust boundary. artifact_hash alone is not a sufficient key: the derived model also depends on GlobalControl semantic fields the evaluation catalog copies in (default_world, metric_resolution_nanos), on the mapping section’s placement worlds (outside the four hashed sections), on the accepted CoreIR/tier/runtime preamble axes, and on the semantics of the whole derivation bundle — not oxc-reasoning alone: build_strict_maintainer composes materialize_predicates + rule preparation + Engine::evaluate (strict_cache.rs:486-498), and a semantic change to any of them re-shapes the model with no format-axis bump. The epoch constant therefore lives in oxc-runtime (DERIVATION_SEMANTIC_EPOCH), the crate that owns that bundle. The consumer recomputes the D2 derivation_commitment from its own view — its recomputed Oxbin::computed_artifact_hash() (oxbin.rs:398-414), its decoded GlobalControl fields, its mapping_body, its accepted axes, its compiled-in epoch, and the payload schema version it ACCEPTS (the producer committed the version it WROTE, so a wire-form bump invalidates by commitment as well as by the explicit version gate) — and requires byte equality with the payload’s commitment; requires payload_version in range; requires seed_generation tuple equality (D4). Any failure → fail-closed recompute. The epoch’s honest limit: no in-repo test can detect a forgotten bump — CI builds and re-derives with the same binary, so both sides of any differential agree by construction (this falsifies the first draft’s claim that the corpus arm nets that omission). What ships instead: (a) a committed epoch-mismatch fixture (a payload stamped epoch−1 via a test-only constructor) proving the refusal path mechanically; (b) a review rule added to compiler/AGENTS.md in the implementation PR — any semantic change under materialize/prepare/evaluate must bump the epoch — making the omission a reviewable defect; (c) the residual risk recorded here as accepted, with mechanical epoch derivation in Open Questions. The events-sensitivity and cache-churn-invariance halves of the inner artifact_hash are already proven in-tree (writer_stamps_deterministic_artifact_hash, artifact_hash_ignores_cache_sections). The weak in-memory module_fingerprint (DefaultHasher, strict_cache.rs:103-122) is not persisted — compile/rule.rs:103-110 records why a 64-bit non-crypto hash must not be collision-load-bearing.

Trust boundary, stated plainly: the commitment authenticates consistency, not honesty — the payload and the hashes it is checked against travel in the same file and share one trust domain. A crafted artifact can pair a truthful event log with a well-formed, wrong model and a matching commitment; no in-file check can detect that, by construction. The cache therefore carries exactly the artifact’s trust level, no more: today a hostile artifact must lie in its inputs (events — auditable by replay), while a cache lets it lie in a conclusion while the inputs look clean. The boundary is therefore enforced by default, not merely documented (D4): consumption defaults ON only for artifacts this user’s own builds produced — same-process builds, plus prior builds recorded in the per-user trusted-artifacts ledger — never for bytes that merely sit at a workspace path; a foreign artifact’s cache is ignored loudly (OW1209) and reads recompute. Trust can be granted deliberately: --derived-cache for one load, the per-user-config trust-derived-cache key standing. Consumers who must audit a third-party artifact’s cache run ox verify-cache (D6), which re-derives the model from the event log and diffs it against the persisted section — the differential arm as an on-demand command, making conclusion-lies detectable without paying recompute on every read. Artifact signing/attestation is the complete answer and a separate arc this RFD does not open.

D6 — The oracle gate ships in the same arc, before the consumer defaults on. Per RFD 0035 D8 (“no optimization ships without the oracle”):

  1. A differential arm in the maintainer harness: a cache-seeded, restamped model (D4) must be RelationCatalog-equal to a from-scratch derivation over the same events — run across the real AUDIT_CORPUS examples (oxc-runtime/tests/examples_corpus.rs:2577-2739), closing RFD 0055’s open follow-up of registering reasoning-evaluable entries into the differential audit. The arm carries an eligibility filter with positive route assertions, and the filter is the production routing predicate itself, not a re-derivation: the implementation exposes the exact conjunction under which the D4 arm serves (the strict-read routing into ensure_strict_cache AND the fragment/provenance/pristine-seed gates) as a deliberately narrow #[doc(hidden)] pub audit APIpub(crate) alone is unreachable from tests/examples_corpus.rs, an integration-test crate; the doc-hidden export keeps the surface out of the documented API while letting the audit call the same function the consumer calls, so filter and consumer cannot drift. The arm asserts the cache route was actually taken for every package the predicate admits (a route counter, mirroring the audit’s existing audited/fellback discipline), asserts byte-identical fall-through behavior for every package it rejects, and requires every fall-through to be explained by the predicate — an arm that silently exercises zero packages is a failing gate, not a passing one. Route-away classes, corrected to measured reality (slice-4 execution evidence, superseding the v5 review’s static reading of strict_cache.rs:124): among the corpus, only the defeat-plane (defeasible) packages route away from the warm-cache read path; federated, modal, temporal, and standpoint strict reads DO flow through ensure_strict_cache. The audit therefore asserts per-class route outcomes as measured — defeat-plane: nonzero routed-away; each served class: nonzero served — and, separately and crucially, that temporal/clock-reading/dated packages are served but never cache-SEEDED (the D3 fragment gate is a seeding gate, one layer below routing). The arm also carries the Can-faithfulness round-trip (RFD 0070 §6): over a well-founded-standoff package (examples/robot_plan_execution — its symmetric conflict pair is well-founded-undefined, and the module is inside the v1 fragment and strict-monotone, so it genuinely cache-seeds), a dedicated named test asserts (a) the standoff atoms are undefined in live evaluation — the non-vacuity half, so the test cannot pass emptily if the package changes — and (b) the cache-served undefined extent (the $undefined:: companions) equals live evaluation’s exactly: the storage-layer regression RFD 0070 §6 names, pinned as its own test rather than implied by whole-catalog equality.
  2. Conformance-corpus entries (RFD 0055 gate, auto-discovered — and exercised for real because D4 makes ox test/ox run-scenario cache consumers): (a) a package whose goldens are answered through a cache-seeded load; (b) a poisoned-cache entry covering both undecodable corruption and a well-formed body with a mismatched commitment — goldens must still pass via re-saturation and diagnostics must show the D4 warning. The remaining adversarial case — well-formed, wrong model, matching commitment — is undetectable in-file by construction; it is covered by arm 1 in CI (cache-seeded ≡ recomputed over the corpus catches any producer bug that would emit it honestly) and by the D5 trust boundary operationally. The front-half placement is the point: this is the pipeline region where the nine historical silent-wrong bugs lived, and goldens are the only oracle class attached there.
  3. Fuzz: check_oxbin already fuzzes read_oxbin end-to-end; seed corpus gains cache-bearing artifacts, and the payload decoder gets structure-aware proptest cases in oxc-fuzz (the MAGIC_BYTES-prefix pattern) with the D4 size bounds asserted (over-bounds inputs must refuse before allocation).
  4. CI placement (RFD 0062 tiers, stated accurately): compiler-test and the corpus gates do not run on PRs — the PR tier is build/clippy/fmt/deny/drift only; they run in the merge queue (where ci-gate enforces) and post-merge. The differential arm, both conformance entries, the epoch-mismatch fixture, and the new unit suites therefore gate in the queue; no new PR-tier job is added (the PR tier’s build+clippy already covers compile health of the touched crates); libFuzzer stays nightly-only per the existing fuzz posture. Nothing new lands post-merge-only.
  5. The on-demand verify mode: ox verify-cache <input> loads an artifact (the trust gate deliberately does not apply — verification is how an untrusted cache becomes auditable), re-derives the model from the event log through the same producer bundle (materialize + rule preparation + evaluate), restamps per D4, and diffs the result against the decoded section-6 payload. Identical ⇒ exit 0; divergent or undecodable ⇒ nonzero exit naming the first divergent relation. This is the D6 differential arm as a command: conclusion-lies in a foreign artifact are detectable on demand without paying recompute on every read, and a CI job can pin any artifact population it chooses.

D7 — Format versioning: no axis bump. Populating section 6 does not move oxbin_format_version (it stays 3.1). The bump trigger for that axis is a section-model or container-encoding change (the versioning chapter’s rule: minor = additive section, major = breaking section/encoding), and neither happens here: section 6 has been declared in the section model since the format’s first cut, every 3.x reader already carries its body as opaque bytes under a directory entry, and the container encoding is untouched. The in-repo precedent is decisive in both directions: the 3.0 → 3.1 bump marked an additive field in GlobalControl — a structure every reader eagerly decodes — while the mapping section (id 11), a genuinely NEW section joining the model, shipped inside 3.1 with no bump; populating a pre-declared reserved section is strictly weaker than the mapping precedent. Consumer evolution of the payload is governed by the payload’s own payload_version (D2), which is both explicitly gated (D4) and inside the derivation commitment (D5) — and cache consumption additionally requires exact format-axis equality (D4) — so a bump would gate nothing on any side: it would be documentary only, and noisier than the repo’s own practice. The §18.5 acceptance predicate is unchanged; older runtimes carry the body as the opaque bytes they already carry today and never consume it.

D8 — Scope: default scope only, v1. (TenantId::DEFAULT, ForkId::DEFAULT, default standpoint, Now) — exactly what build-time knowledge determines and exactly what the CLI seeds. Every other coordinate (named tenants/forks, non-default standpoints, as_of reads, past-belief) falls through to today’s paths untouched. The runtime, multi-scope, post-mutation world remains RFD 0036 D9’s segment manifest; this RFD is the build-artifact tier of the same RFD 0018 D4 CQRS arc, not a competitor to it.

D9 — Docs ship in the same change (repo spec-first rule): FORMAT.md §D.10 rewritten (and its §D.3 “IDs 10..99 reserved” note corrected for shipped 10/11 while touched); the build chapter’s section-model row stands as written (it becomes true); appendix-c gains the D4 diagnostics (the invalid-section warning and the foreign-cache warning); CHANGELOG entry.

D10 — Explicitly out of scope, each a named follow-up rather than scope creep: mmap/FLAG_LAZY realization (aspirational for every section today — the reader byte-copies all of them, no mmap crate exists); the events-section decode-path fixes (the Value-tree double round-trip helpers.rs:313-319 and the per-call QueryDeclBody re-decode — additive wins that need no RFD); checkpointing JoinCache/DredcState (requires a rule-shape fingerprint that does not exist; first measure from_model’s recount cost); section 7 / DBSP Z-sets; a query-body plan cache (none exists at any layer — a separate, likely large win); the mem-backend serve path’s rebuild-per-request (#231-1 territory); multi-standpoint prematerialization.

Rationale

Work should be proportional to change. The artifact is immutable between builds; the model is a pure function of it and of the evaluation configuration (§Context anchors); therefore deriving it once, at the single-writer point, and validating by a content commitment is the minimal-machinery design — no clocks, no generation protocol. The commitment’s core (artifact_hash) exists, is stamped, and is tested for exactly the cache-exclusion property; the first-draft review of this RFD established that the full key must also commit the GlobalControl semantic fields, the mapping section, the accepted axes, and an evaluator semantic version (D5) — hence derivation_commitment rather than the bare hash — and the perf-slice review appended the payload schema version (D2): the version shapes what the consumer accepts, so it must invalidate by commitment, not only by the explicit gate. The payload reuses an existing, documented-deterministic codec (RelationCatalog::to_bytes, sibling of the durable checkpoint’s columnar seal), so the new surface is the placement, not the serialization. Choosing the artifact tier sidesteps the concurrency coordinates where a 2026-07-07 adversarial campaign showed stale-cache bugs actually live. Choosing CBOR over the FORMAT.md sketch follows both the strongest precedent (the newest shipped section) and the already-decided direction (RFD 0035 D7); adopting Cap’n Proto would be a new dependency decision with no RFD and no consumer, taken inside an unrelated change — exactly what the design-citation rule exists to prevent.

The honest limit is stated rather than papered over: a mutation-bearing session on a refinement-heavy module (iff concepts ⇒ not delta-simple) still drops the cache at its first commit and pays the fixpoint at the new generation — unchanged from today. What the persisted model buys is exactly the D4 scope, no more: (a) zero-mutation strict reads through ensure_strict_cacheox query/derive/test/run-scenario on v1-fragment, workspace-resolved artifacts (the dominant CLI and CI shape); (b) forward payload compatibility — the persisted catalog is already the exact input the deferred from_model upgrade needs, so no format migration when it lands (the upgrade itself is out of v1; Open Questions). ox why re-derives by design, --extent reads bypass the model cache entirely, and serve hydration is out of v1 (Open Questions) — none of those are claimed as consequences. Widening the eligibility rules is RFD 0018’s D6 arc, not this RFD.

Alternatives

  • Cap’n Proto per the FORMAT.md sketch. Rejected: undecided dependency, nonexistent schema file, row-oriented shape superseded by 0035 D7, zero precedent (every shipped section is CBOR).
  • A sidecar cache file (root.oxbin.cache). Rejected: the reserved slot exists; in-artifact placement inherits per-section hashing, the composition-signature discipline, single-file distribution, and the tested hash-exclusion property for free; a sidecar invents parallel invalidation and dirties every consumer that ships artifacts around.
  • Runtime/daemon-only answers (ox serve, a build daemon). Orthogonal and insufficient: the CLI one-shot (build/test/CI, agent tooling) is a primary product surface; the serve mem-backend rebuilds per request by design today; residency does not help the first read after a build.
  • Persist the maintainer’s hot state (arrangements + DRedc counters) now. Premature: needs a rule-shape fingerprint (JoinCache keys are positions into a specific optimizer output) and a dictionary-consistency treatment; from_model may already make it unnecessary. Measure first (D10).
  • Do nothing / rely on OS page cache. The page cache preserves bytes, not derivations; the 553 M instructions are paid warm.

Consequences

  • ox build runs one fixpoint it previously deferred to the first reader; artifacts grow by the serialized catalog, bounded by the D3 cap (dated modules and over-cap payloads skip with a diagnostic — those artifacts behave exactly as today).
  • Zero-mutation strict CLI reads on the v1 fragment skip Engine::evaluate, rule replanning, and the model’s share of decode; with D10’s decode follow-ups this is the 10× CLI arc, and with residency the 100× interactive arc. Connector-bearing, dated, clock-reading, and metric-temporal modules see no change until follow-up work extends the fragment (each exclusion is a named predicate with a named upgrade path, not an implicit gap).
  • A new payload type joins the compatibility surface, governed by payload_version + minor format bump; old readers are bit-compatible. DERIVATION_SEMANTIC_EPOCH becomes a discipline with an accepted residual risk (D5): a semantic change that forgets the bump can serve a stale-but-committed model, and no same-binary CI differential can catch that by construction. The mitigations are the committed epoch-mismatch refusal fixture, the compiler/AGENTS.md review rule, and this sentence — not a test that cannot exist.
  • The differential harness and conformance corpus grow permanent entries that outlive this feature — any future derived-state persistence (section 7, maintainer state) inherits the gate.
  • FORMAT.md sheds a fictional dependency and matches shipped reality for the sections this RFD touches; section 7’s OE1205 refusal goes from prose to code.
  • A crafted artifact can carry a lying model behind a matching commitment (D5): cache consumption is a trust-domain decision enforced by a self-built default — same-process builds and the per-user trusted-artifacts ledger admit; a foreign artifact’s cache is ignored loudly with recompute unless explicitly opted in — with ox verify-cache as the on-demand audit for caches trust alone cannot vouch for.
  • ox why keeps re-deriving in v1 (explanation fidelity); its latency is unchanged.

Open questions

  1. Cap values. Are min(64 MiB, 4 × events_body) and the decode-side count caps (D3/D4) the right v1 constants? (Constant tuning is follow-up work, not an RFD; sharded/streaming segments remain 0036 D9 territory.)
  2. The from_model upgrade (deferred out of v1). Two obligations before it lands: (a) the EDB/dictionary reconstruction contract — from_model takes an EDB and a model whose exact relationship the caller guarantees (maintainer/mod.rs:359-367); the EDB must be re-derived from the module’s events against the payload’s dictionary without the re-interning hazard persist.rs:328 names, and the differential arm must gain post-seed mutation coverage (apply deltas, compare against recompute), not just seeded-model equality; (b) the cost question — is the recount (DredcState rebuild + arrangement warm-up) cheap enough to beat frozen-first? Measure on the bench baseline shapes.
  3. Dated modules. v1 declines them (D3, the RFD 0063 epoch invariant). The upgrade is persisting the model’s validity epoch (valid_until frontier) and gating consumption on the read clock falling inside it — worth doing only with evidence dated modules are a hot CLI shape.
  4. ox build self-reuse. Build’s own internal load repetitions (check/discharge/encode) could consume the model they just derived — measure whether that’s worth the plumbing.
  5. Mechanical epoch derivation. Deriving DERIVATION_SEMANTIC_EPOCH from the build itself (e.g. a hash of the derivation-relevant crate sources or of a semantic-vector fixture output) would close the forgotten-bump residual (D5); over-invalidation cost vs. review-rule reliability is the trade to study.
  6. Serve-side consumption. oxc-serve/RuntimeService hydration from the artifact cache (generation-matched cold starts) is deliberately out of v1; it needs its own provenance answer for uploaded artifacts before defaulting on.
  7. Housekeeping surfaced during recon, tracked separately: RFD 0063 is double-allocated (0063-bitemporal-read-point.md indexed; 0063-datalogmtl-temporal-engine.md orphaned from README/SUMMARY); FORMAT.md carries further drift beyond §D.10 (missing §D.4 artifact_hash key, stale appendix cross-references); read_oxbin verifies no section directory hash today (D4 fixes it only for section 6 — a general verification pass is a small hardening follow-up). None block this RFD; all deserve their own PRs.

RFD 0076 — Mutability of relation ends

  • State: discussion
  • Note: landing as a staged design record; enforcement arrives in the check, write, and durable slices — canonical spec text describing this rule is not provisional.
  • Opened: 2026-07-12
  • Decides: whether relation ends adopt the field-mutability rule — immutable unless declared mut — and what the write path, tuple lifecycle, and reflection plane must enforce for an immutable end.
  • Affects: relation declaration syntax; the write-path gate; the reflection plane; the ArgUFO vocabulary package (arg_ufo/relations.ar, arg_ufo/metarels.ar — external to this repository)

Summary

Argon fields are immutable post-construction unless their declaration places mut before the field name. This RFD applies the same rule to relation ends:

rel-param ::= 'mut'? Ident ':' TypeExpr

Every relation end is immutable unless it is declared mut. The modifier is written immediately before the end name, not after its type:

pub rel bindsSpouse(mut marriage: Marriage, spouse: Person) [0..1] [2];

Here marriage is mutable and spouse is immutable. There is no immutable keyword, [immutable] suffix, or relation-wide immutable modifier; the absence of mut is the immutable spelling.

The analogy with properties stops at the declaration posture. Relation ends are not assignable fields: Argon never updates the individuals in an asserted tuple. A change retracts the old relation assertion and inserts a new one. Under this proposal, if an end is immutable, the old assertion cannot be retracted while its dependent individual remains; retracting that dependent would automatically retract the relation assertion.

Problem

UFO makes strong modal claims about certain relation bindings, and ArgUFO can currently only document them:

  • Existential dependence. Relators, modes, and qualities existentially depend on the individuals they bind. The binding is rigid: an aspect inheres in exactly one bearer for its whole existence. A specific headache that inheres in Alice cannot be rebound to Bob; a marriage mediates the same spouses for as long as it exists. The [1] bracket on InheresIn’s bearer caps the count (OE1341), not its stability: delete the edge to Alice and insert one to Bob and the cap is never exceeded. The immutable-end gate must refuse retraction of the Alice tuple while the aspect still exists. The legal way to end the binding is to retract the dependent aspect, which cascades the relation retraction.

  • Essential parts. A person cannot continue as the same individual after replacing their brain. A direct retraction of hasBrain(person, brain) would leave that replacement path open. With an immutable brain end, the person must be retracted first; the parthood assertion then disappears by cascade.

  • Immutable perdurants. metarels.ar repeatedly notes that events are immutable and exist only once they have occurred. The event-part relations (participational, structural, temporal) and the event-linking relations (creation, termination) should therefore keep their asserted endpoint bindings stable.

The write path currently accepts direct tuple retractions and replacements that violate those constraints.

Motivation

Relation-end mutability should follow the field-mutability rule rather than introduce a second modifier system:

DeclarationDefaultOpt-in
Fieldfield: T is immutablemut field: T admits update
Relation endend: T is lifetime-boundmut end: T permits retract/insert replacement

The immutable default is also the safer ontology-authoring default. Stable bindings need no annotation, while a contingent binding is visible at its declaration site. It gives the write path a declaration-level stability bit that cardinality alone cannot provide. Relations retain their tuple semantics: mut controls whether assertion history may vary at an end; it does not make that end an assignable location.

Scope / non-goals

  • Covers asserted, subsumed, and rule-derived extent changes for declared relations: logical tuple retraction, replacement by a distinct assertion, and constrained cascade retraction when a dependent context ends. It does not cover pure derive/query heads, tuple ordering, or uniqueness.
  • Does not introduce immutable, const, [immutable], or a relation-wide mutability modifier. Absence of mut is sufficient.
  • Does not introduce relation-end update or stable relation-binding identity.
  • Specifies the required individual-retraction cascade. The source spelling for requesting a RetractIndividuals set is decided: retract x; for a single individual and retract {x, y}; for a set, lowering to the kernel RetractIndividuals effect. The source forms are documented in the mutate chapter (spec/reference/src/declarations/rules/mutate.md).
  • Initially enforces lifetime semantics only for identity-bearing dependent contexts. An unsupported primordial or reference-valued context is rejected unless the corresponding end is mut.
  • Keeps transaction-time correction distinct from ordinary relation deletion. Its surface and authorization are specified here (the amendment section: amend, capability-gated); its enforcement is a later implementation slice.
  • Enforces the lifetime-bound relation needed by essential parthood; other ontology-specific consequences of termination remain separate constraints.
  • Does not decide relation-body field mutability or its update surface.

Proposed solution

Four coordinated pieces: syntax, tuple-lifecycle semantics, enforcement, and reflection.

1. Syntax — mut before the relation-end name

Extend the existing relation parameter grammar with an optional mut before the identifier:

rel-param ::= 'mut'? Ident ':' TypeExpr

Examples:

// A material relation. People may work for different organizations throughout
// their lives; organizations may have different employees over time.
pub rel worksFor(
    mut employee: Person,
    mut employer: Organization
) [0..*] [0..1];

// A mediation relation. A particular marriage cannot involve different
// spouses, while a person may participate in different marriages throughout
// their life.
pub rel bindsSpouse(
    mut marriage: Marriage,
    spouse: Person
) [0..1] [2];

// An essential-parthood relation. A person cannot have a different brain.
pub rel hasBrain(
    mut person: Person,
    brain: Brain
) [0..1] [1];

hasBrain illustrates the immutable binding of a person to a particular brain. Under this proposal, hasBrain(person, brain) cannot be retracted directly while person exists; retracting person would automatically retract the relation assertion.

The ordering matches field declarations: if relation-end attributes are added later, their order is attribute* mut? Ident. This RFD does not introduce relation-end attributes.

Mutability is declared on concrete relation ends. This RFD does not add mut to metarel signatures or define metarel-level inheritance. A vocabulary that requires a position to remain immutable can reject a concrete relation’s mut opt-out through the reflection check below.

Ordinary relation subsumption does constrain mutability. A child relation may tighten a parent mut end by omitting mut, but it may not weaken an immutable parent end. Effective mutability at position i is therefore conjunctive over the relation and every transitive superrelation at that position. Declaring mut where any applicable parent is immutable is rejected with a RelationEndMutabilityWeakening diagnostic.

Relation-body field declarations are syntactically unchanged. Their mutation semantics remain the separate relation-tuple-field question reserved by Argon RFD 0006; endpoint mut does not decide it.

2. Tuple lifecycle — retract, then insert

A relation assertion is an immutable proposition. Its endpoint values are never updated in place, and this RFD adds no relation-target update form. A logical relation change uses the existing tuple operations:

pub mutate changeEmployer(
    employee: Person,
    currentEmployer: Organization,
    newEmployer: Organization
) {
    delete worksFor(employee, currentEmployer);
    insert worksFor(employee, newEmployer);
}

Surface delete R(args) retracts every live assertion matching that exact tuple proposition; insert R(args) appends a separate assertion. The two operations may be atomic members of one mutation, but they do not preserve or update a relation-instance identity.

Mutability is interpreted positionally, using the same fixed-complement rule as cardinality. For endpoint i, hold every other endpoint fixed. Those other arguments form the dependent context for the value at i:

A dependent context is live while all of its endpoint individuals exist. For each endpoint and live context, consider the set of asserted values at that endpoint:

  • if endpoint i is mut, that set may grow or shrink while the dependent context survives, provided the transition satisfies every other end’s lifetime gate;
  • if endpoint i is unmarked, its complete value set is established atomically by the first transaction that asserts that endpoint fiber. Every value for the fiber must be present in that initialization transaction; after commit, the set cannot grow, shrink, or substitute members while the dependent context survives;
  • the initialization set remains governed by the declared cardinality. The two immutable spouse tuples required by bindsSpouse(...)[2] must therefore be asserted together when that marriage binding is initialized.

For a binary relation, the dependent context is the individual at the opposite end. Therefore:

  • both ends of worksFor are mut, so the old tuple may be retracted directly before the new tuple is inserted;
  • spouse in bindsSpouse is immutable, so Marriage is the dependent individual. Retract the marriage first; its bindsSpouse tuples are then retracted automatically. The mut marriage end permits a surviving person to participate in another marriage later;
  • brain in hasBrain is immutable, so Person is the dependent individual. Retract the person first; hasBrain(person, brain) then retracts by cascade.

For an n-ary relation, the fixed complement contains more than one individual; mut alone does not designate one distinguished owner among them. The complement is treated as the dependent context collectively, and that context ceases when any one of its individual members is retracted. A vocabulary that requires one distinguished dependent must declare that role separately.

This lifecycle rule initially applies only when every member of the dependent context is an identity-bearing individual. Primordial values and references have no individual lifetime to retract. Until a separate rule exists, a declaration that leaves an end immutable when its dependent context contains such a value is rejected with an ImmutableRelationEndNonIndividualContext diagnostic; the modeler must mark that end mut.

3. Enforcement — retraction and replacement gates

The write path enforces the declaration in three places:

  1. Explicit tuple retraction. First locate the exact live tuple named by delete R(args). If no tuple matches, deletion remains an idempotent no-op. If a tuple matches and any endpoint is unmarked, reject direct deletion with a newly allocated ImmutableRelationEndRetraction diagnostic. Because deleting one tuple removes one value from every endpoint fiber, direct tuple retraction is admitted only when every end is mut.
  2. Tuple assertion. insert R(args) continues through the existing existence, endpoint-type, and cardinality gates. For an uninitialized immutable fiber, all values inserted for that fiber in the transaction form its initial frozen set. For an initialized immutable fiber, inserting an already-active tuple remains idempotent, but a new value is rejected with an ImmutableRelationEndInsertion diagnostic.
  3. Individual retraction. Before applying effects, collect the transaction’s complete set of logical individual retractions and validate it atomically. For every incident tuple and every unmarked endpoint, at least one individual in that endpoint’s dependent context must belong to the set. Otherwise the transaction is refused: retracting brain cannot leave its dependent person alive, and retracting spouse cannot leave its dependent marriage alive. When the condition holds, incident relation facts retract by cascade.
  4. Subsumed and derived extent deltas. After relation-subsumption closure and dependency maintenance compute the transaction’s net extent delta, apply the same initialization and retraction gates to every affected declared relation. This includes parent-relation rows contributed by a child and facts derived by rules whose head extends a declared relation. A premise retraction that would make such an immutable derived binding disappear while its dependent context survives refuses the whole transaction; declared relation heads do not bypass mutability merely because no source delete names them directly. The gate has no plane exemption: a standpoint’s composed view — its own facts and rules together with the DEFAULT layer that restricts into every view — is an extent of the declared relation, so a premise write that varies an immutable derived binding in any standpoint’s view refuses exactly as one that varies the base view, whichever plane owns the rule and whichever owns the premise. The freeze witness reads each plane’s own retained assert-polarity history (the DEFAULT layer for the base view, DEFAULT plus the standpoint’s layer for a scoped view): a scoped fiber initialized by a scoped assertion is frozen by that assertion, not deferred with the derived-only residue. (The cardinality gate set the plane precedent — a scoped tuple counts toward a declared cap; gate 1’s base-plane delete match under “Compatibility and migration” is a plane-locality rule for locating the named assertion, not a mutability exemption.) Pure pub derive or query heads with no relation declaration have no end-mutability metadata and remain outside this RFD.

The third rule is conjunctive across all immutable ends. For binary R(a, b) with both ends immutable, retracting only a is insufficient because b is still the dependent context for the immutable a end; both participants must be in the same logical retraction set. For n-ary relations, retracting any one member makes that particular dependent context non-live, but every other immutable-end context must still pass independently.

This closes the delete-then-insert loophole. A direct delete is refused for any tuple with an immutable end. The only legal removal is a cascade whose dependent-context closure passes, after which the removed individual cannot be used as an endpoint of a replacement assertion. (One bounded residue of this no-reuse guarantee — a fiber initialized only through the derived plane, re-derived after identity resurrection — is deliberately deferred and pinned; see “The erasure channel is gated, and one bounded residue is deferred” below.)

Individual retraction and cascade are implementation prerequisites

Argon currently has no ordinary logical individual-retraction statement. delete iof(x, T) only declassifies x from T, while forget x physically erases history; neither is the lifecycle operation required here. This RFD introduces a kernel RetractIndividuals effect over a finite set of identities; a singular lifecycle request lowers to a singleton set. After the dependency-closure gate above succeeds, it atomically appends retractions for every live identity-bearing base event owned by or incident to each selected individual: positive and refuted classifications, individual-property assertions and individual-valued property references, and positive and refuted relation tuples. Normal dependency maintenance then revises derived facts. History is retained; after the operation, no live event introduces or references a retracted individual.

The source spelling of RetractIndividuals is retract x; (single) and retract {x, y}; (set), lowering to the kernel effect; its gates are part of this proposal. Relation-end mutability cannot ship until that operation and its constrained cascade are implemented; neither delete iof nor forget may stand in for it.

No stable relation-binding identifier is required. The old and new tuples remain distinct propositions identified by their relation and arguments.

The erasure channel is gated, and one bounded residue is deferred

forget remains the privileged physical-erasure channel (build-time capability, separate authorization), not a lifecycle operation — but at transaction time the binding an erasure would remove is still live, so the erasure operation runs the same dependent-context coverage as RetractIndividuals over its target’s incident tuples: erasing a value-side individual while the dependent context survives is refused exactly as the cascade would be. forget therefore cannot stand in for a refused cascade. Erasing the dependent itself remains legal and takes its incident bindings with it, mirroring the cascade direction this section prescribes.

The freeze witnesses read retained assert-polarity history, and a committed erasure retains none: after forget x, no assert-polarity event on any plane or polarity introduces or references x (relation arguments walked into nested collection values), and the target’s pre-existing closing retractions are erased with their asserts. Physical erasure is that gate-and-read-visible guarantee, and its safeguard is the authorization boundary, not the lifecycle gates. Two replay artifacts are retained by design and are pinned executably in the runtime write-gate suite: each erased LIVE event leaves one freshly-minted extent-closing retraction event (a body clone whose closed assert no longer exists — the receipt the durable-replay journal needs, so a reopened store drops the re-added assert instead of resurrecting it), and the durable journal itself retains the erased events’ history closed rather than expunged (replay re-adds, then drops). Neither artifact is served by a read or consulted by a freeze witness.

Everything the substrate holds is bitemporal except what forget has touched, and this is the erasure contract on two layers. At the query surface forget destroys its target’s bitemporality: the axiom events and their bitemporal history are expunged from the served store, so an as_of reconstruction over the erased identity returns nothing. This is the point of the channel, not a shortfall — right-to-erasure obligations require that historical reconstructions cease to answer, which a bitemporal retraction cannot satisfy: RetractIndividuals records an ordinary closing event in the bitemporal log, so after retract x an as_of query still reconstructs x’s pre-cessation belief state in full. The two verbs share the same coverage gate and differ exactly here — logical cessation preserves the reconstructable trail, physical erasure removes it. At the storage substrate the durable-replay journal retains the erased bytes closed alongside the tombstone as an append-only-durability necessity, not as queryable history: no query or freeze-witness path reaches them, and replay consults the tombstone to re-erase so recovery is deterministic. A regime demanding physical destruction of even those closed bytes is a storage-lifecycle / compaction concern outside language semantics.

One bounded residue follows and is deliberately deferred to the follow-on lifecycle-surface / richer- forget proposal rather than approximated unsoundly: a fiber initialized only through the derived plane keeps no asserted witness in its own relation’s subsumption closure once its premises are cascade-retracted, so re-classifying the retracted identity and re-asserting a premise re-derives the immutable binding around a different value without refusal. An exact gate needs historical rule re-evaluation (or an identity-retirement rule for logically-retracted individuals, itself a lifecycle-surface decision). The residue is pinned by an executable ledger test in the runtime write-gate suite (oxc-runtime’s relation-mutability laundering tests) so the boundary cannot silently move in either direction. The runtime pin splices the kernel operation directly; a corpus twin using the retract x; / retract {x, y}; source spelling follows as the lifecycle surface matures.

Relatedly, the resurrection gate’s relation-tuple-endpoint arm (an individual retracted this transaction that a surviving tuple still names at an endpoint) has no end-to-end corpus twin: the endpoint-existence floor (OE0232) refuses a tuple over a non-existent endpoint before the net-view gate could ever observe one, so the shape is unreachable except by invoking the gate directly. The arm is therefore pinned by a direct-invocation runtime test as a net-view backstop rather than a source-level scenario, alongside the laundering residue above.

Transaction-time correction is a separate channel

Ordinary delete R(args) always follows the lifecycle gates above; it carries no correction exemption. A mis-recorded immutable assertion may be superseded only through a distinct, privileged transaction-time correction channel. Immutable-fiber initialization and retraction gates read the corrected transaction-time view, so an assertion superseded through that channel does not freeze an erroneous value; ordinary source deletion cannot masquerade as correction. That channel is the amendment operation specified in section 5 below.

4. Reflection — expose the effective is_mut bit

Expose a total, catalog-sorted atom for each declared relation end:

armMutability(relation: TypeRef, index: Nat, is_mut: Bool)

Every end contributes one row. An unmarked end contributes false; a mut end contributes true only when every transitive superrelation also permits mutation at that position. This shares the reflection-plane extension proposed in RFD 0075 (metarel cardinality reflection — the sibling relation-end reflection record, in review concurrently), since both are per-end declaration facts. The bit authorizes value variation through tuple retract/assert history; it does not advertise an endpoint-update operation.

With the immutable default, ArgUFO audits forbidden opt-outs rather than requiring explicit immutable annotations:

#[static]
pub check MutableCharacterizationBearer(relation: TypeRef) :-
    meta(relation) == characterization,
    armMutability(relation, 1, true)
    => Diagnostic {
        severity: Severity::Error,
        code: "ArgUFO::E_MutableCharacterizationBearer",
        message: "The bearer end of a characterization must not be declared mut.",
    };

The storage and wire representation should likewise carry a per-end is_mut boolean, parallel to the existing field mutability flag.

5. Amendment — correcting the record at belief time

Immutability protects two different things that the write path had conflated: the world cannot rebind an immutable end, and the record of what was asserted cannot be silently rewritten. The lifecycle gates above enforce the first. But a binding may be mis-recorded — asserted in error, false when it was made — and the modeler must be able to correct the record without pretending the world changed.

The two exits are distinct:

  • World-exit is a cascade. The binding was true and has ended because a participant ceased; RetractIndividuals retracts the dependent context and the incident tuples fall away. History retains the binding (it was true once), so the freeze witness keeps the fiber initialized and the vacated value can never be reused.
  • Record-exit is an amendment. The binding was never true; the assertion was false ab initio. Withdrawing it releases the freeze contribution it should never have made, so the corrected value is admitted where the frozen fiber would otherwise refuse it. Bitemporal history retains what-was-believed-when — the correction is auditable, not an erasure.

Concretely, with bornTo(mut child, mother) (the mother end immutable) and a child B first recorded as born to A, later known to be born to C:

pub mutate correct_birth_mother(child: Person, wrong: Person, right: Person) {
    // The record was wrong from the start: B was never born to A.
    amend bornTo(child, wrong) => bornTo(child, right);
}

Today this is impossible: delete bornTo(B, A) refuses (OE1402, immutable end), insert bornTo(B, C) refuses (OE1403, the mother fiber froze at {A}), and the only escape — RetractIndividuals{B} and full re-entry — cascades away every binding B participates in. Amendment is the missing narrow tool.

Syntax — one verb, two forms

Amendment adds a single reserved keyword, amend, with a primitive form and an ergonomic composite:

amend-stmt ::= 'amend' predicate-call ';'                          // withdrawal (truth unknown)
            |  'amend' predicate-call '=>' predicate-call ';'      // atomic withdraw + correct
  • amend R(args); — the primitive. Withdraw the assertion as false ab initio when the true value is not (yet) known. It withdraws positive evidence: the proposition returns to unknown, not refuted.
  • amend R(args) => R(args'); — the composite. Withdraw and assert the corrected proposition as one atomic operation validated against a single transaction-time view. The => makes the atomic old-becomes-new intent syntactically evident. It is exactly the primitive followed by a corrected insert, fused so the two cannot separate.

amend is deliberately a distinct verb from delete: delete is the valid-time ender (the binding stops being true now), amend is the belief-time corrector (the binding was never true). Conflating them — the whole hazard this section closes — must not be possible at the surface.

Alternatives considered
OptionReads like the domainUn-confusable with deleteGreppableParser fitComposite atomicity evident
amend R(a); + amend R(a) => R(a'); (chosen)yes — “amended return” is the tax/legal registeryes — separate verbyes — one keywordone reserved word; statement-head, like delete/forgetyes — => fuses old→new
rescind R(a); primitive + amend R(a) => R(a'); compositeyesyesyestwo reserved words for one channelyes
single #[correction] delete R(a);nono — it is a delete with a modifierattribute, easy to missno new keywordno — replacement is a separate insert
keyword bikeshed (recant/rectify/correct)weaker than amendyesyesone wordn/a

The primitive-plus-composite split was first drafted as two keywords (rescind + amend). It was reduced to one: withdrawal and replacement differ only by an optional replacement clause, share one authority, one history treatment, and one atomic validation, and there is no distinct substrate operation behind rescind — the old and new tuples are already distinct propositions, so the composite is a fusion of the primitive and a corrected assert, not a second concept. Reserving a second common domain word (rescind) for no semantic gain was not justified against the language’s posture of returning vocabulary to modelers wherever a keyword is not required. #[correction] delete was rejected outright: it spells correction as a flavour of delete, defeating the un-confusability that motivates the whole channel.

Semantics

Belief-time, corrected-view projection. An amended tuple is marked false ab initio and retained bitemporally — the audit history is never rewritten. The freeze and dependent-context gates read a corrected transaction-time view: the retained assert-polarity history minus the tuples this transaction amends. Amendment is thus the exact inverse of the cascade — where RetractIndividuals grows the freeze witness (a logical retraction retains history, so a vacated value can never be reused), amendment subtracts the amended assertion from the witness the gates read, so the corrected value is admitted. An amendment is a first-class withdrawal event keyed by the superseded assertion’s event identity and plane, not a rewrite of the retained log; the corrected view is a projection over event history, so a tuple still supported by another source, assertion event, or closure route is never silently subtracted. The subtraction is event-keyed, not value-keyed-for-all-time: a withdrawal releases only the assertion events it named (those recorded before it), so a later re-assertion of the same tuple is a fresh initialization that re-freezes the fiber. Concretely amend R(a); insert R(a); re-establishes the freeze, so a subsequent insert R(a') refuses exactly as with no amendment; only amend R(a); insert R(a'); directly (no intervening re-assertion) admits the replacement. A value-keyed marker that subtracted the re-assertion too would launder a rebind — two live values at an immutable end through a stale marker.

Initializer vs non-initializer. Amending the sole initializer of an immutable fiber leaves it uninitialized in the corrected view; the corrected assertion then initializes it afresh. Amending one of several tuples in a fiber whose declared minimum exceeds one (e.g. one of the two spouse tuples a bindsSpouse(...)[2] requires) leaves the fiber initialized — the surviving tuples still witness it — and re-checks the minimum against the surviving corrected set. A withdrawal that would leave an established immutable fiber non-empty but below its declared minimum refuses with OE1407 (AmendmentBelowMinimum). This is deliberately distinct from OE1398: that code refuses a first-transaction under-filled initialization; OE1407 refuses an under-filling correction of an already-established fiber — a different cause and a different point in the fiber’s life.

Atomicity. The composite validates the withdrawal and the corrected assertion against a single pre-state and commits all-or-nothing: if the corrected assertion is inadmissible, the whole amend … => … commits nothing.

One net-view coverage check. The amended tuple is excluded from the corrected view only for the gates, and coverage is validated once over the whole transaction’s net corrected view — withdrawals subtracted, corrected assertions and any lifecycle effects overlaid — never as a per-effect exemption. A composite followed by a cascade (amend R(a,b) => R(a,c); then a retraction touching a) therefore cannot launder anything: the corrected R(a,c) is live in the net view the coverage gate reads, and every unrelated incident binding remains visible to it.

All-mut relations. Amending a tuple of an all-mut relation is admitted, not linted. The freeze-release is vacuous there (nothing is frozen), but the bitemporal marking still differs meaningfully from delete: amend records “never believed”, delete records “true until now”. Choosing between them is a modeling decision, so the surface admits both.

Rule-derived tuples cannot be amended. You amend premises, not conclusions. amend R(args) naming a tuple that is live but has no directly-asserted (or refuted) event — a purely rule-derived conclusion — refuses with OE1406 (AmendmentTargetNotAsserted) rather than succeeding as a silent no-op; the modeler must amend the premises and let dependency maintenance recompute the consequence. A tuple with neither an event nor a live row is the idempotent no-op, exactly as an unmatched delete is. Where an asserted tuple is also independently derivable, amendment withdraws the asserted contribution and the tuple remains live by derivation until its premises are corrected — correcting the record is not the same as deleting the consequence.

Refutations. Amending a standing refutation (not_fact) is the symmetric case — withdrawing a mistaken refutation. The mutate surface has no refutation-mutation form today, and the retract-time status of a refutation is an open question in the negative-facts design (a withdrawn refutation leans toward unknown). Amendment of refutations is therefore deferred to that surface; this section specifies amendment over positive relation assertions.

New diagnostics. OE1405 (AmendWithoutCapability), OE1406 (AmendmentTargetNotAsserted), OE1407 (AmendmentBelowMinimum), OE1409 (AmendCorrectionRelationMismatch — a composite corrects a fact of one relation, so its withdrawal and correction must name the same relation), allocated by the amendment slice; reserved here as design-record forward references.

Abuse guard — amendment is capability-gated

Left free, amendment is a rebind-laundering channel: any code path could “correct” an immutable binding to a different value and call it a fix. The guard has the same posture as physical erasure (forget) — a capability the module grants explicitly, so ordinary code cannot quietly correct facts — while being a strictly weaker act (amendment retains a full bitemporal audit trail; erasure destroys history):

  • Source gate. A mutate body containing amend refuses to build (OE1405 AmendWithoutCapability) unless the enclosing mutate declaration grants #[allow_amend], exactly as forget requires #[allow_forget]. The capability is greppable and visible at the declaration site.
  • Serving-layer authorization. Correction authority is operational, so the serving layer authorizes an amendment against the invoking principal, and the authorization is target-scoped (a deployment may permit correcting birth records while forbidding correction of identity or security relations). This is a stronger posture than a blanket build-time bit precisely because amendment changes which transaction-time assertion is authoritative.
  • Audit trail. Every amendment produces immutable bitemporal provenance — the false-ab-initio withdrawal event is retained, not erased — so the capability is auditable by construction.

A further per-relation restriction (forbidding amendment of named relations) needs no new mechanism: it is expressible as a static check over the schema, the same audit plane ArgUFO already uses to forbid mut opt-outs.

Independent design review: positions and resolutions

The syntax and semantics above were pressure-tested in a structured adversarial design review against the reference manual and the mechanized semantics. The positions that survived verification were adopted; those grounded in a misreading were rebutted. The substantive exchange:

Position arguedResolution
Two keywords (rescind + amend) are unjustified; withdrawal and replacement share one authority, one history treatment, one validation, and there is no distinct substrate operation behind the primitive.Adopted. Reduced to one keyword, amend, with the withdrawal and composite forms.
The freeze release must not delete the tuple from the retained assert-polarity history — that would lose the audit relationship. The corrected view must be a projection over event history, keyed by event identity and plane, so an identical tuple still supported by another source is not subtracted.Adopted. Amendment retains the audit history untouched and records a withdrawal event; the gates read a corrected-view projection that subtracts only the amended events.
Excluding the amended tuple from coverage per effect is unsound: a composite amend R(a,b) => R(a,c) followed by a cascade could pass coverage on the withdrawn R(a,b) while the replacement R(a,c) escapes the check. Coverage must be one atomic check over the transaction’s net corrected view.Adopted. Coverage is validated once over the net corrected view; the corrected assertion is live in that view, closing the laundering hole.
Reusing OE1398 for the below-minimum case misstates cause and timing: OE1398 is first-transaction under-filled initialization, whereas amendment corrects an already-established fiber.Adopted. Allocated a distinct OE1407 for the amendment-below-minimum case.
A distinct code for amending a rule-derived tuple is warranted: such a request writes no premise and has no asserted event to supersede, so it never reaches the premise-write gate (OE1397) and would otherwise fail silently.Adopted. Allocated OE1406; a purely-derived target refuses loudly.
amend not_fact R(args) has no grammatical or semantic basis: not_fact is a declaration form, not a mutation-predicate wrapper, and the mutate surface has no refutation-mutation effect; the retract-time status of a refutation is itself open.Adopted. Refutation amendment deferred to the negative-facts surface; amendment specified over positive assertions.
The capability posture should be stronger than forget’s single build-time attribute: correction authority is operational and should be target-scoped, with mandatory audit provenance, not a blanket principal bit.Adopted. Source gate #[allow_amend] plus a target-scoped serving-layer authorization and mandatory bitemporal audit provenance.

Worked examples

Syntax note: the mut end modifier is carried by this design record; parser and check support land with the check-plane slice, so these examples are illustrative here and gate-verified once that slice is in.

The declaration — what changed

use std::core::{type, rel};
pub type Person;
pub type Organization;
pub type Marriage;

// Both ends vary over time → both opt in. This relation behaves exactly
// as every relation did before this design.
pub rel worksFor(mut employee: Person, mut employer: Organization) [0..*] [0..1];

// A marriage cannot swap spouses (`spouse` keeps the immutable default),
// but a surviving person may marry again later (`marriage` is mut from the
// person's side of the story).
pub rel bindsSpouse(mut marriage: Marriage, spouse: Person) [0..1] [2];

Before this design every relation end was implicitly rewritable: nothing distinguished “this employment can change employer” from “this marriage cannot change spouses” — both edits were silently legal. Now the declaration states which it is, and the default is the safe one: immutable unless said otherwise. mut sits before the end name, exactly where concept fields already put it — no new modifier system.

An all-mut relation is the pre-design semantics

mutate changeJobs(p: Person, from: Organization, to: Organization) {
    delete worksFor(p, from);   // admitted — every end of worksFor is mut
    insert worksFor(p, to);     // admitted
}

An all-mut relation is the pre-design behaviour, proven not just promised: the mechanization’s runMutationChecked_unconstrained_eq says the gated runner is the old interpreter when everything is mut (see “The mechanization, in plain language”). Existing programs keep compiling and keep their meaning.

Modeling posture (two postures, not two kinds of fact). “Isn’t changing jobs really a different employment contract?” — yes, if the domain models the contract. bindsSpouse reifies the relator (the marriage is an entity whose relata are constitutive — immutable ends, lifecycle via retract-and-cascade); worksFor(mut, mut) deliberately does not reify — it tracks only the current association, a coarser view whose tuples are rebindable snapshots (history stays queryable bitemporally). A domain that cares about the employment itself declares EmploymentContract and gives its relation immutable ends, making “changing jobs” a retraction of one contract and the creation of another — the same shape as the marriage. What this design adds is that the schema now states which posture each relation takes; before, everything silently behaved like the coarse view.

Why not worksFor(employee, mut employer) — “the employer is a property of the person”? Because mut is read per fiber, not per individual: an immutable employee end would freeze each employer’s employee set at its first asserting transaction (the company could never hire again), and since a matched delete removes a value from every fiber it touches, direct retraction requires every end mut — so the person could never leave either (OE1402). The one-sided “property of the person” intuition is carried elsewhere in the declaration: the [0..1] bracket on the employer end says each person has at most one employer at a time, and a domain that truly treats employer as a subject-owned attribute should model it as a mut field on Person — whose one-sided update semantics is exactly what this feature’s mut posture is copied from. Single-end mut is for genuinely one-sided lifetimes like the marriage: “for a fixed marriage, the spouse set freezes” is true; “for a fixed person, the marriage set freezes” would forbid remarriage.

The freeze (OE1403)

// Transaction 1 — initializes marriage m1's spouse fiber. The exact [2]
// bracket means both tuples must arrive together (see OE1398 below).
insert bindsSpouse(m1, alice);
insert bindsSpouse(m1, bob);

// Later transaction:
insert bindsSpouse(m1, carol);   // refused OE1403 — m1's spouse set froze at initialization
insert bindsSpouse(m1, alice);   // admitted — re-asserting an active tuple is idempotent
insert bindsSpouse(m2, carol);   // admitted — a fresh marriage initializes its own fiber

For an immutable end, the first transaction that asserts a fiber (here: the spouses of m1) freezes its complete value set. Growing it later refuses; repeating it is harmless; a different dependent context (m2) is a new fiber with its own initialization. The gate reads the closed extent — initializing through a subrelation or a standpoint-scoped view freezes the same fiber, so there is no back route.

Direct retraction (OE1402)

mutate divorceWrong(m: Marriage, p: Person) {
    delete bindsSpouse(m, p);    // refused OE1402 — bindsSpouse has an immutable end
}

Deleting a tuple would shorten an immutable binding’s life while the marriage it depends on still exists — the “not shorter” half of the lifetime equation. A delete that matches nothing stays a no-op: the gate refuses real shortenings, not re-runs.

// Source spelling `retract m1;` lowers to the kernel effect
// RetractIndividuals { m1 }:
retract m1;
// ⇒ retracts m1 AND cascades: bindsSpouse(m1, alice), bindsSpouse(m1, bob)
//   are retracted with it, atomically, in the same transaction. alice and
//   bob survive; their frozen bindings do not outlive m1.

An immutable binding dies exactly when the thing it is about dies — the “not longer” half of the equation. The cascade is ordinary logical retraction (history stays queryable; nothing is erased), and the mechanization proves the sweep is complete: after it, no surviving assertion or refutation names m1.

When the cascade refuses (OE1404)

// Suppose employsCelebrant(mut ceremony: Ceremony, celebrant: Person)
// and c1 is a ceremony with celebrant dave.
RetractIndividuals { dave }
// refused OE1404 — dave sits at the IMMUTABLE end. Retracting him would strand
//   c1's frozen celebrant binding: c1 (the dependent context) survives, so the
//   binding may not die. Retract { dave, c1 } together, or none.

The cascade only releases a frozen binding when its dependent context is in the retraction set. If the context survives, refusing is the only safe answer — and the gate fails closed on anything it cannot decide, including a retraction target buried inside a collection value (pinned by collection_embedded_target_refuses_retraction).

Declaration-time refusals — before any data exists

pub type Amount;                       // a value-sorted type
pub rel hasBudget(project: Project, amount: Amount) [0..1] [1];
//                                 ^^^^^^ refused OE0267 — an immutable end whose
// dependent context is value-sorted can NEVER be released: a value has no
// lifetime to retract, so the fact would be permanently unretractable.
// Fix: declare `mut amount`.

pub rel supervises(mut boss: Person, report: Person) [0..*] [0..1];
pub rel mentors <: supervises (boss: Person, mut report: Person) [0..*] [0..1];
//                                           ^^^ refused OE0268 — a child may TIGHTEN
// a parent's mut end by omitting mut, but may not loosen an immutable one.

insert bindsSpouse(m3, alice);   // refused OE1398 (alone in a transaction) — the
// exact [2] bracket + freeze-at-initialization means an under-filled
// initialization could never grow to completion. Assert both spouses together.

The declaration plane refuses the traps before they can exist: an immutable end must have a context whose retraction could someday release it (OE0267, the unretractable-fact trap); subsumption can only make ends stricter (OE0268); and a frozen minimum must be met at the moment of freezing (OE1398). Status of each in this staged record: OE0267 is allocated and enforced by the check-plane slice; OE0268’s weakening rule is stated normatively (section 1) but its enforcement lands with the check-plane slice; OE1398’s below-minimum-initialization refusal is described in prose only in this record — it is not yet allocated to the catalog nor enforced, and lands with the write slice.

Mechanization of the two declaration-plane gates (OE0267, OE0268) was deferred and tracked, not carved out — the standing allowance for code to run ahead of the Lean covers evaluation / runtime semantics, not a declaration-plane gate over the subsumption lattice, which is substrate the Lean is canonical for. That obligation is now discharged in spec/lean/Argon/Substrate/RelationEndMutability.lean, which mechanizes the effective-mutability fold and both gates:

  • The conjunctive fold effMutAt (effective mut = the relation AND every transitive superrelation declare mut at the position — the runtime EndMutability vector’s per-position value), with effMutAt_le_declared (effective is a lower bound on the declared bit), effMutAt_antitone (adding a parent can only tighten, never loosen), and immutable_parent_forces_immutable (any immutable transitive parent forces the end immutable).
  • OE0268 as oe0268Accepts, with accepted_declared_is_effective: in any accepted schema a child’s declared mut equals its effective mut at every position — a weakening declaration is refused, never folded into an effective loosening.
  • OE0267 as oe0267Accepts, with oe0267_context_individual_bearing (an accepted immutable end has a nonempty, identity-bearing dependent context) and oe0267_unary_immutable_refused (the vacuous unary end is refused — the declaration-time catch of the runtime coverage rule’s vacuous-false arm, tupleContextCovered).

Issue #1786 tracked this obligation and is closed by it.

pub type Kid; pub type Toy; pub type Person; pub type Passport;

// Transferable ownership: a pure association, like worksFor. Giving a toy
// away = delete + insert, and delete needs every end mut.
pub rel ownedBy(mut toy: Toy, mut owner: Kid) [0..*] [0..1];

// Provenance: "this toy was made FOR this kid." The recipient is
// constitutive — remaking it for someone else is a different fact. The kid's
// side still grows (they receive more toys over time).
pub rel madeFor(mut toy: Toy, recipient: Kid) [0..*] [0..1];

// Birth: a child's birth-mother never changes (immutable), but a mother's
// set of children grows with each birth (mut).
pub rel bornTo(mut child: Person, mother: Person) [0..*] [1];

// Issuance: a passport is issued to exactly one person, forever; a person
// accumulates passports over a lifetime.
pub rel issuedTo(mut passport: Passport, holder: Person) [0..*] [1];

How to read these. mut is per fiber — fix the other end, ask whether that value set may change after its first assertion:

  • bornTo: fix a child → their birth-mother set is one value, frozen forever (immutable). Fix a mother → her children set grows with each birth (mut). Same shape for madeFor and issuedTo — one end is constitutive of the tuple, the other end accumulates history.
  • ownedBy: fix a toy → its owner changes when gifted (mut); fix a kid → their toy collection changes (mut). Rebinding requires delete, which needs every end mut — so any transferable association is all-mut by necessity.

What the gates then do for each. insert bornTo(tim, anna) after Tim already has a recorded mother → OE1403 (a second birth-mother is not new information, it contradicts frozen information). delete issuedTo(p42, alice)OE1402 (an issuance cannot be unhappened while both survive). RetractIndividuals { p42 } (the passport is destroyed/expired out of the domain) → cascade releases the frozen binding; Alice survives. RetractIndividuals { alice } alone → OE1404 — her passports sit at the mut end, fine, but her children’s bornTo bindings hold frozen ends; retract the dependent contexts together or not at all.

Decision procedure, three questions per relation:

  1. Will a tuple ever be deleted while both individuals still exist? → every end mut (it is an association; ownedBy, worksFor).
  2. Otherwise: which ends are fixed the moment the tuple exists, and which side keeps accumulating? Constitutive end stays immutable, accumulating end gets mut (bornTo, madeFor, issuedTo, bindsSpouse).
  3. Is the only honest exit the disappearance of a participant? → that is the cascade doing its job, and the declaration needs no mut on that end.

The mechanization, in plain language

The mechanization (spec/lean/Argon/Runtime/MutationSemantics.lean) states the feature as a gated runner layered on the existing mutation interpreter, then proves the properties a reviewer would otherwise take on faith.

Backward compatibility — the keystone.

  • runMutationChecked_unconstrained_eq — with no immutability constraint declared (the theorem is stated over the empty per-end mutability decode — the pre-design reading, which admits exactly what an explicit all-mut vector admits), the gated runner is definitionally the old ungated interpreter. This is the proof that the feature is opt-in and introduces no second semantics.
  • relationGatesAdmit_unconstrained — the same fact at the gate level: an all-mut program admits every effect.

The two write gates (refusals are theorems, not test cases).

  • retract_matched_immutable_refuses — a delete R(args) matching a live tuple at a relation with any effectively-immutable end refuses (the “not shorter” half).
  • retract_unmatched_is_noop — the idempotent arm: a delete matching nothing is a no-op even at an immutable relation.
  • assert_frozen_fiber_refuses — an insert carrying a new value for an already-initialized endpoint fiber refuses. Freeze means frozen.
  • runMutationChecked_refused_noop — atomicity by construction: a refused mutation commits nothing; there is no partially-applied state to reason about.

The cascade half (the sign-off question), with its safety envelope.

  • applyRetractIndividuals_uncovered_refuses — the coverage gate fails closed: if any effectively-immutable end of any incident tuple is not explained by the retraction set, the whole operation refuses.
  • retractIndividuals_classifications_clean / retractIndividuals_relations_clean — after a covered retraction, no surviving classification and no surviving tuple names a retracted individual.
  • retractIndividuals_refuted_classifications_clean / retractIndividuals_refuted_relations_clean — the same cleanliness for negative beliefs: a standing refutation about a retracted individual does not survive either.
  • refuted_only_binding_gates_value_side — a binding that exists only as a refutation still gates its value side exactly as an asserted one would.
  • collection_embedded_target_refuses_retraction — a retraction target embedded inside a collection value refuses the whole retraction (Abort), never silently releases.
  • retractIndividuals_preserves_fiberInitialized — the cascade cannot un-initialize a fiber: retraction is ordinary logical retraction, never erasure, so it cannot launder an illegal rebind of a frozen end.

The amendment suite (amend, section 5) is mechanized in the same file: it releases exactly the amended assertion’s freeze contribution (a corrected insert is admitted where OE1403 refused), preserves the audit history, preserves other fibers, re-checks the minimum on a non-initializer amendment, refuses a purely-derived target, keeps the composite atomic, and leaves all-mut programs unchanged.

The deliberate asymmetry, stated rather than implied. Coverage reads an argument as “about” a retracted individual only at top level (Value.ceases), while incidence detection looks arbitrarily deep (Value.referencesAny). The consequence is fail-closed by design: a retraction target buried inside a collection value makes the evidence incomplete and the whole retraction refuse.

Staged ahead of use. The companion file spec/lean/Argon/Reasoning/Datalog/RelationApplication.lean carries two obligations for relation-value application: structural_containment (a declared child <: parent specialization edge entails extension containment in a structurally-closed program) and dispatch_selected_iff (reading the compiled finite-dispatch helper at an admitted selector is exactly applying the selected relation — the helper adds no semantic premise). Nothing in this record or the check/write-plane slices consumes them; they are obligations for the relation-value-application enforcement plane to discharge when it lands, and the module doc says so.

System shape and slice ownership

One feature, three planes, each owning exactly the rule its layer can decide. (No diagram-embedding markup is used in this spec tree; the shape is given as an ASCII figure.)

source: rel R(mut a: A, b: B)
        │
        ▼
┌──────────────────────────────────────────────────────────────┐
│ DECLARATION PLANE  — check-plane slice                         │
│   oxc-check::end_mutability                                    │
│   OE0267: an immutable end must have a dependent-context sort  │
└──────────────────────────────────────────────────────────────┘
        │ legal declarations only
        ▼
┌──────────────────────────────────────────────────────────────┐
│ WRITE PLANE  — write-gate slice                                │
│   oxc-runtime::end_mutability                                  │
│   decision table: insert on initialized immutable fiber        │
│     → OE1403 · matched delete → OE1402                         │
│   ┌────────────────────────────────────────────────────────┐  │
│   │ CASCADE SUB-UNIT (sign-off-pending)                     │  │
│   │   RetractIndividuals coverage → cascade, or OE1404       │  │
│   │   fail-closed                                           │  │
│   └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘
        ▲
        │ enforcing modules cite chapter + Lean def
        │ (chapter→module back-pointers land with the check/write
        │  slices, which own the enforcing code)
        │
┌──────────────────────────────────────────────────────────────┐
│ DESIGN RECORD  — this slice                                    │
│   spec/reference relations.md: ends immutable unless mut       │
│   spec/reference mutate.md:    write-plane refusals            │
│                                OE1402 / OE1403 / OE1404        │
│   spec/lean MutationSemantics: freeze / coverage / cascade,    │
│     mechanized, fail-closed on undecidable coverage           │
│   relations.md ↔ MutationSemantics: same rule, stated twice —  │
│     the mutation-semantics shapes are Lean-internal and        │
│     untagged, so their parity is the mechanization itself,     │
│     not a drift check                                          │
└──────────────────────────────────────────────────────────────┘

The book and Lean (this record) state the lifetime equation — an immutable binding lives exactly as long as its dependent context; the check plane refuses at declaration time what could never satisfy it (an immutable end whose sort admits no dependent context would make facts permanently unretractable); the write gate enforces it at runtime (no shortening while the context lives; the cascade — the one open sign-off — is the “no outliving” half). Nothing else moves: no parser change beyond one token position, no evaluator change, and the feature is opt-in per end, so every existing declaration keeps its meaning.

Maintainability rationale

Each rule lives in one module at the layer whose book chapter states it, and the links are machine-checked where a gate exists and stated honestly where one does not: the enforcing modules cite their chapters and Lean definitions (the chapter→module back-pointers land with the check/write slices, which own the enforcing code), the diagnostic codes in prose are gated against the catalog, and the drift gate covers tagged-inductive Lean↔Rust mirrors — the mutation-semantics shapes in this slice are Lean-internal and deliberately untagged, so their parity claim is the mechanization itself, not a drift check. Changing a rule is therefore a one-module edit plus its cited record — there is no second copy to forget. The one genuinely contested piece, the cascade, is isolated behind a module boundary whose header carries an honest removal inventory, so either exit-path outcome (cascade, or explicit per-binding retraction) is a bounded, enumerated change rather than an excavation.

Compatibility and migration

The grammar change is source-compatible: existing relation declarations still parse. The semantic default is intentionally stricter, however. Every existing unmarked end becomes lifetime-bound, so a model that currently varies a relation end while its dependent context survives must add mut before exactly that end.

Migration should audit delete R(args) and delete-plus-insert replacement patterns. Keep replacement as retract/insert and annotate only the ends allowed to vary. If an end is immutable, replace direct tuple deletion with retraction of its dependent context and rely on the automatic relation cascade. A plain tuple delete is not legal merely because it is terminal: it must satisfy every immutable-end lifetime gate. Most initial relation assertions and models that never retract tuples need no source change, with three declaration-level exceptions the stricter default itself introduces: a relation whose immutable end has a primordial, value-typed, or reference-sort dependent context is rejected (ImmutableRelationEndNonIndividualContext) even if it is only ever read — the modeler must mark the opposite end mut; a static fact set (or single-transaction initialization) that establishes an immutable fiber below its declared minimum cardinality is rejected rather than frozen permanently under-filled; and armMutability becomes a reserved reflection-atom name (ReservedIntrinsicName) — a previously valid declaration of that name must be renamed, because a same-module declaration would mask the total per-end reflection rows for every dynamic reader and let the very module under a forbidden-opt-out audit shadow the audit atom into an empty extent. This behavior change belongs in a breaking language release; there is no legacy mutable-by-default mode in the proposed design — no edition switch, build flag, or attribute restores the mutable default for source a post-RFD compiler builds.

Migration must also audit relation hierarchies. A child mut end beneath an immutable parent is a mutability-weakening error, and premise mutations that retract immutable derived facts must be converted to dependent-context retractions or rejected.

One further behavior change rides this proposal’s enforcement work, and migration should note it: gate 1’s “exact live tuple named by delete R(args)” is a BASE-plane lookup, aligning tuple deletion with the standpoint sheaf semantics (a fact asserted inside standpoint s { … } is s’s own local belief; the base write path edits the base plane). A DEFAULT-scope delete therefore no longer reaches into a standpoint to retract that plane’s seeded belief — previously it removed matching rows from every plane — and this holds for fully-mut relations too. A model that relied on a base delete clearing scoped beliefs must retract them within their standpoint. This is a plane-locality alignment, not a mutability gate: the per-end is_mut decode of pre-RFD artifacts (below) is unaffected.

At upgrade, each unmarked end’s currently live value set becomes its frozen initialization set. Historical data showing a removal or substitution while the same dependent context survived is a migration diagnostic rather than a value to normalize silently. The non-normalization half is enforced in the write path (a historically-removed value is never silently re-admitted); the REPORTING half — an offline audit listing such historical anomalies — is deliberately deferred to the breaking-release migration tooling (an ox doctor/ox migrate surface that does not exist yet) and is a follow-up gate for shipping that release, not a runtime behavior this proposal depends on.

Relation declarations gain a per-end is_mut flag, riding a MAJOR bump of the artifact core-IR version, so a published artifact adopts the stricter default only through a rebuild. The bump is major, not minor, because the additions are not safe for an older reader to ignore: under the version-robustness rule (a consumer accepts any artifact whose major does not exceed its own) a minor bump would let a pre-RFD runtime load a post-RFD artifact, skip the unknown per-end flags, and execute ordinary tuple mutations with every immutability gate silently absent — under-enforcing semantics the artifact’s source was audited against. With the major bump a pre-RFD runtime refuses the artifact loudly instead. In the other direction nothing is lost: a pre-bump artifact still loads on the post-RFD runtime (its lower major is accepted) and keeps its recorded pre-RFD semantics — every end reads as mut, exactly what its declaring compiler enforced (never all-immutable, which would impose gates its source was never audited against). That is faithful decode of an old artifact, not a mode of the new design: a post-RFD build cannot produce a mutable-by-default artifact, and the version stamp keeps pre-RFD artifacts distinguishable for any stricter deployment policy. The mutation IR/wire surface also gains RetractIndividuals. Relation-tuple events and their stored history remain unchanged: each is still identified by its relation and arguments, and logical individual retraction expands to ordinary retraction events. Existing history can seed the incidence index; no binding-id backfill or relation-tuple event migration is required.

References

  • RFD 0006, Field mutability via mut — the modifier placement and immutable default followed by this proposal; relation mutation remains retract/assert rather than field update.
  • The relation declaration chapter of the reference manual (spec/reference/src/declarations/constructs/relations.md) — the existing rel-param grammar this proposal extends.
  • RFD 0075, Metarel cardinality reflection — the sibling reflection-plane extension (the relation-end notion and its per-end facet atoms), in review concurrently with this record.
  • The ArgUFO vocabulary package (external to this repository): arg_ufo/metatypes.ar (the rigidity axis, the type-plane analogue of relation mutability), arg_ufo/relations.ar (InheresIn, Mediates, ExternallyDependsOn, HasComponent, HasMember, and their cardinality brackets), arg_ufo/metarels.ar (the existential-dependence and event-mereology metarels whose binding-stability axioms are prose today), and arg_ufo/metarel_constraints.ar (the existing relation-arm audits and the intended home of mutability checks).
  • OE0234 — the fixed-metatype reclassification refusal, an analogous modal write gate.
  • OE1341 — the cardinality gate that relation-end mutability complements.

RFD 0081 — Check-pass rule registry: per-axis drivers, one auditable rule table

State: discussion

Question

The check pass wires diagnostics three ways, none of which lets a reviewer see “what runs where” in one place, and none of which enforces that every rule an enumeration axis is supposed to run actually runs on every case in that axis. Can the wiring be reorganized so that each enumeration axis has one data-driven registry — a table a reviewer reads top to bottom — without moving the gates whose placement is semantically load-bearing?

Context

Today diagnostics reach source through three distinct styles:

  1. Body-scoped syntactic rules. Rules that walk a body (a fn, mutate, check, derive, query, or test body). These are now enumerated by the body-walk driver introduced in pull request #1727, after review of that change found coverage holes: the driver replaced seam-by-seam wiring that had kept growing rule by rule as new body-bearing positions were noticed.
  2. Item-level gates. Wired per item kind by hand in the check_file_with dispatch loop (compiler/crates/oxc-check/src/infer.rs): a chain of if let Item::… = &item arms, each calling the checks that item kind needs.
  3. Inference-interleaved rules. Rules that fire inside expression inference (infer_expr and the infer_exprs family) because they need a type the walk is in the middle of computing.

Styles 2 and 3 have no enumeration. Adding a new body-bearing item kind means remembering, by reading the loop, which of the item-level and body rules it needs — and there is no test that the set is complete.

Seam-hole history (evidence)

The syntactic-rule wiring grew by accretion and repeatedly shipped incomplete:

  • Pull request #1701 wired body rules seam by seam. Review found three coverage holes — rules that ran on some body-bearing positions and not others structurally identical to them.
  • Follow-up review found four more adjacent positions missing the same rules, and one double-emit (a rule wired at two seams that both fired on the same node).
  • Pull request #1727 responded with the body-walk driver (for_each_check_body
    • a BodyPlane mask) so a body rule is registered once and the driver visits every body. That closed the body axis but left the item axis and the inference-coupled rules on the old hand-wired styles.

The pattern is structural, not a run of bad luck: hand-enumerated seams have no exactly-once invariant, so every new body-bearing item kind must independently re-derive the full rule set, and the failure mode (a hole) is silent — a rule that should refuse simply does not, and nothing reddens.

Decision

Introduce one rule registry per enumeration axis. A registry is a table of entries; each entry is data:

(codes, axis, mask, fn, wired)
  • codes — the diagnostic code(s) the rule can emit (for audit / cross-index).
  • axis — which enumeration axis the entry belongs to (BODY | ITEM).
  • mask — the plane/kind mask selecting which cases in the axis the rule runs on (BodyPlane for body rules; an ItemKind mask for item rules).
  • fn — the rule function.
  • wireddriver (the axis driver enumerates and invokes it) or inline (the rule is invoked at a hand-placed call site and the row is an INVENTORY entry, present for auditability only — not driver-dispatched).

The table is the single source of truth for where a rule runs. A reviewer reads one table per axis; a new item kind gets every driver-wired rule whose mask admits it, automatically.

Axis 1 — BODY (exists)

The body-walk driver from pull request #1727 (for_each_check_body + BodyPlane) IS the axis-1 registry instance. This RFD adopts it as such: body rules become registry entries with axis: BODY, mask: BodyPlane, wired: driver. No new mechanism for axis 1 — this RFD only names it and brings it under the shared registry vocabulary and the shared invariants below.

Axis 2 — ITEM (new)

A for_each_item driver over the top-level items of a file, dispatching each registered item rule whose ItemKind mask admits the item. This replaces the hand-written if let Item::… chain in check_file_with for the rules that are mechanically item-kind-keyed. Candidate migrations, enumerated by reading check_file_with (infer.rs) and references.rs:

RuleCode(s)Current siteItem kinds
Nested-declaration refusal in standpoint bodyOE0733check_standpoint_body_withStandpointDecl
Impl-member mutate tuple-target undeclared program-wideOE0266flag_mutate_tuple_target_undeclared_everywhereImplBlock
Duplicate-import bindingOE0105check_use_imports_withuse (file-scoped)
Glob-import ambiguityOE0106report_glob_ambiguity via use walkuse (file-scoped)
Module-level mutate target gateOE0223check_mutate_with (scoped)MutateDecl
Refinement-predicate well-formednessOE0101, OE0106check_refinement_predicates + type_check_refinement_predicatesitems carrying refinements
Signature checks (procmacro)OE0727, OE0729, OE0734infer_signature via check_fn_withFnDecl

The file-scoped use rules run once over all imports rather than per item; they enter the registry with a file-scoped mask, dispatched once by the driver — not per-item — so their exactly-once guarantee is over the file, not over each use.

INVENTORY rows (wired: inline)

The following rules stay exactly where they are and enter the table only as wired: inline inventory rows. Each carries one line stating why its placement is semantic and cannot become driver-dispatched:

RuleCode(s)Why inline
Emit-value typingOE0201Needs the value’s inferred type mid-inference; fires inside the emit-consequence inference path, not at item entry.
Collection-operand guard (issue #186)Fires at the operand’s inference point; the offending operand’s type is only known there.
Update-set RHS typingOE0201The set RHS is type-checked against the target column during mutate-body inference; no item-entry point sees the resolved column type.
Elaborator gates (e.g. trait invocation-plane member with body)OE1326Enforced in oxc-instantiate lowering, a different layer; the refusal precedes lowering and cannot move into the check walk.
Runtime backstops (e.g. program-global tuple-target resolution)OE0266Deliberately mirrors the runtime resolver’s program-global match; some cases are legal at runtime and must not be refused statically.

Inventory rows make style-3 and cross-layer gates VISIBLE in the same table without pretending they are mechanically enumerable. They are audited, not migrated.

Invariants

  • Exactly-once per axis. Every case in an axis is visited exactly once by the driver, and every driver rule whose mask admits a case runs on it exactly once. Tested: no hole (a mask-admitted case with the rule not run) and no double-emit (the same rule firing twice on one node).
  • Plane/kind dispatch. A rule runs iff its mask admits the case. Masks are the only selection mechanism; there is no hand-placed conditional inside the driver.
  • Registration is the single source of where a rule runs. For driver rules, the row’s mask fully determines the run set; for inline rows, the row records the hand-placed site. Either way the table answers “where does this run”.
  • Byte-identical behavior per migration wave. Each wave that moves a rule from hand-wiring to the registry must leave the emitted diagnostic set unchanged over the full pin corpus — the migration is a refactor, not a semantics change.

Rationale

  • The seam-hole history shows hand-enumerated wiring fails silently and repeatedly; the fix is to make the enumeration a machine-checked property, not reviewer diligence.
  • Pull request #1727 already proved the pattern on the body axis. Generalizing to a per-axis registry is a small, evidence-backed step, not a speculative redesign.
  • Keeping inference-coupled and cross-layer gates as inventory rows preserves the one property that matters for review — a single readable table — without forcing rules into a driver that cannot correctly host them.

Alternatives

  • Keep hand-wiring, add more tests. Rejected: tests over hand-enumerated seams still require someone to write the test for each new seam; the exactly-once property is not structural.
  • One universal driver over all rules including inference-coupled ones. Rejected: inference-coupled rules need types produced mid-walk; hoisting them to item entry either recomputes inference or changes semantics (see the OE0106-refinement regression in Non-goals).
  • Migrate cross-layer gates into the check pass. Rejected: placement of elaborator and runtime gates is semantic (they guard lowering and runtime resolution respectively); moving them changes when and against what they fire.

Non-goals

  • Migrating inference-interleaved rules. Hooks-in-inference is explicitly out of scope. Cautionary case: making infer_struct_lit type-only (dropping its interleaved refinement work) regressed the OE0106 refinement check — the refinement diagnostic depended on state the literal inference computed in place. That is the evidence that these rules cannot be lifted to a driver without a semantics change; they stay inline and enter the table as inventory.
  • Moving cross-layer gates. Elaborator (oxc-instantiate) and runtime backstop gates stay in their layers; they are inventoried, not relocated.

Consequences (phasing)

  • Wave 1: land the registry table type + the item-axis driver (for_each_item + ItemKind mask); migrate 2–3 mechanical item gates (candidates: OE0733 standpoint nested-decl, OE0266 impl-member mutate target, OE0103/OE0105 use-import resolution/duplicate). The OE0106 glob-ambiguity emitter, first listed here as a wave-1 candidate, is reclassified to a wired: inline inventory row: report_glob_ambiguity fires at resolved use sites across reference resolution and expression checks (type slots, predicates, callee paths, struct-literal heads), so no item-entry driver can preserve its placement. All INVENTORY rows land in wave 1 so the table is complete-as-audit from the start even though most rows are inline.
  • Wave 2: migrate the remaining mechanical item gates (module-level mutate target OE0223, refinement well-formedness, signature checks) as each is shown to be item-kind-keyed and inference-free.
  • Body axis needs no migration wave — it is adopted as-is from pull request #1727.

Verification (per wave)

  • All existing diagnostic pins unchanged (byte-identical emitted set over the pin corpus).
  • Exactly-once tests: no mask-admitted case skipped, no double-emit.
  • Plane/kind mutation checks: unregister a rule (or narrow its mask) → exactly that mask’s pins redden, and no others. This proves the mask is the true and complete selector.
  • Cross-model review of each wave’s diff against the byte-identical-behavior contract.

Open questions

  • Whether the file-scoped use rules warrant a distinct axis (FILE) rather than a file-scoped mask on the ITEM axis.
  • Whether codes on each entry should be validated against the diagnostic registry so an added rule with an undocumented code fails a gate.
  • Whether wave 2’s signature and refinement gates are cleanly item-kind-keyed or carry enough inference coupling to become inventory rows instead.

RFD 0082 — Cross-module defeat: import-scoped #[defeats] resolution, component-level strategy identity, and the combined-artifact defeat pass

  • State: discussion
  • Opened: 2026-07-19
  • Decides: how a #[defeats(target(args))] directive in one module resolves against a head, label, or trait member declared in another module of the same package — extending the defeat-directive plane of RFD 0028 from file-local resolution to import-scoped resolution. Settles the deferred build-pipeline work RFD 0028 D3’s v1-scope amendment named and tracked as issue #362: the defeat pass lifts out of per-file elaboration into a combined-artifact pass over the module graph; #[defeats] targets resolve over the importing module’s visible names (public and imported); the single strategy per connected defeat graph invariant (RFD 0028 D7) is checked at workspace assembly instead of guaranteed by construction, stamped on the connected component; cross-package defeat edges are refused in this version (in-package only — the package, not the workspace, is the trust boundary); and the diagnostic surface gains distinct codes for the failure modes the wider resolution scope introduces. This RFD is a successor amendment to RFD 0028; it continues that RFD’s D-numbering (D12–D19) and changes nothing about the surface (D1–D2), the strategy-as-compilation architecture (D5–D6), or the semantics of the attack itself (D3–D4). The feature is additive: every program that compiles today keeps compiling unchanged.

Prior state (RFD 0028, accepted): the defeat plane ships file-local. A module is a single .ar file with its own rule catalog; #[defeats] resolves only against that file’s catalog, and a target naming a head, label, or trait member outside the file refuses as unresolvable. That restriction was recorded as a deliberate v1 scope cut, not a semantic wall: “workspace-scoped resolution requires lifting the defeat pass out of per-file elaboration into a combined-artifact pass — a build-pipeline change deferred so it does not ride alongside the two correctness fixes [that] PR lands” (RFD 0028 D3 v1-scope amendment). D7 records what the file-local rule buys: “selection is per module, which structurally guarantees that a connected defeat graph is single-strategy — edges resolve within the module that declared them.” That guarantee is exactly what this RFD must reconstruct once edges are allowed to cross the file boundary.


Question

Statutory-law modeling is the driving workload, and it wants a file layout the file-local defeat plane cannot express. A maintainable tax-code corpus is one file per statute section — the file tree mirrors the code’s own structure, each section reads like the statute it encodes, and a section edit re-elaborates only that section. Exceptions are the essence of statutory drafting: a general provision states a rule, and a later, more specific provision — a different section, a different file — overrides it (“except as otherwise provided …”). The modeling ideal is that the overriding section names what it overrides, with the override semantics living on the overriding rule, where a reader of that section finds it:

// section_1016.ar — the exception carries the attack, in its own file
#[defeats(section_1012::cost_basis.section_1012_a(taxpayer, property))]
pub derive adjusted_basis(taxpayer, property) :- /* … */ ;

(The full two-file version of this example, with both modules spelled out and the pass’s steps narrated, is worked in Consequences below.)

Today this refuses: cost_basis is declared in section_1012.ar, and #[defeats] resolves only against the catalog of the file that declares the attack. The only way to make the defeat plane span two sections is to collapse both sections into one file — which destroys the one-file-per- section layout, kills separate compilation of a section, and does not compose across packages at all.

There is a proven alternative that works cross-file today, without the defeat plane: give each provision a provisional conclusion tagged with the scope where it holds, publish an explicit “yields to a more specific region” relation, and adjudicate precedence centrally by negation-as-failure over that relation and a scope-nesting order. No rule ever names another rule; the override is data plus one central adjudication module. This pattern scales — it has been demonstrated at the scale of a full statute subtitle. But it relocates the override semantics away from the overriding rule: to learn that section 1016 displaces section 1012’s basis, a reader must read the central adjudication module and the scope lattice, not section 1016. That relocation is precisely the compositionality defect the defeat-directive plane exists to fix (RFD 0028 defect 4: “an exception … can’t live in another module under its own honest name”). The central-adjudication pattern is a sound and useful tool, and nothing here removes it; but it is not the surface the rewrite is reaching for.

Two questions. (1) How does a #[defeats] target resolve to a head, label, or trait member declared in another module, so that the overriding rule can name what it overrides, across files, and stay goto-def-able? (2) Once defeat edges cross the file boundary, a connected defeat graph can span modules that selected different reasoning strategies (RFD 0028 D6/D7) — where does the single-strategy invariant get re-established, and what refuses when it cannot hold?

Context

The addressing substrate already exists. Post-qualified-catalog naming, every rule head, labeled clause, and trait member has a fully-qualified catalog key (module::head, Trait::member @ Type), and the resolver already parses all three target spellings — head(args), head.label(args), Trait::member(args) @ Type — and the trait-qualified grain already resolves by segment-exact qualified key. What is file-local is not the spelling but the catalog the spelling is resolved against: the resolver builds its head/label/qualified views from a single file’s rule identities and refuses any target that misses in that one file’s views.

The soundness of the attack does not depend on the file boundary. The Governatori compilation (RFD 0028 D6) is stated per labeled rule and resolved edge; the mechanization ranges over abstract rule identities and an abstract superiority relation and never mentions which file an edge’s endpoints came from. The transform-correctness core, team defeat, ambiguity blocking, strict- unattackability, and the defeated-defeater (reinstatement) property are proved over a program’s clauses and its derived blocking sets — objects that are indifferent to whether two clauses share a file. The defeat-complete extraction theorem preserves warrant given that a rule’s support and the support of all its defeaters are preserved, again over abstract rule identities. The merged, cross-module defeat graph is a bigger instance of the same structure, not a new one.

The genuine hard parts are exactly the two the deferral named: (a) target resolution and defeat- graph cycle detection are cross-declaration and today run inside per-file elaboration, so cross- module edges force a pass that sees more than one file; and (b) the strategy id is recorded per module, and a connected defeat graph that spans modules has no single module to own it.

D12 — Target resolution over the importing module’s visible names

A #[defeats(target(args))] directive resolves its target against the import-scoped catalog of the module that declares the attack: the union of that module’s own rule catalog and the catalog entries reachable through its use edges. The three target spellings are unchanged — head(args), head.label(args), Trait::member(args) @ Type — and all three now resolve across the import closure by the same rules they use file-locally today. Scope follows ordinary name resolution exactly: a use of the RULE (or a glob/alias/re-export reaching it) puts its bare short name in scope; a use of the MODULE binds the module name only, so its members are addressable by the qualified-path spelling (module::head(args)), not by bare name. Resolution stays checked at elaboration and stays goto-def-able: an unresolvable target refuses loudly (D17), and an editor can jump from the #[defeats] directive to the targeted declaration in the other file.

Disambiguation by qualified path. When a short head name is visible from more than one imported module, the bare head(args) spelling is ambiguous and refuses (D17). The author disambiguates by writing the qualified path — other_module::head(args) — resolving the head against exactly the named module. The trait-qualified spelling Trait::member(args) @ Type is already segment-exact and needs no change. Qualified-path targeting is the general disambiguation mechanism and is available for every grain, not only the trait member grain.

Visibility: you can defeat only what you can see. A #[defeats] target must resolve to a declaration the attacking module can actually name — a pub declaration in a module it imports. A private (non-pub) rule is not a legal defeat target from any module that could not otherwise name it, exactly as it is not a legal use target. This is decided: defeat visibility equals ordinary name visibility — including the one carve-out ordinary visibility already grants: a declaring module’s descendants see its private names, so a descendant may attack an ancestor’s private #[default] rule. No new visibility notion is introduced in either direction.

Rationale. There is no action-at-a-distance on private rules. A rule a module keeps private is an implementation detail; letting a distant module attack it would make that private rule’s conclusions silently depend on code its author cannot see and did not consent to expose. Tying defeat visibility to ordinary visibility means the set of things that can attack a rule is exactly the set of modules that can name it — a bound the author already controls with pub. It also keeps one visibility rule in the language rather than a second, defeat-only visibility notion, which was the alternative and is rejected: a separate “defeatable but not otherwise public” grade is a new axis of surface complexity with no demonstrated need, and can be added later without breaking anything if a need appears (recorded open, D19).

D13 — The combined-artifact defeat pass

Defeat-plane resolution and validation lift out of per-file elaboration into a combined-artifact pass that runs at workspace assembly, after every module in the workspace has lowered to its per-module rule identities and before the workspace’s rules go to stratification. This is the build-pipeline change RFD 0028 deferred. Concretely: each module’s elaboration still produces its per-module rule-identity table (head qualified paths, labels, is_default, the raw parsed #[defeats] targets, argument bindings) exactly as today; the combined pass consumes the tables of a module and its import closure, builds the import-scoped catalog (D12), resolves every target, runs the discipline gates over the merged graph (strict-unattackability, per-tuple argument binding, duplicate-label, acyclicity), stamps the resolved edge set onto each attacker’s wire record, and stamps the component strategy id (D14).

Where it runs. In oxc-instantiate, at workspace assembly, driven by the workspace pipeline — not a separate link executable. The elaborator already owns the rule-identity grain the pass needs and already owns the wire records the pass stamps; the pass is a workspace-level phase over per-module elaboration outputs, not a new tool. It runs before the loud evaluability gate so a cross-module target that fails to resolve refuses with the same finality as a file-local one, and no .oxbin is written for a workspace whose defeat plane does not resolve.

Fate of the per-file refusal. Target resolution moves wholesale into the combined pass; there is no file-local resolution step left to keep. The reasoning: a file cannot decide in isolation whether a target that misses its own catalog is a genuine misspelling or a legitimate reference to another file — the two are indistinguishable without the import closure, so a per-file “file-local misspelling” refusal would produce false positives on every valid cross-module edge. The unresolvable-target diagnostic (RFD 0028’s OE0716) therefore becomes the combined pass’s code: it fires when a target misses the import-scoped catalog, and its message distinguishes the misspelling case (no such name anywhere visible) from the not-imported case (the name exists in a module this one does not use) — see D17. The three purely-local gates that need nothing beyond the declaring module — the malformed-target refusal, the per-tuple argument-binding refusal, and the duplicate-label-per-head refusal (labels are per-head identities within one module) — still fire per-file at read time, for editor latency, and are re-checked in the combined pass; only resolution against a catalog wider than the module is combined-pass-only.

Incremental recompute. The combined pass’s validity is scoped to the connected defeat component, not the whole workspace. An edit to a module invalidates the resolved defeat plane of exactly the components its rules participate in: changing a #[default] marker, a #[label], a #[defeats] target, or the pub/visibility of a potential target, or adding or removing a rule that a cross-module target resolves to, re-runs the pass over the affected component(s). A module that participates in no cross-module edge has a single-module component and recomputes exactly as the file-local pass does today — the common case pays nothing new. This keeps separate compilation of a section: editing one section re-runs the defeat pass only over the components that section’s rules touch, which for a statute corpus is the section plus the specific provisions that attack it or that it attacks, not the whole title. (The worst case — an edit that merges or splits a large component — recomputes the whole merged component; whether finer sub-component invalidation is worth its complexity is open, D19.)

D14 — Strategy identity lives on the connected defeat component

The strategy id (RFD 0028 D6 — which compilation scheme gives the program its meaning) moves from per-module to per connected defeat component. A connected component of the merged defeat graph is the unit that gets compiled together by one strategy’s transform, so it is the natural owner of the id. The combined pass computes the components, and for each component records its strategy id on the wire record of every module that participates in the component — so each artifact stays honest about which compilation gave its defeat-bearing rules their meaning, and no single module is privileged as “the” owner of a shared component.

What refuses. A module selects a strategy (in this version, implicitly, the sole strategy — Governatori with explicit superiority; RFD 0028 D6). When a new edge connects two modules that selected incompatible strategies, the component spans conflicting strategies and the combined pass refuses with a distinct strategy-conflict code (D17), naming the two modules and their strategies. This is the D7 invariant — single strategy per connected defeat graph — now checked at workspace assembly rather than guaranteed by the file boundary. In this version, with one strategy in the language, the conflict is unreachable in practice; the check exists from day one so that when a second strategy arrives as a use-imported vocabulary (RFD 0028 D7), the invariant is already enforced at the right grain and the refusal is already specified.

D15 — Cross-package edges are refused; in-package only

A #[defeats] edge may cross module (file) boundaries within one package. It may not cross a package boundary — in a workspace holding several packages, the binding constraint is the package, which is strictly tighter than the workspace: a #[defeats] target that resolves into a different package refuses in this version (D17), even though the addressing substrate could name it.

Rationale. A package is a trust and versioning boundary. Cross-package defeat would mean a dependency update could silently change your conclusions — a new version of a package you import could add a #[defeats] edge that overrides one of your defaults, or change which of its clauses your edge resolves to, altering what your program derives with no change to your source. For a legal or contractual corpus that is an unacceptable failure mode: the meaning of your rules must not move underneath you because a dependency published a point release. Within one workspace the author owns every package and every version simultaneously, so there is no unversioned third-party surface and no silent drift; the same author can see and reason about every edge in the component. Cross-package composition through honest heads (ordinary use of a public predicate, strategy- independent) is unaffected and works exactly as it does today — it is only the defeat edge across the package boundary that is refused. Whether cross-package defeat can ever be made safe (a package declaring which of its clauses it exposes as defeatable, versioned as part of its public interface) is left open (D19); refusing it now forecloses nothing and keeps the dangerous case out of a version whose semantics we can fully defend.

A module you import must not be silently overridden without a marker its author placed. In the defeat plane that marker already exists: #[default] (RFD 0028 D1) marks a clause as overridable, and RFD 0028 D4 already refuses any #[defeats] target that resolves to a clause not marked #[default] (strict conclusions are unattackable). That refusal carries the whole consent story across the module boundary unchanged: a cross-module #[defeats] edge is legal only against a #[default] clause, so a rule can be overridden from another file only if its own author wrote #[default] on it. #[default] is the opt-in. A rule with no #[default] marker is strict and cannot be attacked, from its own file or any other.

Combined with D15, the consent rule is: a cross-module #[defeats] target must name a #[default] clause in a module of the same package. The #[default] marker supplies per-clause consent to be overridden; the same-package restriction supplies the versioning guarantee that the consent cannot be exercised by code the author does not control and cannot see change. This is the defensible position: overridability is never implicit (you must write #[default]), and it is never reachable from outside the trust boundary (you must be in the same package). Requiring an additional cross-module-specific consent annotation beyond #[default] was considered and rejected as redundant within a package: #[default] already means “this clause may be displaced by a more specific rule,” and inside one package, authored together, that is the entire meaning the author needs to grant. The heavier annotation belongs to the cross-package case, which is refused here and left open (D19).

D17 — Diagnostics: the failure modes wider resolution introduces

All codes are stated by role; exact numbers are assigned at implementation against the live catalog, per RFD 0028 D11’s discipline (no raw-string emissions; feature-named codes). The defeat plane owns OE0716–OE0721; the intervening numbers are occupied by other subsystems, so the new codes open a fresh slot (OE0736–OE0739) rather than extending that block contiguously.

  • Unresolvable cross-module target (the existing unresolvable-target code, widened). A target misses the import-scoped catalog. The message distinguishes two sub-cases so the fix is obvious: no such name is visible anywhere (a misspelling), versus a matching public name exists in a module this one does not use (add the import, or write the qualified path). Resolution-checked at elaboration; goto-def-able.
  • Ambiguous cross-module target (new). A bare short head name resolves to a public head in more than one imported module. The message names each candidate’s module and instructs the author to disambiguate with the qualified path module::head(args) (D12).
  • Defeat target not visible (new, or folded into the unresolvable code as a third sub-case). A target resolves to a declaration that exists but is not pub in its module, so the attacker cannot name it (D12). The message states that defeat visibility equals name visibility and that the target must be made pub by its own author.
  • Cross-package defeat refused (new). A target resolves into a different package (D15). The message states the in-workspace-only rule and its reason (a dependency update must not silently change your conclusions), and points at honest-head composition as the cross-package tool.
  • Defeat component spans conflicting strategies (new). A component’s modules selected incompatible strategies (D14). The message names the two modules and their strategies. Inert in this version (one strategy), specified for the multi-strategy future.

Every one of these is resolution-checked at elaboration in the combined pass; none is deferred to runtime. (Assigned at implementation: OE0736 ambiguous target, OE0737 target not visible, OE0738 cross-package refused, OE0739 component strategy conflict; OE0716 widened as described.) The existing defeat-plane codes — strict-conclusion-unattackable, defeat-graph-cycle, duplicate-label-per-head, unbound-directive-argument — are unchanged in meaning and wider in scope (the graph and the catalog they range over are now the merged, cross-module ones).

D18 — Semantics: the graph gets bigger, the theorems do not change

The well-founded/grounded semantics of a #[default]/#[defeats] program is unchanged by cross-module resolution. The meaning of the program is still the meaning of its Governatori compilation onto the core stratified/WFS semantics (RFD 0028 D6); cross-module resolution only changes which resolved edges populate the defeat graph the transform consumes. The mechanization already ranges over abstract rule identities and an abstract superiority relation and never refers to a file boundary, so the theorems carry to the merged graph without change in substance:

  • Transform correctness / team defeat / ambiguity blocking / strict-unattackability. Stated over a program’s clauses and its derived blocking sets. A clause’s blocking set is the image of its attackers’ surviving tuples under the resolved edge relation, indifferent to which module an attacker lives in. Merging the graph adds edges to the relation; the per-tuple warrant specification and its equality with the compiled extent are unchanged.
  • Reinstatement across modules (the defeated-defeater property). A defeated defeater stops blocking its target because the blocked set is the image of the attacker’s surviving tuples; if the attacker was itself defeated — by a rule in any module, since the surviving set is computed over the merged graph — the target reinstates. Monotonicity of blocking in the attacker’s surviving set is the load-bearing lemma and is stated over sets of tuples, not modules. Cross-module reinstatement is therefore already covered.
  • Extraction across modules. The defeat-complete extraction theorem preserves a rule’s warrant given its support and all its defeaters’ support are preserved; the defeat-closure it requires is direct-defeat only, over abstract rule identities. When defeaters live in other modules, the closure crosses the module boundary — the theorem’s assumption (“all its defeaters”) is unchanged, only the set it quantifies over now spans files. Extraction operating pre-transform over labeled rules and edges (RFD 0028 D5) is the relevant grain and needs no restatement of substance.

What the mechanization needs, restated for the merged graph:

  1. Cycle detection over the merged graph. Acyclicity (the defeat-graph-cycle refusal) must be decided over the whole connected component, not one file. Two independently-authored, individually- acyclic modules can form a cycle when a new edge connects them — the defeat graph can have a cycle even when the module import graph does not (edges may point up, down, or across the import DAG). Acyclicity is checked on the defeat graph directly, over the whole component; it remains decidable on a finite graph and its computational witness is the combined pass’s cycle finder. This is a restatement of the existing decidable-acyclicity obligation over a wider, still-finite graph.
  2. The topological order the reinstatement fixpoint relies on now ranges over the merged, acyclic component. The defeat-algebra theorems (proved) are unchanged; the topological induction that iterates them (narrowed to the well-founded-semantics catch-up) iterates over the component instead of the file. No new proof obligation beyond re-instantiating the existing one at the component grain.
  3. The extraction signature includes cross-module edges. The theorem statement is unchanged; its edge carrier is the merged edge set.

No new semantic obligation is created; the obligations are the existing RFD 0028 D10 obligations re-instantiated over the merged graph. The differential-oracle corpus (RFD 0028 D10.2) gains cross-module fixtures — a two-file transitive-defeat case and a two-file reinstatement case — so the wider graph is exercised end-to-end against the compiled semantics.

D19 — Open questions this RFD does not close

  • Cross-package defeat. Refused here (D15). Whether it can be made safe — a package declaring a defeatable interface (which of its #[default] clauses it exposes to cross-package attack), versioned as part of its public surface so a dependency bump cannot silently move conclusions — is open. This is the “a regulation package defeats a clause it does not own” aspiration; it is powerful and dangerous, and it needs a versioning story before it can land.
  • Strategy-id ownership across packages. D14 stamps the id on the component within a workspace. When a defeat component could span packages (only if cross-package defeat lands), whose declared strategy vocabulary owns the id, and how a strategy mismatch across a package boundary is surfaced as a dependency-resolution error rather than a build error, is open — coupled to the cross-package question above.
  • A separate defeat-visibility grade. D12 decides defeat visibility equals name visibility. A distinct “defeatable but otherwise private” grade — a rule an author exposes only as a defeat target, not for ordinary use — is a plausible future refinement with no demonstrated need yet; recorded, not adopted. It can be added without breaking D12.
  • Incremental recompute granularity under adversarial editing. D13 scopes invalidation to the connected component. The worst case — an edit that merges or splits a large component (adding the first edge between two big sub-graphs, or removing the only edge joining them) — recomputes the whole merged component. Whether a finer sub-component invalidation is worth its complexity is an implementation-staging question, open.
  • Implementation staging. The pass-lift (D13), the resolution widening (D12), the component- strategy stamping (D14), and the new diagnostics (D17) are separable slices; the order in which they land, and whether the strategy-conflict machinery (inert with one strategy) waits for the second strategy, is open and is a sequencing decision, not a design one.

Rationale (summary)

  • Import-scoped resolution puts the override on the overriding rule, in its own file, goto-def-able — the compositionality the defeat plane exists to deliver, now across files. It reuses the existing three spellings and the existing qualified-catalog substrate; nothing about the surface changes.
  • Lifting the pass to workspace assembly is the minimal pipeline change that makes cross-declaration resolution and cycle detection see more than one file, and scoping its validity to the connected component preserves separate compilation of a section.
  • Putting the strategy id on the component and checking the invariant at assembly reconstructs exactly the guarantee the file boundary gave for free (D7), at the grain that will still hold when a second strategy arrives.
  • Refusing cross-package defeat keeps the version whose semantics we can fully defend: overridability is explicit (#[default]) and unreachable from outside the trust boundary, so no dependency update can silently change a program’s conclusions.

Alternatives considered

  • Keep the defeat plane file-local; model cross-section override with provisional facts + central negation-as-failure adjudication. This works cross-file today and scales, and it stays available — nothing here removes it. It is rejected as the answer to this RFD’s question because it relocates the override semantics away from the overriding rule into a central adjudication module and a scope lattice, which is the compositionality defect the defeat plane exists to fix. It is the right tool when engine-level proof tags and strategy diagnostics are not needed; it is not the surface the statutory-corpus modeling effort is reaching for.
  • Collapse a defeat cluster into one file. The only way the file-local plane spans sections today. Rejected at statute scale: it destroys the one-file-per-section layout, kills separate compilation, and does not compose across packages at all. It remains fine for a small, tightly- coupled cluster that genuinely fits one file.
  • A separate defeat-only visibility grade. Rejected for this version (D12): a second visibility axis with no demonstrated need; addable later without breaking D12.
  • Allow cross-package defeat now. Rejected (D15): it lets a dependency update silently change a program’s conclusions, unacceptable for a legal/contractual corpus, and it needs a versioned defeatable-interface story that does not yet exist.
  • A per-file “file-local misspelling” refusal kept alongside the combined pass. Rejected (D13): a file cannot distinguish a misspelling from a valid cross-module reference without the import closure, so a per-file resolution refusal would false-positive on every valid cross-module edge.

Consequences

  • Nothing breaks. The feature is additive. Every program that compiles today has a defeat plane whose edges all resolve file-locally; under the combined pass each such module is a single-module connected component that resolves and validates exactly as before, and pays no new incremental- recompute cost.
  • The tax-corpus authoring pattern it enables: one file per statute section, each section reading like the statute, exceptions carrying their own attacks across files under honest heads.
  • The build pipeline gains a workspace-assembly phase in oxc-instantiate; the wire record gains a component strategy id (stamped on every participating module); the diagnostic catalog gains the D17 codes.
  • The mechanization’s RFD 0028 D10 obligations are re-instantiated over the merged graph; the differential-oracle corpus gains cross-module fixtures. No new semantic obligation is created.

What changes for the author

Before this RFD, a #[defeats] target had to name a rule in the same file; the only way to make an exception overridable across provisions was to collapse the provisions into one file. Now:

  • What you can write. All three target spellings — head(args), head.label(args), Trait::member(args) @ Type — may name a rule in another file, provided your module can see it: import the RULE (a use leaf, glob, or alias) and the bare spelling resolves exactly as a local one does; import only its MODULE and the qualified-path spelling module::head(args) names it — the same reading every other name position has. The qualified path also disambiguates when a bare short name is visible from more than one imported module.
  • What the target must be. A pub #[default] clause in a module of the same package — the same consent rule as before (#[default] is the opt-in), now carrying across files.
  • New refusals you can hit, and their fixes. A target that exists but is not imported refuses with the not-imported sub-case of the unresolvable-target code (fix: add the use, or write the qualified path). A bare name visible from two imported modules refuses as ambiguous (fix: qualified path). A non-pub target refuses (fix: its author makes it pub). A target in another package refuses (no fix by design — compose through public predicates instead). A cycle formed by edges across files refuses exactly as a local cycle does.
  • What does not change. Every file-local program keeps its exact meaning and messages; runtime evaluation is untouched; no new syntax exists — these are the existing directives with a wider resolution scope.

Worked example — a general basis rule overridden across files

Two files, two modules, one package. section_1012.ar states the general rule that a property’s basis is its cost. section_1016.ar states the exception: certain adjustments displace that general basis. The exception names what it overrides, in its own file.

// section_1012.ar — the general rule: basis is cost. Overridable by a more specific provision.
pub derive is_property(taxpayer, property) :- /* … acquisition facts … */ ;

#[default]
#[label(section_1012_a)]
pub derive cost_basis(taxpayer, property) :-
    is_property(taxpayer, property),
    acquisition_cost(taxpayer, property, _);
// section_1016.ar — the exception: an adjustment to basis displaces the general cost basis.
use pkg::usc26::subchapter_o::section_1012;   // binds the module name; its members
                                              // are addressable as `section_1012::…`

// The overriding rule names exactly the clause it overrides, in the file where the override lives.
#[defeats(section_1012::cost_basis.section_1012_a(taxpayer, property))]
pub derive adjusted_basis(taxpayer, property) :-
    is_property(taxpayer, property),
    basis_adjustment(taxpayer, property, _);

Reading the two files: section 1012 states that cost is the default basis and marks it #[default], granting consent to be overridden by a more specific provision (D16). Section 1016 states the adjustment rule and, on that rule, declares that it defeats section 1012(a)’s cost basis for the same (taxpayer, property) tuple (per-tuple blocking, RFD 0028 D3). The combined pass resolves section_1012::cost_basis.section_1012_a against section 1016’s import-scoped catalog (it imports the section-1012 module, so the qualified spelling names it — D12), confirms the target is #[default] (D16) and in the same package (D15), places the two rules in one connected defeat component, stamps the strategy id on both modules (D14), and checks the merged graph is acyclic (D18). Where an adjustment applies, cost_basis is blocked for that tuple and adjusted_basis supplies the basis; where no adjustment applies, the default cost basis survives. Editing section 1016 re-runs the defeat pass over this two-module component only, not the whole title (D13).

Relationship to RFD 0028

This RFD is a successor amendment to RFD 0028 (Defeasibility redesign). It continues that RFD’s D-numbering (D12–D19) and discharges the deferred build-pipeline work its D3 v1-scope amendment named, tracked as issue #362. It changes nothing about RFD 0028’s decided surface (honest heads, the directive plane, the three target spellings — D1–D3), the attack discipline (D4), the strategy-as-compilation architecture and its four hooks (D5), the Governatori strategy (D6), or the proof-tag channel (D8). It amends RFD 0028 D3 (target resolution becomes import-scoped, workspace-assembly-time, not file-local), RFD 0028 D7 (the single-strategy-per-connected-graph invariant is checked at workspace assembly and owned by the connected component, not guaranteed by the file boundary), and RFD 0028 D11 (the diagnostic inventory gains the D17 codes). The Lean obligations are RFD 0028 D10 re-instantiated over the merged defeat graph, with no new obligation in substance.

Open questions

Collected from D19: cross-package defeat and its versioned defeatable-interface story; strategy-id ownership across packages; a separate defeat-visibility grade; incremental-recompute granularity when an edit merges or splits a large component; and implementation staging of the separable slices (pass-lift, resolution widening, component-strategy stamping, diagnostics) including whether the inert strategy-conflict machinery waits for a second strategy to exist.

RFD 0083 — Defeat-plane compilation: the Governatori transform as elaboration-time strict rules

  • State: committed
  • Opened: 2026-07-21
  • Decides: the defeasibility semantics of RFD 0028 — honest heads, the defeat-directive plane, Governatori-with-explicit-superiority as strategy #1 — are realized by compiling the mechanized transform at elaboration time into ordinary strict WFS rules emitted into the .oxbin, replacing the interpretive runtime resolver (oxc-runtime/src/defeasible.rs). RFD 0028’s decisions D1–D11 are unchanged; this RFD only changes where and how strategy #1’s compilation scheme (D5 hook 3, D6) runs — from a runtime survivor-traversal to a lowering-time program transform. Settles the semantics question left open by #1791 (recursive #[default] dependencies): the recursive fixpoint is defined, not refused. The regression floor is partly in place: #1789 (for #1788) has merged; the #1790 reinstatement fix and the interim fail-open hardening (#1796, cone-sensitive reentrancy) remain open. Folds in #1792 structurally. Green-lit after the ground-truth oracle (#1793, merged) exposed the bug family’s shared architectural signature; the recursive-fixpoint Lean entry gate is delivered in #1797 (CompiledFixpoint.lean, D6, merged).

In plain language — what changes and why

Nothing about the language changes. #[default] rules, #[defeats] edges, and every program that uses them keep their exact meaning; no program is rewritten, refused, or reinterpreted. What changes is how the engine carries that meaning out.

Today, defeat is executed by a dedicated interpreter inside the runtime: when a query touches a defeasible head, a resolver walks the defeat graph head by head, computing which clauses survive, caching intermediate results, and guarding itself against revisiting a head it is already working on. That machinery re-implements, by hand, work the rule evaluator already knows how to do — and the recurring wrong-law bugs all came from exactly the hand-made parts: results that depended on the order heads were resolved, a cache that could be poisoned by a partial answer, and a self-protection fallback that served pre-defeat rows when startled.

After this change, defeat is compiled instead of interpreted: at build time, each #[default] clause and #[defeats] edge is translated into a few ordinary strict rules (a rule for the clause’s support, a rule deriving a blocked marker from surviving attackers, and a public rule saying “the head holds where supported and not blocked”). The ordinary evaluator then runs those rules like any others. Fixpoint iteration, ordering, reinstatement chains, and depth-independence stop being properties we implement and test for — they are what the evaluator does by construction, for every rule.

Why we trust the translation: it is not designed here — it is the construction the mechanized semantics already specifies and proves correct, and the executable oracle (the reference evaluator plus generated-program differential) accepts the compiled path only if it agrees with that semantics across every generated program. Why now: the interpreter’s bug family kept reaching users as silently wrong derived law; and the incremental-circuits work compiles rules, so an interpreted defeat plane would need permanent special-casing there, while a compiled one is supported for free.

Question

RFD 0028 D6 promoted a promise from implementation note to definition: the meaning of a #[default]/#[defeats] program is the meaning of its compiled core program. The shipped implementation did not honor that. It kept an interpretive runtime resolver — a per-head survivor traversal that resolves the defeat graph on every derive query, computing the warranted extent by folding per-clause contributions against blocking sets built from surviving attackers, in topological order, guarded by a reentrancy check.

That resolver has produced a family of correctness defects that share one signature. The question: is the resolver fixable in place, or does the architecture itself have to change — and if it changes, to what, and what does the substrate have to prove first?

Context — the bug family and its shared signature

The resolver (Store::defeasible_survivors, oxc-runtime/src/defeasible.rs, Store::defeasible_survivors) approximates a well-founded / team-defeat fixpoint with three mechanisms that together are the fault line:

  1. a per-head memo (DefeasibleSurvivorsMemo) keyed by relation short name, mutated as heads resolve;
  2. a reentrancy guard (visiting: BTreeSet<String>, let reentrant = !visiting.insert(...), the visiting-stack guard in defeasible.rs); and
  3. a fail-open fallback — on re-entry the resolver serves catalogs.support.clone(), the raw pre-defeat extent (the re-entry arm of defeasible_survivors), and writes that under-computed result into the shared memo.

Every defect is a facet of these three:

Issue / PRSymptomShape classMechanism
#1788 / PR #1789Defeat filtering lost beyond stacked #[default] readers; answers change with head-resolution orderOrder-dependent memo poisoningOver-broad dependency seeding trips the reentrancy fallback; the raw extent is memoized and poisons later readers. Bound is stack order, not chain length.
#1790A reinstated (pardoned defeated-defeater) tuple fails to propagate to a second stacked readerPropagation under-suppressionThe same surviving-extent seeding bookkeeping, failing in the opposite direction — the mirror image of #1788.
#1791A recursive #[default] head (a default reading itself transitively) is served its raw pre-defeat extentFail-open termination fallback / declaration-plane gapElaboration refuses cycles only in the defeat graph (OE0718), never in the rule-dependency graph; the accepted surface is wider than the resolver’s total-correctness domain, and the guard chosen for termination is unsound.
#1792Two same-short-named defeasible heads in different modules mergeNamespace-collision keyingThe resolver’s internal sets/memos key on un-namespaced short names while cross-module reads (RFD 0082) become first-class.

Three of the four (#1788, #1791, #1792) are the same primitive: the fail-open reentrancy fallback writing raw pre-defeat data into a shared, order-sensitive, short-name-keyed memo. #1790 is that bookkeeping failing in the suppression direction. The mechanized transform (spec/lean/Argon/Reasoning/Defeasibility/Transform.lean) already specifies the sound target — a topological team-defeat fold over blocking derived from surviving attackers, F2 reinstatement, strict-unattackability — and proves the defeat algebra; the runtime diverges from it by approximation. The indictment is architectural: an interpretive resolver’s correctness depends on resolution order, exact seed scope, key namespacing, and elaboration-time graph guarantees that do not all hold, and no local patch removes that dependence — #1789 removed one spurious trigger, but the fail-open branch remains reachable on accepted programs (#1791).

The ground-truth oracle (PR #1793, merged) made this measurable: a brute-force reference evaluator of RFD 0028 D2 (the executable mirror of Transform.lean), a seeded program generator, a #1788 leak ladder, and an env-gated soak (generated_differential_soak), wired as a differential against the production .oxbin/Store path (oxc-runtime/tests/defeat_plane_oracle.rs). It is the instrument that turned each defect into a repeatable divergence and revealed they are one shape.

The strategic alignment is independent and points the same way. The DBSP circuit program (RFD 0018, lines 60/136) delivers defeasibility as Maher’s three-stratum compilation — the defeat plane becomes ordinary stratified Datalog with proof tags as derived-predicate labellings, which sits inside DBSP’s proven, standard incremental fragment (positive recursion + stratified NAF). An interpretive resolver is not a set of stratified derived predicates; it cannot be lifted by the circuit’s lift(Q) and would force out-of-band special-casing the circuit cannot incrementalize (the only piece needing a new kernel — recursive-WFS-on-DBSP — is explicitly deferred and is the richer, ambiguity-propagating defeasibility, not the compiled plane). Compiled defeat is circuit-ready by construction; interpretive defeat is a permanent exception.

Decision

Compile the defeat plane at elaboration time. The strategy transform (RFD 0028 D5 hook 3) runs in oxc-instantiate at lowering, beside the existing defeat validation/stamping pass (lower/defeat.rs::resolve_workspace_defeat_plane), and emits the Governatori compilation as ordinary strict rules into the .oxbin. Everything downstream of lowering — the reasoner, the runtime, the projection cache, the DBSP circuit — sees an ordinary WFS program. The interpretive resolver in oxc-runtime/src/defeasible.rs is deleted. There is no defeat-specific evaluation path.

The architectural contrast in one figure — a hand-rolled fixpoint recomputed per query versus a program transform run once at lowering:

  INTERPRETER  (deleted)                      TRANSFORM  (this RFD)
  ──────────────────────                      ─────────────────────
  every derive query, per head:               elaboration, once per source:
    Store::defeasible_survivors                  resolve_workspace_defeat_plane emits, per head:
      topological walk over heads                  $defeat::support::<clause>   (support)
      per-head memo  (short-name key) ◀─┐          $defeat::block::<head>[::<label>]
      reentrancy guard ──── fail-open ──┘            H :- support, not block     (public)
        catalogs.support.clone()                → .oxbin: ordinary strict WFS rules
        (raw pre-defeat extent)                  ─────────────────────────────────────
    ── fixpoint computed BY HAND ──             runtime: one Engine::evaluate
      order-, scope-, key-dependent               the WFS fixpoint, computed NATIVELY
      (the bug family)                            (NAF memo, cone slicing, projection
                                                   cache all apply — for free)

The memo, the guard, and the fail-open fallback — the whole left column — have no counterpart on the right. They were an interpreter’s way of computing a fixpoint the evaluator computes natively.

D1 — What the elaborated program looks like

For each defeasible head H (a head with a #[default] clause, or a #[defeats] target), the transform emits, per Transform.lean’s construction:

  • Support atoms — the per-clause contribution over the converged catalog. Each source clause of H lowers to a strict rule whose head is an internal support relation carrying the clause identity; recursion in the clause body reads the converged public head (the F1 fix, defeasible_clause_contribution made static — the head is never cleared).
  • Blocking atoms — for each #[defeats] edge into H, a strict rule deriving an internal blocking relation from the attacker’s surviving support projected through the edge’s resolved argument binding. Because the attacker’s surviving support is itself an emitted relation (recursively transformed), a defeated defeater contributes no blocking tuple — F2 (blockFromSurvivors, defeated_defeater_does_not_block) becomes a structural NAF join, not a runtime intersection.
  • The team-defeat fold — the public head H is emitted as strict rules projecting each clause’s support NAF-guarded against the applicable blocking relation (head-level blocking for every default clause; clause-level blocking naming the clause’s #[label]). A strict (non-#[default]) clause projects unguarded — strict-unattackability (strict_clause_unattackable) is the absence of a NAF guard, not a runtime flag. An unbeaten teammate keeps the conclusion because its unguarded (or unblocked) projection still fires (team defeat, team_defeat_unbeaten_survives).
  • Reinstatement via F2 — falls out: the blocking relation reads surviving attacker support, which is itself a fold over its blocking, so a pardoned attacker’s blocking tuple is simply never derived.

The blocking guards introduce NAF; the target/attacker resolution order the interpreter walked by hand is now the reasoner’s ordinary WFS evaluation of a stratified-NAF (or, on a target↔attacker negative cycle, genuinely well-founded) program. The topological traversal, the memo, the guard, and the fallback have no counterpart — they were an interpreter’s way of computing a fixpoint the evaluator computes natively.

Naming discipline. Internal relations are minted under a $-prefixed family, qualified by the source head’s catalog path — e.g. $defeat::support::<qpath>::<label>, $defeat::block::<qpath>, $defeat::block::<qpath>::<label> — never short names. Qualified keying is the structural fix for #1792: the short-name collision class cannot recur because there are no short-name keys. Internal heads are non-public (excluded from query surfaces, SDK generation, and extraction signatures); an old generic reader that walks all rules sees ordinary RuleDecl bodies with unfamiliar $-names and executes them harmlessly (they are range-restricted strict rules like any other).

Worked example — one compiled program, rule for rule

A minimal defeat-bearing program: one #[default] (a general rule), one #[defeats] (a stricter exception), one downstream reader. Surface (illustrative):

#[default] #[label(general)]
eligible(P) :- resident(P).       // the default: residents are eligible, defeasibly

#[defeats(general)]
excluded(P) :- felon(P).          // the defeater: felons are excluded, defeating `general`

canVote(P) :- eligible(P).        // the reader: a downstream consumer of the defeasible head

The transform emits (canonical $-qualified names abbreviated; Transform.lean / CompiledFixpoint.lean give the authoritative construction):

$defeat::support::eligible::general(P) :- resident(P).      (1) support
$defeat::support::excluded::c0(P)      :- felon(P).         (2) support (attacker)
excluded(P) :- $defeat::support::excluded::c0(P).           (3) public head (strict clause)
$defeat::block::eligible::general(P)   :- excluded(P).      (4) block
eligible(P) :- $defeat::support::eligible::general(P),      (5) public head (default clause)
               not $defeat::block::eligible::general(P).
canVote(P) :- eligible(P).                                  (6) reader (unchanged)

One sentence per rule: (1) the default clause’s per-clause contribution over the converged catalog — a plain strict rule, no guard. (2) the attacker’s contribution, likewise. (3) the attacker’s public head; excluded is a strict (non-#[default]) clause, so it projects unguarded — strict-unattackability is the absence of a NAF guard, not a runtime flag. (4) the blocking relation for the general clause reads the attacker’s surviving support, which is exactly its public head excluded (F2 as a structural NAF join: a defeated defeater would have no excluded tuple, so no block tuple). (5) the public head eligible projects its support NAF-guarded against the blocking relation — the team-defeat fold; a felon resident derives $defeat::block::eligible::general and so eligible does not fire, while a non-felon resident is unblocked and keeps the conclusion. (6) the reader is untouched — it reads the ordinary public head eligible like any relation. The topological walk, the memo, the guard, and the fallback have no rule here.

The recursive-default flip (#1791), where the resolver mis-served. A default that reads itself transitively:

#[default] reaches(X, Y) :- edge(X, Y).                     // base
#[default] reaches(X, Z) :- edge(X, Y), reaches(Y, Z).      // recursive step

There is no #[defeats] edge, so no blocking relation is emitted; the compiled program is:

$defeat::support::reaches::c0(X,Y) :- edge(X,Y).
$defeat::support::reaches::c1(X,Z) :- edge(X,Y), reaches(Y,Z).   // recursion reads the
                                                                  // CONVERGED public head (F1)
reaches(X,Y) :- $defeat::support::reaches::c0(X,Y).
reaches(X,Z) :- $defeat::support::reaches::c1(X,Z).

With no live defeater the public head is the ordinary least fixpoint — the full transitive closure (D2 case 2, WFS-native Datalog recursion). The interpreter mis-served here: its reentrancy guard fired on the self-recursive head and returned the raw pre-defeat one-hop extent (catalogs.support.clone()), so multi-hop reaches tuples were lost. The compiled path has no guard to fire and derives the complete closure — the rewrite’s headline correctness fix.

D2 — Recursive defaults (the Lean prerequisite; settles #1791)

Three cases must be distinguished; the interpreter conflated the first two into its fail-open branch:

  1. Defeat-graph cycle (attacker/target identities form a cycle): refuse, OE0718, unchanged. This is where the well-behaved compilation stories diverge (RFD 0028 D4); v1 picks none.
  2. Positive rule-dependency cycle among #[default] heads, with no defeat edge among them (a default head recursive through itself or mutually with peers, e.g. transitive closure marked #[default]): admit. This is ordinary Datalog recursion; WFS defines it. The interpreter refused it only by accident (the reentrancy guard), an interpreter limitation with no cause once compilation removes the recursive host-language calls.
  3. A #[default] head that is both recursive and a defeat target (its blocking guard reads an attacker that recursively depends on it): admit. The emitted blocking guard is a NAF atom; recursion through NAF is precisely WFS’s domain. Under the compiled program the head’s extent is the WFS model — true where warranted, possibly undefined on a genuine target↔attacker negative cycle. RFD 0028 D6 already defines defeasible meaning as the compiled program’s meaning, so an undefined tuple is a defined outcome, not a resolver failure. No OE1317-style stratification gate is added for defeat — OE1317 exists because recursion through aggregation lacks a fixpoint; recursion through NAF has WFS. One existing gate is adjacent and must be respected, not contradicted: OE1365 (NegatedWfsRelation, oxc-reasoning compile/stratify.rs) refuses a cross-stratum not R where R is a WFS SCC that can carry undefined atoms — the honest interim until three-valued undefined propagates into derive heads (#134/#165). This does not gate case 3: a head both recursive and a defeat target sits in one WFS SCC with its own blocking guard (same-SCC NAF is evaluated by the well-founded evaluator directly, never OE1365-refused). The emission must keep that property — the compiled H :- support, not $defeat::block::… and the negated blocking relation stay in a single WFS SCC whenever a target↔attacker negative cycle exists, so no cross-stratum not over an undefined-carrying blocking relation is produced. Establishing this (or refusing the residual split shape) is a D6 obligation, called out there.

The fail-open fallback (#1791) dies structurally: a compiled program has no reentrancy guard and no raw-support branch to fall into. The declaration-plane gap closes not by widening OE0718 to the rule-dependency graph but by making the rule-dependency graph a legal WFS input.

This is the rewrite’s entry gate. Transform.lean today is honest that it proves the defeat algebra over a clause’s contribution as a converged input and narrows the fixpoint↔attribution correspondence for genuinely recursive heads to the WFS catch-up (#134), backed by differential-oracle corpus tests (defeasible_recursion_v0, defeasible_defeated_defeater_v0). Compiling the transform makes that correspondence load-bearing: the emitted program’s WFS model is the shipped semantics, so the theorems must state it. The Lean prerequisite is scoped in D6 and delivered in PR #1797 (CompiledFixpoint.lean, merged) plus the #134 Part A stratified-defeat bridge; the only surviving reduction is the positive-recursive-stratum lemma reported in D6.

D3 — Wire discipline, artifact, and versioning

The transform runs at elaboration, so a naive artifact would carry both the executable defeat metadata (the GovernatoriSuperiority strategy id and resolved DefeatEdge sets stamped by resolve_workspace_defeat_plane) and the emitted compiled rules — inviting a runtime that still had an interpreter to re-resolve the plane over the public heads and double-apply defeat. The wire discipline forecloses that:

  • Executable rules carry only the compiled strict program. For every derive rule of a defeat-bearing module, the emitted .oxbin sets is_default = false, defeats = [], defeat_strategy = None. The original clauses are not executable rules; the public head and $-internal helpers are ordinary strict rules. An old runtime therefore evaluates the compiled program and cannot re-interpret — there is nothing left to trigger an interpreter.
  • A separate, additive, inert provenance carrier (DefeatCompilationProvenance, module-level) retains what the executable rules shed: representation = compiled, the strategy id, source rule identities, #[label]s, #[default] markers, resolved edges + bindings, and the deterministic map from source clauses to emitted rules and proof-tag classes. Reflection, ox derive --explain, provenance, extraction audit, and the D8 proof-tag surface read this carrier; evaluation never does. This is what preserves RFD 0028 D8 (proof tags computed from defeasibility metadata) and the audit surface after the executable metadata is stripped.

Consequences for versioning:

  • The artifact grows, linearly in clauses plus defeat edges (added RuleDecl bodies, symbols, $-relations) plus the provenance carrier. This is a normal artifact-content change: the artifact hash moves for any compiled program, exactly as any lowering change moves it.
  • No Core-IR version bump. No RuleIR / AtomIR / Term shape changes — only more ordinary rules. Adding rules is not an IR-shape change.
  • .oxbin format minor bump for the additive DefeatCompilationProvenance section carrier (backward-safe: an old runtime ignores an additive section and evaluates the compiled rules).
  • DERIVATION_SEMANTIC_EPOCH (oxc-runtime/src/lib.rs, the constant gating persisted projection-cache validity per RFD 0072 D5) is not bumped for the compile flip, and no new committed field is added. The switch is elaboration-time (D4): flipping it changes which artifact elaboration emits for a given source — a legacy-metadata artifact or a compiled-strict-program artifact — not which path a runtime takes over a fixed artifact. The two artifacts have distinct hashes. The artifact hash is the discriminator: it is already an input to oxc_oxbin::projection_cache::derivation_commitment, so a cache built against one artifact can never be served against the other — the caches separate with no derivation_commitment change. For any fixed artifact both representations of the runtime produce the same projection: a compiled artifact evaluates its strict rules identically on any runtime (a pre-rewrite runtime no-ops the absent defeat plane and runs the compiled rules — see D4); a legacy artifact drives the resolver on any runtime that still has one. Nothing changes a derived result for a fixed artifact, so there is no same-artifact dual-path hazard to guard — the D3-stripping-vs-same-artifact contradiction that a runtime switch would create does not arise. Hence no epoch bump and no derivation_commitment slot for the switch value. (The compiler/AGENTS.md contract — “any semantic change under materialize/prepare/evaluate MUST bump DERIVATION_SEMANTIC_EPOCH” — is satisfied vacuously: the semantic change rides the artifact hash, which the contract already covers, not the epoch.)

D4 — Migration and the elaboration switch

The switch is an elaboration-time flag, not a runtime evaluation mode. Env var, house OX_/OXC_ family: OXC_DEFEAT_ELABORATE (1 = elaboration compiles the defeat plane into strict rules per D1; 0 = elaboration emits the legacy executable defeat metadata for the retained runtime resolver). Flipping it selects which artifact elaboration produces for a given source, not which path a runtime takes over a fixed artifact — so the two settings yield two different artifacts whose hashes already separate every downstream cache (D3): no derivation_commitment change and no epoch bump accompany the flip. It is still a kill switch in the compiler/AGENTS.md sense — an emission switch that gates a semantic change is a kill switch too — and it obeys the same taxonomy (house-flag naming, staged default, incident-rollback reach); it simply discriminates via the artifact hash rather than a runtime branch.

  • Phase 1: default 0 — elaboration emits legacy metadata; the compiled artifact is opt-in, exercised by the oracle in CI. The interpretive resolver is retained and drives legacy artifacts.

  • Phase 2: default 1 — elaboration compiles by default; legacy emission reachable for incident rollback only. The resolver is retained through the soak (it still evaluates any legacy artifact and the rollback path).

  • Phase 3: legacy emission removed and defeasible.rs deleted together. The resolver exists precisely to evaluate legacy artifacts, so it is deleted only when legacy emission is — one step, not two.

  • Migration story.

    • (a) Old artifacts (legacy metadata, emitted before the flip) continue through the retained resolver during the soak window, unchanged and unaffected by the flip; recompiling their source under =1 produces the compiled artifact. The resolver is not deleted until legacy emission is (Phase 3), so no old artifact is ever left without an evaluator.
    • (b) The soak compares the two artifacts’ models — compiled vs legacy for the same source — which is exactly the oracle differential + EmitVectors (D5): one source, two elaboration outputs, model equality.
    • (c) Mixed deployments are forward- and backward-safe. A pre-rewrite runtime given a compiled artifact finds no defeat metadata (has_defeat_plane() false → strict path, projection_consume.rs:268) and evaluates the compiled strict rules correctly, with no resolver involvement (forward-compat verified in review). A post-rewrite runtime given a legacy artifact drives the retained resolver, unchanged.
  • Rollout order. #1789 (#1788) lands first, on the interpreter — merged; its dependency-cone seeding fix and conformance ladder (the #1788 leak ladder) are the first course of the regression floor. The #1790 reinstatement fix and the interim fail-open hardening (#1796, cone-sensitive reentrancy) are still open on the interpreter; their conformance chains complete the floor. The compiled path must be oracle-clean on every case those fixes made pass, and must additionally fix the #1791 recursive-default case they leave open (the oracle’s known-failure entry for #1791 flips to agreement, which the harness’s stale-entry guard forces to be edited out).

  • Acceptance gate. The compiled path is accepted when it is oracle-clean against the merged oracle (PR #1793): (a) zero new divergences over the deterministic CI seed set (the 64 pinned seeds run in-CI); (b) every pinned known-failure the rewrite fixes now agrees, forcing its removal from the expected-divergence list; (c) the env-gated soak (generated_differential_soak, OXC_DEFEAT_PLANE_SOAK=<count>; single-seed replay via OXC_DEFEAT_PLANE_SEED=<n>; run under binary(defeat_plane_oracle)) stays clean at the standing count. The accepted Phase-2 gate is the standing 1000-seed differential soak run clean against the flip candidate, alongside the deterministic per-CI 64-seed set (a). (An earlier revision named a one-off owner-scheduled acceptance-scale run of ~10⁶ seeds before the flip; that extra-large run was retired — the standing 1000-seed differential is the accepted gate, the 64-seed set runs per-CI.) Acceptance is soak-clean plus the emission vectors (D5).

D5 — The Lean golden transformed-program vectors (canonical-form check)

The oracle (D4) checks the compiled path against an independent reference evaluator. This RFD additionally commits to checking the production transform against the mechanized transform, rule-for-rule, via the existing EmitVectors infrastructure (spec/lean/EmitVectors.lean, lake exe emit-vectors <layer>compiler/tests/lean-vectors/, replayed by a Rust conformance harness alongside conformance_replay.rs). Concretely, a new defeasible vector layer:

  • Lean emits, per vector: the normalized defeasible input (clauses, rule identities, labels, default flags, resolved targets + bindings); the expected compiled strict program in canonical form (deterministic public-projection and $-helper names, heads, positive atoms, NAF atoms, canonical rule order); and the Lean-computed WFS result (true extents, undefined extents, proof-tag attribution). The compiled program is produced by Transform.lean’s construction, not hand-written.
  • Rust asserts: (1) feed the normalized input through the production transform; (2) canonically serialize the emitted strict rules; (3) assert equality with Lean’s compiled-program payload — rule-for-rule, no missing or extra rules (this is the “byte-level transformed-program check”: canonical-form equality of the emitted rule set, not raw .oxbin bytes, which EmitVectors does not carry); (4) evaluate the produced program through Engine::evaluate; (5) assert true/undefined/proof-tag results equal Lean’s model. Steps 3 and 5 separately detect transform drift and evaluator drift.

Honest scope: EmitVectors emits computed semantic models, and its EvalProgram Term/AtomIR are oracle twins outside the Argon.lean mech-audited closure (spec/lean/AGENTS.md). The defeasible layer inherits that oracle-vs-canonical split; the canonical serialization it diffs against is the transform’s normalized output form, defined by this layer, not the canonical CoreIR shape.

D6 — Lean prerequisite (scoped here; delivered in PR #1797, merged)

Transform.lean must be lifted from defeat algebra over a converged contribution to the recursive fixpoint the compiled program computes (D2). This is the rewrite’s entry gate. It is scoped by this RFD and delivered (merged) in PR #1797 (spec/lean/Argon/Reasoning/Defeasibility/CompiledFixpoint.lean, ~635 lines added; no sorry, no axiom; the full Argon library builds). The transform emits an ordinary Datalog.Program whose wfTrue is the shipped semantics, reusing wfModel / wfTrue / wfModel_union_old — no second evaluator. PR #1797 (merged) introduced the obligations below and their theorems:

  • Replace Clause.support : Set Tuple (an opaque converged input) with rule bodies interpreted against a global WFS approximation/model — reuse Reasoning/WellFounded.wfModel and the compiled-rule adequacy of Reasoning/Compiled (CRule.fire_eq_TP); do not define a second evaluator.
  • Define qualified rule/head identity and the whole-program defeat DAG (the latter already decidable, DefeatGraph / decidableSelfLoop).
  • Define the executable transform to a Datalog.Program: support relations, attacker-survival / blocking relations, per-clause NAF-guarded survival, and public-head projection.
  • Prove: (i) emitted $-names are fresh (no collision with source vocabulary — the Extension.wfModel_union_old conservativity shape); (ii) the transform preserves safety/range-restriction (Compiled.Safe.head_determined, the OE1303 witness); (iii) a recursive clause’s support equals the transformed program’s WFS support projection (the F1 correspondence, proved for the block-free and stratified-defeat fragments in #134 Part A); (iv) induction over the acyclic defeat DAG yields F2/reinstatement (lift the existing pardoned_target_survives from opaque blocking to derived blocking under the fixpoint); (v) the public-head WFS projection equals declarative warrant (warranted), the D10.1 theorem now over the recursive construction; (vi) the result is independent of rule/head enumeration order (the property the interpreter’s memo violated — #1788); (vii) the emitted stratification keeps every negated blocking relation and the head that negates it in one WFS SCC (proved as negated_block_same_scc in CompiledFixpoint.lean) on a target↔attacker negative cycle, so the OE1365 cross-stratum-not-over-undefined gate (D2) never fires on a compiled defeat program — or, if a residual split shape is reachable, it is refused rather than silently under-defined.
  • Retire the RuleStrength mirror with the surface triple (RFD 0028 D10.6) if not already gone.

Delivered status (PR #1797, merged), obligation-by-obligation. Obligations (i) rule bodies over a global WFS model with no second evaluator (emitted, wfTrue_mem_iff, wfPossible_mem_iff), (ii) fresh $-names / non-collision (internal_atoms_fresh, emitted_internal_HeadsIn; helpers_conservative is an abstract conservativity property over internal-avoiding base programs, never instantiable at emitted itself, whose public rules read internal atoms), (iv) preserved range-restriction (pubRules_range_restricted, blockRules_range_restricted), F2/reinstatement over derived blocking inside the fixpoint (block_extent_iff, derived_block_pardoned, derived_pardoned_target_survives), enumeration-order independence — the property the interpreter’s memo violated, #1788 (emitted is a Set; suppFromList_perm_invariant), and (vii) the emitted NAF staying within one WFS SCC so OE1365 never fires (negated_block_same_scc) are proved. The decision-table worked examples are discharged against Transform.warranted (the franchise ladder verdicts).

The stratified-defeat bridge — delivered (#134 Part A). Obligations (iii) recursive-support correspondence and (v)/(vi) public-head WFS projection = declarative warrant are proved for the block-free / positive fragment (pub_projection_blockfree) and for the stratified-defeat fragment — a head that is a live defeat target but whose defeat graph is acyclic (no target↔attacker mutual-defeat cycle). There the two-valued warrant genuinely exists and is bridged: the blocking layer is positive over the attacker’s public head, so given decided attackers it collapses to two-valued (block_two_valued, blockL_two_valued); the public head is the two-valued team-defeat fold over that fixpoint-derived blocking (pub_extent_iff); two-valuedness propagates from the attacker stratum to the target stratum (stratified_head_step); and for the acyclic non-recursive fragment the whole well-founded model is provably two-valued with no undefined atom (stratified_two_valued, stratified_wfModel_two_valued), witnessed end-to-end on a live-defeat program (voteLadder_two_valued, voteLadder_ben_definitely_false: ben’s franchise is definitely false, not undefined). This discharges the no-cycle half of (vii) for the acyclic non-recursive sub-fragment as a theorem-plus-inspection (stratified_wfModel_two_valued) rather than prose alone; the general no-cycle case with positive recursion inside a stratum remains the reduction named below.

What the mutual-defeat cycle is — a decision, not a remainder. For a head that is simultaneously recursive and a defeat target with a genuine target↔attacker negative cycle (D2 case 3 — a rule-dependency cycle through the attacker’s derivation, not the defeat-graph identity cycle OE0718 refuses), there is no two-valued declarative warrant to bridge to: the cyclic heads are undefined in the well-founded model, and — per RFD 0028 D6, meaning is the compiled program’s meaning — that three-valued reading is the canonical semantics. This is exactly the region where the deleted interpretive resolver was order-dependent (the topological memo / reentrancy guard / fail-open fallback produced an answer that depended on enumeration order); removing that defect family is a goal of this RFD, and emitted being a Set under wfTrue has no order to depend on (suppFromList_perm_invariant). So the cyclic case is not a debt tracked against #134 — it is a recorded outcome, load-bearing via (vii) (negated_block_same_scc) and now witnessed: on the worked two-head cycle program (acyclic defeat graph, negative rule-dependency cycle through each attacker’s support) both public heads compute to undefined in the mechanized well-founded model (cyc_a_undefined, cyc_b_undefined in CompiledFixpoint.lean) — the Can the reference chapter promises, produced by the fixpoint, not asserted in prose. Because that undefined outcome is surprising to an author who expected a definite verdict, elaboration now warns on the shape: OW0740 (mutual-defeat-cycle-undefined) detects the target↔attacker negative cycle over the resolved defeat edges + lowered rule bodies (representation-independent — it does not depend on the D4 elaboration switch) and names the mutually-defeating public heads. It is a WARNING, not a refusal — the outcome is canonical, not an error.

The genuinely-reduced residual. One narrow case remains reduced rather than re-proved: a head recursive through positive support within a stratum that is also an acyclic defeat target (positive recursion entangled with a satisfiable blocking guard). stratified_head_step is its stratification induction step; assembling it across a positive recursive cycle needs the standard “a positive/negation-free program’s well-founded model is its two-valued least model” fact, which the substrate does not yet carry. This is the only surviving #134 catch-up for the defeat plane; it does not block the Phase-1 opt-in path (oracle + soak cover it empirically) and it is named in CompiledFixpoint.lean’s honest-scope note rather than hidden in a sorry.

D7 — What dies, what remains

Deleted from oxc-runtime/src/defeasible.rs (the interpretive resolver surface):

  • defeasible_survivors — the topological survivor traversal (the memo, the visiting reentrancy guard, the fail-open catalogs.support.clone() fallback);
  • warranted_support_catalog, warranted_contribution_catalog, raw_classical_support_catalog, defeasible_clause_contribution — the catalogs re-seeded per head, and the per-clause attribution over them;
  • DefeasibleSurvivorsMemo and the short-name keying throughout;
  • strength_stratified_snapshot and the RFD-0026-era fused strict ∪ (defeasible \ defeater) fast path — superseded (the RuleStrength triple is retired surface).

The fail-open fallback (#1791) and the reentrancy guard have no counterpart in a compiled program: there is no runtime recursion to guard.

Remains in the runtime: nothing defeat-specific in the evaluation path. The public head extent comes from the compiled strict rules via the one Engine::evaluate path (compiler contract: one evaluation path). The reflection/audit projection is thin and read-only: ox derive --explain, provenance, and the D8 proof-tag surface read DefeatCompilationProvenance (D3), not a resolver. The #[default]/#[defeats]/#[label] reading, validation, resolution, and cycle refusal (OE0716–OE0721) stay in oxc-instantiate (defeasibility.rs, lower/defeat.rs) — that is the elaboration-side surface the transform is built beside, unchanged in what it validates; resolve_workspace_defeat_plane gains the emission step and the metadata-stripping/provenance-writing discipline (D3).

Other live consumers of the deleted catalogs — the reroute, named explicitly. Beyond the resolver internals above, three call sites read the deleted catalogs today. Under the one- Engine::evaluate model each reads the compiled public head via ordinary derived provenance instead; the DefeatCompilationProvenance carrier (D3) supplies any per-clause / proof-tag attribution these paths surfaced from the old catalogs:

  • Check evaluationoxc-runtime checks.rs, warranted_support_catalog (warranted_support_catalog). A check over a defeasible head reads the compiled public-head extent directly; the warranted support catalog is exactly what the compiled projection computes, so the read becomes the ordinary derived extent, no catalog.
  • Projection-cache defeat-eligibility gateprojection_consume.rs:268, branching on has_defeat_plane(). A compiled artifact reports no defeat plane, so the branch takes the ordinary strict path and compiled defeat becomes projection-cache-eligible: an ordinary strict program is cacheable where the interpreted plane was gated out of the cache. This is an unstated positive consequence — promoted to D8.
  • Scenario / snapshot pathderive_read.rs:1214/1223 (incl. strength_stratified_snapshot). Scenario snapshots read the compiled public-head extent through ordinary provenance; the RuleStrength-stratified snapshot is retired with the surface triple (listed in “what dies”).

D8 — Performance

The transformed program adds internal rules and atoms per defeasible head (support, blocking, projection), linear in clauses + edges. Cost model:

  • More rules, but ordinary semi-naive incrementality applies — the emitted rules are normal IDB rules; the reasoner evaluates them with the same machinery as any program. The interpreter paid a per-query graph traversal with per-head catalog rebuilds (materialize_predicates_at cloned repeatedly); the compiled program pays one fixpoint over a larger rule set, amortized by the maintained model.
  • The already-merged NAF memo / cone slicing optimizations apply to the internal blocking atoms for free — they are ordinary NAF atoms, indistinguishable to the optimizer from any other. The interpreter’s hand-rolled survivor traversal could benefit from none of them.
  • Projection-cache eligibility, gained. A compiled defeat-bearing artifact reports no defeat plane (has_defeat_plane() false, projection_consume.rs:268), so it takes the ordinary strict path and becomes projection-cache-eligible — the interpreted plane was gated out of the persisted cache; the compiled program is cached like any strict program (D7).
  • DBSP-ready (RFD 0018): the compiled plane is the Maher three-stratum form that lifts into the circuit; incremental maintenance of defeasible heads comes from the circuit compiler with no defeat-specific operator.

Benchmark plan: the oracle’s generated programs at scale (the soak corpus at high seed counts, as a throughput probe alongside its correctness role); and defeasibility enters plan_bench — a defeat-bearing program family added to the standing benchmark set, measured dispatch and batched, compiled vs the (retained, Phase-1/2) interpreter as the baseline arbiter at small N.

D9 — Cross-module defeat and future work

RFD 0082 (cross-module defeat) requires defeat-edge resolution across the workspace-combined artifact rather than per file (RFD 0028 D3’s v1 file-local narrowing, #362). The compiled transform honors this by construction: it runs at lowering over resolved edges, and lifting resolution to the combined artifact changes only which edges reach the transform, not the transform. Qualified $-naming (D1) means #1792’s short-name collision folds in for free — cross-module same-named heads produce distinct qualified internal relations.

Rationale

  • The fixpoint belongs to the evaluator. The interpreter re-implemented a well-founded fixpoint by hand — topological order, memo, reentrancy guard — and every defect is a way that hand-rolled fixpoint diverges from the real one under order, scope, or keying. Compiling to strict rules hands the fixpoint back to the machinery that is proven to compute it (Reasoning/WellFounded, AFT, the differential oracle). Three of four defects vanish structurally, not by patch.
  • RFD 0028 D6, honored. “The meaning of a #[default]/#[defeats] program is the meaning of its compiled core program” stops being aspirational: the compiled program is the only program. D5 hook 3 (the transform slot) is used as designed.
  • Circuit alignment is free, not engineered. Compiled defeat is already inside DBSP’s proven fragment (RFD 0018); the interpreter would have been a permanent exception in the circuit.
  • The oracle makes acceptance objective. PR #1793 is a differential the rewrite either passes or does not; the Lean golden vectors add a mechanized transform-drift check on top.

Alternatives considered

  • Patch the resolver in place. Rejected. #1789 patched one trigger; the fail-open branch is still reachable (#1791), and the memo/order/keying dependence is intrinsic to interpreting a fixpoint. Each patch narrows the divergence without removing its cause.
  • Refuse recursive #[default] dependencies (the D2 track-(a) alternative). Rejected as the semantics track: it refuses legitimate WFS-native Datalog (self- and mutual positive recursion), conflates rule-dependency edges with defeat edges, and preserves an interpreter limitation after compilation removes its cause. Retained only as a possible temporary migration gate with a diagnostic distinct from OE0718 — not the shipped semantics.
  • Keep executable defeat metadata + a runtime discriminator (instead of stripping it, D3). Rejected: an old runtime ignores the discriminator and re-interprets the retained markers — double application. Stripping to a self-contained strict program is the only backward-safe form.
  • Behavioral-only golden vectors (input + expected warranted model, no compiled-program check). Rejected: it detects evaluator drift but not transform drift; the RFD wants both (D5 steps 3 and 5).
  • A core_ir version bump for the internal heads. Rejected: no IR shape changes; a minor bump does not protect against the double-application hazard anyway (the format gate accepts future minors) — the strip-and-provenance discipline (D3) is what protects.

Consequences

  • The Lean prerequisite (D6) is delivered in PR #1797 (merged; CompiledFixpoint.lean) plus the #134 Part A stratified-defeat bridge: recursive-support / warrant (iii)/(v)/(vi) proved for the block-free and stratified-defeat (acyclic) fragments, with provable two-valuedness (no undefined) on the acyclic non-recursive fragment. On a genuine target↔attacker mutual-defeat cycle the heads are undefined and that is the semantics (RFD 0028 D6) — not a bridge to build. The only surviving reduction is the positive-recursive-stratum two-valued lemma.
  • #1789 has merged (regression floor, first course); the #1790 reinstatement fix and the interim fail-open hardening (#1796) are still open and complete the floor. #1791’s semantics are settled here (admit, define). #1792 folds in via qualified naming.
  • A .oxbin format minor bump is owed (the additive DefeatCompilationProvenance carrier); no DERIVATION_SEMANTIC_EPOCH bump and no Core-IR bump. The switch is elaboration-time, so the artifact hash — not the epoch — separates compiled from legacy caches (D3/D4).
  • oxc-runtime/src/defeasible.rs is deleted at Phase 3; the runtime carries no defeat-specific evaluation, only a read-only provenance projection for explain/audit.
  • Defeasibility becomes DBSP-incrementalizable with no special case (RFD 0018).

Open questions

  • The exact DefeatCompilationProvenance shape and whether it is a new .oxbin section or an extension of an existing metadata section — a lowering/format detail settled at implementation.
  • Whether the temporary refusal gate (the rejected D2 track-(a)) is worth landing for Phase 1 as a belt-and-suspenders guard while the recursive theorems (D6) are in progress, or whether the oracle + soak are sufficient assurance for the recursive cases from the start.
  • Proof-tag attribution for undefined tuples on a target↔attacker negative cycle (D2 case 3): the Governatori four-tag scheme (+Δ/−Δ/+∂/−∂) maps cleanly onto true/false extents; the tag for a WFS-undefined defeasible tuple is a D8 mapping question deferred to the provenance layer.

Relationship to existing records

  • RFD 0028 — the semantics this RFD realizes, unchanged (D1–D11). This RFD changes only where strategy #1’s compilation runs (D5 hook 3 → elaboration).
  • RFD 0018 (incremental rule circuits) — compiled defeat is inside the proven incremental fragment; the alignment motivating this RFD (Context, D8, D9).
  • RFD 0072 — the projection-cache validity contract (artifact hash + derivation commitment) that makes the cache-discriminator reasoning load-bearing: the elaboration-time switch separates caches by artifact hash with no epoch bump (D3/D4).
  • RFD 0082 (cross-module defeat) — honored by construction; the transform runs over resolved edges regardless of resolution scope (D9).
  • #1788 / PR #1789 (merged), #1790, #1791, #1792 — the bug family; the shape classes (Context) and the structural fixes (D2, D7, D9).
  • PR #1796 — interim fail-open hardening (cone-sensitive reentrancy) of the soon-to-be-deleted resolver guard; references #1791, does not close it (the compiled path deletes the guard).
  • PR #1793 (merged) — the acceptance oracle (D4) and the Phase-2 golden-vector path (D5).
  • PR #1797 — the Lean entry gate (CompiledFixpoint.lean, D6, merged): the recursive-fixpoint transform correctness, all obligations proved bar the positive-recursive-stratum reduction.
  • #134 — the WFS mechanization catch-up; D6’s theorems (iii)/(v)/(vi) plus the #134 Part A stratified-defeat bridge remove its narrowing for the defeat plane on the block-free and stratified-defeat (acyclic) fragments. The mutual-defeat-cycle case is undefined-by-decision, not an open bridge; the only surviving catch-up is the positive-recursive-stratum two-valued lemma.

RFD 0084 — Mapped relation subsumption

  • State: discussion
  • Opened: 2026-07-22
  • Decides: that <: on relations takes an explicit argument-mapped form as its only surface form — a child names, for each parent end (by the parent’s end name), what fills it: a child end (a rename), a child end widened to a parent supersort (a cast), or a constant (a pin) — and that the bare positional form (<: Parent) is removed: it is refused at the declaration with a catalogued diagnostic, with no deprecation window and no lint stage. Settles how the mapped form interacts with every plane that already consumes relation subsumption: end mutability (RFD 0076), the retract/amend cascade (RFD 0076), relation-value application dispatch (#1805, #1806), reflection, coverage, and the incremental rule circuits (RFD 0018 / RFD 0021 lineage). This RFD is a successor amendment to RFD 0005; it generalizes that RFD’s positional check to the mapped form and retires the bare spelling. The bare form’s meaning is preserved as the identity instance of the general rule — but it is no longer a writable surface form; the identity mapping survives only as an internal representation and as the migration bridge (D10).
  • Surface policy: one surface form (explicit mapped). No bare form, no deprecation window, no lint. In-repo bare sites migrate mechanically in the enforcing slice; the external ontology-library corpus migrates via an ox-migrate rewrite at its next toolchain upgrade.
  • Affects: relation declaration grammar; the elaborator subsumption checks (Argon.Substrate.RelationSubsumptionarityEqual, endpoint covariance); the relation-value application dispatch lowering (Argon.Reasoning.Datalog.RelationApplication); the reflection plane ($specializes, the relation tier); the write-path end-mutability and cascade gates (RFD 0076); the artifact wire format (additive, optional-on-decode; non-identity edges gated by a core-IR version stamp so a too-old decoder refuses loudly).

In plain language — what changes and why

A relation in Argon is a named table of tuples — Loc(p, c) says person p is located in city c. Subsumption (<:) says one relation’s tuples flow into another’s: every child tuple is also a parent tuple. Today <: only works when the child is shape-identical to the parent — same number of ends, same order, compatible sorts — and the pairing is positional and implicit: Home(p, c) <: Loc silently pairs Home’s first end with Loc’s first, and so on, by position alone.

That is too rigid for a very common modeling situation: a narrow relation that should participate in a general family under a translation. Consider a general “income item” relation carrying a currency, and a specific “US wages” relation that is always in dollars and therefore does not carry a currency end at all:

pub rel IncomeItem(p: Person, amount: Int, c: Currency);
pub rel WagesUSA(p: Person, amount: Int);

Today you cannot write WagesUSA <: IncomeItem — the shapes differ (two ends vs. three). Your only option is to hand-write a separate derive rule that fills in the dollar constant. But that rule is not a subsumption edge, so the relation family loses WagesUSA as a member: any code that dispatches over “everything that is an IncomeItem” (the relation-value application feature landing in #1805) silently never sees US wages. A member drops out of the family and nothing tells you. That silent omission is the rot this RFD removes.

The change. Every <: clause states, explicitly, exactly how the child’s ends land on the parent’s, and may fill a missing parent end with a constant:

pub rel WagesUSA(p: Person, amount: Int) <: IncomeItem(p = p, amount = amount, c = USD);

Each filler is keyed by the parent’s end name (p, amount, c) and states where that end’s value comes from: parent p from child p, parent amount from child amount, and the parent’s c end — which the child does not have — pinned to the constant USD. Now every WagesUSA(p, a) tuple contributes IncomeItem(p, a, USD) to the family, exactly as a hand-written rule would, but as a real subsumption edge — so WagesUSA is a family member and dispatch finds it. The clause can also cast a child end whose sort is a subsort of the parent’s end (a = friend as Person), so a relation over a narrower type can join a family declared over a wider one.

The bare form is removed. pub rel Home(p, c) <: Loc; no longer compiles; it is refused with a catalogued diagnostic. The equivalent must be written explicitly, keyed by the parent’s end names: pub rel Home(p, c) <: Loc(p = p, c = c);. There is therefore one surface form for subsumption, not two.

Why remove it rather than keep it as a shorthand? The relation corpus is written predominantly by automated tooling and read/reviewed by people, so the reader-over-writer trade favors self-describing declarations. Bare positional pairing silently re-pairs the wrong ends if the parent’s ends are reordered — the exact silent-failure class this design exists to eliminate — whereas the explicit form keys each filler to the parent end by name, so the pairing is anchored to names and robust to reordering on either side. (A positional filler list — a bare token per parent position — was considered and rejected for the same reason: it re-pairs under a parent-end reorder exactly as the bare form does; only parent-name keying is mechanically reorder-safe. See Alternatives (d).) And two spellings for one idea guarantee style drift in a machine-written corpus. Nothing outside this repository depends on the bare form (the language is pre-1.0), so every in-repo bare site — reference-book examples, the conformance corpus, fixtures — is migrated mechanically in the same change that turns on the refusal, and an ox-migrate rewrite handles the external ontology-library corpus at its next toolchain upgrade. The identity mapping is preserved internally (it is what a migrated bare site elaborates to), and a theorem proves that migration is meaning-preserving (D10), so removing the bare surface path is safe.

Querying the family — and knowing which member answered. Once relations join a family, you ask questions over the whole family at once. Sometimes you want the merged result (“every income item, in dollars”) — a plain query on the parent relation gives it. Other times you need the witness: not just that a tuple is in the family but which member relation it was declared in (“every income item and the relation it came from”). The canonical surface for that is a trailing via on the parent atom:

pub derive incomeSource(p: Person, a: Int, c: Currency, k: Relation) :-
    IncomeItem(p, a, c) via k;

IncomeItem(p, a, c) is the ordinary parent-union query; via k additionally binds k to the member relation each tuple was declared in. Given WagesUSA(alice, 100) and InterestUSD(alice, 30) it binds k to WagesUSA and InterestUSD respectively — each row once, tagged with its source. Constant pins live in the parent frame, so IncomeItem(p, a, USD) via k selects only the USD members.

via is canonical because the parent frame is information-complete: D1 forbids dropping a child end, so every value a member carries reaches the parent frame through the declared mapping (renames and reorders relocate values, pins add constants). Its meaning is the generalized specializes atom, which names both frames in one atom and is via’s exact desugar target:

pub derive incomeSource(p: Person, a: Int, c: Currency, k: Relation) :-
    specializes( k(_fresh…) , IncomeItem(p, a, c) );   // what `IncomeItem(p,a,c) via k` desugars to
    // (the member frame's width is the selected member's own arity — this
    //  family mixes 2- and 3-place members, so no fixed wildcard count can spell it)

Parent(args) via k desugars exactly to specializes(k(_fresh…), Parent(args)) — one semantics, one desugar point. The obvious hand-rolled alternative — enumerate the family reflectively with the bare specializes(k, IncomeItem), k(p, a, c), then apply each member — returns every row twice (once under its member, once under the family parent, because the parent literally contains a copy of every member’s tuples) and leaves the argument shape implicit. via reads each member’s own extent once and fixes the parent frame explicitly, with no new machinery: it reuses the same dispatch the family membership feature already builds (D6).


Question

<: on relations, since RFD 0005, requires the child to be positionally shape-identical to the parent: equal arity (arityEqual, OE0150), covariant endpoint sorts position-by-position (OE0151), refined cardinality (OE0152), compatible metarel (OE0153). The induced semantics is tuple-inclusion: child’s extent is a subset of parent’s extent, tuple-for-tuple.

This forbids the single most common non-trivial modeling shape: a narrow relation participating in a general family under a translation — an end renamed, an end absent-because-constant, an end at a subsort. Today that shape must be expressed as a hand-written derive rule, which is not a subsumption edge and therefore loses family membership: the relation-value application dispatch of #1805 (specializes(r, Family), r(args)) enumerates subsumption children and never finds the derive-backed relation. The omission is silent — the worst failure mode for a knowledge base.

<: takes an explicit argument-mapped form, keyed by parent end name — renames (parentEnd = childEnd), constant pins, and subsort casts — as its only surface form; the bare form and the positional filler list are both removed and refused. What does the elaborator check, how does the mapping thread through every plane that consumes subsumption, and how do the in-repo and external corpora migrate off the bare form?


Context

The open-family motivation

RFD 0005 records the driving demand as a sub-relation whose endpoints narrow and whose tuples flow to the parent, and notes the fallback cost precisely: “the modeler has to write a derive rule by hand, losing the structural property and the elaborator’s covariance check.” That fallback loses more than the covariance check now that #1805 makes family membership operational: a derive rule is invisible to specializes, so a family whose members are meant to be open-ended (add a new income kind, an audit rule over the whole family keeps working) silently excludes every member that needed a translation. The mapped form makes the translation itself a subsumption edge, so the member stays in the family.

Substrate readiness

The substrate carrier is unchanged from RFD 0005: SubsumptionAxiomBody is generic over sub_id/super_id UUIDs. What a mapped edge adds is not a new axiom kind but a per-edge argument mapping — metadata that says, for each parent position, which child end (possibly cast) or which constant fills it. The subsumption-closure logic is unchanged in shape; the extent contribution of a child is computed through the mapping rather than by identity.

What “mapping” is, precisely

A mapped edge child(e₁ … eₙ) <: parent(f₁ … fₖ) carries, for each parent end fⱼ (j ∈ 1..k), a filler written fⱼ = … that is exactly one of:

  • a child-end referencefⱼ = eᵢ, contributing child end eᵢ’s value at parent end fⱼ (a rename, stated by both names);
  • a cast child-end referencefⱼ = eᵢ as S, where S is the parent’s declared sort at fⱼ and child’s sort at eᵢ is a subsort of S (childSort(eᵢ) <: S); the value is the child end’s, admitted at the wider parent sort;
  • a constant pinfⱼ = κ, a literal value κ of the parent’s declared sort at fⱼ, contributing κ at that end for every child tuple.

Each filler names the parent end it fills by name, and a child-end reference names the child end by name too, so the mapping is anchored to names in both directions: reordering the child’s ends leaves each eᵢ resolving to the same end, and reordering the parent’s ends leaves each filler still filling the same fⱼ — the resolved parent position updates, the mapping’s meaning does not. This is the property both the removed bare form and the rejected positional filler list (Alternatives (d)) lacked: bare and positional pairing both re-pair silently under a same-sort parent-end reorder.

There is no bare surface form. child <: parent (no filler list) is refused (D1). The identity mapping — every parent end fⱼ filled by the child end at the same position (parent(f₁ = e₁, …, fₖ = eₙ), requiring n = k and reducing to RFD 0005’s checks exactly) — survives only as an internal representation: it is what a migrated bare site elaborates to, and the object of the migration-soundness theorem (D10). It is never written by a human or by tooling as surface syntax.


Decision

Ten decisions, one section each. The plane-interaction rows (D3–D9) are the heart of the RFD and are stated one decision per row; D6 is the user-facing family-query surface — the canonical trailing via, whose meaning is the generalized specializes atom — that the dispatch seam (D5) exists to serve.

D1 — Surface grammar and the well-formedness gates

Extend the relation <: clause with a required parenthesized filler list. Each filler is keyed by the parent’s end nameparentEnd = filler-value — so the mapping states, per parent end, where that end’s value comes from:

rel-subsumption ::= '<:' TypePath '(' filler ( ',' filler )* ')'
filler          ::= Ident '=' Ident                  // rename: parent end ← child end
                 |  Ident '=' Ident 'as' TypePath     // cast:   parent end ← child end, widened
                 |  Ident '=' Literal                 // pin:    parent end ← constant

The left Ident of every filler is a parent end name, resolved against the parent’s declared ends; the right side is the value that lands there — a child end name (a rename), a child end name widened with as (a subsort cast), or a literal (a constant pin). The filler list is required. A <: clause with no filler list — the bare form child <: parent — is refused (gate (h) below); there is no bare surface form.

Because each filler names the parent end it fills (not a position in the list, and not a bare child-end token whose parent slot is implied by order), the mapping is anchored to names in both directions. Reordering the parent’s ends leaves every filler naming the same parent end — the mapping’s meaning is unchanged, only the parent position each filler resolves to updates. Reordering the child’s ends leaves every child-end reference resolving to the same end by name. This is the property the earlier positional filler design (fillers as a bare token list, filler i filling parent position i) lacked: a positional list re-pairs silently under a same-sort parent-end reorder exactly as the bare form does — the sort-covariance gate cannot catch it because every position still type-checks (see Alternatives (d)). Name-keyed fillers close that gap mechanically.

There is no same-name abbreviation. A parent end filled by a same-named child end is still written in full — p = p, never a bare p. Admitting a bare token as sugar for p = p would reintroduce a second surface spelling (and revive the positional reading the name-keying exists to remove), against the one-surface-form philosophy this RFD holds throughout (Open questions §6, Alternatives (d)).

Well-formedness (elaborator, Argon.Substrate.RelationSubsumption). The following must hold; each violation is a distinct refusal. New codes are allocated notionally as next-free (the current maximum allocated relation code is OE1409, RFD 0076); the numbers below are placeholders the implementation slice pins against the catalog, not commitments.

#RuleRefusal (notional)
aEvery filler’s left name resolves to a declared parent end; a name matching no parent end is refused.MappedSubsumptionUnknownParentEnd
bEvery parent end is filled exactly once: none uncovered (missing filler) and none doubly covered (two fillers naming the same parent end). Coverage is by name, so both halves are name checks, not a length/position count.MappedSubsumptionParentEndCoverage
cEvery child end is referenced by at least one filler (no dropped child ends — see rationale).MappedSubsumptionUnmappedChildEnd
dA child-end filler’s sort is covariant with its named parent end’s sort: childSort(eᵢ) <: parentSort(fⱼ). An uncast reference must already satisfy this; the as S form makes the widening explicit and S must equal parentSort(fⱼ).MappedSubsumptionEndpointVariance (generalizes OE0151)
eA constant pin’s literal has its named parent end’s sort.MappedSubsumptionPinSort
fCardinality refinement and metarel compatibility (OE0152/OE0153) are checked through the mapping — see D2.OE0152 / OE0153 (reused)
gThe subsumption graph stays acyclic (OE0154 reused).OE0154
hThe subsumption clause has a filler list — in either glyph: the <: operator or the specializes keyword synonym (spec §6.3 (spec/reference/src/constructs/relations.md)). A bare child <: parent or child specializes parent with no explicit mapping is refused — there is one surface form (see Surface policy, Migration).MappedSubsumptionBareFormRemoved (notional, next-free)

Rule (c) — no dropped child ends — is decided deliberately: a child end that maps to no parent position would let two child tuples differing only at that end collapse to the same parent tuple with no declared meaning for the collapse. We forbid dropping child ends in this RFD (a would-be projection is instead a separate derived relation the modeler declares explicitly). This is revisited in Open questions as a possible future relaxation with an explicit projection marker.

A child end may be referenced by more than one parent end (a diagonal: parent(f₁ = e, f₂ = e)); this is permitted and contributes the same child value to both ends. A constant may be pinned at multiple parent ends likewise. (What is forbidden by (b) is the reverse — one parent end named by two fillers.)

The specializes keyword synonym takes the filler list too. The subsumption clause has two glyphs — the <: operator and the specializes keyword — which the parser routes through one clause path (compiler/crates/oxc-parser/src/grammar/decls.rs, supertype_clause), the modeler-friendly spelling of spec §6.3 (spec/reference/src/constructs/relations.md). The explicit-only decision is about the mapping being explicit, not the glyph, so it binds both spellings identically: gate (h) refuses a bare clause in either glyph, and the required filler list is written the same way after specializes as after <: (child specializes parent(f₁ = e₁, …, fₖ = eₙ)). The bare comma-separated multi-parent form child specializes R1, R2 — the keyword analogue of bare <: R1, R2 — is refused for the same reason bare <: is; a multi-parent edge is written child specializes R1(…), R2(…) (each parent carries its mapping; Open questions §1). Collapsing the two relation-side glyphs to a single spelling is out of scope here: the specializes keyword is shared with concept subsumption (supertype_clause_concept), so eliminating one glyph is a §6.3-wide surface decision, not one this relation-scoped RFD makes unilaterally. This RFD fixes only that neither glyph escapes the explicit-mapping requirement.

D2 — Semantics: mapped-tuple-inclusion

Subsumption generalizes from tuple-inclusion to mapped-tuple-inclusion. Let the mapping be the function φ that sends a child tuple t = (v₁ … vₙ) to the parent tuple φ(t) = (w₁ … wₖ) where wⱼ = vᵢ if the filler for parent end fⱼ is fⱼ = eᵢ (or fⱼ = eᵢ as S), and wⱼ = κ if the filler is fⱼ = κ. The induced family extent contribution of the child is the image of the child extent under φ:

ext(parent) ⊇ φ(ext(child)) = { φ(t) | t ∈ ext(child) }

The identity mapping recovers ext(parent) ⊇ ext(child) exactly (RFD 0005). Because φ may be non-injective (constant pins collapse the currency end; a diagonal collapses two ends), the image is a set — duplicate parent tuples from distinct child tuples coincide, consistent with set-semantic extents.

Cardinality/metarel through the mapping (D1f). Cardinality refinement is checked at each parent position against the filler: a constant-pinned position contributes a fixed single value per child tuple, so its parent-side count constraint is evaluated against the pin; a child-end position inherits the child end’s cardinality, which must refine the parent’s. Metarel compatibility is checked between child and parent as before; the mapping does not change the metarel classification, only the argument routing.

Lean-first plan.

  • Argon.Substrate.RelationSubsumption (RFD 0005) gains the mapping as data on the edge and generalizes arityEqual to arityMapsParent (filler count = parent arity) and endpoint covariance to covariance-through-the-filler (covariantFillers, over the per-position helper entryCovariant). The existing (now-removed-from-surface) positional lemmas are recovered as the identity-mapping specialization — stated as a mappedChecks_identity_eq_bareChecks corollary. With the bare surface form gone, this corollary is no longer a “sugar can’t drift” guarantee; it is the migration-soundness witness — it proves that rewriting a bare site to its identity mapping preserves every accept/refuse verdict, so the mechanical migration and the deletion of the bare check path are semantics-preserving.
  • Argon.Reasoning.Datalog.RelationApplication (as merged with #1805/#1806, carrying structural_containment and dispatch_selected_iff) gains the obligation mapped_containment: a mapped child <: parent edge entails φ(ext(child)) ⊆ ext(parent) in a mapped-closed program (MappedClosed, the generalization of structural_containment’s StructurallyClosed from identity inclusion to φ-image inclusion). Its dispatch_selected_iff obligation is restated as mapped_dispatch_selected_iff so that reading the compiled finite-dispatch helper at an admitted selector applies the selected relation through its mapping — the helper still adds no semantic premise; it applies φ.

What slice 1 proves vs. what it assumes. mapped_containment is stated relative to a MappedClosed program — the hypothesis that every declared edge’s φ-image is already contained in the parent’s extent — and discharges the edge→containment step from that hypothesis. It does not prove that an elaborator emits clauses establishing that closure; the emission is the check-plane / dispatch slices’ obligation and is explicitly deferred (see the module’s “Deferred” note). Similarly mapped_dispatch_selected_iff fixes the value-level contract of the dispatch helper (reading it at a selector = applying φ); it is not a proof about generated clauses. The obligation those later slices owe is: the elaborator-emitted mapped clause (D5) computes exactly φ(ext(child)) and nothing more — establishing the MappedClosed premise that slice 1 assumes — and the subsumption-closure over a graph of mapped edges is the composition of the per-edge images.

Dispatch and cascade are the same function (single-authority design). The dispatch expansion (D5) applies φ in the forward direction (child tuple → image), and the cascade/amend translation (D4) withdraws image support when a child tuple is withdrawn — the same φ, read the same direction. The design commitment is that there are not two hand-kept translations. This is recorded as a Lean obligation dispatch_cascade_same_map: the image a dispatch clause materializes for a child delta and the image a cascade withdraws for the withdrawal of that same delta are computed by one φ — the maintenance direction is the delta’s sign, not a second implementation.

What this obligation is, honestly. In the slice-1 Lean, dispatch_cascade_same_map holds by rfl because dispatchAdds and cascadeRemoves are defined as the same function (image m). The rfl is therefore the specification — it records that one φ, applied per delta sign, is the intended design — not a guard. Once the wire/dispatch slice introduces two independent Rust call sites (a forward dispatch materializer and a cascade withdrawer), a definitional identity in Lean carries no anti-drift content over that Rust boundary. The real anti-drift artifact is therefore owed by the dispatch-translation slice (Staging §5): Rust dispatch and cascade must route through one shared translation function (the single-authority pattern of D10.1), and the rfl obligation is only discharged as a genuine guard once both call sites are wired to that single authority — until then it is the spec, not the proof of non-divergence. This is a named obligation on that slice, not a property this slice already secures.

D3 — Plane: end-mutability inheritance (RFD 0076, OE0267/OE0268)

Per-end mutability inheritance (RFD 0076: effective mutability is conjunctive over a relation and its transitive superrelations at each position) must be evaluated through the mapping, position by parent position.

Parent position j filled byMutability rule
child end eᵢ (rename/cast)the child end eᵢ inherits the parent position j’s constraint: eᵢ’s effective mutability is conjunctive with parent’s at j. The RFD 0076 weakening refusal (OE0268) fires if the child declares mut eᵢ where parent’s position j is immutable — restated: a child end may not weaken the immutable posture of any parent position it maps onto.
constant pin κno child writer exists for position j. A frozen (immutable) parent end at a pinned position is trivially satisfied: the value is a compile-time constant, asserted identically for every child tuple, never retracted independently, never grown to a second value. The freeze witness for a pinned fiber is the constant itself. A mut parent end at a pinned position is also fine (the constant simply never varies). No weakening is possible because there is no child-side mut to declare.

The consequence: OE0268’s weakening check is “for each parent position mapped by a child end, the child end’s mutability refines the parent’s”; pinned positions are exempt from the check because they have no child-side mutability to compare. This is a strict generalization — under the identity mapping it is RFD 0076’s positional check verbatim.

Mechanized (slice 1, Argon.Substrate.RelationEndMutability): the per-position gate is mappedEndAccepts, exhaustive over MappingEntry. constant_pin_freeze_trivial proves the pinned-freeze witness — a constant-pinned position is accepted for every parent posture — and mappedEndAccepts_childEnd_eq_positional / mappedEndAccepts_cast_eq_positional prove a child-end/cast filler computes exactly the positional OE0268 term, so at the identity mapping the gate is RFD 0076’s check verbatim; allPins_accepted lifts the pin case to a whole all-pins mapping.

The RFD 0076 OE0267 declaration-trap (an immutable end whose dependent context is value-sorted can never be released) is evaluated on the child as declared; the mapping does not create a new immutable end, it routes existing ones.

D4 — Plane: cascade (retract/delete) and amend (RFD 0076)

The family extent is a union of images; image tuples are derived, not asserted. Therefore:

  • Direction. A child tuple’s withdrawal maps to withdrawal of its image: cascade flows child → image, never image → child. Retracting WagesUSA(alice, 500) removes IncomeItem(alice, 500, USD) from the image contribution, exactly as retracting a derive premise removes the derived conclusion (RFD 0076 gate 4: “subsumed and derived extent deltas”).
  • Two children onto one parent tuple. Because φ may be non-injective across edges (two different children may both map onto IncomeItem(alice, 500, USD)), the parent image tuple is live while any contributing child tuple is live. This is ordinary set-union support: the image tuple is a derived conclusion with multiple independent supports; it is withdrawn only when the last support is.
  • Parent-side direct assertion vs. image overlap. A directly asserted parent tuple and an image tuple may coincide. They are distinct supports for the same proposition (one asserted, one derived-via-φ), resolved by the standard support-counting the runtime already applies to a tuple that is both asserted and independently derivable (RFD 0076 §“Rule-derived tuples cannot be amended”: “where an asserted tuple is also independently derivable, amendment withdraws the asserted contribution and the tuple remains live by derivation”). A delete/amend on the parent touches only the directly asserted support; the image contribution persists until its child is withdrawn.
  • Amend. amend (RFD 0076 §5) names a directly-asserted tuple. An image tuple is derived, so it cannot be the target of amend — the RFD 0076 OE1406 (AmendmentTargetNotAsserted) refusal applies unchanged. Correct the record by amending the child premise; the image recomputes.

The end-mutability gates of RFD 0076 gate 4 already apply the freeze/retraction checks to “parent-relation rows contributed by a child.” This RFD makes explicit that the contributed row is φ(t), not t, and that the freeze witness for a pinned parent position is the constant (D3).

D5 — Plane: relation-value application dispatch (#1805/#1806)

This is the integration seam, and the payoff. The dispatch of #1805 expands a family query specializes(r, Family), r(args) into one strict clause per family member by positional substitution. For a mapped member the emitted clause applies the mapping — static mapping means static expansion; no runtime machinery.

For the WagesUSA <: IncomeItem(p = p, amount = amount, c = USD) edge, the family query

pub derive anyIncome(p: Person, a: Int, c: Currency) :-
    specializes(r, IncomeItem), r(p, a, c);

expands, for the explicit identity members, to the positional clauses of #1805, and for the mapped WagesUSA member to:

anyIncome(p, a, USD) :- WagesUSA(p, a);

The parent’s currency variable c is bound to the constant USD in the head; the child ends p, a are threaded positionally. A cast member emits the child end at the parent’s sort (no coercion node — the subsort relation guarantees admissibility). This is exactly the clause the hand-written derive rule would have carried, but generated from the subsumption edge, so WagesUSA is a specializes child and the family query reaches it. The witnessed user-facing spelling of this query — binding which member answered — is D6’s canonical trailing via (IncomeItem(p, a, c) via k), whose meaning is the generalized specializes(k(memberArgs), Parent(parentArgs)) atom, desugaring onto exactly this expansion.

The mapped_dispatch_selected_iff obligation (D2) fixes the value-level contract this relies on: reading the compiled dispatch helper at the WagesUSA selector is exactly applying WagesUSA through φ. It does not itself prove that the elaborator emits the clause above — the clause generator is the dispatch-translation slice’s work (Staging §5); the obligation is the correctness target that generator discharges.

D6 — Plane: the family-query surface (via canonical, generalized specializes its meaning)

The dispatch of D5 makes a mapped member reachable. The user-facing family query has one canonical surface spelling — the trailing via — and one meaning for it — the generalized specializes atom, which is via’s desugar target and its spec-level semantics. Both lower to the same D5 dispatch seam; via desugars to the generalized atom, which desugars to dispatch. No second evaluator, one lowering path.

Rule of thumb.

  • Parent(args) — the plain parent atom — is the union: every family member’s tuple, merged, no witness.
  • Parent(args) via k — the parent atom plus via k — is the union plus “which one”: the same tuples, each additionally bound to k, the member relation the tuple was declared in.
  • specializes(k(memberArgs), Parent(parentArgs)) — the generalized atom — is via’s meaning written out: its desugar target and spec-level semantics, not a recommended alternative surface spelling.

Decision (2026-07-22) — presentation emphasis; semantics unchanged. Parent(args) via k is the canonical user spelling of a family query. The generalized specializes(k(memberArgs), Parent(parentArgs)) form is the meaning of via — its desugar target and the spec-level semantics — not a recommended alternative spelling. Rationale (load-bearing): D1 forbids dropped child ends, so the parent frame is information-complete — every value a member frame carries is available in the parent frame through the declared mapping (renames and reorders relocate values; pins add constants; nothing is lost). Therefore via plus the parent atom expresses every practical family-query rule, and a member-frame spelling binds nothing the parent atom does not already bind. The member frame becomes independently expressive only if mappings that drop child ends are ever admitted (Open questions §4, the projection-marker relaxation), at which point the generalized form — already fully specified below — is waiting. Until then, teach and write via; read the generalized atom as its definition.

The canonical surface — trailing via.

Parent( args ) via k

reads “every Parent-family tuple args, together with k, the member relation the tuple was declared in.” args is the parent’s argument list — the parent frame, where constant pins live; k binds to the declaring member, each row once. via writes only the parent frame, so a family query never names a relation bare: the arity is carried by Parent(args) at every site. The parent frame is information-complete (Decision above), so every practical family-query rule is a via rule.

via’s meaning — the generalized specializes atom. via desugars, at elaboration, exactly to:

Parent( args ) via k    ≡    specializes( k( _fresh₁ … _freshₘ ) , Parent( args ) )

— the member frame is a list of fresh wildcards (m = the selected member’s arity), so via elides the member frame entirely and writes only the parent frame, binding k to the declaring member. This desugar identity is the template; the example set below pairs each canonical via query with this generalized form written out beneath it, so the equivalence is visible at every example.

The generalized atom specializes(k(memberArgs), Parent(parentArgs)) names both frames explicitly and does three jobs:

  • bounds k to Parent’s family — k ranges over the family’s members (reflexively including Parent itself);
  • applies k in its own framememberArgs is k’s declared argument list (the member’s own shape);
  • constrains the image in the parent frameparentArgs is Parent’s argument list, constant pins included.

Both argument lists are required in the generalized form; wildcards _ are allowed in either frame (and via’s member frame is all-wildcard by construction). The image constraint is what makes pins load-bearing: IncomeItem(a, b, USD) via k (desugaring to specializes(k(_fresh…), IncomeItem(a, b, USD))) selects every family member — of any arity — whose declared mapping can produce USD at the currency position and statically prunes the rest (a member pinning EUR there can never match, so it is dropped from the expansion at elaboration — the D5 static-expansion property, now driving query-side member elimination).

Requiring arguments at every family-query site resolves the bare-relation-reference problem: the bare two-argument form specializes(k, Parent) — a relation standing as a value with no arguments — is deprecated then refused, on the same explicit-only migration pattern as the bare <: (D1 gate (h), Migration): it parses during the check-plane slice, is refused when the enforcing slice lands, no separate lint stage. Relation literals in value position (k != Wages, a relation-valued endpoint) are the residue that argument-carrying forms do not reach and remain the sole bare sites (Open questions §7).

The φ-not-position rule — stated loudly (it governs the meaning via hides). The correspondence between memberArgs and parentArgs in the generalized form is the member’s declared mapping φ_k, never visual position. A variable shared between the two lists flows its value through φ_k — the member’s declared filler map (D1) — not by lining up columns left-to-right. This is the single documented frame convention; it replaces every scattered “column i here means column i there” reading. Writing via means you never touch the member frame, so this trap cannot arise at a via site; it is stated here because the generalized atom — via’s meaning — is where it lives, and where a hand-written member-frame query would meet it.

Query atoms are positional; the declaration is where names key. The name-keyed filler list of D1 is a property of the <: declaration — the one site where a parent-end reorder must not silently re-pair the mapping. A family-query atom (k(memberArgs), Parent(parentArgs)) is an ordinary atom and its argument lists are positional, exactly like every other atom in a rule body; they are not keyed by end name. That is not an inconsistency: an atom’s positions are read in one relation’s own frame, where position is unambiguous, whereas the <: filler list bridges two frames and must survive a reorder of either — which only name-keying delivers. So the declaration carries parentEnd = childEnd fillers, and a query carries positional Parent(v₁ … vₖ); φ_k (fixed once at the declaration by name) is what bridges the generalized form’s two positional lists (the φ-not-position rule above).

Worked example — a mapping that reorders ends:

pub rel Edge(src: Node, dst: Node, w: Int);
// declared mapping REVERSES the endpoints:
//   parent src ← child `to`,  parent dst ← child `from`
pub rel BackEdge(from: Node, to: Node, w: Int) <: Edge(src = to, dst = from, w = w);

BackEdge’s φ sends the member tuple (from, to, w) to the parent tuple (to, from, w). To read every back-edge’s parent image with its witness, write the parent frame — Edge(s, d, w) via k — binding (s, d, w) to the image (to, from, w); φ does the reordering, the surface never mentions it. The trap via spares you lives in the generalized meaning: specializes(k(x, y, z), Edge(x, y, z)) at k = BackEdge looks like a pass-through, but the member frame binds x = from, y = to, z = w while the parent frame constrains the image Edge(to, from, w) = Edge(y, x, z), so Edge(x, y, z) demands x = y and the atom silently collapses to the diagonal (only self-loops match). Because via writes only the parent frame, the reader never lines two frames up positionally and the collapse cannot happen — the reason via is the canonical surface.

Semantics. specializes(k(u…), Parent(v…))via’s meaning — holds iff:

  1. k is in Parent’s family — reflexive: k may be Parent;
  2. k(u…) holds in k’s own extent — the own-extent rule: the tuple’s declaring relation, each row once, reading the pre-closure extent (carried over from the own-extent decision, so no reflexivity × closure duplicate; see the naive idiom below);
  3. φ_k(u…) unifies with (v…) — the member image, routed through the declared mapping, equals the parent-frame arguments.

For k = Parent the reflexive arm reads the parent’s own-tuples view — the tuples declared directly on Parent, excluding the RFD 0005 closure images copied up from members — under the identity φ. Constant pins in the parent frame prune members statically (above): where φ_k’s image is incompatible with a pin, the member contributes nothing and is dropped before any row is read.

The naive idiom this fixes. The obvious hand-rolled provenance query is the bare specializes(k, IncomeItem), k(p, a, c) — the now-deprecated bare two-argument specializes followed by a member-framed application — and it is wrong twice, which is why via (and the own-extent semantics behind it) exist:

  1. Over-returns — the reflexivity × closure duplicate. $specializes is reflexive (compiler/crates/oxc-reasoning/src/compile/rule.rs, the reflexive-transitive <: closure; compiler/crates/oxc-runtime/src/standpoint.rs documents $specializes as reflexive-transitive), so k ranges over {WagesUSA, InterestUSD, IncomeItem}. But the RFD 0005 closure (compiler/crates/oxc-runtime/src/standpoint.rs: a member tuple is added to its own extent and copied into each ancestor’s extent) has already placed the image into IncomeItem’s extent, so each family row returns twice — once under its member (own extent), once under the reflexive parent (closure copy). Reading own extents (rule 2) returns each row once.
  2. Buries the frame convention. k(p, a, c) applies every k at the parent’s three-place frame, but WagesUSA is two-place; which frame a bare application speaks is left implicit. via writes only the parent frame and lets φ bridge, so no frame is left implicit (the φ-not-position rule).
QuerySpellingRows returned
Parent-union (no witness)IncomeItem(p, a, c)(alice,100,USD), (alice,30,USD)
Provenance, naive (deprecated bare form)specializes(k, IncomeItem), k(p,a,c)(alice,100,USD,WagesUSA), (alice,30,USD,InterestUSD), (alice,100,USD,IncomeItem), (alice,30,USD,IncomeItem) — duplicated under the reflexive parent
Provenance, via (canonical)IncomeItem(p,a,c) via k(alice,100,USD,WagesUSA), (alice,30,USD,InterestUSD) — each row once under its declaring member
Provenance, generalized (via’s meaning)specializes(k(_fresh…), IncomeItem(p,a,c)), …identical rows — the desugar of the via spelling above

Maintainability — one lowering path. via never has independent semantics: it desugars to the generalized atom, which lowers onto the D5 dispatch. There is nothing for via to drift from — it is the general form with fresh wildcards. The one new named construct behind either spelling is the parent’s own-tuples view (the pre-closure parent extent) for the reflexive k = Parent arm; every other arm is an existing D5 member arm with the selector surfaced as the k binding. A via (or its generalized desugar) query and the equivalent hand-written per-member derive compile to the identical circuit (D9).

via collision check (why via, not from/[k]). via was chosen because it is the sole spelling collision-free in rule-body position:

CandidateVerdictCollision
Parent(args) via kchosenvia is not a keyword and appears in no grammar (compiler/crates/oxc-syntax/grammar.d/keyword.toml, no identifier-position use); free in rule bodies.
Parent(args) from krejectedfrom is reserved with two live rule-region uses: the select … from … clause (compiler/crates/oxc-parser/src/grammar/rules.rs) and field: T from Rel.endpoint navigation (compiler/crates/oxc-parser/src/grammar/decls.rs). A trailing from is ambiguous against the projection clause.
Parent[k](args)rejected[ after a path is index syntax (compiler/crates/oxc-parser/src/rule_atom.rs); Parent[k] parses as indexing Parent by k.

Example set — each example a pair: the canonical via spelling first, its generalized specializes meaning desugared directly beneath.

The two spellings in each pair produce identical rows by constructionvia desugars to the generalized atom, and both lower along one path onto the D5 dispatch, so there is nothing for via to drift from (see Maintainability — one lowering path above). Read the second line of each pair as the definition of the first, not as an alternative surface to write. _fresh… is the fresh, all-wildcard member frame via elides (m = the selected member’s arity; the desugar identity above).

pub rel IncomeItem(p: Person, amount: Int, c: Currency);
pub rel ForeignIncome(p: Person, amount: Int, c: Currency) <: IncomeItem(p = p, amount = amount, c = c);
pub rel WagesUSA(p: Person, amount: Int) <: IncomeItem(p = p, amount = amount, c = USD);

// (1) IDENTITY family.
IncomeItem(p, a, c) via k                          // canonical
specializes( k(_fresh…) , IncomeItem(p, a, c) )    // the same query, desugared
// Meaning: every IncomeItem-family tuple (p, a, c), with k bound to the member relation that declared it.

// (2) PIN SELECTION — only USD-producing members.
IncomeItem(p, a, USD) via k                         // canonical
specializes( k(_fresh…) , IncomeItem(p, a, USD) )   // the same query, desugared
// Meaning: family tuples whose parent-frame currency is USD, with witness; members pinning another currency are pruned statically.

// (3) REORDERING — φ handled by the declared mapping, not the surface.
Edge(s, d, w) via k                                 // canonical
specializes( k(_fresh…) , Edge(s, d, w) )           // the same query, desugared
// Meaning: every family edge read in the parent frame (s, d, w) = the image (to, from, w), with witness; φ does the reversal, the surface never names it.

// (4) WILDCARD parent frame — the parent frame may carry `_`.
IncomeItem(_, a, USD) via k                         // canonical
specializes( k(_fresh…) , IncomeItem(_, a, USD) )   // the same query, desugared
// Meaning: amount only, USD image, with the declaring member as witness.

pub rel Engagement(firm: Firm, client: Client);
pub rel AuditEngagement(firm: Firm, client: Client)      <: Engagement(firm = firm, client = client);
pub rel ConsultingEngagement(firm: Firm, client: Client) <: Engagement(firm = firm, client = client);

// (5) MULTI-WITNESS — two witnesses on one client, compared. Auditor independence.
// NOTE — the two derive rules in each pair below are ALTERNATIVE SPELLINGS of
// one query, shown together for comparison; in a real program write one of them
// (two co-resident rules with the same head would union — both would fire).
pub derive independenceViolation(firm: Firm, client: Client) :-  // canonical
    Engagement(firm, client) via k1,
    Engagement(firm, client) via k2,
    k1 != k2;
pub derive independenceViolation(firm: Firm, client: Client) :-  // the same query, desugared
    specializes( k1(_fresh…) , Engagement(firm, client) ),
    specializes( k2(_fresh…) , Engagement(firm, client) ),
    k1 != k2;
// Meaning: a firm may not both audit and consult for one client. The witnesses must be DISTINCT members.
// The parent query alone cannot express it: the union self-joined on (firm, client) is satisfied by two
// engagements of the SAME kind — only the witness distinguishes which member each row was declared in.
// Reflexivity caveat (semantics 1): k1/k2 range reflexively, so k = Engagement — the parent's own-tuples
// view (own-extent rule; "For k = Parent …" above) — is itself a witness. This example assumes Engagement
// carries NO bare own-tuples (every engagement is declared in a member), so k1 != k2 means two proper
// members. Where the parent does carry own-tuples, a bare Engagement + one member would satisfy k1 != k2
// and over-return; guard k1/k2 to proper members there.

// (6) WITNESS-AS-JOIN — the witness is an ordinary join key, shared across atoms.
pub derive sameCategory(p1: Person, p2: Person, kind: Relation) :-      // canonical
    IncomeItem(p1, _, _) via kind,
    IncomeItem(p2, _, _) via kind,
    p1 != p2;
pub derive sameCategory(p1: Person, p2: Person, kind: Relation) :-      // the same query, desugared
    specializes( kind(_fresh…) , IncomeItem(p1, _, _) ),
    specializes( kind(_fresh…) , IncomeItem(p2, _, _) ),
    p1 != p2;
// Meaning: two distinct people whose income was declared in the SAME family member `kind` — the witness
// bound once and reused as a join key across both atoms.

Every pair names the parent relation with its full argument list on both lines, so no relation ever stands bare (the overloading-proof property; Open questions §7).

Interactions.

  • Negation. not (Parent(args) via k, …) is an ordinary safe-negation body; the atom obeys the usual bound-variable / range-restriction rules, and k is a body variable like any other.
  • Aggregates. The witness is a first-class grouping key: count{ a } group by k over IncomeItem(p, a, c) via k counts per declaring member — per-member aggregation without naming each member.
  • Multiple atoms. Two witnessed atoms compare their witnesses: IncomeItem(p, a, c) via k1, IncomeItem(p, b, d) via k2, k1 != k2 finds a person with income from two distinct family members. Ordinary term comparison on the bound relation-identity values.

specializes repositioned. The generalized specializes atom is the spec-level meaning of via and the uniform reflection instrument over both tiers (concepts and relations); it is the same reflection atom (D7), argument-carrying in both slots. via is the canonical user surface over it. Because a family-query site names the parent relation literally, the family is statically known and the OE1386-family refusals that guard reflective relation-application over an unknown or ill-typed dynamic selector are unreachable here — there is no dynamic selector to reject.

Staging. Slice D delivers via (and its generalized desugar) over IDENTITY families — implementable on the merged relation-value application immediately, since identity members need only the existing dispatch and the parent own-tuples view. The pin / mapped cases (examples 2–4 above) activate when the wire, check-plane, and dispatch-translation slices land (Staging §3–§5), because they require the per-edge mapping to be stored, checked, and lowered through φ. Slice D is otherwise the canonical via surface desugaring onto the dispatch seam and adds no evaluator; see Staging §6.

D7 — Plane: reflection ($specializes, the relation tier)

$specializes and the relation reflection tier must carry the mapping — the edge is no longer fully described by (sub_id, super_id). Add per-edge mapping metadata: for each parent position, a tagged filler (child-end index / cast / constant literal). The wire encoding is additive and optional-on-decode: a legacy decoder that does not know the mapping field reads an edge as the identity mapping (which is correct for every bare edge, the only kind a legacy producer emits), consistent with the house discipline for reflection additions (RFD 0076 §4 reflection; the armMutability additive precedent). A reflective reader that does understand mappings can enumerate the fillers; a $specializes consumer that only needs membership sees the edge unchanged.

Because a post-RFD producer can emit a genuinely non-identity edge that a legacy decoder would silently misread as identity, the mapping field rides the same artifact core-IR posture as RFD 0076’s per-end is_mut: additive within a version whose stamp lets a stricter deployment distinguish artifacts that use mapped edges. (See Migration for the exact bump discipline.)

D8 — Plane: coverage / OE1404 and resurrection guards

Any plane that consumes subsumption to compute a closure — coverage checks, the RFD 0076 OE1404 cascade-coverage gate, resurrection guards — consumes the image φ(ext(child)), not the child extent. Concretely:

  • The RFD 0076 cascade coverage gate (OE1404, “an immutable end of an incident tuple is not explained by the retraction set”) walks incident tuples of the parent including image tuples; for an image tuple the “dependent context” is the image’s, and the freeze witness for a pinned position is the constant (D3), which is never a retractable individual, so a pinned position never demands coverage. A child-end position demands coverage of the child’s individual, mapped through φ.
  • Resurrection / no-reuse guards (RFD 0076 §“erasure channel”) read the image’s frozen fibers; a pinned fiber has a constant witness that no identity resurrection can launder, closing that residue trivially for pinned positions.

D9 — Plane: incremental circuits (RFD 0018 / RFD 0021 lineage / RFD 0018 IVM)

A mapped member compiles to standard incremental operators: a rename is a column permutation (a projection with reordering), a constant pin is a constant-fill (map) node, a cast is a no-op tag-widening. The image φ(ext(child)) is therefore a project ∘ map node over the child relation, feeding the parent’s union — all monotone, standard DBSP operators (RFD 0018). No new operator kind is introduced; a mapped edge is incrementally maintained by the same machinery as a hand-written projecting derive rule, which is what it compiles to (D5).

D10 — Maintainability by construction

Maintainability is a design axis of this RFD, not an afterthought: the goal is that mapping-related drift is impossible to introduce, not merely unlikely to survive review. Most of these commitments are compile-enforced (one structure, one code path, exhaustive wildcard-free matches, loud refusals); the equality obligation (#5) is the exception — in slice 1 it is a definitional specification, and becomes a real guard only once the dispatch slice routes both directions through one shared translation function (see #5). Six commitments, each a decision the slices implement:

  1. One canonical mapping representation, one owner. The parsed <: Parent(f₁ = m₁, …, fₖ = mₖ) clause elaborates, once, to a single resolved-mapping structure — each parent-end name fⱼ resolved to its parent position, and a MappingEntry stored per parent position (so name-keying is a resolution-time concern; the stored structure is position-indexed). Every consumer (end-mutability inheritance D3, cascade/amend translation D4, dispatch expansion D5, reflection metadata D7, coverage D8, incremental lowering D9) reads that one structure. No plane re-derives the mapping from surface syntax. This follows the callable-catalog / freeze-role precedent (single resolution authority, all other views downstream): there is exactly one place the mapping is computed and exactly one shape it is stored in.

  2. One surface form ⇒ one code path. There is a single surface form (explicit mapped), so there is only ever a resolved mapping to consume — no “unmapped subsumption” branch anywhere downstream. Every consumer handles exactly one case (a resolved mapping); identity is just a mapping whose every entry is the positional ChildEnd, and it exists only as the internal image of a migrated bare site, never as a second surface path to keep in sync. Where the previous design achieved one code path by desugaring a second surface spelling, this design achieves it more strongly by having no second surface spelling. The mappedChecks_identity_eq_bareChecks corollary (D2) is the internal bridge that licensed deleting the old bare check path: it proves the identity mapping run through the general checker computes exactly the old positional Booleans, so removing the bare path changes no verdict (it is the migration-soundness witness of D2/Migration, not a sugar-drift guard, since no sugar remains).

  3. Exhaustive, wildcard-free matches over MappingEntry. The entry enum is ChildEnd { index } | Constant { literal } | Cast { index, to_sort }. Every consumer matches it exhaustively, with no wildcard arm. Adding a fourth mapping form (e.g. the D1c projection relaxation) then fails compilation at every site that must handle it — the compiler enumerates the work, not a reviewer.

  4. Refusals over silent adaptation. Any shape the mapping rules (D1) do not cover refuses at the declaration with a catalogued diagnostic — never inferred, never defaulted, never silently adapted. On the wire the mapping field follows the house three-state discipline: present-and-understood, present-and-too-new (refuse loudly — D7/Migration), or absent = the explicit legacy identity state (an old artifact emits only bare edges, so absence is identity, decidably). Absence is a defined state, not a guess.

  5. Single-authority translation obligation. The dispatch translation (D5) and the cascade translation (D4) are the same φ applied per delta sign, recorded as the dispatch_cascade_same_map obligation (D2). In slice 1 this obligation is definitional (holds by rfl because both directions are the one image function) — it is the specification of the intended design, not yet a drift guard. The guard is owed by the dispatch-translation slice (Staging §5), which must route Rust dispatch and cascade through one shared translation function (D10.1) so the two directions cannot be independently hand-maintained; only then does the rfl become an anti-drift proof rather than a statement of intent (see D2, “What this obligation is, honestly”).

  6. Per-plane drift-impossibility. Each plane interaction names what makes drift impossible rather than unlikely:

    PlaneWhat makes drift impossible
    End-mutability (D3)Inheritance reads the resolved mapping’s entries; a pinned position has no mut field to compare, so the OE0268 check is total over entry kinds (exhaustive match) — a new entry kind cannot silently skip the weakening check.
    Cascade/amend (D4)Uses the same φ as dispatch (obligation D2#dispatch_cascade_same_map); there is no separate withdrawal-mapping to keep in sync. Slice-1 status: the obligation is definitional (rfl) — the spec of that intent; the drift guard is realized when the dispatch slice routes both Rust call sites through one shared translation function (Staging §5).
    Dispatch (D5)The emitted clause is generated from the resolved mapping, not re-parsed from the <: clause; the generator is the sole producer, mapped_dispatch_selected_iff its correctness pin.
    Reflection (D7)The reflected fillers are a projection of the same resolved mapping; a legacy-decode path is the one defined absence state, not an inferred fallback.
    Coverage / OE1404 (D8)Coverage walks image tuples produced by the same φ; a pinned position’s constant witness is derived from the Constant entry, so coverage cannot disagree with dispatch about what a pinned fiber contains.
    Incremental (D9)The circuit nodes are compiled from the resolved mapping (rename→permute, Constant→map, Cast→widen); no operator kind is bespoke, so IVM maintenance cannot drift from the batch image.

The through-line: one mapping structure, one code path, exhaustive matches, and loud refusals make most desynchronizing edits fail to compile. The equality obligation is the weaker link — definitional in slice 1, a genuine guard only after the dispatch slice funnels both directions through one shared translation function (#5) — so it is named as an obligation on that slice rather than claimed here. The aim is that a future edit that would desynchronize two planes fails to compile (or, for the equality axis, fails the obligation once the single-authority wiring lands), rather than passing silently and being caught (or not) by review.


Worked examples

Syntax note: the mapped <: form is carried by this design record; parser, elaborator, and dispatch support land with the slices in Staging, so these blocks are illustrative here and gate-verified once those slices are in.

1 — WagesUSA, end to end

pub type Person;
pub type Currency;
pub const USD: Currency;

// The general family relation — carries a currency.
pub rel IncomeItem(p: Person, amount: Int, c: Currency);

// A specific member that is always in dollars, so it carries no currency end.
pub rel WagesUSA(p: Person, amount: Int) <: IncomeItem(p = p, amount = amount, c = USD);

// An explicit identity member for contrast — same shape as the parent.
pub rel ForeignIncome(p: Person, amount: Int, c: Currency) <:
    IncomeItem(p = p, amount = amount, c = c);

// A family query over everything that is an IncomeItem.
pub derive anyIncome(p: Person, a: Int, c: Currency) :-
    specializes(r, IncomeItem), r(p, a, c);

Dispatch (D5) generates, per family member:

// identity member — positional, exactly RFD 0005 / #1805
anyIncome(p, a, c) :- ForeignIncome(p, a, c);
// mapped member — the currency end is pinned to the constant in the head
anyIncome(p, a, USD) :- WagesUSA(p, a);

Given facts:

insert WagesUSA(alice, 500);
insert ForeignIncome(bob, 300, EUR);

Results table for anyIncome:

pacvia
alice500USDWagesUSA (mapped, currency pinned)
bob300EURForeignIncome (identity)

WagesUSA is a genuine specializes(_, IncomeItem) child, so the family query reaches it — the omission the hand-written derive rule caused (this RFD’s motivation) does not occur.

2 — Sort cast (friend as Person)

pub type Person;
pub type Friend <: Person;        // a Friend is a kind of Person

pub rel Knows(a: Person, b: Person);
// Friendship holds between two Friends but participates in the general Knows
// family; the child ends are cast up to the parent's Person sort.
pub rel Friendship(a: Friend, b: Friend) <: Knows(a = a as Person, b = b as Person);

Every Friendship(x, y) contributes Knows(x, y) with x, y admitted at Person. The cast is checked by covariance (childSort <: parentSort, D1d — Friend <: Person), needs no runtime coercion, and compiles to a tag-widening no-op in the incremental circuit (D9).

3 — An audit rule over the mapped family

// A static check over the whole IncomeItem family — reaches mapped members
// because they are real subsumption children.
#[static]
pub check IncomeItemNeedsCurrency(r: TypeRef) :-
    specializes(r, IncomeItem),
    not rel_end(r, 3, _)            // a member missing the currency end...
    => Diagnostic {
        severity: Severity::Warning,
        code: "Example::W_UncurrenciedIncome",
        message: "An IncomeItem family member declares no currency end; \
                  confirm its subsumption clause pins one.",
    };

WagesUSA satisfies the audit precisely because it does pin the currency (c = USD); a would-be member that neither carried nor pinned a currency would be flagged. The audit sees mapped members only because the mapped form keeps them in the family — a hand-written derive rule would make them invisible to this check.

pub rel IncomeItem(p: Person, amount: Int, c: Currency);

// D1b — the parent `c` end is left uncovered (no filler names it).
pub rel Bad1(p: Person, amount: Int) <: IncomeItem(p = p, amount = amount);
//                                     refused MappedSubsumptionParentEndCoverage (notional)

// D1a — `qty` names no declared parent end of IncomeItem (p, amount, c).
pub rel Bad1b(p: Person, qty: Int) <: IncomeItem(p = p, qty = qty, c = USD);
//                                    refused MappedSubsumptionUnknownParentEnd (notional)

// D1b — the parent `amount` end is named twice (double cover).
pub rel Bad1c(p: Person, amount: Int) <: IncomeItem(p = p, amount = amount, amount = amount);
//                                    refused MappedSubsumptionParentEndCoverage (notional)

// D1c — child end `note` is referenced by no filler (dropped child end).
pub rel Bad2(p: Person, amount: Int, note: Text) <: IncomeItem(p = p, amount = amount, c = USD);
//                                    refused MappedSubsumptionUnmappedChildEnd (notional)

// D1d — child end sort does not widen to the parent position sort.
pub rel Bad3(p: Person, amount: Text) <: IncomeItem(p = p, amount = amount, c = USD);
//                                        refused MappedSubsumptionEndpointVariance (generalizes OE0151)

// D1e — the pinned literal has the wrong sort for the parent position.
pub rel Bad4(p: Person, amount: Int) <: IncomeItem(p = p, amount = amount, c = 42);
//                                     refused MappedSubsumptionPinSort (notional)

// D3 / OE0268 — the child weakens an immutable parent position it maps onto.
pub rel Locked(mut a: Person, b: Person);
pub rel Loosened(mut a: Person, mut b: Person) <: Locked(a = a, b = b);
//                              ^^^^^ refused OE0268 — child may not weaken an
//                                    immutable parent position through the mapping.

Alternatives considered

OptionWhat it isWhy not
(a) Status quo: bare-only <: + hand-written derive rulesKeep RFD 0005 unchanged; express any translation as a separate derive.Loses family membership: a derive head is not a specializes child, so #1805 dispatch and every family audit silently exclude the relation. Silent-omission rot on the exact open-family workload RFD 0005 cited. The covariance check is also lost (RFD 0005’s own complaint).
(b) Views as a separate, non-subsumption featureA distinct “relation view” mechanism (a named projection/rename) parallel to <:.Two mechanisms for one idea, and the fatal flaw: a view is not a subsumption edge, so dispatch (#1805) misses views exactly as it misses derive rules — reproducing the very rot in (a). Family membership must be subsumption.
(c) Layered: bare = identity spelling, mapped = general formOne mechanism, two spellings; the bare positional <: Parent is kept as sugar for the identity mapping, the mapped filler list is optional.Previously the chosen design; rejected here. Zero migration is its only real advantage, and it is not worth its cost in a corpus written predominantly by tooling and read by people: two spellings for one idea guarantee style drift, and the bare spelling’s positional pairing silently re-pairs the wrong ends under a parent-end reorder — the exact silent-failure class this RFD exists to remove. The reader-over-writer trade favors self-describing declarations, and no external consumer depends on the bare form (pre-1.0), so keeping it buys drift and a latent footgun for a migration cost that is mechanical anyway. A deprecation lint keeping the spelling as a warning was also considered and rejected: with no dependents there is nothing to protect during a window, and a warning-only stage just prolongs the two-spelling regime.
(d) Positional filler listThe filler list is required, but fillers are a bare token per parent position (filler i fills parent position i, naming a child end / constant / cast) — no parentEnd = key.Considered and rejected on an empirical finding. This was the first cut of the required-filler design and reads more tersely (<: Transfer(src, dst)). It was verified on the branch to carry the exact defect the whole RFD exists to remove: when two parent ends share a sort, reordering them re-pairs a positional filler list silently — filler i now fills a different parent end, every position still type-checks, and the child declaration is byte-identical, so the sort-covariance gate (D1d) cannot catch the flip. A positional list is therefore no safer than the bare form under a parent-end reorder; its only gain over bare is that the pairing is visible at the child site, not that it is stable. Parent-name keying (chosen) makes the pairing stable — a filler names the parent end it fills, so a reorder re-anchors the mapping to the same meaning and updates only the resolved position. The terseness is not worth reintroducing the silent-re-pair footgun in a machine-written corpus. (Same-name renames are still written in full — p = p, no bare-token abbreviation — because a bare token is the positional form this row rejects; Open questions §6.)
(chosen) Explicit mapped form only, keyed by parent end name; bare form removedThe filler list is required on every <:, and each filler is keyed to the parent end it fills (parentEnd = childEnd / as cast / constant pin); the bare <: Parent and the positional list (d) are both refused. One surface form. Identity mapping survives internally as the migration image and the object of the migration-soundness theorem.One surface form ⇒ one code path (D10.2) with no second spelling to keep in sync; self-describing declarations for the machine-written / human-reviewed corpus; and — unlike (c) bare or (d) positional — pairing that is mechanically reorder-safe: a filler names its parent end, so reordering either relation’s ends re-anchors the mapping to the same meaning and cannot silently re-pair (D1). Mapped members are first-class specializes children so dispatch and audits reach them. Migration is mechanical (D2 mappedChecks_identity_eq_bareChecks proves it meaning-preserving) and one-time; the only cost is rewriting in-repo bare sites and shipping an ox-migrate for the external corpus (Migration).

Migration

Source migration is required and mechanical. Removing the bare form is a breaking source change, taken deliberately (Alternatives (c)). It is bounded and one-time:

  • In-repo bare sites (reference-book examples, the conformance corpus, fixtures) are migrated in the same slice that turns on the refusal (Staging §4a). The rewrite is mechanical: a bare child <: parent becomes child <: parent(f₁ = e₁, …, fₙ = eₙ) where f₁ … fₙ are the parent’s declared end names and e₁ … eₙ are the child’s own end names, paired position-for-position in declaration order — the identity mapping, read directly off the two declarations (both name lists are in scope at the migration site). The rewrite presupposes equal arity n on both sides (fⱼ = eⱼ for j = 1 … n); that precondition is inherited unchanged from the bare form it replaces, whose positional pairing already requires child arity to equal parent arity (a mismatch is the pre-existing arity refusal, not a migration case). The specializes-keyword spelling of a bare clause (child specializes parent) migrates the same way, to child specializes parent(f₁ = e₁, …, fₙ = eₙ) (D1, gate (h) is glyph-agnostic). mappedChecks_identity_eq_bareChecks (D2) proves this rewrite preserves every accept/refuse verdict, so it is a semantics-preserving textual transform, not a re-authoring.
  • External ontology-library corpus. An ox-migrate rewrite performs the same identity-argument-list expansion for the out-of-repo ontology corpus; that corpus’s migration is already queued for its next toolchain upgrade. Because the language is pre-1.0 there is no other external consumer of the bare form.
  • Deprecation lint considered and rejected. A warn-only stage that kept the bare spelling compiling during a window was weighed and rejected: with no dependents there is nothing a window protects, and it would only prolong the two-spelling regime the removal exists to end. The refusal lands directly, with the in-repo migration in the same change.

Wire/reflection is still additive within the toolchain. The reflection/wire mapping field (D7) carries the resolved mapping. Because every surface edge is now mapped, a producer always emits an explicit mapping; the identity mapping is emitted for migrated bare sites exactly as for any other. A legacy decoder that predates the field reads an edge as the identity mapping — correct only for the identity edges a legacy producer could emit; a post-change producer emitting a genuine non-identity edge rides a core-IR version stamp under the same discipline as RFD 0076’s per-end is_mut, so a decoder too old to understand a non-identity mapping refuses loudly rather than silently misreading it as identity (the RFD 0076 major-bump rationale applies whenever a non-identity edge is present).


Open questions

  1. Can a child map to multiple parents? A relation may today have multiple <: edges. With mappings, each edge carries its own φ; the child’s extent contributes an image to each parent independently. Nothing in the semantics forbids it, but the interaction of different mappings to different parents with the RFD 0076 conjunctive mutability inheritance (a child end mapped to an immutable position in one parent and a mut position in another) needs an explicit rule. Provisional stance: the child end’s effective mutability is conjunctive over every parent position it maps onto across all edges; confirm at mechanization.

  2. Constant pins under bitemporal amendment of the constant’s meaning. A pin USD is a compile-time constant reference. If the referent of USD (say a Currency individual) is itself subject to bitemporal correction/amendment (RFD 0076 §5), does the pinned fiber’s freeze witness track the correction? The pin is a value, not an independent assertion, so provisionally the image tuple simply reflects whatever USD denotes; but whether a pin may reference a mutable individual at all, versus only a const, is open.

  3. Do the bare positional checks (OE0150OE0154 / arityEqual + positional covariance) survive as a runtime path? Answered: no — subsumed. With the bare surface form removed, there is no declaration that reaches a positional-only checker, so checkSubsumption/arityEqual/endpointsCovariant are not retained as a live elaborator code path. Every edge is elaborated to a resolved mapping and run through checkMappedSubsumption, and the meanings of OE0150OE0154 are subsumed by the general gates: arity (OE0150) → arityMapsParent; endpoint covariance (OE0151) → covariantFillers; cardinality (OE0152) and metarel (OE0153) are threaded through the mapping unchanged; acyclicity (OE0154) is reused verbatim. The bare Lean predicates remain only as the target of mappedChecks_identity_eq_bareChecks — they are the specification the identity mapping is proved to compute, i.e. the migration-soundness witness, not an executed check. Any performance question is therefore about the single general checker; there is no second path to benchmark against.

  4. Relaxing D1c (dropped child ends) with an explicit projection marker. This RFD forbids dropping a child end. A future relaxation could admit child(a, b, drop c) <: parent(x = a, y = b) with a marked projection (drop naming the child end that maps to no parent end), defining the collapse semantics (image is the projection; two child tuples differing only at c coincide in the parent). Deferred — it needs its own coverage/cascade story because the collapse loses the child identity the cascade direction (D4) relies on.

  5. Diagonal mappings and cardinality. A child end referenced at two parent ends (parent(f₁ = e, f₂ = e)) — does the parent’s per-position cardinality interact soundly, or should a diagonal be refused pending a worked cardinality rule? Provisionally admitted (D1); flagged for the check-plane slice.

  6. Must the identity mapping be written out even when trivial — and may a same-name filler abbreviate? Answered: written out in full; no abbreviation. A shape-identical child must still write the full filler list keyed by the parent’s end names (Home(p, c) <: Loc(p = p, c = c)), and a filler whose child end shares its parent end’s name is still written p = p, never a bare p. Two abbreviations were weighed and rejected: (i) omitting the list entirely for the identity case (that is the bare form, Alternatives (c)); and (ii) admitting a bare token p as sugar for p = p (that is the positional filler form, Alternatives (d) — the very spelling whose silent re-pair under a parent-end reorder motivated parent-name keying; a bare token has no parent-end key and so re-pairs positionally). Either would reintroduce a second surface spelling this RFD removes. Recorded plainly so no later “convenience” change re-adds an implicit identity form or a bare-token filler: the identity mapping is an internal representation only, and every surface filler carries its parentEnd = key.

  7. Arity overloading would be blocked by bare relation references — a forward constraint, mostly settled by D6. If relation-name overloading (Loc(a, b) and Loc(a, b, c) as one name) is ever wanted, arguments disambiguate the overload at almost every site: an ordinary atom carries its arity, a mapped <: edge carries it in the filler list, and the family-query form (D6) carries it in both the member frame k(memberArgs) and the parent frame Parent(parentArgs). The bare specializes(k, Loc) two-argument form — a relation standing as a value with no arguments — was the dominant remaining bare site; that is now decided. D6 makes the argument-carrying family-query surface canonical — the trailing via (Parent(args) via k), whose parent atom carries its arity, with the generalized specializes(k(memberArgs), Parent(parentArgs)) (both argument lists required) as its meaning — and the bare two-argument specializes(k, Parent) is deprecated then refused on the same explicit-only migration pattern as the bare <: (D1 gate (h), Migration). The previously-provisional answer here — “a future overloading design would need an argument-carrying specializes form” — is therefore no longer provisional for specializes: the argument-carrying forms are the surface, and the bare form is removed on the same schedule. The residue is relation literals in value position (k != Wages, a relation-valued endpoint): these still stand bare and are the sole sites a future overloading design must settle with an arity-qualified literal spelling. Any surface that renders relation values (diagnostics, provenance columns, reflection output) would need arity-qualified display from day one. Recorded so the constraint is weighed before the D6 slice ships its rendering, and so overloading is not attempted without settling the remaining relation-literal sites first.

  8. Divergent-map subsumption diamonds. Answered: refused until motivated. A relation may reach the same ancestor through more than one chain of subsumption edges. When every chain composes to the same mapping onto that ancestor, the diamond is harmless — a child fact frames to one ancestor row no matter which path is read — and stays legal. When two chains compose to different mappings, there is no single answering frame. Worked example: an ancestor A(x, y) with two intermediates that swap the ends,

    rel A(x: Person, y: Person)
    rel Left(a: Person, b: Person)  <: A(x = a, y = b)   // straight
    rel Right(a: Person, b: Person) <: A(x = b, y = a)   // swapped
    rel D(p: Person, q: Person) <: Left(a = p, b = q), Right(a = p, b = q)
    

    D(alice, bob) composes to A(x = alice, y = bob) via Left and to A(x = bob, y = alice) via Right: it would occupy two different rows in A, and a family query over A follows the first path in the subsumption-image closure — so it silently reports one framing and drops the other. Silent under-report is the worst failure shape, so the divergent diamond is refused at declaration (OE1424, in the declaration pass over the resolved per-edge mappings); the message names the child, the ancestor, and both paths with their composed mappings in parent-name-keyed filler form. This makes the closure’s first-path selection exact by construction (every diamond that reaches evaluation agrees on every path). Both-images support — the child genuinely occupying both ancestor rows, one per divergent path — is a deliberate future relaxation: it needs a provenance model for one-row-per-path and a cardinality story, so it is deferred rather than guessed. A modeler who wants it files a feature request; until then, reconcile the fillers so the compositions agree, or remove one edge.


Staging

Mirrors the callable A/B/C slice pattern (#1805/#1806) and builds directly on #1805’s dispatch seam:

  1. RFD accepted — this record; the explicit-only surface policy, the identity mapping as internal-representation-and-migration-bridge, and the plane-interaction decisions are fixed.
  2. Lean mechanizationArgon.Substrate.RelationSubsumption gains the mapping data, arityMapsParent, covariantFillers, and the mappedChecks_identity_eq_bareChecks corollary (the migration-soundness witness); Argon.Reasoning.Datalog.RelationApplication gains mapped_containment and the restated mapped_dispatch_selected_iff.
  3. Wire / metadata slice — the per-edge mapping field on the subsumption edge, additive and optional-on-decode (D7), with the core-IR version discipline (Migration).
  4. Check-plane slice — parser + elaborator: the filler grammar (D1), the new refusals (D1a–e notional codes), and the through-mapping cardinality/metarel checks (D2). Reflection tier exposes the fillers. The mapped form is accepted here, but the bare form still parses (as the identity mapping) so this slice is non-breaking on its own.
    1. Bare-form removal + in-repo migration slice (its own slice) — turns on the D1 gate (h) refusal for the bare form and, in the same change, migrates every in-repo bare site (reference-book examples, conformance corpus, fixtures) to the explicit identity form by reading each child’s own end names off its declaration. Kept a distinct slice because it is the only breaking step and pairs the refusal with the migration atomically; the external ontology-library ox-migrate rewrite is prepared here and applied at that corpus’s next toolchain upgrade (Migration). No deprecation lint — the refusal is direct.
  5. Dispatch-translation slice — the mapped-clause generator (D5) over #1805’s expansion, and the incremental-circuit lowering (D9). This is the slice that delivers the motivating payoff: mapped members become live specializes children.
  6. Family-query surface slice (callable-port slice D) — the generalized specializes(k(memberArgs), Parent(parentArgs)) grammar (both argument lists required), the canonical via surface (Parent(args) via k ≡ specializes(k(_fresh…), Parent(args))), the parent’s own-tuples view for the reflexive k = Parent arm, and the deprecate-then-refuse schedule for the bare two-argument specializes(k, Parent) (D6). This slice splits by mapping kind:
    • IDENTITY families now — the generalized atom and via over identity members need only the merged relation-value application (#1805 dispatch) plus the own-tuples view, so they are implementable immediately on the merged work; both frames coincide under the identity φ.
    • Pin / mapped cases later — pin selection, image constraints, and reordering mappings (D6 examples 2–4) require the per-edge mapping to be stored (§3 wire), checked (§4 check-plane), and lowered through φ (§5 dispatch-translation), so they activate only once those slices land. The surface is otherwise the canonical via spelling desugaring onto the dispatch seam and adds no evaluator. Depends on #1805 (member arms) and #1806 (the selector gates it renders unreachable at family-query sites).

References

  • RFD 0005, Relation subsumption — the bare positional form this RFD generalizes; its elaborator checks (OE0150OE0154) become the identity-mapping instance.
  • RFD 0076, Mutability of relation ends — the end-mutability inheritance (OE0267/OE0268), the retract/amend cascade, the additive-reflection and core-IR-version discipline this RFD reuses.
  • #1805, Relation-value application — finite-domain dispatch over first-class relation values — the dispatch seam mapped members plug into; the module Argon.Reasoning.Datalog.RelationApplication and its structural_containment / dispatch_selected_iff obligations.
  • #1806, Relation-value application check-plane gates — the gate galleries the mapped-subsumption refusals extend.
  • RFD 0018, Production reasoner: the incremental DBSP engine — the incremental operators a mapped member compiles to (project / map / union), D9.
  • Argon.Substrate.RelationSubsumption (spec/lean/Argon/Substrate/RelationSubsumption.lean) — the elaborator-check predicates generalized here.

RFD 0085 — Incremental rule circuit compiler

State: accepted — partially implemented (incremental circuits remain gated; Product rollout is implemented but not yet the process default)

Decision

Argon will compile accepted rule programs into immutable incremental circuit plans and execute signed mutation batches through persistent circuit instances. The circuit compiler is a new reasoner backend. Existing maintenance and cache systems are reusable evidence and fallback mechanisms, not this RFD’s plan or architecture.

This record refines RFD 0018’s DBSP objective after RFD 0021 rejected a Feldera fork in favor of Argon’s own operators. It realizes RFD 0018’s signed Z-set, arrangement, incremental-maintenance, differential-oracle and durable-read-model direction as a persistent compiled rule circuit. It does not replace RFD 0018’s event-log authority, valid-time separation or independent Established oracle. Where RFD 0018 describes DRedc as the shipped incremental ceiling or batch WFS as the only WFS implementation, this record adds the gated circuit and Product backends while retaining those implementations as fallback and oracle paths.

Objective

Compile an accepted Argon rule program Q into a persistent circuit that maps signed base-relation batches to signed derived-relation batches. This is rule-program incrementalization, distinct from retaining parser or checker queries between source revisions.

For every committed input prefix, the load-bearing equality is:

integrate(QΔ(input deltas through frontier t))
    = Engine::evaluate(integrate(input deltas through frontier t))

The construction follows DBSP:

QΔ = differentiate ∘ lift(Q) ∘ integrate

Primary references:

The implementation sequence follows the DBSP implementation packet assembled from the repository’s decision records, current-engine audit and primary literature. That packet is design evidence rather than repository authority; this RFD is the decision record that ratifies the sequence.

The unpublished manuscript A Well-Founded Reasoning Substrate for Knowledge Representation informed the operator inventory. It is a local research input, not a publishable authority or normative dependency of this decision.

This RFD owns the complete program, but each phase has an independent admission boundary. Phase A is the ordinary set-valued, strict-stratified circuit. Phase B adds the evidence-for/evidence-against pair representation. Phase C admits retraction through that pair only after its provenance projection obligation is mechanized. Phase D adds incremental well-founded recursion. Phase E makes the state durable. No later phase is implied by an earlier phase’s implementation.

Terms and identity

  • RelationId is the module-qualified relation identity plus its declared arity and canonical column schema. Generated helper relations use their generated, module-qualified identity. A short relation name is never an execution key.
  • NodeId is a collision-checked digest over the module commitment, local identity stratum/SCC identity, producer-rule identity, structural operator path, operator kind, input ports, relation identities, keys, projections and expressions. Node vector position is not identity. A digest collision is a compile refusal. An operator separately retains its global scheduling stratum. Whole-program rebasing changes scheduling order without changing local node identity.
  • CircuitPlanId commits to the circuit schema version, the sorted multiset of exact compiled-rule commitments, and the complete canonical CircuitPlan encoding. The plan retains 32 bytes per source rule, not another rule copy.
  • CircuitInstanceId is (tenant, fork, module commitment).
  • A Frontier is the totally ordered event-log position for one tenant/fork. A batch names (prior_frontier, next_frontier, batch_commitment), where next_frontier is the exact successor committed by storage. A duplicate with the same next frontier and commitment is idempotent; a conflicting duplicate, gap or out-of-order batch is refused without state change.

The module commitment covers the canonical compiled rules independent of input enumeration order, rule identities, stratification, inline and other module-static relations, world assumptions, metric/clock declarations, canonical tuple/value encoding version, lowering version and optimizer version. It is the artifact’s semantic module commitment, not a storage generation. Storage generation is a coherence signal and frontier input. Circuit schema version belongs to the plan and checkpoint compatibility keys.

Boundary

source rules + catalog statistics
                  |
                  v
 QueryEngine physical preparation
  | optimized rules
  | one accepted stratification
  | physical strategy
  | module commitment
                  |
                  v
       incremental circuit compiler
                  |
                  v
 immutable CircuitPlan + module commitment
                  |
                  v
 CircuitInstance per tenant/fork/module
       | persistent operator state
       | persistent arrangements
       | published frontier
       v
signed derived batches and current model

System ownership

source packages
      |
      v
parser -> checker -> elaborator -> compiled rules + module commitment
                                      |
                                      v
                         incremental circuit compiler
                                      |
                         immutable CircuitPlan
                                      |
               +----------------------+----------------------+
               |                                             |
               v                                             v
     full evaluator and oracle                  CircuitInstance registry
                                                         |
                                           tenant / fork / module
                                                         |
                                      traces + arrangements + frontier
                                                         |
                                                         v
                                             runtime read model

Implementation boundaries

incremental/
  ir                 immutable typed plans and identities
  commitment         canonical versioned plan/module encodings
  compile            acyclic lowering and admission
  compile/recursion  SCC analysis and feedback-cut planning
  circuit            instance, frontier, scheduling, atomic publication
  circuit/analysis   immutable fusion, liveness and retention metadata derived from the plan
  circuit/profile    retained operator state and publication journals
  circuit/profile/antijoin_support temporal exclusion support storage and preparation
  circuit/profile/aggregate_state  canonical retained aggregate fold model
  circuit/profile/count_patch  aggregate group preparation and atomic publication patch
  circuit/profile/parity profiled-to-integrated retained-state parity checks
  circuit/profile/sparse_profile sparse signed support, cardinality and rational histories
  circuit/profile/transaction validated retained-state transaction preparation
  circuit/profile/record_state/access  selective record-backed evaluator reads
  circuit/profile/record_state/codec  semantic-to-record identity conversion
  circuit/profile/record_state/prepare  scalar and Product transaction preparation
  circuit/profile/time_profile  generic sparse logical-time delta history
  circuit/join       bilateral join state and journals
  circuit/antijoin   keyed exclusion state and journals
  circuit/multijoin/record_codec record-backed scalar-key and evaluation-row adaptation
  circuit/aggregate  grouped fold state and journals
  circuit/profile_eval/distinct  temporal zero-crossing state
  circuit/profile_eval/count     temporal grouped cardinality state
  circuit/profile_eval/count/change  semantic and record input-delta derivation
  circuit/profile_eval/count/fold  generic retained aggregate-fold maintenance
  circuit/profile_eval/count/output  temporal aggregate output differentiation
  circuit/profile_eval/profile_flow generic temporal transport, projection and publication helpers
  circuit/profile_eval/factorized_aggregate  factor/group transition orchestration
  circuit/profile_eval/factorized_aggregate/record record-backed factorized adaptation
  circuit/profile_eval/factorized_projection canonical projection folds and results
  circuit/profile_eval/state_view/record shared ordinary/Product record execution; logical-time antijoin modes
  checkpoint/schema frozen versioned wire declarations; no conversion or restore policy
  checkpoint/canonical representation-neutral canonical sparse-profile validation
  checkpoint/integrated integrated operator state conversion and reconstruction
  checkpoint/factorized factorized aggregate profile conversion and reconstruction
  checkpoint/migrate explicit conversion from supported historical wire schemas
  checkpoint/payload payload validation and off-path instance reconstruction
  checkpoint/profile whole-profile conversion and immutable-plan validation
  checkpoint/record record-backed profile conversion and reconstruction
  checkpoint/semantic semantic-profile conversion and reconstruction
  checkpoint/state  semantic/wire conversion and off-path instance reconstruction
  checkpoint/validate restored-state invariants against the immutable plan
  checkpoint/validate/semantic semantic-profile restored-state invariants
  checkpoint/validate/record record-backed restored-state invariants
  checkpoint/envelope bounded canonical framing, checksum and version dispatch
  recursive_rules/adapter semantic recursive construction and transaction orchestration
  recursive_rules/adapter/construction admission and immutable SCC-state construction
  recursive_rules/adapter/assignment shared semantic and record-backed assignment preparation
  recursive_rules/adapter/interned record-resolved read projection; no scheduling authority
  recursive_rules/adapter/legacy_oracle test-only encoding boundary for differential evaluation
  recursive_rules/adapter/plan_support fragment admission, relation shapes and join-cache construction
  recursive_rules/adapter/record_codec stateless semantic/record delta conversion
  recursive_rules/adapter/semantic_transaction frontier admission and semantic transaction preparation
  recursive_rules/adapter/sink_profile recursive sink-profile selection and composition
  program/well_founded           record-native WFS state and atomic journals
  executor/wfs                   immutable gamma lowering and batch oracle
  nested_trace       time-indexed signed trace algebra
  recursive          SCC-local nested-time scheduler
  arrangement        persistent indexed Z-set state
  zset               exact checked signed multiset

maintainer/circuit   catalog adapter and whole-program routing
query_engine         public construction seam
runtime              durable append, visible frontier and cache ownership

Dependencies point downward through this list: runtime uses the query-engine facade; the maintainer adapts catalogs to circuit batches; circuit execution consumes immutable plans; operator modules depend on Z-sets and arrangements. The compiler never depends on mutable circuit state. Operator implementations do not read runtime catalogs, environment flags, storage generations or tenant registries. Recursive scheduling does not call the full evaluator; fallback is a maintainer-level whole-program decision.

factor and outer corrections
            |
            v
factorized_aggregate  -- group selection, retained-state merge, journal
            |
            v
factorized_projection -- None | Linear | Distinct | Extrema, validation, value fold
            |
            v
temporal output correction

Live projection variants encode the aggregate family they maintain. Adding an indexed nonlinear fold changes projection mutation, merge, validation and observation there without adding representation branches to group-transition orchestration. The checkpoint DTO retains the generic legacy leaf shape only as a wire-compatibility boundary; runtime state never adopts it.

Files are split by state ownership, not merely by size. Transaction preflight and publication remain centralized so an operator cannot publish independently. Each operator module owns its state, journal, preflight, delta evaluation and invariant tests. Canonical encoding is a separate module and must not use Rust Debug output. Integration and benchmarks use only public circuit interfaces.

Join correction semantics are representation-independent; journal encoding is not. The semantic backend journals time-indexed tuples. The record backend journals canonical (key identity, time, tuple identity, weight) entries created from the same semantic correction in the caller-owned record transaction. Publication consumes those identities directly and never resolves a tuple to reconstruct its key. The backend contract associates the journal type with the state representation, so a semantic journal cannot reach record publication or vice versa. Both forms still execute one bilinear join and correction algorithm.

Implemented architecture status (2026-07-18)

The implementation establishes this ownership path:

QueryEngine
    |
    +-- seed module-static inputs
    +-- select policy and catalog compatibility
    +-- prepare one owned physical rule program
    v
typed admission decision
    |
    v
maintainer adapter -> immutable acyclic or whole-program circuit plan

As of 2026-07-20, recursion through negation is admitted through a typed well-founded component. This is a correctness and integration step, not the Phase-D incremental alternating-fixpoint algorithm:

signed program batch
        |
        v
dependency-filtered record deltas
        |
        +-- empty ------> retain inputs + (true, possible) pair
        |                        |
        |                        v
        |                 emit no derived delta
        |
        `-- changed ----> transaction-local input snapshot
                                 |
                                 v
                      batch WFS from precision bottom
                                 |
                                 v
                    validate true subset-of possible
                                 |
                                 v
                    derive true + undefined deltas
                                 |
                   validate all component journals
                                 |
                                 v
                 atomically publish state + frontier

The component retains exactly one record-native representation of its inputs, well-founded-true extent and well-founded-possible extent. Checkpoints use the same semantic state. Preparation either produces an explicit unchanged journal or a complete replacement journal; failure cannot mutate published state. Every changed dependency currently rebuilds the complete component from precision bottom when the dependency’s logical membership changes. Positive support changes that do not cross zero update sparse absolute input assignments without changing the pair: WFS reads EDB set membership, so 1 -> 2 and 2 -> 1 cannot change gamma or its fixpoint. Reusing the previous approximation across a membership change is forbidden until the Phase-D equivalence obligation is discharged.

The stable-revision loop is representation-independent: it owns the sequential upper = gamma(lower); lower = gamma(upper) order, precision-bottom start, per-round K3 validation, convergence bound and round count. The current encoded gamma oracle and future record-native coordinate circuits share this driver. Committed transaction telemetry distinguishes WFS component rebuilds from stable-revision rounds; unchanged and membership-invariant journals report zero for both.

The record-native gamma coordinate is compiled as an ordinary program circuit after replacing in-component NAF reads with fixed-assumption input ports:

base record deltas -----------------------+
                                          |
assumption-coordinate deltas              v
        |                         signed program circuit
        v                                  |
fixed-assumption antijoin ports            |
        |                                  |
        +----------------------------------+
                                           |
                               exact gamma output delta

Executable gates cover both acyclic gamma bodies and positive-recursive gamma bodies while inserting and retracting assumptions. The encoded batch stability step remains production authority until the coordinate circuit is driven by the shared stable-revision scheduler and differentially reproduces complete true and possible pairs.

Gamma admission is immutable plan state. Its closed component plan contains an ordered vector of acyclic and positive-recursive lowerings; a nested well-founded component is a compile refusal. A domain-separated gamma commitment is included in the enclosing WFS component commitment and therefore in the whole-program plan identity. Runtime execution cannot silently switch between encoded and circuit gamma after checkpoint compatibility has been established. Admission also requires every embedded acyclic plan, nested recursive plan and recursive SCC to retain its canonical identity. Because an operator identity commits its input identities, relation columns, join keys, predicates, expressions, aggregate configuration and projection, this validates the complete connected physical body without a second lowering representation. Exact source-rule commitments and per-rule output correspondence then bind that canonical graph to the rewritten source. Semantic differential tests against fresh Established evaluation remain the independent compiler-correctness oracle.

The acyclic gamma session is a closed interned component over the parent WFS record arena and record transaction. It owns only transaction-local derivative state, an inner frontier and complete typed output extents. Each assumption revision prepares, validates and commits into that ephemeral component; record identities remain valid in the parent transaction. No nested program executor, record arena, checkpoint authority or published frontier exists. The session is discarded after the bottom-founded outer revision completes.

Positive-recursive gamma uses the same session boundary. Its signed assumption changes enter the interned recursive correction journal; nested-time profiles, causal distinct state and complete output extents remain transaction-local and share the parent record transaction. Both gamma component kinds therefore expose one revise(changes) -> complete typed extents contract to stable revision.

Gamma plans execute in checked mode: one fresh shared-arena session runs from precision bottom through the representation-independent stable revision scheduler, while the encoded batch evaluator independently computes the same transaction. Preparation publishes only when every gamma step and the complete true/possible pair agree. Direct EDB facts on WFS heads remain a separate transaction-local base extent and are set-unioned with derived gamma output.

Removing negative edges can split one WFS SCC into multiple gamma strata. The immutable gamma plan therefore owns an ordered closed vector of acyclic and positive-recursive components plus the canonical program flow plan. Each revision propagates record deltas through that vector in dependency order; all components share the parent transaction and advance once per revision. A failed inner stage poisons the disposable session. No partially advanced session, record transaction or gamma frontier enters published WFS state.

Gamma preparation uses owned sparse overlays. Acyclic stages accumulate record profiles, sink assignments and ordered multi-join journals against immutable published state, then finalize once. Positive-recursive stages additionally use a structurally shared scheduler snapshot: trace-spine batches remain shared while each correction publishes privately into the staged spine. Sparse projection assignments are chained against the staged scheduler and finalize as one generation advance. Outer preparation therefore performs no mutation of a published gamma component and copies no retained tuple history.

Gamma publication uses a sealed commit object. Sealing consumes the prepared component journals, validates their count, variant, generation and record transaction, and pairs each journal with its concrete mutable component before the record arena is published. The infallible commit phase therefore contains no parallel-vector alignment, variant fallback or validation-by-convention.

Gamma derivative state is a recoverable physical index, not checkpoint authority. WFS checkpoints continue to encode only inputs, true and possible extents. Restore instantiates empty gamma components, evaluates exactly gamma(possible) over checkpoint inputs, requires equality with checkpoint truth, and publishes the reconstructed profiles only inside the private restore transaction. The first post-restore mutation therefore resumes retained gamma state without serializing a second logical representation.

A fixed-delta benchmark over independent self-loop game nodes confirms the boundary. Adding one exit edge changes one logical region but rebuilds the full component:

Retained edgesComponent rebuild p50Established full evaluation p50Ratio
100337.67 us286.63 us1.18x
1,0003.767 ms3.160 ms1.19x
10,00042.470 ms35.736 ms1.19x

The overhead is record conversion, K3 materialization, delta construction and transaction validation around the same batch WFS kernel. Unaffected frontiers reuse retained WFS state, report zero rebuilds and measured 0.538 us, 0.604 us and 1.057 us p50 at the same retained sizes. Mutation-local speed requires the proof-gated coupled-circuit design below.

Cross-frontier reuse retains gamma derivative state, not the previous WFS approximation. For a changed base E -> E', the coordinate applies the signed base correction together with the signed reset from its prior assumption to the empty assumption. Stable revision then starts again with logical lower extent empty. This computes gamma_E'(empty) incrementally while preserving the bottom-founded lfp(gamma^2) construction. Seeding the new revision with the prior true or possible extent is forbidden: arbitrary signed base changes do not preserve the subset direction required by that shortcut.

The circuit-checked validation backend deliberately runs both the encoded batch kernel and a fresh record-native stable revision before publication. Its measured component-rebuild p50 is 1.854 ms, 23.442 ms and 276.36 ms at 100, 1,000 and 10,000 retained edges: 6.47x, 7.42x and 7.73x the established evaluator. These numbers measure a differential-validation configuration, not a production cutover. Circuit authority requires the differential gates below; retaining the encoded evaluation after those gates would make the cutover predictably slower.

Release circuit authority omits the encoded catalog and oracle evaluation. Its component-rebuild p50 is 1.560 ms, 20.472 ms and 243.51 ms at the same sizes, 15.8%, 12.7% and 11.9% below checked mode but 5.44x, 6.48x and 6.81x the established evaluator. Circuit authority removes duplicate execution; it does not make a fresh bottom-founded rebuild mutation-local. Production speedup requires retaining the coupled WFS coordinates across frontiers under the proof obligations below. Debug and test builds retain per-step and final-pair encoded differential checks; both configurations publish the circuit-produced pair.

Retaining gamma derivative state across frontiers produces rebuild medians of 1.517 ms, 19.499 ms and 230.54 ms. The corresponding established medians are 0.287 ms, 3.181 ms and 35.577 ms, making retained gamma 5.28x, 6.13x and 6.48x slower. The stable revision remains bottom-founded and must retract the prior possible assumption; on the independent self-loop workload that reset touches nearly every retained tuple. Retained physical indexes are necessary for incremental WFS, but they do not make the logical coordinate reset mutation-local. Removing that reset requires the coupled-coordinate proof gate in Phase D, not another storage optimization.

Changing one existing input’s positive support without crossing zero measured 1.349 us, 1.440 us and 1.915 us p50 at 100, 1,000 and 10,000 retained edges.

The common sampled-maintainer harness measures heap and semantic restoration at 10,000 retained edges. With separate gamma roles, caller-owned coordinate time and one causal coordinate representation, a 30-sample one-edge mutation measured 3.868 ms p50 for the circuit versus 44.493 ms for established maintenance: the circuit was 11.50x faster. Retained heap was 6,938,074 bytes versus 3,523,417 bytes, or 1.97x. Construction peaks were 48,986,175 and 9,906,899 bytes; mutation peaks were 13,279,511 and 14,451,485 bytes. The circuit transaction reported one WFS rebuild and two stable-revision rounds. A 100-edge mutation measured 6.089 ms circuit versus 46.918 ms established, or 7.71x faster, with the same retained heaps. Record-native rebuilding is already faster than the encoded established evaluator, but its work still depends on retained component size. The journal and commit work is proportional to touched input records; the alternating fixpoint remains a whole-component reset/replay rather than mutation-local maintenance.

Physical preparation fixes rule order, dictionary-independent module identity, catalog-bound execution constants, stratification, dependency metadata and coarse join strategy once. Accepted incremental lowering consumes that stratification directly. It does not invoke a second positive-recursion or stratification analysis. Unsupported physical operators and strategies remain typed refusals, and their evidence is retained in the selected fallback route.

Recursive trace time is the product order of outer committed frontier and inner fixed-point iteration. Its partial order and least upper bound are componentwise; compaction and suffix correction therefore cannot collapse an inner iteration merely because its outer frontier is earlier. The program instance owns outer frontier and transaction identity. Recursive components own only nested trace state.

Well-founded execution has two distinct clocks. A gamma revision frontier may advance multiple times while stable revision computes one coordinate publication. A coordinate epoch advances exactly once when the causal truth and falsehood scheduler publishes that replacement. Coordinate scheduler state and journals therefore use a private CoordinateEpoch type; conversion to the generic recursive frontier occurs only at the nested scheduler boundary. Gamma revision helpers cannot accept a coordinate epoch, so accidental synchronization or comparison of the two clocks is unrepresentable.

Signed changes that reach a positive recursive component execute through the nested circuit. One transaction-local evaluation session propagates each newly produced feedback delta, maintains causal distinct zero crossings and prepares the recursive trace, circuit state and relation projection as one journal. Retractions confined to an acyclic upper stratum continue through the ordinary circuit. Unsupported rule operators remain a whole-program admission refusal; they do not produce a mixed circuit/batch transaction.

The published recursive trace materializes its current inner-iteration profile behind a shared immutable snapshot. Starting a mutation is therefore constant time per SCC relation rather than proportional to outer history. Publication updates only changed tuples and the canonical history and current profile are committed by the same prepared trace assignment.

Recursive body evaluation is stage-local. A feedback stage evaluates only its new profile against published state and the transaction-local overlay. For a join it emits each bilinear term exactly once:

new left  join (published right + staged right)
+
(published left + staged left) join new right
+
new left join new right

The session returns only newly produced sink profiles. The recursive controller retains future iteration buckets until their causal iteration is sealed. A global suffix recomputation exists only as a test oracle; it is not a production maintenance path.

Causal distinct sealing is correction-driven and tuple-local. A tuple becomes active only when a candidate correction reaches it. Its cursor retains old and corrected support, old membership and the current membership difference. Late activation reconstructs that tuple’s retained prefix pointwise and validates its retained output history. A transaction-local wake index schedules only current candidate cells and future nonzero cells from an activated retained profile; iterations with no old or new support change do not revisit the tuple. The wake index is derived scheduling state, not a second checkpoint authority. Prepared cursor transitions and wake assignments publish only after every relation succeeds. The evaluator registers future candidate-bucket horizons separately, so an empty current output cannot terminate before an already-produced future candidate bucket. Untouched retained tuples are neither activated nor replayed.

Recursive implementation ownership follows the same publication boundary:

recursive/state.rs                 SCC facade and insertion scheduler
recursive/state/relation.rs        candidate history and membership projection
recursive/state/distinct.rs        tuple-local causal distinct session
recursive_rules/adapter.rs         construction, admission and evaluator binding
recursive_rules/correction.rs      signed causal correction orchestration
recursive_rules/transaction.rs     prepared validation and atomic publication
recursive_rules/publication.rs     jointly published scheduler/evaluator state

Candidate staging only consolidates the current bucket. Tuple activation and retained-profile validation occur atomically at sealing. Consequently an unsealed followed by neither activates a cursor nor extends the iteration horizon, and staging does not clone the active cursor map.

Each recursive profile trace owns its cumulative inner-iteration deltas. Each tuple-local sparse profile caches its checked integrated support; membership is the support > 0 projection. A prepared patch changes cells, computes cached support from the completed atomic assignment, and validates the caller’s exact membership zero crossing before infallible publication. No independently mutable candidate-support or settled-membership collection exists. Point membership remains logarithmic in retained profile tuples. Full catalog projection scans retained profiles and filters zero support; deletion-heavy catalog materialization must be benchmarked before routing because causal history deliberately outlives current membership.

Integrated operator output is retained only when a later derivative reads its prior value: join and antijoin inputs, aggregate inputs and distinct support. Other node deltas remain frontier-local. A sole-consumer scan followed directly by projection is fused during delta propagation. The immutable plan and its canonical identity remain unchanged. This removes redundant persistent stateless outputs and transient per-row binding maps without changing operator semantics or checkpoint authority.

Equality by construction

Differential equality is a gate, not a synchronization mechanism. The engine must not retain several mutable semantic copies and rely on tests or procedural update order to keep them equal. DBSP defines one algebraic state: integrated Z-set traces. Arrangements, settled recursive extents, catalogs, checkpoints and emitted batches are indexes or projections of that state.

The live ownership model is:

CommittedDelta<RelationId>
           |
           v
ProgramPlan.prepare(&PublishedSnapshot, delta)
           |
           v
ProgramJournal
           |
           v
publish once, infallibly
           |
           v
PublishedSnapshot
  |-- one transaction token and outer frontier
  |-- integrated typed relation/operator traces
  |-- indexes owned by their authoritative traces
  |-- nested recursive time below the outer frontier
  `-- one union catalog + sparse base provenance on rule heads

PublishedSnapshot is the only live semantic state. For maintainer-owned acyclic circuits, its catalog is authoritative for base input membership because ProjectionMaintainer::read_model must return &RelationCatalog; the circuit explicitly delegates input authority and retains only derivative state. A standalone CircuitInstance remains self-contained. Delegated instances refuse standalone checkpoints until the enclosing recovery record can restore the catalog and circuit together. Derived support remains typed circuit authority. The catalog is its read projection, and direct facts are retained separately only for rule-head tuples whose base and derived support can overlap. The same journal publishes all three zero-crossing decisions; no complete base or derived catalog is retained.

Every component implements the same transaction algebra:

prepare(&PublishedState, Delta) -> Journal
validate(&PublishedState, &Journal) -> Result
publish(PublishedState, Journal) -> PublishedState

Preparation and validation are read-only. Publication consumes a fully checked journal and cannot fail. Component journals compose in plan order into one whole-program journal; lower strata, recursive components and sinks never publish independently. The program instance alone owns outer frontier, idempotence and last-commit identity. Recursive components own only nested (outer frontier, iteration) traces.

Redundant physical state is allowed only behind an owner that makes divergence unrepresentable. An arrangement belongs to its relation trace and is changed by the same prevalidated assignment. Recursive membership is derived directly from the candidate profile’s private cached support. A checkpoint serializes one published snapshot; restore constructs and validates a replacement snapshot before registry adoption. No API can restore fields into a live instance individually.

String relation names and RelationCatalog exist only at the projection/input boundary. Plans, committed deltas, dependencies, operator state, journals and sinks use RelationId. Runtime and conformance callers construct and observe incremental maintenance only through QueryEngine; circuit implementations, checkpoint DTOs and journals are crate-private.

Target module ownership is:

incremental/
  plan/         immutable typed IR, identity, compile, validate, encode
  state/        published snapshot, traces, arrangements, operator state
  transaction/  commit token, journal composition, prepare, publish
  operator/     linear, join, antijoin, aggregate, distinct
  recursion/    nested time, scheduler and recursive trace; no catalog
  projection/   typed catalog input and snapshot-to-catalog projection
  recovery/     snapshot checkpoint codec and candidate validation

maintainer/circuit  admission and fallback adapter only
query_engine        sole external construction and observation seam
runtime             event-log durability and snapshot-registry adoption

File moves follow ownership changes; splitting a file without removing duplicated mutable authority does not satisfy this section.

Argon’s one-plan rule applies: source rules are lowered and optimized once, and the incremental compiler derives component plans from that accepted optimized plan. It does not re-stratify cloned CompiledRule trees into a parallel planning hierarchy. Coarse physical choices such as worst-case-optimal joins, factorized aggregates and recursion remain visible to incremental lowering. The current rule-to-circuit compiler is bootstrap code until it consumes this shared optimized plan.

The existing full evaluator remains the correctness oracle and fallback. The existing semi-naive insertion and delete/rederive maintainer remains a benchmark baseline; it is not the new circuit’s implementation substrate.

Canonical row ownership

The program snapshot owns one row identity space shared by every incremental component. Persistent scheduler traces and temporal operator indexes store RecordId; they do not own decoded payloads. The projection boundary retains the encoded catalog required by ProjectionMaintainer::read_model, while the row arena owns the single decoded payload used by rule evaluation.

ProgramSnapshot
  |
  +-- RecordArena
  |     |-- truncated payload digest -> collision bucket of RecordId
  |     `-- RecordId -> one adaptive boxed value slice
  |
  +-- ProgramCircuit
  |     |-- recursive candidate profiles: RecordId -> iteration cells
  |     `-- temporal arrangements: RecordId -> iteration weights
  |
  `-- ProgramRelationStore
        `-- encoded catalog required by the public read-model contract

RecordId is local to one live program snapshot. Allocation order has no semantic meaning and is never persisted, hashed into a plan identity or exposed through the query interface. Checkpoint writers resolve IDs to canonical tuple bytes and sort by tuple order. Restore interns those bytes into one fresh arena before it publishes any component state. Different replay grouping or allocation order therefore produces identical checkpoint bytes and read models.

The arena distinguishes ordered value vectors from variable bindings. Ordered vectors cover relation tuples and projected join or antijoin keys. Bindings cover operator rows and count correlation or outer keys; their variable entries must be strictly increasing and unique. A record-kind tag prevents a value vector from aliasing a binding with the same serialized shape.

The arena index is a 16-byte truncated BLAKE3 digest over the record-kind tag and canonical bytes. The primary tree stores one RecordId directly per digest; a separate sparse side table stores additional IDs only on an actual digest collision. Candidate payload equality is checked after every digest match, so truncation affects index size and lookup cost, not identity correctness. Published records retain one boxed value slice. Values whose complete tuple can use the fixed-width RecordCell carrier are stored inline in that slice. A tuple containing an allocation-owning or larger value retains one ordinary semantic Value slice instead. Fallback is selected for the whole tuple; the arena never adds one allocation per uncommon cell. Both forms expose the same borrowed RecordValues view and preserve structural Value equality. Canonical bytes and the digest are transaction-local and are not duplicated in the record. Bindings use a boxed sorted entry slice rather than a tree per record. An index keyed by a second decoded tuple or binding is forbidden. Every referenced ID must resolve, and encoded restore input is accepted only when the decoded values re-encode to the same canonical bytes.

Preparation uses a transaction-local row overlay:

published arena + input delta
             |
             v
       RecordTransaction
       |-- reuse published IDs
       `-- allocate provisional IDs in an isolated suffix
             |
             v
 component journals reference published or provisional IDs
             |
       validate complete program journal
             |
             v
 append arena suffix, then publish component journals infallibly

A refused transaction drops the overlay and cannot leak rows into published state. Validation proves that every provisional ID belongs to the journal’s overlay and that the arena base length still matches the published snapshot. Appending the validated suffix precedes component publication and performs no fallible semantic work.

The migration boundary is a complete maintainer-owned program. Every newly admitted strict program uses one ProgramCircuit, including programs whose component graph is entirely acyclic. Candidate profiles, temporal operator rows, arrangement keys, antijoin keys and aggregate group keys change from decoded payloads to RecordId together. Evaluator input, encoded and arrangement stores are delegated to the enclosing projection and remain empty. Adding an arena while either the scheduler or temporal profiles still retain the same decoded tuple payload is forbidden: that would add a third physical representation rather than replace one. Standalone circuits retain their self-contained generic tuple state only as a differential oracle and as the restore target for legacy acyclic checkpoints. New production admission never constructs that carrier.

accepted optimized physical program
                |
                v
       ProgramCircuitPlan
                |
       +--------+--------+
       |                 |
acyclic components  recursive components
       |                 |
       +--------+--------+
                |
       shared RecordArena
                |
       atomic ProgramJournal
                |
       ProgramRelationStore
                |
       public read-model catalog

legacy acyclic checkpoint --> restore-time migration --> record-native component

Every newly constructed strict component is record-native. Join, antijoin, distinct, Count, CountDistinct, Sum, Avg, Min and Max corrections publish through the shared record-profile journal. Legacy semantic acyclic checkpoint DTOs are migrated into that record-native component before the restored program becomes observable; no semantic acyclic runtime variant remains. Checkpoint validation derives the supported aggregate set from the same operator capability predicate used by record-profile admission.

Sparse row histories use one canonical small-vector representation. The common single-cell history is stored inline; longer recursive histories spill without changing evaluation semantics. Checkpoint DTOs continue to encode ordinary vectors, so this storage choice is neither a wire shape nor a second authority.

All currently admitted positive-recursive operator shapes cross this boundary together. There is no workload-specific alternate executor. Metrics count arena rows separately from candidate cells and temporal index cells so payload ownership is distinguishable from semantically necessary time and support indexes.

Semantic and record-backed execution share one value-access contract. Scan matching, repeated-variable equality and Generic Join key construction are one algorithm parameterized by borrowed value access; storage adapters cannot silently acquire different semantics. Comparisons borrow arena values. Cloning occurs only when an operator emits a new tuple or crosses a checkpoint, read-model or full-evaluation boundary.

A record-backed join interns each correction key and tuple once. The same canonical correction entries probe published and transaction-staged arrangements, then move into the join publication journal. No second projection, digest lookup or semantic-key journal is retained.

Transient operator rows use a session-local identity space. A local row owns a canonical binding or value vector until it becomes part of a persistent arrangement, support index, sink or prepared journal. Promotion then moves the payload into the transaction record suffix once and caches the resulting RecordId; repeated and overlapping promotion reuses that identity. Rows discarded by filter, cancellation or liveness release never enter persistent state.

one record transaction
        |
        +-- evaluation session ordinal 0
        |      |-- local scan/filter rows
        |      `-- explicit escape set --> cached RecordId promotion
        |
        +-- evaluation session ordinal 1
        |      `-- independent local identity space
        |
        `-- validated persistent record suffix

The transaction token plus caller-owned session ordinal prevents a stale local identity from resolving in a later recursive round. Session IDs and local row IDs are neither semantic nor durable. Promotion validates the complete escape set before mutating the record transaction. Digest matches always confirm structural payload equality, including after the local payload has moved and only its cached record remains.

Session-local lookup keys are ephemeral acceleration state. Each evaluation owns a randomized hash builder and maps a 64-bit key to an exact structural collision bucket. The seed and key never enter a row identity, journal, checkpoint or plan commitment. This avoids a cryptographic digest for every transient join binding while retaining exact equality and collision safety at the only semantic boundary.

Profile orchestration is parameterized by backend-owned transient and sink row identities while retaining one immutable schedule. Scan, filter, map, project, sink, join, distinct, antijoin and aggregate behavior remains behind one ProfileStateView contract. This permits the record backend to replace owned semantic rows operator by operator without copying the schedule or introducing workload-specific execution routes.

The ordinary record backend implements the first complete native slice:

record scan
  -> borrowed record-backed binding
  -> binary join / fused or explicit projection
  -> support distinct
  -> sink identity
  -> selective persistent promotion

A bijective unique-variable scan binding retains its canonical source tuple and an immutable shared variable-to-position map. Owned and record-backed bindings use the same allocation-free structural digest and exact equality check, so their identity is representation-independent. Join corrections reuse the source tuple identity when variable order matches and promote only keys and tuples required by the persistent arrangement journal. Distinct promotes only touched support identities and emits corrections under the original transient identity. Sink validation checks row kind without materializing values.

Filter evaluates directly through borrowed record bindings and preserves row identity. Computed map retains the shared expression evaluator as semantic authority, materializes one touched binding because that evaluator requires stable borrowed Value references, and interns the result immediately. It does not materialize the input profile. Scan, projection and sink carrier operations are time-generic shared record-backend components rather than ordinary/Product copies. Ordinary antijoin constructs persistent arrangement corrections from transient bindings, reads retained support and timelines directly by record identity, and emits borrowed retained bindings. Semantic and record execution share one support zero-crossing transition law. Ordinary count-family input reads transient rows without constructing semantic profiles. Each touched compact binding is decoded once, then semantic and record execution share one binding-change function. Corrections and retained groups remain record-native through evaluation; record output resolves only an emitted outer binding or extrema winner. Semantic and record output share one generic aggregate-level law and temporal differentiation driver. Factorized aggregate outer and factor inputs also read transient rows without constructing semantic profiles and share aggregate binding-selection helpers with count-family input. Ordinary factorized corrections and retained groups remain record-native through evaluation. Projection preparation, group validation, scalar aggregate-level evaluation and temporal differentiation are representation-generic. The record emitter constructs only transient row identity; the shared temporal law owns support weight. Ordinary multiway joins read produced transient value rows and emit transient value identities directly. Semantic and record carriers share one changed-input partition, trie-overlay Generic Join, temporal weight product and error propagation path; only key access and output construction vary. Product/WFS creates one transient identity space per staged revision and uses the same record carrier for scan, filter, map, project, join, distinct, count, factorized aggregate, multiway join and sink. Only escaping operator and sink rows are promoted before persistent preparation. Product antijoin groups corrections, reads touched retained windows and journals state by record identity. Semantic and record execution share one generic causal differentiation law over representation-owned keys and rows. The record emitter borrows the retained tuple identity as a transient binding; the former semantic retained window and reinterning adapter no longer exist.

The native multiway carrier codec is time-generic and accepts both semantic-key Product tries and compact interned-key ordinary tries. Both representations use the same changed-input partition, overlay traversal, temporal product and output emission driver. Product uses the same carrier without changing checkpoint key representation.

canonical tuple bytes
        |
        v
   RecordArena intern
        |
        +-- all cells fixed-width --> Box<[RecordCell]>
        |
        `-- otherwise -------------> Box<[Value]>
                         |
                         v
                  RecordValues view
                         |
             +-----------+-----------+
             |                       |
       scan / equality          join key visitor
             |                       |
             +-----------+-----------+
                         |
                 emitted values only
                         |
                      cloned

Distribution posture

Distribution is sequenced after single-node correctness, not excluded. Phase-A execution remains local to one circuit instance, but its plan and state model must preserve these later distribution paths:

  • every stateful operator declares its deterministic partition/exchange key;
  • operator semantics remain pure Z-set transformations independent of worker identity and physical placement;
  • plans retain explicit monotone strata and sealed non-monotone boundaries;
  • transaction and iteration frontiers are instance-local logical times, never a process-global completion assumption;
  • deterministic event-log replay can construct an identical read replica;
  • tenant migration and replica cutover occur at an explicit logical frontier; and
  • pushed federation fragments carry a placement-policy certification record sufficient to audit whether local evaluation plus merge equals global evaluation.

Positive set-valued strata may eventually use asynchronous, duplicate-tolerant lattice exchange. Signed Z-set deltas are not idempotent: distributed circuit exchange requires exactly-once, in-order delivery or an equivalent deduplication protocol. Negation, aggregates, well-founded alternation and bitemporal reads observe only sealed input frontiers. The implementation must not blur these two delivery contracts.

Circuit sharding across machines is last in the distribution sequence. Earlier paths are per-tenant placement, logical-time migration and deterministic read-replica replay. Any sharding proposal must demonstrate a measured crossover against the corresponding single-node arrangement.

Fallback is whole-program in this RFD. If any reachable rule, producer or operator is unsupported, compilation returns a typed refusal and creates no circuit state. The runtime evaluates the complete program with the full evaluator. A mixed circuit/batch plan requires a separate frozen-boundary and cross-backend atomicity design.

The correctness equality compares the complete published base-plus-derived catalog after projecting away engine-private state. Relation identities, canonical decoded tuples, Argon set membership, well-founded companion relations, and refusal/error outcomes must match. Dictionary allocation order, arrangement layout and node order are unobservable. For a circuit-admitted program every published relation has set semantics; internal operator streams retain exact integer multiplicities.

Data contract

A Z-set is an ordered map from canonical tuples to exact signed weights. Zero weights are absent. Weight accumulation is checked: overflow fails the batch and cannot mutate visible circuit state. Saturating arithmetic is forbidden.

Every input and intermediate batch is consolidated before it crosses an operator boundary: equal tuples are summed with checked arithmetic and zero weights are removed. Tuple encoding and ordering are canonical. RelationId carries arity; an arity mismatch is an input or compile error, never a dropped row.

Circuit plans are immutable. Circuit instances own all mutable traces and arrangements. A plan never contains tenant or frontier state. An instance is usable only with the module commitment from which its plan was compiled.

CircuitPlan contains typed relation IDs, typed node IDs, typed input/output ports, schemas, strata, monotonicity/sealing metadata, exchange keys, feedback edges, sinks and the plan commitment. It is canonically serializable and contains no mutable state. Scheduling is the stable topological order of (stratum, SCC, NodeId), with feedback evaluated under the nested-time order defined below.

Circuit operators

Rule operatorDelta behaviorRetained state
singletonemit one empty binding at the initial logical timenone unless consumed by a stateful operator
scanforward input batchrelation trace
filter/map/computeapply directly to batchnone
binding projectionretain selected variables as a joinable bindingnone
joinbilateral delta joinboth indexed inputs
multiway joinordered multilinear delta joinone trie trace per logical atom
distinctemit support zero-crossingstuple weights
antijoinemit right-support zero-crossingsleft trace and right key counts
semijoinemit positive right-support zero-crossingssame left trace and right key counts as antijoin
aggregateupdate changed groupsgroup multiset and accumulator
recursionnested delta fixpointrecursive traces by iteration
sinkconsolidate rule supportoutput weights

union is an explicit linear operator between rule producers and a relation boundary. It consolidates signed contributions without deduplicating them.

predicate-led body                  predicate-free prefix

input delta                         initial logical time only
    |                                       |
    v                                       v
  scan                                  singleton {}
    |                                       |
    +-----------> join/filter/map <---------+
                         |
                         v
               antijoin / aggregate
                         |
                         v
                 project -> distinct -> sink

Later frontiers inject only relation deltas. Stateful consumers retain the
initial singleton support; the singleton itself has no mutable authority.

Operator semantics:

  • singleton is the relational unit. It emits exactly one empty binding at the minimum logical time of a new circuit and never emits at a later frontier, recursive generation or evaluation-session ordinal. Empty bodies and bodies beginning with comparison, computation, negation or aggregation lower through this operator. The compiler does not invent a catalog relation or reorder the authored body. Stateful consumers retain its support through their ordinary join, antijoin or aggregate state, so checkpoints require no mutable singleton state.
  • scan accepts only the batch for its RelationId, validates arity, and stages the exact base trace update. It does not decode or allocate relation identity.
  • filter retains a row iff the existing expression evaluator returns true. False and undefined drop it, matching full evaluation.
  • Scalar filters carry one canonical predicate (Comparison or expression Defined) plus an explicit selection (Satisfies or DoesNotSatisfy). This represents not <comparison> and not <compute> as stateless selection, including nested negation. DoesNotSatisfy retains undefined results because Established NAF asks whether the inner atom has an extension; it is not Boolean operator inversion. Loud comparison or expression errors remain loud under either selection. Semantic and record-backed carriers interpret this same predicate IR; neither compiler path synthesizes an antijoin or reorders the rule.
  • map and compute preserve input weight. Non-injective outputs consolidate. A partial compute that is undefined drops the row. Expressions use the same value operations and error behavior as the full evaluator.
  • binding projection is the internal relational projection. It retains a canonical sorted subset of variables as a binding row, so subsequent joins can consume it. It does not impose set semantics; callers place distinct explicitly where projection collapses multiple supports. Terminal project remains the binding-to-head-tuple boundary.
  • union adds all producer weights. It does not impose set semantics.
  • sink consolidates producer support, passes it through the relation-boundary distinct operator, and emits the resulting set delta in canonical order.

For join inputs A and B:

Δ(A ⋈ B) = ΔA ⋈ B_old + A_old ⋈ ΔB + ΔA ⋈ ΔB

Multiple rules may support the same tuple. Their signed contributions are summed before the relation-boundary distinct operator. Per-rule deduplication is not a substitute for relation-level support accounting.

Join

A join key is the canonical encoding of shared columns in declared key order. The output contains the left columns followed by right non-key columns. For each matching pair, weights multiply with checked arithmetic; contributions add and consolidate. Constants and repeated variables remain residual equality filters. Aliased inputs and self-joins still use two logical ports and the same old/old, old/delta, delta/old, delta/delta accounting.

Both committed arrangements are probed as pre-batch state. Temporary indexes over ΔA and ΔB may be built for the cross term, but are staged state and are not visible as committed arrangements until publication.

Antijoin and semijoin share one keyed existence-state representation. For left stream L, right-key support R, and antijoin output A = L ▷ R, existential semijoin is defined by the exact partition:

S = L - A
ΔS = ΔL - ΔA

The circuit therefore prepares one left arrangement/right-support journal and selects its absent (A) or present (S) projection. Multiple right witnesses never multiply left support. Nested predicate NAF with odd polarity lowers to antijoin; even polarity lowers to semijoin. Checkpoint conversion and validation use the same existence journal for both operators, preventing their zero-crossing and recovery laws from diverging.

Interval-valued antijoin follows the same ownership rule. Its immutable compiled key and output projections are owned once by the plan and shared with the retained component. The component owns only mutable left/right temporal indexes; it does not clone semantic operator configuration into state. Checkpoint wire shape is unchanged because immutable projections remain plan-derived.

Explicit Kripke modal rules

The supported single-frame modal fragment lowers into ordinary relational operators. It has no modal-specific mutable state. The inner proposition is one positive predicate whose first column is the world; a modal is the sole body atom. The world domain contains both accessibility endpoints and every world in the proposition extent. A non-empty payload domain is the distinct projection of proposition payloads; a proposition with only a world column uses the relational singleton.

accessible(w, s) ----+--------------------------+
                     |                          |
proposition(s, p) ---+--> join --> diamond(w,p) |
                                                |
accessible endpoints --+                        |
proposition worlds -----+--> union --> distinct +--> World(w)
proposition payloads -------- project --> distinct --> Payload(p)
                                                     |
World x Payload -------------------------------------+--> Candidate(w,p)
                                                           |
Candidate x accessible -- antijoin proposition --> Missing(w,p,s)
                                                           |
Candidate ------------------------ antijoin Missing --> box(w,p)

diamond is existential support. box removes a candidate when any accessible successor lacks positive proposition support. Absence therefore remains open-world unknown and fails necessity. A dead-end world has no missing row, so box is vacuously true. Edge and proposition changes use the ordinary join, antijoin and distinct correction journals, including simultaneous commits and checkpoint restoration.

An optimized 10-sample Criterion run on an Apple M5 Pro measured retraction of one proposition from a retained 1,000-world self-loop frame at 46.046 µs for the circuit and 519.62 ms for fresh Established evaluation at the median: an 11,284-fold latency ratio. The benchmark validates both diamond and box extents against fresh evaluation before timing. This is a fixed-delta modal shape, not a substitute for cross-shape latency and memory gates.

Physical preparation binds the modal accessibility and proposition relations by exact catalog name or one unique qualified-name suffix. Established evaluation and circuit compilation consume that same catalog-bound rule representation; generic scans never guess relation suffixes. Established processes plain and modal SCCs in canonical stratification order. Later positive, negation, aggregate and modal strata therefore consume complete lower modal extents, matching the circuit’s stratified relational plan.

Worst-case-optimal multiway join

A physical worst-case-optimal join is a first-class circuit node; it is never silently expanded into a binary join tree. The immutable node records the ordered predicate atoms, constant and repeated-variable guards, global variable order, head projection and one logical input port per atom. Plan identity includes that complete physical shape.

For ordered inputs R_0 .. R_n, define R'_i = R_i + ΔR_i. The transaction uses the disjoint multilinear decomposition

Δ join(R_0 .. R_n)
  = Σ_i join(R'_0 .. R'_(i-1), ΔR_i, R_(i+1) .. R_n)

Every changed derivation is assigned to its greatest changed input ordinal, so simultaneous changes appear exactly once without enumerating all 2^n - 1 old/delta combinations. Signed weights multiply with the same checked arithmetic as binary joins; relation-boundary distinct remains responsible for set semantics.

Each logical atom retains a trie trace in the global variable order. A transaction supplies read-only overlay nodes for R_i, ΔR_i or R'_i as required by one decomposition term. Generic Join intersects the smallest candidate set and probes the other participating tries. Constants, variable-free guards and repeated variables use the same matcher as full evaluation. Trie corrections are staged once and publish only after all output terms, downstream operators and program validation succeed.

Published trie and prepared-patch child storage use the same adaptive ordered map: zero fanout has no allocation, one child uses a single boxed node and higher fanout uses a B-tree. Insertion promotes and retraction demotes this representation. Traversal, overlays and checkpoint entries expose one ordered trie abstraction, so storage density does not create a second evaluation, transaction or persistence path.

Inside a recursive component, an atom trie cannot retain only integrated support. It retains the sparse inner-iteration profile for every key. A correction joins atom profiles at the maximum participating inner time and emits the correction at that time; collapsing the profile would move a retraction to iteration zero and violate causal distinct support. Transactional overlay layers therefore preserve both key support and iteration cells. Recursive reader identity is (multiway-join node, atom ordinal), not only the relation or node, so repeated aliases remain distinct. The nested plan assigns each reader to immediate or delayed input from same-stratum producer structure; runtime batches remain relation-addressed.

physical prepared rule
          |
          v
 multiway join plan ------------------------+
  atoms + variable order + projection       |
          |                                 |
          v                                 v
 per-atom trie traces <-- signed deltas -- overlay selector
          |                                 |
          +-------- Generic Join visitor <--+
                            |
                            v
                    signed output Z-set
                            |
                            v
                  relation-boundary distinct

The implementation must extract the existing full-evaluation trie builder and Generic Join visitor behind one relation-read interface before circuit wiring. Required gates are full-evaluator parity, binary-join parity, simultaneous multi-input corrections, self-join aliasing, ground guards, repeated variables, negative corrections, checked overflow, prepared-journal atomicity, checkpoint round-trip, recursive temporal correction, atom-addressed feedback, and triangle/clique scale controls. Admission remains a typed whole-program fallback unless these gates pass for the selected physical shape.

The implemented cut admits this node for acyclic and positive-recursive components. Recursive atoms retain sparse inner-time terminal weights; scalar and temporal tries share one topology, overlay, preparation and Generic Join visitor. Same-stratum atom reads are delayed structurally, and checkpoint recovery preserves the next signed correction. Well-founded recursive components remain outside this admission boundary.

Profiled tries store arena-owned scalar identities on prefix edges rather than owned semantic values. Semantic construction and record-backed serving are explicit state variants; conversion drops integrated scalar tries. One generic trie, overlay, journal and Generic Join implementation serves both variants. The record variant interns keys through the transaction that prepares the component and resolves identities only when producing a semantic binding. Checkpoint encoding resolves keys into canonical semantic order; restore re-interns them through the prepared record suffix. Journal validation requires the state and journal variants to match before publication.

Antijoin

The right side retains exact support per join key; the left side retains exact rows grouped by that key. A left delta is emitted unchanged only when the new right support for its key is zero. A right transition 0 -> positive retracts the complete post-left-delta bucket; positive -> 0 asserts it. Simultaneous left/right changes are evaluated from the old traces plus the consolidated batch so each output row is emitted once. Negative right support or positive-to-negative overshoot at the settled lower-stratum boundary is an invariant failure.

The entire lower stratum reaches its staged fixed point before an upper-stratum antijoin observes its new trace. No lower or upper result is published until the whole transaction succeeds.

Inside a positive recursive SCC, negation is admitted only for a predicate in a sealed lower stratum. The antijoin retains the left side as an inner-iteration profile and the right side as integrated support by correlation key. Right corrections are valid only at inner iteration zero. The plan records and validates that the antijoin right input is a non-delayed sealed scan; recursive negation remains a well-founded fallback. Profiled left state and right support publish through one prepared journal. Checkpoints require one canonical state entry for every profiled antijoin and validate key shape, tuple width, positive support, ordering, and uniqueness before restoration.

Aggregates

An aggregate is keyed by its correlated outer/group columns and retains the weighted multiset for each group. A changed result emits -1 for the old output and +1 for the new output. Empty-group behavior and undefined values match the full evaluator.

  • count sums multiplicities with checked integer arithmetic.
  • sum uses the exact numeric domain and errors of the full value evaluator.
  • average retains exact sum and count and performs the same exact division as full evaluation.
  • count-distinct retains per-value support and counts zero-to-positive values.
  • minimum and maximum retain an ordered support map so deletion reveals the next supported value.

Projection, grouping, numeric coercion, empty input and overflow are tested per aggregate kind before that kind is admitted. An unimplemented kind refuses the whole program.

Projection-free count inside a positive recursive SCC uses a compact temporal representation. Each correlation key retains one sparse BigInt cardinality profile and an index of complete outer bindings to sparse support profiles; it does not retain the joined input rows a second time. Cardinality prefixes must remain in 0..=usize::MAX; outer prefixes are nonnegative checked i64 supports. Outer support is not Boolean: distinct recursive derivations can contribute the same binding at different causal iterations. Integrated evaluation, temporal profiles and checkpoints preserve the same multiplicity, while the recursive scheduler applies relation-level zero-crossing semantics. The upstream join arrangement owns row identity. Before derivative authority is delegated, exact integrated aggregate parity is reconstructed from the retained aggregate-input operator, not from a duplicate row map in count.

Factorized aggregate admission preserves the full evaluator’s authored factor partition. It does not lower independent factors into their Cartesian product or into independently materialized Int counts: either expansion changes the physical bound, and early projection of a factor cardinality can report an overflow even when another factor is empty. A factorized aggregate circuit node therefore owns one summary trace per independent factor and combines factor cardinalities in BigInt before the single value-domain projection.

For a cardinality aggregate, every factor retains cardinality by outer key. For a projected aggregate, the value factor additionally retains the fold support; the other factors retain only cardinality. A touched outer key reads old and prepared summaries, emits one old retraction and one new assertion, and stages all factor patches in one journal. sum scales the value fold by the exact product of other cardinalities. minimum, maximum, average, and count-distinct use other factors only as a nonempty gate. Checkpoint order is outer key, factor ordinal, then canonical value.

outer parameter trace ------------------------------+
                                                     |
factor 0 pipeline --> cardinality summary -----------+
factor 1 pipeline --> value/cardinality summary -----+--> exact factor fold
factor N pipeline --> cardinality summary -----------+          |
                                                                v
                                                    old/new aggregate delta
delayed recursive outer -----------+
                                    +---- correlated count ---- output
sealed aggregate body -------------+              |
                                                   v
                                    correlation key
                                      |-- shared cardinality timeline
                                      `-- full outer binding -> support timeline

The sealed body may be an ordered connected pipeline of predicates, filters and computations when every filter and computation dependency is bound inside that pipeline. Disconnected factors and dependencies available only from the outer binding retain the generic aggregate lowering. The compiler emits the explicit operator; evaluators do not infer it from a Distinct/join graph.

Sealed body scans carry the existing immutable sealed-relation metadata. Node time domain is derived transitively from the plan: filters, computations and projections preserve it, and a join is origin-only exactly when both inputs are origin-only. Origin-only joins use an ordinary signed arrangement without a per-row iteration map and reject nonzero inner time. Joins reachable from a delayed scan retain the nested-time representation. Checkpoints store the same logical iteration rows and reconstruct the physical layout from the committed plan; no independent layout flag is persisted.

The shared arrangement stores a unique join-key bucket inline and allocates a nested Z-set only when a second distinct tuple has the same key. Promotion, demotion and ordinary weight changes use one absolute-weight transition. This preserves signed multiplicity and deterministic iteration while avoiding one tree allocation per key in the common fully keyed body-join shape.

Profiled evaluation derives transient consumer counts from the immutable DAG. An operator delta is released after its last scheduled consumer unless integrated derivative authority requires it across frontiers. This applies the same plan liveness to scans, filters, computations and joins instead of keeping every frontier-local intermediate through session preparation.

Whole-program compilation likewise derives a typed flow plan from stratum scan and recursive dependency metadata. Each relation is identified by module, name and arity and records its last consuming stratum. One flow entry composes two ownership classes: frontier-owned external tuple deltas and arena-owned derived RecordId deltas. External rows are not interned into the append-only arena; derived rows retain identity across component boundaries. Both sources resolve as one logical input and are released together at the last consumer. Public output is a separate signed record set and therefore does not extend flow lifetime.

external InputBatch --------------------------+
  frontier-owned Tuple Z-sets                 |
                                                v
                                   ProgramFlow[RelationId]
                                                |
component record output --> derived RecordId --+
           |                                    |
           +--> public record output            +--> current component input
                                                |
                              compiled last consumer --> release entry

A profiled evaluation session validates each stage against published state plus its transaction-local overlay. Only the consolidated session is validated against published state at commit. This permits a later feedback stage to cancel a row created earlier in the same transaction without treating the intermediate retraction as an underflow.

Compilation pipeline

CompiledRule[]
      |
      +--> range restriction and tier admission (already complete)
      |
      +--> signed dependency graph and stratification
      |
      +--> rule-local logical operators
      |
      +--> union support from all producers of each relation
      |
      +--> relation-boundary distinct
      |
      +--> stratum scheduling and feedback edges
      |
      +--> stable structural operator identities
      v
CircuitPlan { inputs, strata, nodes, sinks, commitment }

The production compiler consumes the optimized logical plan plus stratification. It preserves fused recursion, worst-case-optimal joins, CSR access and factorized operators as coarse incremental operators when expanding them into binary nodes would discard their physical advantage. A direct CompiledRule lowering is permitted as a Phase-A bootstrap and oracle surface; it is not the final runtime seam. The coarse physical plan is not sufficient by itself when it hides antijoins, aggregates or feedback edges.

Compilation first applies the same rule preparation as full evaluation. Values stored in plans are dictionary-independent; instance-local dense IDs are resolved when an instance is seeded. Inline-set and other module-static relations become explicit immutable input relations. Every CompiledAtom variant is either mapped to a circuit operator with identical expression semantics or produces a typed, whole-program refusal. No atom, rule or producer is omitted. The compiler verifies relation arity, variable binding, stratum order, positive feedback and that every relation producer reaches exactly one relation-boundary distinct node.

Stateful join

                  old A arrangement       old B arrangement
                          ^                       ^
                          |                       |
new ΔA -- staged index --+---- probe B_old ------+----+
       \                  \                            |
        +------------------ join new ΔB ---------------+--> Δ(A join B)
new ΔB -- staged index --+---- probe A_old ------+----+

The batch result contains all three bilinear terms. Both arrangements update only after probes observe the pre-batch state, then commit atomically with the operator output.

Nested-time join trace

Recursive joins retain inner iteration because nested time uses max(current_iteration, retained_iteration). Equal consolidated relations with different inner profiles are not interchangeable. Prior-frontier state must not be retimed to iteration zero.

canonical iteration profile
          |
          | one prepared assignment
          v
temporal arrangement: join key -> iteration -> matching tuples
          ^
          |
correction tuple -- project key once -- probe matching timeline
          |
          v
emit at max(correction iteration, retained iteration)

The iteration profile is semantic state. The temporal arrangement is its owned, derived index: it is updated through the same prepared assignment and rebuilt from the profile during checkpoint restore. A legacy checkpoint projection may remain encoded for format compatibility, but restore validates it against the rebuilt index and never adopts it as authority. The index is not independently published. The physical representation is key-first, not one independent arrangement per iteration. An absent join key therefore costs one ordered-map probe rather than one probe per retained iteration; present keys visit only matching time buckets and emitted matches.

Distinct support transition

old weight   delta   new weight   emitted set delta
---------    -----   ----------   -----------------
    0          +n       > 0              +1
    >0          -n        0               -1
    >0          +/-      > 0               0
    0           0         0                0
    0          -n       < 0               invariant failure
    >0         -n       < 0               invariant failure

Negative transient weights are legal inside differential operators before a complete batch is consolidated. A relation-boundary distinct state has the invariant old_weight >= 0; its consolidated new_weight must also be non-negative. It emits only 0 -> positive and positive -> 0 crossings. A published set-valued Argon relation therefore contains weights only in {0, 1}.

Atomic application

One mutation commit is one circuit batch. The instance stages every operator update, checks weight arithmetic and convergence, computes derived deltas, and only then publishes the new state and frontier. Failure leaves the prior state and frontier observable.

Storage first appends the authoritative event batch atomically. Circuit maintenance then advances the read model from prior_frontier to next_frontier. Until publication, readers continue to see the complete prior frontier; they never combine durable base state at the new frontier with derived state at the old frontier. A caller requiring read-your-commit waits for circuit publication or receives an unavailable/maintenance-failed result. Event-log reads may expose the durable append independently, but are not derived-model reads.

Publication is a compare-and-publish under the instance registry: the current instance ID, plan ID and frontier must equal those used to stage the batch. State, arrangements, output deltas and frontier replace the old snapshot together.

durable mutation batch
         |
         v
 validate plan commitment and frontier
         |
         v
 stage affected-node delta journals
         |
         v
 propagate signed deltas in stratum order
         |
         v
 check arithmetic, convergence, output invariants
         |
    +----+----+
    | success | failure
    v         v
 publish      discard staged state
 state +      retain old frontier
 frontier     poison if rollback is not provable

Readers address published frontiers only. No reader can observe a base batch whose derived batch is not published at the same frontier.

Typed input refusal, checked overflow and convergence exhaustion occur entirely in staged state: they retain a reusable prior instance, mark the durable frontier as pending, and require retry or rebuild before later batches can pass. An internal invariant failure, corrupt committed state, or failure after mutation of shared state poisons the instance. A poisoned instance accepts no batch. Recovery restores a compatible checkpoint or rebuilds from the event log, then replays the failed batch and suffix in order. Later frontiers cannot skip it.

Recursion

Positive recursive components use nested time: outer transaction time and inner fixpoint-iteration time.

transaction delta
       |
       v
external SCC inputs ----> iteration 0 delta
                               |
                               v
                        recursive body circuit
                               |
                               v
                    relation-boundary distinct
                               |
                    +----------+----------+
                    | nonempty            | empty
                    v                     v
              feedback iteration      publish SCC delta

The compiler admits this path only when recursive dependencies are positive. Recursion through negation remains on the well-founded batch evaluator until an incremental alternating-fixpoint design has its own correctness argument.

Within an SCC, timestamps use the product order over (transaction_frontier, iteration). Storage may sort timestamps lexicographically, but that storage order is not semantic time. A correction at outer frontier t joins retained inner iterations at (t, max(correction_iteration, retained_iteration)). Each iteration consumes the prior iteration’s consolidated new support, applies every recursive producer, unions support at the relation boundary and feeds only support crossings back. The SCC settles when every feedback batch is empty. Iteration count is bounded by the configured convergence policy and failure remains staged.

This construction admits signed batches reaching a positive recursive SCC. Negative tuples are not replayed through an ordinary insertion loop. The body circuit retains nested-time arrangements, and a causal distinct session compares the corrected candidate profile with the prior candidate and output profiles at each sealed iteration. It therefore removes unfounded cyclic support while retaining independent or later alternative support. Cycles, diamonds, last support, alternative support, retract/reassert, mutual recursion and generated mutation traces are differential gates against fresh full evaluation.

The recursive operator follows the DBSP fixed-point construction itself; it is not an ordinary feedback loop around the derivative of distinct:

outer signed input
        |
        v
 inner delta-zero injection -----> (+) <---- checked inner delay
                                   |                 ^
                                   v                 |
                          inner integration          |
                                   |                 |
                                   v                 |
                         lifted rule body            |
                                   |                 |
                                   v                 |
                              distinct               |
                                   |                 |
                                   v                 |
                         inner differentiation ------+
                                   |
                                   v
                         inner limit extraction
                                   |
                                   v
                       signed recursive output

The whole nested operator is incrementalized across the outer transaction stream. Its inner integration and differentiation are semantic operators and must be present in the circuit IR and retained state. Applying only a distinct suffix correction and feeding it through delay is not equivalent: for p(x) :- seed(x); p(x) :- p(x), deleting seed(x) makes such a loop move a spurious self-support to successively later iterations forever. Inner integration makes the delayed retraction visible to the lifted body snapshot; inner differentiation then emits only the change between adjacent snapshots, eliminating circular support.

The body evaluator reads retained per-iteration profiles, so a correction at iteration i joins a retained row at j at max(i, j). It preserves transient inner profiles whose outer-integrated weight is zero because those transients can still change later recursive iterations. The extracted limit, not an empty ad-hoc correction queue, defines completion.

The integrated inner stream is authoritative for relation-boundary support; its distinct-and-differentiated stream is authoritative for recursive changes. Delayed body inputs, arrangements and encoded catalogs are owned indexes or projections and must accept removals. A signed transaction cannot publish the nested stream while leaving any projection at the prior membership. One prepared token owns all assignments. The production signed route is admitted only for circuit-selected programs; the established evaluator remains the independent fallback and differential oracle. DRed with counting remains a correct alternative backend, but it is not substituted inside this DBSP circuit.

Persistence and recovery

projection checkpoint envelope
  |-- tenant + fork + module commitment
  |-- physical ProgramCircuitPlan commitment + circuit schema version
  |-- derivation semantic epoch + execution configuration
  |-- durable generation + exact event-prefix commitment
  |-- independent circuit frontier + last published batch commitment
  |-- authoritative union catalog + sparse rule-head base provenance
  `-- ordered component states
        |-- acyclic derivative state; delegated inputs and sinks omitted
        `-- recursive scheduler + profiles; evaluator projections omitted
          |
          v
decode off-path -> validate complete envelope -> replay suffix -> audit
          |
          v
atomic projection-snapshot adoption -> serve

Circuit state is replaceable. The append-only event log remains authoritative. An incompatible or corrupt checkpoint is discarded and rebuilt; it is never partially adopted.

Checkpoint checksums detect accidental corruption; they are not authentication tags. Restore trusts the configured durable store as the checkpoint source. Tenant/fork scope, module and plan commitments, durable-prefix identity and retained-state invariants prevent accidental cross-context adoption, but an attacker able to replace trusted checkpoint bytes is outside this cache threat model and must be handled by storage integrity and access control.

The acyclic projection codec implements this boundary in the reasoning layer. It binds tenant, fork, module, physical plan, derivation epoch, exact durable prefix, circuit frontier, last batch and complete projection commit in one canonical checksummed envelope. The union catalog and sparse rule-head base provenance are encoded once. Embedded circuits carry explicit input, output and derivative authority markers; delegated inputs and sinks are absent. Restore constructs the circuit off-path, validates both directions of catalog/circuit membership, and only then constructs a replacement projection. Integrated and profile-only embedded circuit codecs both preserve the next mutation. Recursive component checkpoints retain tuple-major candidate profiles with exact inner iteration multiplicities, and restore the nested profile circuit without serialized encoded catalog or arrangement projections. Standalone components reconstruct evaluator base support from their enclosing base catalog. They do not retain an encoded catalog copy: catalog views encode nonrecursive base support on demand and read recursive membership from the scheduler. Evaluator projection ownership is a sum type. Its internal variant contains base support and arrangements; its external variant contains neither, so authority and payload cannot disagree. Maintainer-owned restore constructs the external variant directly instead of constructing internal projections and immediately discarding them. The enclosing projection owns published membership and the nested profiled circuit owns correction support. A restored component produces the same next signed correction while its nested circuit remains at local frontier zero. All nested checkpoint layers use one shared framing encoder. It reserves the 20-byte magic/version/length header, writes canonical CBOR directly into the final buffer, backfills the body length and appends one checksum. Program, recursive-component, recursive-scheduler and circuit envelopes therefore no longer construct a complete body and copy it into a second complete buffer. The resulting bytes, versions and checksums are identical to the former two-buffer encoding. Nested checkpoint payloads remain separately materialized; removing that wire-level byte-vector nesting requires an explicit versioned migration. Whole-program checkpoints bind an ordered plan-derived component identity for every acyclic and recursive stratum, retain program frontier/publication/config once in the enclosing projection, and assign a fresh process instance on restore so prepared journals from before recovery are invalid. Mixed programs round-trip canonically, preserve duplicate retry identity, and produce the same recursive retraction and upper-only mutation after restoration. Runtime durable adoption exports and restores these envelopes only through the QueryEngine facade; the runtime never names circuit plans or concrete maintainers.

durable manifest root
  |-- ordered immutable event segments
  `-- exact append-batch lengths
              |
              v
coherent snapshot + scope/order/content/count prefix commitment
              |
              +-- checkpoint prefix --> restore circuit off-path
              |
              `-- later append batches --> replay one ProjectionCommit each
                                             |
                                             v
                              validate final storage generation
                                             |
                                             v
                           replace ModelCache under one lock

The file-backed durable store records each successful append length beside the ordered segment list in the same atomic manifest. Recovery refuses suffix acceleration for legacy manifests that cannot prove those boundaries; it never guesses transaction grouping. A checkpoint is recovery-eligible only when its canonical event-prefix digest matches the durable log and the sealing store’s event bodies and next event sequence match that same prefix. Local-only warm segments may still accelerate the same live store, but cannot be adopted during durable recovery.

A persisted segment contains exactly one model representation. Circuit routes store the enclosing incremental projection envelope and leave the legacy columnar field empty; established routes store only the columnar model. Warm startup and crash recovery both restore the same circuit envelope and derive the read-only columnar index from its validated catalog. Transitional segments that contain both remain readable, but new writers never create dual payload authority.

Tip recovery restores the hot maintainer directly. Behind-tip recovery builds a private scoped store from the checkpoint prefix, restores there, replays each preserved append batch sequentially, and transfers the completed model cache only after its final storage generation equals the fully hydrated serving store. Any refusal leaves the serving cache cold, so the established full evaluation path remains the correctness fallback. The restored cache remains incrementally mutable; the first post-recovery mutation does not force a full evaluation.

The persistence unit is the enclosing projection snapshot, not an isolated CircuitInstance. Production components delegate input, output and evaluator projection authority; component bytes are incomplete without the union catalog and sparse direct-fact provenance that own those projections. Persisting a complete base catalog or a second derived catalog would create dual authority. The envelope therefore stores the union catalog once, stores direct facts only on produced relations, and stores derivative state in delegated components. Restore validates every produced tuple against sparse base provenance unioned with component-owned derived support.

Checkpoint encoding is canonical, resource-bounded and content-committed. It includes relation schemas, operator-state versions, per-iteration recursive candidate profiles, bilateral arrangements, scheduler support and the last published program batch. Restore reconstructs physical indexes from the live committed plan, validates component ordinal and kind, consolidates every recursive profile, reconstructs catalog membership, and rejects extra, missing, unordered, zero-weight or shape-incompatible entries. Profiles are not reconstructed from integrated state because integration discards inner time.

Circuit frontier and durable event watermark are independent: initial seeding, no-op commits and conservative recursive rebuilds do not establish a fixed arithmetic mapping. Recovery binds both values explicitly. An event sequence number and count prove freshness but not prefix identity, so adoption requires an append-only durable-log prefix commitment. A checkpoint is written to a temporary object, verified, and made visible atomically. Restore builds and validates a new snapshot off-path, assigns a fresh process instance identity, replays the ordered suffix, and swaps only the complete snapshot. Prepared journals are never persisted or reusable across restart. The differential audit is mandatory in tests and debug recovery; production may use configured sampling, but always checks encoding, commitments and operator invariants.

Fallback, recovery and module replacement

compiled program
      |
      +-- admitted --> build/restore circuit --> replay suffix --> publish
      |
      +-- typed refusal -----------------------> full evaluator

live circuit -- staged batch failure --> retain old frontier --> retry/rebuild
      |
      +-- invariant failure --> poison --> checkpoint restore
                                      \-> full event-log rebuild

new module commitment --> compile + seed/replay new instance
                                      |
                              compare old registry key
                                      |
                              atomic registry swap
                                      |
                         old instance drains/discards

A module commitment change stops admission of new batches to the old instance. The runtime compiles the new plan, seeds or replays it through a chosen event-log frontier, verifies it against full evaluation, and atomically swaps the registry entry only if the scope and expected old registry key still match. Batches that arrive during construction remain ordered in the log and replay before or after the swap. The old instance may serve its published frontier until replacement; it never processes a batch under the new commitment.

Delivery program

Phase A: strict-stratified circuit
   |
   +--> measured go/no-go against full evaluation and DRedc
   |
   v
Phase B: evidence-for/evidence-against pair circuits (assert-only)
   |
   v
Phase C: pair retraction after formal provenance gate
   |
   v
Phase D: proof-gated coupled well-founded circuits
   |
   v
Phase E: checkpoint, recovery and production scale

Phase A — circuit compiler and strict-stratified parity

Phase A is independently useful and replaces no fallback until its oracle and benchmark gates pass. Its internal order is:

Runtime circuit selection is off by default during Phase A and requires the OXC_ENABLE_INCREMENTAL_CIRCUITS operational flag. An unsupported program, catalog representation or disabled flag selects the established whole-program maintainer before circuit state is created. OXC_REQUIRE_INCREMENTAL_CIRCUITS is the validation mode: it enables circuit selection and turns any catalog, preparation, compilation or restoration fallback into a typed failure. Product-specific require flags select their named fail-closed policy. The Established well-founded escape hatch can combine with the ordinary require flag without permitting admission fallback. The Linux compiler-test job runs every oxc-runtime test binary once with this mode. The route-pinned and default Established runs each passed 945 tests with three skipped. This includes mutation, recursive checkpoint, durable recovery, ontology example-corpus and projection-cache integration coverage.

  1. Exact Z-sets, typed refusals, immutable plans and per-frontier differential equality with full evaluation.
  2. Positive acyclic scan, filter, map, compute, union, join, distinct and sink.
  3. Delta-local transactional state and signed arrangements. Work and memory on the mutation hot path must scale with the affected delta, not total retained state. Settled relation support may not become negative.
  4. Qualified schema-carrying RelationId; structural collision-checked NodeId; committed CircuitPlanId and scoped CircuitInstanceId.
  5. Explicit (prior frontier, next frontier, batch commitment) application with duplicate idempotence and conflict, gap and stale-plan refusal.
  6. Stratified antijoin with lower-stratum settlement, followed by count, sum, average, count-distinct, minimum and maximum.
  7. Positive recursive SCCs using the DBSP recursive derivative with outer transaction time and inner iteration time. Signed feedback is admitted only after cycle, diamond, alternative-support, last-support and generated mutation differentials.
  8. ProjectionMaintainer implementation exposed only through QueryEngine. Durable append precedes maintenance; the per-scope visible watermark advances only after successful circuit publication. The full evaluator remains the permanent fallback and kill-switch target.

The Phase-A measurement compares full recomputation, the existing incremental maintainer and the circuit on identical commit streams. It covers shallow insertion, recursive insertion, low-overlap retraction, high-overlap retraction waves, dense mutual-support cliques and a mixed recorded trace. Fixed-size deltas are measured while retained state grows through 10^4, 10^5, 10^6 and 10^7 facts. Results include p50/p95/p99 mutation-to-visible latency, peak and retained memory, arrangement probes, fixpoint rounds, affected cardinality and emitted cardinality. Compilation, seeding, cloning and destruction are reported separately.

Proceeding beyond Phase A requires both:

  • exact catalog equality after every commit; and
  • at least a fivefold win on high-overlap retraction at 10^6 facts or above, while shallow insertion, recursive insertion and low-overlap retraction remain within 20 percent of the existing maintainer.

A high-overlap win below twofold parks Phases B through D. An intermediate result requires a recorded owner decision using the published measurements.

Targeted measurements on 2026-07-19 do not yet discharge the complete matrix gate. A ten-sample 100-fact high-overlap retraction over one million input facts measured circuit p50/p95/p99 latency of 143.459/167.083/167.083 microseconds, versus 623.479/631.566/631.566 milliseconds for the established maintainer. Exact retained-entry telemetry identified 1,000,000 typed input tuples duplicating the authoritative encoded catalog. Delegating base-input authority to that catalog reduced requested retained heap from 292,527,744 to 100,908,937 bytes, 33.9 percent below the established maintainer’s 152,749,040 bytes. Measured peak heap fell from 708,189,124 to 538,703,743 bytes, 12.4 percent above the established maintainer’s 479,310,928 bytes. The focused latency and retained memory requirements are exceeded; the remaining workloads, scales and variance measurements are still required by the Phase-A gate.

Observed runtime telemetry enabled an adjacent million-fact gate run on 2026-07-20 at revision 51932c581. With 100-fact deltas and 30 samples, circuit versus established p50 was 0.505 versus 317.818 ms for shallow insertion, 0.509 versus 2,093.220 ms for low-overlap retraction, 0.660 versus 654.261 ms for wide recursive insertion, and 0.250 versus 631.499 ms for high-overlap retraction. The four speedups are 629, 4,114, 991 and 2,528 times. Observed circuit work was respectively 100/0, 100/0, 0/1 and 0/0 arrangement-probe/recursive-round counts. The focused latency gate passes. Peak heap remains 1.68–2.10 times established; recursive insertion retains 1.68 times established while the other three controls retain less. The complete 10^410^7, dense, mixed-trace and cross-machine matrix remains open.

The evaluator’s last-consumer policy also covers Distinct. Zero-crossing evaluation borrows its input; after validation, an exclusive input profile moves into the distinct journal. Shared or retained profiles remain borrowed and are cloned only because another consumer exists. This is the same ownership contract used by union, join and sink, not a distinct-specific representation. On the million-fact shallow control, construction peak fell from 2,509,624,433 to 2,394,523,289 bytes, 4.6 percent, with unchanged 486,716,234-byte retained heap. Wide and dense recursive peaks were unchanged.

Record-backed join corrections are encoded into arena identities at the join boundary rather than retained as semantic tuples until state preparation. The join-delta type is parameterized by the owning profile representation, making a semantic journal invalid for record-backed state at compile time. Both representations use one orchestration function for input conversion and the nonlinear correction term; representation adapters own only retained-state probing and journal encoding. The resulting single semantic kernel preserves the exact three-term bilinear delta law without maintaining two evaluators.

Thirty-sample million-fact controls with 100-fact deltas reduced shallow construction peak from 2,394,523,289 to 2,385,809,537 bytes and wide-recursive construction peak from 1,962,000,198 to 1,910,669,498 bytes. Retained heap was 486,716,042 and 425,172,351 bytes respectively. Observed transaction work remained 100 probes and zero rounds for shallow insertion, and zero probes and one round for wide recursive insertion. The shallow peak now occurs during join-output construction, so further reduction belongs inside the join input and output lifetime rather than state-journal conversion.

The nonlinear correction join uses a sorted borrowed arrangement over its authoritative correction Z-set. A contiguous vector stores tuple references and weights, compares projected fields in place and owns neither tuple payloads nor one materialized key per tuple. This preserves the single join visitor and removes both transient tuple duplication and per-key tree storage. On the million-fact shallow control, construction peak fell from 2,385,809,537 to 2,081,562,849 bytes, 12.75 percent, while retained heap remained 486,716,042 bytes. The origin-only million-fact wide-recursive path does not construct this index and remained at a 1,910,669,498-byte peak and 425,172,351-byte retained heap. Transaction work remained respectively 100/0 and 0/1 arrangement probes/recursive rounds.

Phase instrumentation placed the next construction peak after profile overlay preparation, at record publication. The transaction and arena previously used different digest-index representations, and every pending payload repeated its digest. Both now use one compact RecordDigestIndex, with a first-ID tree and a separate tree only for actual digest collisions. Publication transfers disjoint index nodes, explicitly merges overlapping collision buckets and moves digest-free payloads into stable record IDs. The million-fact shallow construction peak fell from 2,081,562,849 to 1,988,233,345 bytes, 4.48 percent, with unchanged 486,716,042-byte retained heap. The wide-recursive peak fell from 1,910,669,498 to 1,817,345,146 bytes with unchanged 425,172,351-byte retained heap. Observed probes and rounds were unchanged.

Two publication-index prototypes were rejected after measurement. A 256-way digest-sharded binary-leveled B-tree left the shallow construction peak at 1,988,233,345 bytes and added 189,072 retained bytes: the peak is the required coexistence of published and prepared indexes, not global merge workspace. A second prototype froze the prepared index into a sorted (digest, record ID) segment. It also left peak unchanged; direct sorted replay increased retained heap by 17,001,864 bytes through sparse B-tree packing, while record-ID-order replay restored retained heap but still did not change peak. Both prototypes were removed. Further publication reduction requires a broader atomic ownership protocol or persistent index, not another local container representation.

The retained arena therefore uses the persistent-index alternative. Its primary digest-to-record mapping is one sorted contiguous vector searched by binary search. A transaction independently uses a mutable ordered map. Atomic publication consumes that map, classifies pre-existing digests into the sparse collision table and merges new primary entries into the vector. Exact payload comparison remains the authority after every digest lookup; the index changes storage only and cannot alias distinct records.

On the 10,000-component recursive multiway control with a 100-row delta, this reduced retained heap from 15,846,195 to 14,980,023 bytes and mutation peak from 40,254,972 to 39,388,800 bytes. Construction peak and every logical record, trace and trie count were unchanged. Against the 9,842,015-byte Established control, retained overhead fell from 1.61 to 1.52 times.

The cumulative representation changes also remove the dense-recursion construction blocker. A 100,000-fact dense mutual-support workload that had stopped after 612 seconds at 17.77 GiB RSS now completes with a 1,162,073,678-byte requested-allocation peak and 262,193,596-byte retained heap. Thirty 100-fact retraction samples measured 194.595 ms p50 and 254.383 ms p95, with 34,600 arrangement probes and five productive recursive rounds per operation. No dense speedup is inferred because the established control was not rerun after these circuit-only changes.

The end-to-end conformance replay builds every positive ontology through the driver, materializes its canonical EDB and compares established and circuit-preferred maintenance with fresh evaluation after retract, absent-retract, reassert and duplicate-assert frontiers. Seeded rows are used when an actual compiled rule input contains one; otherwise the harness creates a deterministic tuple from that predicate’s constants and repeated-variable shape. Empty relation presence is normalized while signed rows remain exact. The current corpus exercises 29 eligible ontologies. Twenty-eight mutation replays comprise three seeded-input and 25 synthetic-input cases; one rule-free package has no input to mutate. All 29 packages select a circuit route: 27 acyclic and two recursive. The gate pins the two recursive package paths and fails any eligible package that regresses to fallback, so semantic equality cannot pass by silently leaving the circuit. All replayed frontiers agree with fresh evaluation under both maintenance modes.

The expanded matrix exposes a separate recursive retained-state blocker. At 100,000 facts, recursive insertion retained 1,051,926,002 bytes after base-input delegation, versus 24,601,021 bytes for the established maintainer. Exact entry counts were: 200,000 encoded read-model tuples; 200,000 integrated operator rows; 200,000 integrated join-arrangement entries; 100,000 integrated sinks; 400,000 temporal-profile rows; 400,004 candidate-trace entries; 200,004 output- trace entries; 100,000 candidate-support tuples; 100,000 settled tuples; 200,000 evaluator-encoded tuples; and 200,000 evaluator-arrangement entries. The recursive circuit’s duplicated integrated and temporal join states, trace history/current profiles and evaluator projections require an ownership redesign before production routing.

The join shape is now immutable circuit-plan metadata and participates in plan identity. This removed the prior empty-baseline and repeated-positive-mutation shape ambiguity. Maintainer-owned recursive components consequently use temporal profiles as their sole derivative authority; integrated operators, ordinary join arrangements and sinks are absent by construction. Profiled evaluation also stops retaining operator profiles already represented by temporal join inputs. Standalone circuits retain integrated state and checkpoint behavior.

A ten-sample 100-fact insertion over 100,000 input facts measured 278,859,701 retained bytes and 626,956,372 peak bytes after these changes. The recursive scheduler now retains cumulative inner-iteration profiles directly instead of duplicating them with per-frontier historical traces; durable outer history belongs to event replay. Before the ownership change the same scale retained 1,051,926,002 bytes. Exact circuit counts fell to zero integrated operator rows, arrangements and sinks, and 200,000 temporal- profile rows; candidate trace entries are bounded at 200,000 and the persistent output timeline is absent; maintainer-owned evaluator encoded, evaluator arrangement, and candidate-support entries are zero. Recursive membership is projected from cached support in the candidate profiles; there is no separate settled index. The measured circuit p50 was 44.195 ms versus 59.605 ms and 24,601,021 retained bytes for the established maintainer. This is a substantial structural reduction, not a production gate win: the remaining temporal profile, candidate history, settled membership, and published relation overlap still retains 11.3 times the established heap. This insertion sample is 1.35 times faster, but temporal trace ownership and compaction remain prerequisites for routing.

Symbolicated sampling identified avoidable transient work rather than an inherent incremental cost. Profile staging cloned accumulated input and sink maps on every recursive round, while causal distinct cloned and replaced its complete active map at every iteration. Per-profile prepared journals and a globally validated per-entry distinct seal preserve failure atomicity without either copy. At 10,000 retained facts and the same 100-fact delta, p50 fell from 75.293 to 44.799 milliseconds; at 100,000 facts it fell from 75.324 to 45.846 milliseconds. The nearly flat retained-state scaling remains, with 39–40 percent less mutation latency. Candidate-profile patch preparation now also consolidates every logical (tuple, iteration) cell before producing absolute assignments, so multiple changes cannot overwrite rather than compose.

Durable candidate history is tuple-major: each tuple owns a sorted, zero-pruned sparse iteration profile. The logical trace remains the same signed (tuple, iteration, weight) relation, while causal activation reads only the touched tuple’s events. Transaction staging stays iteration-major and prepares one checked assignment per consolidated logical cell. An activated tuple caches its immutable sparse history once, avoiding a retained-map lookup on each later round. At 100,000 facts this reduced retained heap by 3.2 percent and peak heap by 6.2 percent without regressing the profile-guided latency gain.

Sealed lower-stratum negation is now incremental inside positive recursive SCCs. A branched-chain workload asserts 100 lower ban facts and changes exactly 1,500 derived tuples. At 10,000 edges the circuit measured 11.097 ms p50 versus 46.992 ms established, a 4.23-times speedup. At 100,000 edges it measured 11.179 ms versus 521.101 ms, a 46.6-times speedup. Retained heap was 17.1 and 16.7 times established at the two scales. The semantic and latency results are favorable; the memory result prevents default routing.

Projection-free recursive count is a dedicated fused operator. It consumes the recursive outer and independently scanned body without constructing the parameter-domain distinct or outer-by-body join. At 10,000 lower-stratum rows, a one-row mutation of one 10,000-row correlated group measured 0.024 ms p50 versus 5.167 ms established, a 219-times speedup. Changing 100 groups of 100 rows measured 2.460 ms versus 410.961 ms, a 167-times speedup. Retained heap was 3,520,778 and 3,982,413 bytes respectively, 2.23 and 2.49 times established. The profiled state contained three cells for the single-group case and 300 for the 100-group case, rather than 10,000 expanded joined rows.

Independent aggregate factors retain one sparse summary per factor rather than their Cartesian product. With 10,000 input facts split into two ten-row factors per group, retracting one row from each of 100 groups measured 1.041 ms p50 for the circuit versus 1.162 seconds established, a 1,116-times speedup over five samples. Retained heap was 2,008,488 bytes versus 1,693,936 bytes established, or 1.19 times; mutation peak was lower at 2,910,804 bytes versus 4,341,282 bytes. The mutation changed 200 observable aggregate tuples and performed 100 keyed arrangement probes.

The same fused operator accepts a connected body pipeline containing a predicate, filter, computation and second predicate. For 100 correlated groups, 10,000 rows in each of the two body relations and a 200-row mutation across those relations, 30 samples measured 2.961/3.052/3.091 ms p50/p95/p99 versus 395.477/399.538/401.162 ms established, a 134-times p50 speedup. Origin-only join storage and inline singleton arrangement buckets reduced circuit retained heap from 28,855,027 to 17,949,079 bytes in the first ten-sample run and 17,964,387 bytes in the 30-sample run, a 37.7 percent reduction. Replacing complete base, derived and union catalogs with one union catalog plus sparse rule-head base provenance reduced a later ten-sample run from 17,964,387 to 16,376,752 bytes, another 8.8 percent; this is 5.18 times the established 3,162,697 bytes. Releasing frontier-local deltas after their last DAG consumer reduced construction peak from 68,533,677 to 51,523,245 bytes, 24.8 percent. This remains 7.77 times the established 6,634,768 bytes. Mutation peak is 18,270,222 bytes versus 8,244,838 established, or 2.22 times, down 8.0 percent from 19,861,793 bytes. Construction is the larger transient blocker. The 20,300 retained logical rows show that exact opposite-side join support, rather than timestamp or nested-bucket overhead, is now the dominant retained cost. This workload remains ineligible for default routing until that ownership overlaps less with the published lower-stratum catalog.

The canonical RecordId implementation was subsequently measured with exact arena cardinalities after replacing retained canonical bytes and shared payload allocations with collision-checked digest buckets and boxed decoded payloads. At 10,000 rows with a 100-row wide-recursion mutation, 30 samples measured 0.992/1.223/1.311 ms p50/p95/p99, 19,243,524 retained bytes and 19,866,221 mutation-peak bytes. The arena retained 20,300 value records and no binding records. The connected count pipeline mutates both body relations, so the same requested delta produces 200 changed rows; it measured 3.325/3.482/3.673 ms, 17,492,400 retained bytes and 19,488,181 mutation-peak bytes. Its arena retained 20,800 records: 20,400 values and 400 bindings. The compact index removed about 1.4 MB from the pipeline result, but the remaining one-use value records still overlap the encoded published catalog. This memory result blocks default routing and requires a catalog/read-view ownership cut; binding representation is not the dominant cost.

A targeted ten-sample comparison against the actual full-recompute maintainer, not the matrix performance gate, used the same 10,000-row, 100-row mutation. Wide recursion measured 0.911 ms circuit p50 versus 16.436 ms full recompute; retained heap was 19,243,524 versus 2,418,708 bytes, and construction peak was 59,436,189 versus 13,499,191 bytes. The connected count pipeline measured about 3.325 ms circuit p50 versus 430.119 ms full recompute; retained heap was 17,492,400 versus 3,160,138 bytes, and mutation peak was 19,488,181 versus 8,242,279 bytes. This targeted result demonstrates roughly 18-times and 129-times mutation speedups respectively, while confirming 7.96-times and 5.53-times retained-memory regressions. It does not satisfy the default-routing memory gate.

The subsequent ownership cut removed the persistent encoded catalog from program circuits. Published union membership and sparse rule-head base provenance are sets of RecordId; only prepared sink deltas retain signed weights. Catalog and columnar forms are boundary projections, and columnar construction sorts semantic rows independently of record allocation order. Direct point lookup resolves a tuple to its existing record identity without scanning or interning. Initial construction and restore use validated seed transactions in the program arena; mutation membership uses the same prepared transaction as the affected operator state.

On the combined direct-restore and ownership-cut implementation, the same 30-sample wide workload measured 0.884/0.993/1.001 ms p50/p95/p99, 17,865,525 retained bytes and 19,769,990 mutation-peak bytes. The same count pipeline measured 3.271/3.419/3.722 ms, 16,142,663 retained bytes and 18,136,684 mutation-peak bytes. Relative to the compact-record implementation, retained heap fell 7.2 percent for wide recursion and 7.7 percent for the count pipeline. The pipeline still retains 5.11 times the full-recompute heap and 2.20 times its mutation peak. Construction peaks remain 59,436,189 and 51,523,629 bytes. The duplicate catalog is no longer the retained-memory blocker; temporal arrangements, candidate profiles and arena indexing now dominate. Default routing remains disabled pending the complete matrix gate and further retained-state reduction.

A 2026-07-21 breadth run fixed state at 10,000 rows, delta at 100 rows and five samples for every Phase-A workload. It validated every transition before measurement. Circuit versus Established p50 latency was:

WorkloadCircuit (ms)Established (ms)Ratio
shallow insertion0.5882.6464.50x faster
wide recursive insertion0.6725.3067.89x faster
well-founded exit assertion6.22947.0837.56x faster
deep-chain recursive insertion21.0469.8462.14x slower
recursive multiway retraction5.03673.00814.50x faster
upper recursive retraction0.61010.61017.39x faster
lower negation assertion7.75645.9715.93x faster
lower count assertion2.148401.311186.83x faster
lower count pipeline assertion4.304613.258142.49x faster
factorized count retraction1.2021,195.429994.53x faster
low-overlap retraction0.58618.30231.24x faster
high-overlap retraction0.3424.93914.44x faster
projected sum retraction1.484266.124179.33x faster
dense mutual-support retraction115.963397.9553.43x faster
mixed recorded trace3.48820.5375.89x faster

Deep-chain insertion violates the 20-percent non-regression requirement. Retained memory also remains excessive for recursive multiway state at 73.06 MB versus 9.84 MB and dense mutual support at 26.97 MB versus 6.02 MB. This run is a breadth diagnostic, not the Phase-A gate: it lacks the required larger scales, 30-sample variance and full recomputation leg. The next gate work targets the deep-chain scheduler and recursive multiway/trie ownership before scaling the matrix.

A 1 kHz sample of 30 deep-chain mutations attributed hot-path work to record lookup, trace-key traversal, B-tree insertion and per-probe timeline materialization. Record-native retained joins now consume an inline sparse timeline directly from the trace visitor; the common one-time, one-tuple probe allocates no timeline map or Z-set. On matched 20-sample runs, circuit p50 fell to 20.470 ms while Established measured 9.756 ms. The 2.10-times regression is smaller but remains outside the gate; record conversion and per-round profile publication remain the next targets.

Trace key visitation now bypasses the general level-merging cursor when a trace has exactly one immutable consolidated batch. Record point lookup hashes a borrowed value slice and compares that slice through the existing digest index; it no longer clones a tuple into a temporary boxed payload. The canonical CBOR encoder accepts a value slice, so this path retains one encoding and identity authority. On 50 deep-chain samples, circuit p50 fell to 18.490 ms with unchanged retained and peak memory, but remains 1.90 times the matched 9.749 ms Established result. Recursive multiway measured 4.669 ms and dense mutual support measured 76.125 ms after the same change; neither regressed relative to their preceding 4.910 ms and 82.568 ms circuit measurements.

An empty transactional Z-set view now validates and bulk-builds its ordered absolute assignments instead of point-inserting every changed row. Later stages retain the point-update path because they must compose against prior staged assignments. The bulk path leaves the view unchanged if any row overflows. On matched workloads, deep-chain p50 fell again to 17.634 ms, recursive multiway to 4.540 ms and dense mutual support to 63.985 ms. Memory and observed work cardinalities were unchanged. Deep-chain remains 1.81 times slower than Established; multiway is 14.99 times faster and dense mutual support is 5.24 times faster.

A post-architecture breadth rerun at 10,000 rows, 100-row deltas and five samples measured 0.524 ms shallow insertion, 0.517 ms wide recursion, 6.386 ms well-founded exit assertion, 4.629 ms recursive multiway retraction, 62.616 ms dense mutual-support retraction and 1.894 ms for the mixed recorded trace. Each matched or improved its preceding circuit result. A separate 20-sample deep-chain run measured 17.126 ms circuit versus 9.725 ms Established, a 1.76-times regression. It executes 100 recursive rounds, 4,950 arrangement probes and emits 5,050 tuples. Borrowed record interning was tested and rejected because matched measurements showed no reliable improvement; no additional record representation was retained. Deep-chain round overhead remains the sole latency non-regression failure at this control scale.

A 2026-07-22 current-build rerun used 10,000 retained rows, a 100-row delta and 30 samples. Deep-chain insertion measured 14.456/16.526/16.549 ms circuit p50/p95/p99 versus 10.698/11.234/11.249 ms Established. The circuit remains 35.1 percent slower at p50 and therefore still fails the 20-percent non-regression gate. It executed 100 recursive rounds and 10,200 observed arrangement probes. Circuit retained heap was 3,137,463 bytes versus 2,420,557 bytes Established (1.30 times); construction and mutation peaks were 0.89 and 0.79 times Established respectively.

The matched recursive multiway retraction measured 4.868/5.208/5.420 ms circuit versus 68.394/73.427/74.399 ms Established, a 14.0-times p50 speedup. Circuit retained heap was 19,275,571 bytes versus 9,842,015 bytes (1.96 times), construction peak was 48,134,420 versus 38,067,003 bytes (1.26 times), and mutation peak was 43,684,348 versus 28,024,369 bytes (1.56 times). The latency gate passes; retained and mutation memory remain replacement blockers.

Recursive semantic staging previously encoded each accepted tuple once for duplicate detection and again for journal insertion. The read view now accepts the already validated canonical encoding, retaining one encoded assignment authority. On the same deep-chain configuration and 30 samples, circuit p50/p95 fell from 14.456/16.526 ms to 11.483/11.864 ms. Established measured 9.563/11.515 ms. The circuit p50 is 20.08 percent slower: a 20.6 percent improvement over the preceding circuit result, but still narrowly outside the 20-percent gate. Retained, construction-peak and mutation-peak memory were unchanged.

Staged duplicate detection and journal insertion then became one ordered-map entry operation instead of a membership probe followed by insertion. Two independent 30-sample runs measured circuit p50 at 11.458 and 11.523 ms versus 9.709 and 9.678 ms Established. The circuit was respectively 18.0 and 19.1 percent slower, passing the 20-percent control-scale non-regression gate in both runs. P95 was 11.818 and 11.766 ms. Logical work, retained state and all heap measurements were unchanged.

Unary trie paths then stopped caching live_keys; the value is exactly the path terminal’s liveness plus its sole child’s count. This removes a derived field whose equality previously depended on every promotion, demotion and publication path updating it consistently. On the 10,000-component recursive multiway workload, retained heap fell from 19,275,571 to 18,715,571 bytes and mutation peak fell from 43,684,348 to 43,124,348 bytes. Construction peak was unchanged. The 30-sample circuit p50 was 4.959 ms versus 81.809 ms Established; the circuit retained-memory ratio improved from 1.96 to 1.90 times.

Unary prefixes with no terminal tuple then received a distinct compact node inside the same generic trie. Terminal absence is structural; these nodes own only their key and child rather than an always-empty temporal weight. Prefixes that are also terminals retain the weighted path representation, and promotion, demotion, overlays and checkpoint entries remain representation-independent. On the same 30-sample workload, circuit retained heap fell from 18,715,571 to 17,035,571 bytes and mutation peak from 43,124,348 to 41,444,348 bytes. Construction peak was unchanged. Circuit p50 improved from 4.959 to 4.692 ms versus 65.007 ms Established; the retained-memory ratio improved from 1.90 to 1.73 times.

Canonical value records then stored compact one- and two-cell tuples inline. These frequent records no longer require a separately allocated cell slice; wider compact tuples and uncommon semantic values retain their existing lossless representations. On the same 30-sample workload, circuit retained heap fell from 17,035,571 to 15,844,179 bytes, construction peak fell from 48,134,420 to 46,943,028 bytes and mutation peak fell from 41,444,348 to 40,252,956 bytes. Circuit p50 was 4.716 ms versus 63.254 ms Established. The retained-memory ratio improved from 1.73 to 1.61 times; construction and mutation peaks were respectively 1.23 and 1.44 times Established.

At 100,000 components with the same 100-row delta and 30 samples, the inline record representation measured 43.009/44.045/44.271 ms circuit p50/p95/p99 versus 686.979/699.287/724.369 ms Established. Circuit retained heap was 149,834,083 bytes versus 101,715,851 bytes (1.47 times), construction peak was 462,067,018 versus 380,667,267 bytes (1.21 times), and mutation peak was 400,249,974 versus 282,025,420 bytes (1.42 times). The circuit remained 16.0 times faster at p50 while the retained-memory ratio improved from the prior 1.83-times measurement at this scale.

The corpus-driven differential then built every eligible conformance package and compared Established, circuit-preferred, Product-preferred and fresh evaluation. All 31 packages retained circuit admission. Thirty packages completed retract, repeated-retract, reassert and duplicate-assert replays with exact catalog equality after every frontier; five used existing input rows and 25 used deterministic synthetic rows. The remaining package contained no rule input to mutate. Both expected recursive packages selected RecursiveCircuit; no eligible package fell back.

The final-head 100,000-row, 100-row-delta, 30-sample Phase-A control ran on 2026-07-24 at revision 81fc95d58. Each workload ran in a separate optimized process. Every circuit selected its required route and validated the full catalog before and after sampling.

WorkloadCircuit p50/p95Established p50/p95p50 speedupProbes/rounds
shallow insertion0.392/0.648 ms28.806/30.718 ms73.5×100/0
wide recursive insertion0.430/0.523 ms58.066/60.978 ms134.9×300/1
low-overlap retraction0.385/0.537 ms206.494/210.381 ms536.6×100/0
high-overlap retraction0.263/0.327 ms61.569/64.291 ms233.7×0/0
WorkloadRetained C/E (ratio)Construction peak C/E (ratio)Mutation peak C/E (ratio)
shallow insertion32.911/54.972 MB (0.599×)199.574/121.647 MB (1.641×)135.411/131.869 MB (1.027×)
wide recursive insertion26.775/24.601 MB (1.088×)88.494/114.787 MB (0.771×)95.086/119.249 MB (0.797×)
low-overlap retraction32.900/54.955 MB (0.599×)199.494/121.623 MB (1.640×)135.365/128.747 MB (1.051×)
high-overlap retraction8.537/15.120 MB (0.565×)97.927/48.190 MB (2.032×)37.845/49.010 MB (0.772×)

The first three workloads affected and emitted exactly 100 derived tuples. High-overlap retraction affected 100 inputs and emitted no derived change. Observed work therefore tracks the changed frontier rather than total model size. This scale passes the Phase-A latency conditions; the retained recursive ratio is 1.088× and construction peaks remain above Established for the two acyclic controls and high-overlap construction. Reproduce one row with:

OXC_SAMPLED_WORKLOAD=shallow_insert OXC_SAMPLED_STATE=100000 \
OXC_SAMPLED_DELTA=100 OXC_SAMPLED_COUNT=30 \
cargo bench -p oxc-reasoning --bench incremental_sampled --features bench-support

The final-head one-million-row controls used separate processes per implementation to keep allocator reservation from overlapping between maintainers:

WorkloadCircuit p50Established p50Circuit retainedEstablished retained
shallow insertion0.363 ms320.874 ms311,902,826 B561,408,256 B
wide recursive insertion0.423 ms645.292 ms250,855,327 B253,069,789 B
low-overlap retraction0.364 ms2,090.161 ms311,890,546 B561,391,896 B
high-overlap retraction0.248 ms633.624 ms84,866,742 B152,749,744 B

The circuit was respectively 883, 1,526, 5,738 and 2,558 times faster at p50. All four circuit retained heaps were at or below Established: the ratios were 0.556, 0.991, 0.556 and 0.556. Circuit mutation peak was 1.03 times Established for shallow and low-overlap maintenance, 0.81 times for recursive insertion and 0.80 times for high-overlap retraction. Construction peak was 1.63 times Established for shallow and low-overlap, 0.75 times for recursive insertion and 2.03 times for high-overlap. Mutation work remained independent of retained cardinality.

At ten million rows, the 30-sample high-overlap control measured 0.258 ms circuit p50 versus 6,793.497 ms Established, a 26,361-times speedup. Circuit retained heap was 851,881,566 bytes versus 1,552,374,336 bytes Established; mutation peak was 3,865,498,830 versus 5,120,219,008 bytes. Circuit construction peak was 10,162,097,863 bytes versus 5,035,068,150 bytes Established. The required large-scale high-overlap latency and retained-state result passes; construction remains a separate optimization target.

Every remaining circuit workload also completed 30 samples at 100,000 rows, selected its required route and validated the complete catalog transition. This covered factorized count, well-founded exit, deep recursion, text-key multiway recursion, upper- and lower-stratum changes, recursive count and count pipelines, projected sum and maximum, dense mutual support, and a mixed recorded trace. Three-sample Established controls were used only to detect regressions, not as release-grade variance evidence:

WorkloadCircuit p50Established p50Retained ratio
well-founded exit52.624 ms524.349 ms1.41
deep recursive insertion11.470 ms59.028 ms1.09
text-key multiway retraction48.512 ms1,086.944 ms1.98
upper-stratum retraction0.314 ms114.613 ms0.84
lower-stratum negation assertion4.173 ms496.313 ms1.52
recursive count assertion1.818 ms3,816.232 ms0.55
recursive count pipeline assertion2.201 ms3,946.810 ms0.85
projected sum retraction1.248 ms2,911.354 ms0.71
projected maximum retraction1.767 ms2,903.440 ms1.97
mixed recorded trace1.351 ms171.176 ms0.84

The circuit therefore showed no latency blocker in this cross-shape pass. Retained memory is concentrated in text-key multiway state and the extrema index; those representations remain default-routing optimization targets. The factorized-count Established control exceeded three minutes before completing three samples, and dense recursion has no bounded matched Established result; neither incomplete control is reported as a comparison.

Extrema-tree nodes then stopped caching subtree length and support-cell count. Both values are exact structural folds used only by diagnostics and checkpoint capacity planning; balancing and semantic lookup do not consume them. Removing the duplicate counters makes their equality structural rather than dependent on every path-copy and rotation constructor. On the 100,000-row projected-maximum workload, retained heap fell from 31,182,127 to 29,582,127 bytes and mutation peak fell from 62,936,471 to 61,336,471 bytes. Construction peak remained 125,415,315 bytes. The 30-sample p50 was 1.857 ms versus 1.767 ms before the change; this single-run difference is not treated as a latency claim. The retained-memory ratio improved from 1.97 to 1.87 times Established.

The extrema tree then stopped retaining a winner profile at every internal node. Ordered winner queries now traverse the authoritative temporal support profiles directly; all winner times are the causal closure of support times. Negative or overflowing support remains a loud query error. Tree construction, path copying and rotation consequently contain no orientation-dependent semantic maintenance. On the same workload, retained heap fell again from 29,582,127 to 27,182,127 bytes and mutation peak from 61,336,471 to 58,936,471 bytes. Construction peak remained 125,415,315 bytes. The 30-sample p50 was 2.072 ms versus 1.857 ms with cached winners, an 11.6 percent cost retained in exchange for removing duplicated semantic state; the circuit remains more than 1,400 times faster than the three-sample Established control. The retained- memory ratio improved from 1.87 to 1.71 times Established.

All-text tuples then stopped owning duplicate String values. The record arena already owns a canonical singleton record for each scalar used by trie edges; wider text tuples now contain those ScalarId values. Singleton records remain the sole string owners, tuple records and tries share their identity, and record views resolve the reference through their published or transactional arena context. Restore reconstructs the same ownership through ordinary record interning, without a string pool or second checkpoint representation.

On the 100,000-component text-key multiway workload, retained heap fell from 307,479,837 to 185,691,947 bytes, construction peak from 640,413,315 to 518,625,425 bytes and mutation peak from 681,600,893 to 559,813,003 bytes. The final 30-sample p50 was 49.027 ms versus 48.512 ms before the change; the 1.1 percent difference is not treated as a latency regression. Relative to Established, the retained-memory ratio improved from 1.98 to 1.19 times, construction peak from 1.39 to 1.12 times and mutation peak from 1.56 to 1.29 times.

Construction phase telemetry then located the shallow-program peak inside the correction-by-correction join. Record arrangement deltas already own canonical (key, time, tuple) order; their constructor now enforces that order, and the join merges the two ordered streams instead of building a duplicate keyed index over the right correction. Repeated-key ranges still form the complete Cartesian product, while probe telemetry retains its prior per-left-row definition.

At 100,000 rows the shallow construction peak fell from 216,754,809 to 211,925,329 bytes. At one million rows it fell from 2,120,750,729 to 2,072,630,281 bytes, removing 48,120,448 bytes of transient index state. Retained and mutation-peak memory were unchanged. The 30-sample million-row p50 was 0.419 ms; the circuit remains 770 times faster than the matched Established p50 while its construction-peak ratio improves from 1.77 to 1.73 times.

Phase boundaries then isolated the next peak inside record distinct evaluation. The old path separately materialized the touched-row set, a promotion map, retained profiles, grouped corrections, output corrections and the record journal. Record distinct now consumes its input into one grouped correction authority. Each row is validated and promoted as that authority is drained, and the same row correction directly produces the zero-crossing output and persistent journal. Batch and single-row promotion share the same cached record identity; batch promotion retains its validate-before-mutate contract.

On the 100,000-row shallow control this reduced construction peak from 211,925,329 to 207,040,154 bytes. On the million-row control it reduced the peak from 2,072,630,281 to 2,037,063,114 bytes, another 35,567,167 bytes or 1.7 percent. Retained heap remained 311,888,378 bytes and mutation peak remained 1,357,928,337 bytes. The construction ratio against the 1,196,584,158-byte Established control is 1.70 times. A repeated 30-sample run measured 0.010 ms p50, but this revision claims only the memory change because that latency is not comparable with the earlier feature-instrumented run.

The next phase boundary placed the remaining peak in sink-row promotion. That path previously collected every escaping evaluation identity into a set, then constructed a second identity-to-record map before producing the record sink profiles. Promotion now validates the complete input before mutation and then consumes it directly into record profiles through the same cached single-row promotion primitive used by distinct. This removes two transaction-sized identity indexes without weakening failure atomicity.

The 100,000-row shallow construction peak fell from 207,040,154 to 204,692,322 bytes. The million-row peak fell from 2,037,063,114 to 2,001,032,146 bytes, removing another 36,030,968 bytes. Retained heap and mutation peak remained 311,888,378 and 1,357,928,337 bytes. The construction ratio against Established is now 1.67 times. Two non-instrumented 30-sample controls measured 0.009 ms p50; the memory reduction remains the claimed effect. The remaining shallow construction peak is inside the initial record join, not distinct, promotion, state preparation or sink publication.

Join phase telemetry separated arrangement construction from cross-term emission. The correction output previously grew while both consumed input profiles remained live, even though their record arrangements were already self-contained. Record join now releases each owned input immediately after arrangement; borrowed inputs remain unaffected. This is a lifetime change, not a second representation or specialized execution path.

The 100,000-row shallow construction peak fell from 204,692,322 to 197,837,922 bytes. The million-row peak fell from 2,001,032,146 to 1,945,059,330 bytes, removing another 55,972,816 bytes. Retained heap and mutation peak again remained 311,888,378 and 1,357,928,337 bytes. The construction ratio against Established is now 1.63 times. The 30-sample million-row p50 remained 0.009 ms. The next measured peak is persistent distinct-state preparation after join evaluation.

Record distinct corrections then adopted the same row-major row -> sparse temporal profile representation as retained distinct support. Evaluation no longer expands a grouped row correction into a time-major journal for state preparation to regroup. Ordinary and Product preparation use one kernel, and prepared assignments use the same map shape; an empty temporal profile denotes deletion. Initial assignments therefore move the correction tree directly into prepared state. Candidate sinks derive their required time-major external delta without becoming a second state authority.

This representation change is memory-neutral in retained and mutation state. At 100,000 rows, shallow construction increased from 197,837,922 to 199,492,842 bytes while wide-recursive construction decreased from 91,551,406 to 88,491,705 bytes. At one million shallow rows construction increased from 1,945,059,330 to 1,949,033,290 bytes, 0.20 percent; retained heap remained 311,888,378 bytes, mutation peak remained 1,357,928,337 bytes and the 30-sample p50 remained 0.009 ms. The small shallow cost is accepted because it removes a representation conversion and makes ordinary and product-time state preparation structurally identical.

Adaptive trie child storage reduced the 10,000-state recursive multiway retraction workload’s retained heap from 73,058,699 to 31,378,459 bytes and its mutation peak from 97,468,172 to 55,784,956 bytes. The same 20-sample run measured 4.955 ms p50, compared with 5.036 ms before the representation change; logical state remained 70,000 keys, 140,003 nodes and 140,000 edges. Applying the same adaptive child map to prepared trie patches subsequently reduced construction peak from 139,208,745 to 64,075,097 bytes. Retained heap remained 31,378,459 bytes, mutation peak remained 55,784,956 bytes and the 20-sample p50 measured 4.910 ms. Prepared patches and published tries now share one storage policy rather than relying on a dense transaction-only map. A matched Established run measured 68.047 ms p50, 38,066,979 construction-peak bytes, 28,024,345 mutation-peak bytes and 9,842,015 retained bytes. Circuit construction is therefore 1.68 times Established instead of 3.66 times, while mutation peak is 1.99 times and retained state is 3.19 times Established. Recursive multiway retained memory is not yet within the replacement gate.

Trie terminal nodes subsequently stopped carrying prefix-only metadata and child storage. A terminal is represented by its temporal weight; only a node with a suffix owns a boxed branch. The existing generic update, overlay, Generic Join and logical checkpoint-entry interfaces are unchanged, so this is one physical representation shared by full and incremental evaluation rather than an arity-specific second index. Promotion preserves a key that is both a terminal and a prefix; demotion restores the compact terminal after its final child is removed.

On matched optimized 30-sample recursive_multiway_retract runs at 100,000 components and a 100-row delta, p50/p95 changed from 43.062/43.810 ms to 43.974/44.783 ms. Retained heap fell from 283,043,307 to 270,245,419 bytes (4.5 percent), and mutation peak fell from 533,457,438 to 520,661,310 bytes (2.4 percent); construction peak was unchanged at 543,278,362 bytes. The 2.1 percent p50 cost is retained because the workload remains 15.6 times faster than the matched Established p50 of 683.942 ms. Retained heap remains 2.66 times Established’s 101,715,851 bytes, so this improves but does not discharge the replacement memory gate.

Singleton prefixes were then represented as boxed path nodes whose sole child is inline. Zero, one and multiple children normalize to terminal, path and branch forms after every publication. This removes one allocation per unary prefix without inflating terminal nodes; logical node/edge metrics, ordered traversal, prepared overlays and checkpoint entries remain representation- independent.

On the same 100,000-component, 100-row, 30-sample workload, path compression changed p50/p95 from 43.974/44.783 ms to 42.857/43.898 ms. Retained heap fell from 270,245,419 to 253,445,419 bytes (6.2 percent) and mutation peak from 520,661,310 to 503,861,310 bytes (3.2 percent); construction peak remained 543,278,362 bytes. Relative to the trie before terminal and path compaction, retained heap is 10.5 percent lower and p50 is 0.5 percent lower. The circuit is 16.0 times faster than Established, but still retains 2.49 times its heap; the replacement memory gate remains open.

That representation was subsequently refined because projection-seed interning made stateless base rows permanent arena records. Projection membership is now adaptive per semantic tuple: an existing program-arena record is referenced by RecordId; otherwise the projection retains the canonical encoded tuple. Projection construction and checkpoint restore only look up existing records and never intern rows. The two physical forms are disjoint and materialize through one semantic catalog interface.

base relation tuple
        |
        v
existing program record? ---- yes ----> RecordId membership
        |
        no
        v
canonical encoded membership
        |                                  circuit profiles / traces
        |                                             |
        +-------------------+-------------------------+
                            v
                union materialized-model view
                            |
                            v
               semantic catalog / checkpoint

This preserves compact recursive membership without forcing projection-only inputs into the append-only arena. On a one-million-row high-overlap workload, 100-row retraction and ten samples, retained heap fell from 254,163,116 to 101,506,613 bytes while p50 measured 0.348 ms. Arena cardinality fell from 1,100,100 to 100,000 records. A one-million-row recursive-wide control retained 602,119,645 bytes, 872 bytes above the record-native measurement, with 2,000,300 arena records and a 1.020 ms ten-sample p50. The all-encoded prototype retained 747,591,236 bytes on that control and was rejected because it duplicated record-backed recursive outputs as encoded keys.

The projection subsequently stopped retaining derived membership entirely. AdaptiveRelations owns only base facts; circuit profiles and recursive traces are the sole derived authority. Full-model reads, point membership, columnar construction and checkpoint serialization synthesize the set union with sparse direct facts on rule-head relations. Relation-specific reads use a component API and full materialization streams borrowed derived values directly into the catalog, avoiding an intermediate derived-tuple vector.

At one million rows, ten samples and delta 100, wide recursion retained 590,500,456 bytes, down 11,619,189 bytes from adaptive union membership. P50 improved from 1.020 to 0.911 ms and mutation peak fell from 793,347,886 to 781,729,513 bytes. At 100,000 rows, retained heap changed from 61,475,053 to 60,313,616 bytes for wide recursion, 67,682,276 to 66,519,967 bytes for upper retraction, 152,964,526 to 149,479,025 bytes for lower NAF, and 26,590,992 to 26,164,212 bytes for the mixed trace. Candidate/settled and profile/temporal metric pairs remain views of shared physical traces, not additional stores.

Recursive components borrow accepted external relations instead of cloning each semantic Z-set before staging iteration zero. Validation remains strict at the recursive boundary and the staged profile owns the transaction journal. On the million-row wide workload, construction peak fell from 5,203,105,269 to 5,011,486,993 bytes.

Published retry identity now retains the last output delta as weighted RecordId sets. Public ProgramBatchOutcome values and checkpoint envelopes remain semantic: preparation resolves the compact retry state only for an idempotent duplicate, checkpoint encoding resolves it canonically, and restore interns the logical output after component validation. Unknown record IDs fail loudly. The million-row wide workload retained 425,167,720 bytes, down 165,332,736 bytes from semantic retry output; mutation peak fell by the same amount to 616,396,777 bytes, with 0.980 ms p50 over ten samples. High-overlap retained heap fell from 101,506,613 to 90,210,574 bytes. Construction peak is unchanged because preparation still owns semantic output until the atomic commit.

Program flow is keyed by typed relation identity. Immutable consumer metadata is compiled into the program plan and its commitment. External deltas retain frontier ownership; derived deltas, public output and downstream flow share record identities. A relation is released before preparing its last consumer. Prepared program journals retain no semantic output copy. Interned acyclic components publish record sink deltas directly. Interned recursive components expose their scheduler’s record output directly to program flow; the program no longer clones and re-interns the resolved semantic output. Fanout tests cover a relation consumed by adjacent and nonadjacent strata; signed four-stratum differentials match full evaluation for insertion and retraction. Public apply resolves semantic output only after atomic commit.

Record-backed profile evaluation owns the semantic-to-record sink boundary. Operator rows remain semantic only while a transaction evaluates its schedule; settled and candidate sink profiles leave that boundary as RecordId profiles. Recursive scheduling and interned acyclic publication consume those identities without cloning tuples or interning them again. The record transaction token continues to bind prepared sink identities to the transaction that created them. Semantic profile evaluation retains its semantic sink type, so the two backends share rule evaluation without retaining parallel output authorities. Product-time rule sessions use the same boundary: component settled and candidate profiles remain record-native through outer-coordinate consolidation. Only an inter-component relation with a later consumer is resolved into the semantic profile required by the next component’s scan interface. The program flow owner performs that projection and immediately retires it after the last consumer; final Product publication never reconstructs semantic sink profiles. The Product session module owns only construction, staged evaluation, audit, and atomic publication. Its lifecycle, fault-injection and physical-kernel fixtures live in an owned test submodule, so expanding the conformance surface does not enlarge the production transaction boundary. Semantic, record-backed and Product sessions call one input-identity validator before evaluation. A relation must equal the plan relation selected by its qualified name, including module and arity; unknown or same-name mismatched identities fail before record interning or session mutation. Product retry with the valid identity then proceeds from unchanged state.

Distinct corrections follow the backend representation. Semantic evaluation journals Row; record-backed ordinary and Product evaluation journal RowRef. Record distinct preparation interns each touched row once, uses that identity for retained support preparation, and exposes tuple-valued candidate sinks as the underlying RecordId. Candidate publication therefore cannot diverge from the support journal or create a second record identity. Backend-associated distinct and candidate types keep this distinction structural while sharing the zero-crossing algorithm.

Antijoin journals follow the same representation rule. Record-backed ordinary and Product evaluation retain arrangement-key, tuple and blocker-key identities in ProfiledRecordAntijoinDelta; semantic evaluation retains semantic keys. The retained-window read interns each touched key once and returns its identity map with the semantic evaluation window. Journal construction reuses that map, and record state preparation consumes the identity journal directly. The backend contract associates the complete antijoin journal type rather than only its blocker correction, preventing mixed semantic and record fields.

Factorized aggregate journals are also backend-associated. Record evaluation publishes BindingKeyRef groups containing record-backed factor corrections; record state preparation accepts only that type. Non-extrema corrections cross the semantic-to-record boundary once after output differentiation. Extrema evaluation already requires record corrections to compare retained indexes, so the journal retains that exact correction instead of discarding it and re-interning the semantic source. Count journals follow the same rule: semantic evaluation publishes semantic groups, while record-backed ordinary and Product evaluation publish BindingKeyRef groups and record-backed folds. Record state preparation consumes those identities directly. Record extrema evaluation also retains outer keys as BindingKeyRef and resolves a semantic binding only when constructing an externally visible output row. Conversion therefore occurs once at the evaluation boundary, and the journal and prepared state cannot diverge through separate representations. Extrema validation integrates the record-native input profile directly; it does not intern a sentinel binding or publish an unreferenced record solely to exercise validation.

Multiway-join input follows the same backend boundary. The evaluation backend associates its input type: semantic evaluation receives tuple profiles, while record-backed ordinary and Product evaluation receives RecordScanProfile. Orchestration delays either semantic or record profiles without converting them. Interned multiway state resolves each input record only long enough to validate constants and repeated variables, then interns only the projected join-key values. Product gamma currently retains semantic multiway keys, so its record-aware semantic codec performs the same projection without interning; this preserves the Product state representation while eliminating complete tuple-profile reconstruction. Produced operator profiles remain semantic until operator outputs become record-native.

Interned multiway tries use a canonical hybrid scalar key. Integers representable as i32 remain inline; every other value, including out-of-range integers, uses the arena’s canonical scalar identity. The representation is exclusive rather than a cache: an integer has exactly one key form, checkpoint serialization resolves both forms to the same semantic Value, and restore deterministically selects the same form. This removes singleton-record payload and digest-index ownership for common join keys without widening every trie edge to Value.

An all-interned external-flow prototype was rejected: the million-row high-overlap workload retained 242,913,309 bytes and 1,100,000 arena records. Preserving external ownership restored retained memory to 90,215,846 bytes and 100,000 records; p50 was 0.338 ms over three samples. This boundary is an ownership invariant, not a second persistent model representation.

The million-row single-recursive-component control measured 5,011,493,121 bytes construction peak, 425,173,008 bytes retained and 616,402,065 bytes mutation peak. The 6,128-byte construction difference from the preceding 5,011,486,993-byte run is noise: this workload has no inter-component edge. Component preparation still reconstructs semantic scan profiles, so the next cut is record-native component input and sink output rather than another program-level representation.

Direct recursive record-output propagation measured 5,011,492,656 bytes construction peak, 425,172,279 bytes retained and 616,401,336 bytes mutation peak on the one-sample million-row control. The negligible change confirms that the dominant remaining construction allocation precedes the component output boundary: semantic scan/profile preparation and recursive assignment journals.

Recursive admission subsequently became ownership-transferring. The program moves the accepted semantic batch into the recursive component; impulse deltas become iteration-zero profile buckets without cloning. Signed projection assignments use one typed encoded-key accumulator borrowing those profiles and the recursive result, shared by semantic and interned paths. The million-row construction peak fell from 5,011,492,656 to 4,819,874,188 bytes, 3.8 percent. Retained memory remained 425,172,279 bytes and mutation peak remained 616,401,336 bytes. Component input is still semantic at scan evaluation, but it now has one frontier owner rather than parallel batch and profile owners.

Record-profiled sessions no longer retain a session-local input profile map. That map was updated after every stage but was never read, committed or checkpointed; operator derivative state is the sole staged authority. Removing the redundant input authority reduced the million-row construction peak from 4,819,874,188 to 4,628,255,056 bytes, 4.0 percent. Retained and mutation memory were unchanged.

Record-profiled scan admission uses one mixed input envelope. External frontier-owned deltas remain semantic profiles; derived feedback remains RecordId profiles. Representation dispatch occurs only at Scan and DelayedScan: both paths call the same slice-based binding constructor, while the record path resolves pending or published IDs through the frontier transaction. Downstream rows and operators remain representation-independent. Missing or non-value records fail loudly before state preparation.

Recursive feedback therefore no longer follows RecordId -> Tuple Z-set -> tuple profile -> binding row. It remains record backed until scan construction. The million-row construction peak fell from 4,628,255,056 to 4,245,017,958 bytes, 8.3 percent. Retained memory remained 425,172,279 bytes and mutation peak remained 616,401,336 bytes. Record scan tests cover semantic parity, repeated variables, constants, signed weights, pending transaction IDs and non-value record refusal.

The ten-sample latency control measured 0.995 ms p50 and 1.355 ms p95, versus 0.980 ms and 1.326 ms before the construction cuts. The 1.5 and 2.2 percent differences are within run variance; record-backed feedback did not introduce a material mutation-latency regression.

Interned recursive prepared state is now a distinct type rather than a semantic prepared journal parameterized by RecordId. It retains the scheduler record transaction, record-profile journal and signed assignments, but no resolved semantic outcome, duplicate payload or standalone frontier metadata. Signed assignments resolve record tuples directly into their final journal and do not materialize an intermediate semantic output Z-set. The million-row construction peak remained 4,245,017,958 bytes, locating the peak before final assignment and publication assembly.

Record-profile finalization consumes staged operator, join, antijoin, distinct and aggregate maps when constructing semantic conversion journals. It no longer clones the entire staged profile before conversion; each source container drops as its journal is built. The wide construction peak remained 4,245,017,958 bytes, confirming that the measured peak occurs before final profile conversion. The ownership cut remains structural: prepared state has one owner through finalization.

Benchmark-only phase observation located the remaining peak inside the feedback join: record scan peaked at 3,207,115,614 bytes and join construction peaked at 4,245,019,046 bytes. The evaluator now transfers a node profile into join preparation when the join is its last consumer. Binding rows are consumed into tuple corrections by moving values from their bindings; shared or retained nodes remain borrowed. This is derived from the same immutable consumer counts used for operator liveness.

The million-row construction peak fell from 4,245,017,958 to 3,791,864,069 bytes, 10.7 percent. Retained memory remained 425,172,279 bytes and mutation peak remained 616,401,336 bytes. The benchmark observer is opt-in and records only allocator counters at named preparation boundaries; production builds contain no observer state.

Join accumulation consumes its staged cross-term profile. This preserves transactional overflow validation while moving disjoint iteration buckets and values instead of cloning them. On the million-row workload, the feedback join previously held both the 846,284,576-byte staged result and its accumulated copy; its phase peak fell from 3,526,734,470 to 3,207,115,614 bytes. The overall construction peak remains 3,791,865,157 bytes at final semantic-to-record preparation. This identifies record-native session staging, rather than further join-local ownership changes, as the next representation boundary.

Final record preparation consumes semantic correction journals and moves row, tuple, binding, and aggregate-key values into the transaction-owned record suffix. Preparation remains atomic: published state is borrowed, assignments remain private, and only a fully validated prepared state can commit. This removes clone-based ownership at the representation boundary but does not lower the million-row construction peak, which remains 3,791,865,157 bytes. The remaining peak therefore reflects simultaneous semantic staged authority and record preparation metadata, not cloned payload values inside the conversion loop.

Record-profile state supports direct correction-overlay preparation. Operator, join, antijoin, distinct, and aggregate corrections remain in record identity space; checked merge produces the same private prepared assignments as semantic conversion without resolving payloads. Exact cancellation prunes every state family. Overflow, negative support, or shape divergence leaves published state unchanged. This primitive is the finalization boundary required before the recursive session itself can retain record-native staged authority.

The recursive session now retains that correction overlay directly in record identity space. One representation-typed state-view interface preserves the single scheduled evaluator: semantic standalone circuits read semantic state, while program-owned circuits read published and transaction-staged record overlays. Join evaluation emits correction against both record overlays plus correction against correction exactly once. Distinct, antijoin, and aggregate operators fold the same two record overlays before applying their correction. Finalization applies the record overlay directly; the semantic join, antijoin, distinct, and aggregate conversion adapters no longer exist.

On the million-row recursive insertion workload, construction peak fell from 3,791,865,157 to 3,143,330,926 bytes, 17.1 percent. Retained heap was 425,120,895 bytes and mutation peak was 616,349,952 bytes. The remaining construction peak occurs during feedback scan expansion at 3,143,330,926 bytes. Final record-profile publication no longer raises the peak.

A ten-sample mutation control measured 0.756 ms p50 and 1.167 ms p95. Its construction peak was 3,143,329,838 bytes, retained heap was 425,119,807 bytes, and mutation peak was 616,348,864 bytes.

The remaining scan peak is addressed through one canonical compact binding: strictly increasing (variable, value) pairs in one exact-size allocation. Record payloads adopt this type first while preserving their canonical CBOR bytes and digest exactly. Evaluator rows, linear operators, joins, aggregates, and profile keys will adopt the same type together; an alternate compact row variant is forbidden because equal bindings must remain structurally equal by construction.

After the evaluator-wide cut, the million-row phase run measured a 2,536,851,923-byte construction peak, 19.3 percent below record-native staging and 51.2 percent below the original 5,203,105,237 bytes. Retained heap remained 425,120,895 bytes because compact bindings replace only frontier-local rows. The diagnostic mutation sample measured 1.234 ms. A ten-sample control measured 0.842 ms p50 and 1.302 ms p95. Construction peak was 2,536,850,835 bytes and retained heap was 425,119,807 bytes.

Map evaluation reads compact bindings through a variable-lookup interface on the existing expression interpreter. The prior dense vector adapter, whose length depended on the largest sparse variable identifier, is removed. Missing variable, conditional, builtin, arithmetic, and undefined-expression behavior continues to come from the one evaluator.

Temporal join arrangements subsequently moved from nested singleton maps to a shared immutable differential trace spine. Each batch is sorted and consolidated by (key, iteration, tuple). Occupied geometric levels own immutable batches; preparation probes only touched cells, validates checked weights and cardinality changes, and plans level merges before an infallible spine replacement. Key reads binary-search every occupied batch and merge only the matching ranges. Checkpoints enumerate the logical consolidated trace, so batch boundaries and compaction timing do not enter durable identity.

signed operator delta
        |
        v
consolidate delta batch -----> exact touched-cell probes across retained levels
        |                                      |
        +----------------------+---------------+
                               v
                  validate weights and cardinality
                               |
                               v
                    plan geometric level merges
                               |
                               v
                    sealed replacement spine
                               |
                   atomic publication boundary
                               v
       [L0] [L1] [L2] ... [Ln] immutable sorted batches
          \    \    \         /
           +---- binary key-range merge ----> operator reader

A generated mixed-update oracle exposed and preserves a critical compaction regression: resizing for a smaller delta must not truncate occupied higher levels. A fixed-delta regression also proves that preparing one changed cell against 4,096 retained cells performs fewer than 128 key comparisons and no full trace materialization. Record-backed join and antijoin readers use the same key-local cursor; full logical materialization is restricted to explicit checkpoint and conversion boundaries.

Repeated same-machine measurements used five runs of 300 samples at 10,000 retained rows and a 100-row requested delta. Before trace compaction, wide recursion measured 0.962–1.053 ms p50 with a 0.995 ms median; the compact trace measured 0.931–0.985 ms with a 0.949 ms median. The connected count pipeline measured 3.513–3.591 ms before compaction and 3.443–3.508 ms afterward, with 3.527 and 3.448 ms medians. The trace is 4.6 and 2.2 percent faster at the medians. Earlier isolated 30-sample results suggesting a 31–35 percent regression were not reproducible under repeated measurement.

Recursive candidate history subsequently adopted the same trace spine. Sparse tuple profiles now exist only as key-local ephemeral views; snapshots share immutable batches, preparation changes only touched keys and checkpoints retain the logical tuple-to-ordered-cells wire. At 10,000 retained rows, wide recursion retained 6,931,869 bytes, down 61.2 percent from the compact-record record-membership implementation. At 100,000 rows, 30 samples measured 0.945/1.041/1.220 ms p50/p95/p99, 67,435,629 retained bytes and 86,551,342 mutation-peak bytes. The temporal and candidate traces held 200,000 entries each in two and one batches, consuming 4.8 and 3.2 MB of entry capacity. Candidate compaction reduced retained heap another 13.2 percent from the temporal-only trace result. The connected count pipeline retained 6.21 MB at 10,000 rows. Latency is fixed-delta local; arena payload/index ownership remains the principal memory difference from the established maintainer.

Ordinary acyclic joins apply the same ownership rule without temporal profiles: bilateral arrangements are the sole retained join-input state. A ten-sample 100-fact insertion over two 100,000-row binary inputs measured 352.583 microseconds p50 and 214,339,662 retained bytes, versus 27.461 milliseconds and 54,972,080 bytes for the established maintainer. Removing the redundant generic join inputs eliminated 200,000 operator rows and reduced circuit retained heap by 42.0 percent. The remaining 100,000 support rows are explicitly owned by the ordinary-stratum Distinct operator; generic operator rows are zero. The latency result is favorable. Maintainer-owned circuits retain no typed sinks: the encoded read catalog owns output membership, while direct facts on rule-head relations use sparse provenance and derived membership comes from Distinct support. This preserves EDB/IDB overlap without three complete catalogs. Retained heap fell another 8.2 percent, but the 3.90-times retained and 4.84-times peak heap ratios are not production gate wins.

A separate recursive-correction benchmark compares one committed causal mutation with fresh full evaluation. Repeated Criterion measurements after the recursive hot-path optimizations and seal-boundary activation were:

workloadscalecausal circuitfull evaluationratio
deletion cascade160.177 ms0.080 ms2.2x slower
deletion cascade320.433 ms0.294 ms1.5x slower
deletion cascade641.214 ms1.173 ms1.0x slower
deletion cascade1284.229 ms4.892 ms1.2x faster
alternative support160.030 ms0.088 ms2.9x faster
alternative support320.053 ms0.313 ms5.9x faster
alternative support640.140 ms1.211 ms8.6x faster
alternative support1280.543 ms4.927 ms9.1x faster

Correctness passed. Stage-local body evaluation, key-first temporal arrangements and tuple-local distinct sealing reduced causal latency by 89–99.8 percent relative to the first causal implementation. Alternative-support maintenance is 2.9–9.1 times faster than full evaluation. Deletion cascades cross over from 2.2 times slower at scale 16 to 1.2 times faster at scale 128. Each cell is the midpoint of ten Criterion samples after a three-second warmup and an approximately five-second measurement interval. This remains a focused microbenchmark, not a general performance claim; retiming retained inner profiles is not a valid optimization.

After candidate history moved to the trace spine, 30-sample causal-circuit confidence intervals were:

workloadscalecausal circuit intervalprior full-evaluation midpointratio at circuit midpoint
deletion cascade160.135–0.136 ms0.080 ms1.7x slower
deletion cascade320.281–0.284 ms0.294 ms1.0x faster
deletion cascade640.603–0.616 ms1.173 ms1.9x faster
deletion cascade1281.352–1.367 ms4.892 ms3.6x faster
alternative support160.023–0.025 ms0.088 ms3.7x faster
alternative support320.031–0.033 ms0.313 ms9.9x faster
alternative support640.053–0.054 ms1.211 ms22.5x faster
alternative support1280.172–0.229 ms4.927 ms25.4x faster

The full-evaluation column is the unchanged prior baseline and was not rerun in the 30-sample trace measurement. The causal values therefore demonstrate the trace implementation’s progression, not a new matched-machine ratio claim.

Heap values are live requested allocations relative to the pre-construction process baseline, not RSS, allocator reservation, stack or mapped-file size. The million-row Phase-A matrix now covers shallow and recursive insertion, low/high-overlap retraction, lower-stratum negation, fused count, a connected count pipeline, a mixed three-commit trace and dense mutual support. Completed pairs are 13.5–5,594 times faster at p50. High-overlap retraction retains 33.9 percent less heap than the established maintainer and reaches 15.4 percent more peak heap. Every other completed pair retains 2.6–7.9 times the established heap. Established recursive count exceeded a ten-minute bound; its connected variant was stopped after 245 seconds. Both dense-clique implementations failed to construct and sample within ten minutes: the circuit reached 17.77 GiB RSS and the established path reached 24.15 GB maximum RSS. Exact p50/p95/p99 and heap values are recorded beside the benchmark. Repeated-machine variance, the 10-million-fact run and recursive round/probe telemetry remain open. The latency gate passes; retained and construction memory prevent default routing.

Dense phase tracing later identified a separate frontier-local amplification: the transitive rule materialized (x, y, z) join bindings although its sole Project consumer retained only (x, z). Project nodes with one Join predecessor now pass their projection into the join emitter. Each bilinear term consolidates the projected tuple directly; the immutable plan, join-input arrangements and checkpoint identity do not change. At 100,000 dense inputs this reduced construction peak from 2,539,749,342 to 1,687,412,386 bytes, 33.6 percent, and measured 98.913 ms p50 over three samples. Retained heap remained 262,193,524 bytes because candidate and temporal histories remain authoritative.

The scheduler boundary subsequently became consuming. Once an inner iteration is sealed, its sink Z-set moves from the candidate queue into the distinct scheduler; only future buckets remain queued. This preserves delayed future work while removing a transaction-length duplicate of every consumed bucket. The dense construction peak fell again to 1,367,329,752 bytes, 19.0 percent below projection-aware emission and 46.2 percent below its original 2,539,749,342-byte control. Retained state remained unchanged.

Operator-profile ownership follows one plan-derived last-consumer policy. Unretained profiles move into an exclusive unary Union, Join input or terminal Sink; shared and retained profiles remain borrowed. Terminal sinks consume tuple rows directly rather than cloning tuple payloads into a second profile. The same dense control measured 83.197 ms p50 and 85.279 ms p95 over ten samples. Construction peak fell from 1,367,329,752 to 1,250,791,694 bytes, 8.5 percent, and 50.8 percent from the original control. Retained state remained 262,193,524 bytes.

Join evaluation exposes one canonical row-emission kernel. Callers that need a Z-set collect that stream; temporal correction evaluation instead accumulates rows directly into the destination iteration. This changes neither bilinear terms nor plan/checkpoint identity. On the million-row wide-recursion control, construction peak fell from 2,536,851,555 to 1,962,000,150 bytes, 22.7 percent; two five-sample controls measured 0.744–0.937 ms p50 and 1.314–1.502 ms p95. Retained heap and mutation peak remained 425,119,807 and 616,348,864 bytes. Dense construction changed only 0.3 percent to 1,246,692,866 bytes, so its remaining transient is not the materialized join-result boundary.

Signed publication records two independent effects: relation visibility and evaluator projection assignments. Once projection authority is delegated, a newly populated relation publishes only its relation identifier, and an already-visible relation publishes no evaluator journal. Internal projection authority and base-support ownership still require the checked tuple journal. This removes state that had no consumer instead of adding a second tuple representation. Dense live heap after signed-assignment preparation fell from 1,113,786,852 to 899,725,844 bytes, 19.2 percent. Construction peak remained 1,246,692,866 bytes at the earlier recursive join/scheduler transition.

Scheduler finalization consumes its transaction-local candidate and output profiles. The prepared trace validates against a borrowed settled delta; that same delta then moves into the transaction outcome, while timed buckets move into the journal. Dense construction peak fell from 1,246,692,866 to 1,241,018,934 bytes, 0.5 percent. Ten samples measured 84.216 ms p50 and 87.775 ms p95. The million-row wide control retained its 1,962,000,150-byte peak and measured 0.701 ms p50 over five samples.

Join correction emission accumulates into the transaction-local output that already owns published terms. The allocating interface is an adapter over that same accumulator, so ownership transfer does not create a second correction algorithm or representation. Invalid overlay metadata is rejected before the caller-owned output changes; later errors discard the enclosing prepared transaction. Dense construction peak fell from 1,241,018,934 to 1,205,741,822 bytes, 2.8 percent, while retained heap remained 262,193,524 bytes. Ten samples measured 83.934 ms p50 and 89.958 ms p95. The million-row wide control remained byte-identical at a 1,962,000,150-byte peak and measured 0.732 ms p50 over five samples.

Work telemetry follows the same prepare/validate/commit boundary as semantic state. Each circuit evaluation session counts keyed arrangement probes; each recursive scheduler records productive feedback rounds. Prepared component journals carry those values and the whole-program journal checks and sums them before publication. Failed preparation cannot change the last published measurement. Duplicate and effective-empty transactions publish zero. The counters are observational: checkpoint payloads, plan identities and derivation commitments exclude them, and restore begins without a measurement. The sampled benchmark records forward-transaction counters before the inverse restoration and sums multi-commit traces. A 10,000-row, ten-fact wide recursive insertion reported zero arrangement probes and one recursive round; the dense recursive retraction control reported 3,460 probes and five rounds. On an adjacent warm 30-sample 100,000-row dense control, observed telemetry measured 86.043 ms p50 versus 83.087 ms without telemetry, a 3.6 percent cost. Peak and retained heap increased by 48 and 72 bytes respectively.

Phase B — pair-encoded value model

Every logical relation becomes two parallel integer-weighted streams carrying evidence-for and evidence-against. Negation swaps streams; bilattice operations are cross-stream value operators. Integer multiplicity remains the DBSP weight: Truth4 and positive-Boolean provenance are not weight semirings. This phase is assert-only and must reproduce both ordinary and $undefined:: extents while preserving the I ⊆ J K3 gate.

Phase C — retraction under the pair

Retraction is admitted only after the deletion behavior of the provenance projection is mechanized. Provenance remains a side-track keyed by atom, polarity, rule and witness; it does not replace integer circuit weights. Until the gate is discharged, pair-bearing retractions use the existing conservative fallback.

Phase D — incremental well-founded semantics

Recursion through negation uses precision-positive coordinates. For fixed base extent E, let gamma_E(I) be the reduct least model under assumption I and let the finite relevant domain be D_E = gamma_E(empty). Antitonicity gives gamma_E(I) subset D_E for every assumption. The retained semantic coordinates are definite truth T and definite falsehood F; compatibility with the public representation is U = D_E ∖ F, where U is possible truth. Define:

C_E(T, F) = (
    gamma_E(D_E ∖ F),
    D_E ∖ gamma_E(T)
)

C_E is monotone in componentwise subset order: increasing F decreases the first gamma assumption and therefore increases its output; increasing T decreases the second gamma output and therefore increases its complement. Its least fixpoint from (empty, empty) maps exactly to the precision-least stable revision fixpoint (T,U), hence to the batch well-founded model. This equivalence is mechanized before the dynamic circuit becomes production authority.

Argon.Reasoning.Datalog.DynamicCoupled also proves the affected-region settlement theorem used by Product. Regional least-fixpoint recomputation equals the complete positive-coordinate least fixpoint when the prior and new fixed points agree outside independently selected true and false regions and the regional fixed point is also fixed outside those regions. A second theorem proves that exterior closure for every region-restricted revision step is a stronger sufficient condition. This matches Product’s separate affected- coordinate sets and separates the minimal semantic obligation from a static closure proof. Product still requires evidence that its concrete dependency closure establishes the premises and that its worklist computes the regional least fixed point for every admitted mutation; finite differential tests do not prove those properties. The same module proves a worklist theorem: any sequence starting at the empty regional state, whose every step is below one complete regional revision, and which terminates at a regional fixed point equals the regional least fixed point. Consequence partition and processing order do not appear in the result. The same file proves the concrete bridge used by the implementation: any next coordinate state contained in both the selected region and the complete gamma-derived positive revision is below the regional revision. Its worklist corollary reduces runtime correspondence to gamma-bound containment, region containment and final fixed-point agreement rather than an opaque regional- step premise. Runtime regional feedback is represented by an opaque certified-step type. Production construction is private to the differentiated driver and admits only positive truth transitions produced by the current truth gamma revision and positive falsehood transitions produced by the current domain-minus- possible transition. It cannot invent a candidate through the scheduler API, cannot admit signed overdeletion, and cannot reseed an already-published tuple outside the selected region. This makes the runtime step an under-approximation of the gamma transitions by construction. Before each scheduler output enters accumulated regional state, delta-local checks require every truth addition to exist in the current truth-gamma output and every falsehood addition to exist in the current domain-minus-possible output. This enforces the Lean gamma-bound and region-bound premises at the concrete worklist boundary without scanning the complete retained component. Connecting gamma derivative exactness to the formal complete gamma revision remains an audit and proof obligation. An independent persistent-state differential exercises that concrete derivative boundary over all 512 three-node move graphs and all eight fixed assumptions. One state advances only through signed assumption deltas while a second state receives complete target assignments; all 4,096 gamma outputs agree. This checks the exact runtime API used by Product separately from the existing direct semantic oracle comparison. Recursive finalization accepts only a quiescent-session type. Converting a mutable session into that type exhaustively rejects an unprocessed required profile suffix, pending evaluator feedback, or an unsealed current candidate bucket. Product and ordinary recursive callers share this boundary; a future caller cannot publish a merely presumed fixed point by calling finalization directly. The differentiated driver returns an opaque certified regional revision only after sparse coordinate containment, expected scheduler deltas, gamma outputs, and both coupled assumptions agree on the complete selected region. Its fields are private and publication consumes the certified result. Other subsystem callers therefore cannot construct a publishable regional result while bypassing the fixed-point boundary checks.

                         signed base correction
                                  |
                 +----------------+----------------+
                 |                |                |
                 v                v                v
          domain gamma       true gamma       possible gamma
          gamma_E(empty)   gamma_E(D_E ∖ F)   gamma_E(T)
                 |                |                |
                 v                v                v
               delta D          delta T          delta U
                 |                |                |
                 +----------------+----------------+
                                  |
                                  v
                 causal coupled-coordinate scheduler
                     delta F = delta(D_E ∖ U)
                                  |
                         consolidate + feedback
                                  |
                                  v
                  settled least fixpoint (T, F)
                                  |
                    U = D_E ∖ F; undefined = U ∖ T

The three gamma roles share one immutable plan and record arena but own separate mutable derivative state. The domain role maintains gamma_E(empty) outside coordinate feedback. The true and possible roles accept signed assumption corrections from the coupled scheduler. Complement is a set zero-crossing operator owned by this component; it is not admitted as an ordinary recursive antijoin because monotonicity holds only for the complete coupled operator. Its derivative evaluates domain and possible changes simultaneously and emits only net falsehood zero crossings; transient update order cannot manufacture a false correction. The gamma session boundary accepts record-native assumption deltas and returns the exact settled output delta. Full-assumption revision is a compatibility wrapper over this boundary. An inactive role consumes an explicit empty step at the common epoch. This keeps tuple encoding and complete-extent construction out of the coupled scheduler; it does not collapse gamma’s internal recursive profile into coordinate authority.

Fresh construction and checkpoint recovery also retain sparse coordinate candidate profiles keyed by stable-revision round. The existing causal recursive distinct scheduler is the sole durable T/F representation; the former parallel coordinate trace and durable T/U pair are absent. T/U is a transient projection of scheduler truth, scheduler falsehood and the retained domain. Stable revision streams each exact desired candidate bucket to the scheduler, which subtracts the corresponding retained bucket and clears any obsolete retained tail before atomic publication. Desired and correction profiles are not materialized in parallel. This establishes the ownership and deletion boundary without making evaluation mutation-local: gamma still computes desired buckets from precision bottom. Direct signed gamma candidate corrections remain required so a mutation can avoid whole-component profile replacement.

The candidate boundary is the input support of each final semantic distinct, not its settled sink delta. Acyclic circuit staging exposes that pre-distinct profile together with the settled profile. Recursive staging exposes the causal scheduler’s consolidated candidate journal together with its settled output. This distinction is observable when one of two witnesses is removed: candidate support changes by -1 while membership does not change. The coupled WFS driver must consume candidate corrections; using only settled gamma deltas can retain a nonleast self-supported fixed point after deletion.

Gamma revision therefore has one typed result containing both semantic candidate-support corrections and settled membership corrections. Component candidate relations are normalized to the gamma output identities; intermediate relations continue to flow only as settled membership. Direct output facts are candidate roots except where the recursive scheduler already owns that relation’s direct impulse. This prevents counting one direct fact through two physical owners.

A single mutable gamma state per role is insufficient for direct coupled maintenance. Driving that state from the outer scheduler’s latest settled correction degenerates into equation iteration around the previous fixpoint. The six-edge Gray-code differential test reaches a two-state oscillation after adding (1, 0): one truth atom and one falsehood atom alternate signed admission forever. Increasing the limit from 32 to 256 does not converge. The candidate boundary is correct; the missing state is gamma support indexed by the WFS iteration whose candidate bucket it produced. A correction to bucket k must re-evaluate the retained gamma profile at k, not the role’s final assumption from the prior transaction.

The required logical time is therefore (program frontier, WFS iteration, gamma recursion iteration). The latter two coordinates are a nested product time within one atomic program transaction. Flattening them into one scalar or cloning an entire gamma circuit per WFS round is not the target architecture: the former obscures ordering and the latter multiplies retained arrangements. Profile operators must become parametric over an ordered iteration time, with ordinary recursion using u32 and coupled gamma using an explicit product time. The coordinate scheduler remains the sole T/F authority.

Product-time distinct cannot reuse a scalar prefix scan. For incomparable times a and b, two candidate witnesses require settled corrections a:+1, b:+1, and join(a,b):-1; otherwise integrating at the join reports membership two instead of one. The implementation computes the finite join-closure of touched times, thresholds causal support at each point, and applies Möbius inversion over the induced partial order. Bilinear joins stamp output with the componentwise lattice join, and recursive delay advances only the gamma-inner coordinate.

Product-time antijoin uses the same join-closure and Möbius kernel. For each touched key, it integrates the left tuple support and right-key support at every causal time, selects the left support exactly where right support is zero, then differentiates that output lattice. An incomparable right blocker therefore does not suppress a left tuple until their join time. Retained left rows are key-indexed; left or right corrections visit only touched keys and never clone the complete retained relation. Negative causal support on either input is an invariant failure carrying the operator and product time.

Coordinate candidate history uses the recursive scheduler’s transaction frontier and stable-revision round. Gamma history uses the common gamma epoch as its outer time and positive-recursive iteration as its inner time. Domain, true and possible roles publish the same final epoch; the domain role advances through explicit empty steps when a revision round does not change its assumption. Upper and lower gamma calls execute in distinct role states, so one physical profile never mixes gamma(T) with gamma(U). This avoids a third timestamp dimension: the enclosing program frontier controls atomic publication while the globally ordered gamma epoch uniquely identifies coordinate work. Gamma revision time is supplied by the coupled driver and each session refuses anything other than its exact successor epoch; role-local clock inference cannot silently desynchronize the coordinate circuit.

The scheduler is a differentiated least-fixed-point circuit, not equation iteration starting at the previously published pair. Each retained coordinate tuple carries causal support rooted in current base, domain and gamma deltas. Removing the last root retracts unsupported coordinate cycles. This history-independent support invariant is required because iterating the stable equations from an old fixed point after an arbitrary signed mutation can settle at a non-least fixed point.

Preparation stages the base correction into all three gamma roles, derives domain and coordinate corrections, and alternates consolidated delta T and delta F feedback until both nested-time frontiers are empty. It then derives true and undefined output deltas, seals every gamma and coordinate journal, and publishes records, inputs, coordinates and frontier atomically. Debug and test builds independently evaluate from precision bottom and require exact (T,U) equality after every mutation.

The role-separated product-time prototype is deliberately not production authority yet. Its acyclic component path supports ordered gamma strata, multiway joins, ordinary and correlated aggregates, and factorized aggregates through the same record-native temporal operator state used by ordinary incremental execution. Positive-recursive components reuse the record-native recursive SCC scheduler, preserve nested candidate and settled profiles, and publish through the same variant-safe component transaction. On the sampled 10,000-state, 100-edge exit mutation with ten samples, the established path measured 6.17 ms p50 and 6.94 MB retained heap. After assigning derivative authority exclusively to nonlinear product profiles, the product-time path measured 17.09 ms p50 and 7.17 MB retained heap. The authority correction removed 20,100 duplicate binding records and reduced the retained-memory ratio from 1.81x to 1.03x. At that revision, stable-equation iteration still materialized complete role outputs. The committed Criterion comparison over 10,000 retained self-loop states and one exit-edge insertion, with 20 samples, measured 4.136 ms for the established gamma circuit and 14.069 ms for the product-time gamma circuit: a 3.40x latency ratio. The later joint-reset result below supersedes this historical measurement. Neither result supports changing the production default. An attempted direct candidate-feedback cut failed the smallest WFS differential by oscillating to the iteration limit. That cut is rejected evidence that candidate corrections require the exact WFS-iteration-indexed support described above; passing memory gates alone does not admit the product path.

Signed candidate retractions cannot be placed mechanically in the bucket where their invalidation is discovered. A retained witness may have entered at a later WFS bucket; placing its retraction earlier makes the causal support prefix negative. Retractions are placed at the earliest bucket at or after discovery whose complete suffix has sufficient support. This placement is computed from the one retained candidate profile and does not introduce a second extent.

Coordinate and gamma candidate roots form one overdelete/rederive transaction. Resetting only the gamma assumptions can leave an obsolete coordinate witness; resetting only coordinates can allow gamma state to reintroduce an unsupported cycle. A signed mutation first overdeletes affected roots in both stores, settles their joint corrections, then rederives from surviving base roots. Role assumptions at publication must equal their coordinate inputs exactly; stable output equality alone is insufficient because it can persist a lagging assumption that fails on the next mutation.

The product prototype computes a tuple-local affected closure by probing both Boolean extremes of newly affected true and false coordinates. It clears only the selected coordinate candidate histories, leaves unrelated histories resident, then rederives Boolean membership inside the closed region. Selected candidate profiles use tuple-keyed trace reads; they do not materialize and filter the complete relation history. Raw proof multiplicity remains internal to gamma operators, and gamma delta calls construct membership changes only for touched records. Equivalent rule decompositions are therefore observationally identical at the outer coordinate boundary. Signed mutation, recursive, mixed-plan, Gray-code and conformance replay gates pass.

On the committed 10,000-state fixed-delta benchmark with 20 samples, this whole-component reset measured 14.341 ms against 4.146 ms for the established circuit, a 3.46x ratio. The prior product path measured 14.069 ms in the same harness. The 1.9% product regression is consistent with clearing all retained coordinate roots for a one-edge mutation. That result validated correctness, not replacement performance, and motivated the affected-region cut below.

After tuple-local closure, touched-output gamma deltas, selected complement projection and direct falsehood reads, the same benchmark measured 8.372 ms against 4.122 ms for the established circuit, a 2.03x ratio. This is a 41.6% reduction from the 14.341 ms whole-component reset and a 66.9% reduction from an intermediate 25.296 ms selective implementation that still materialized complete gamma outputs on every probe. Production selection remained unchanged because the replacement performance gate had not passed.

Positive rederivation expands the affected region without speculative two-extreme probes. A candidate outside the overdeleted region is suppressed when its published Boolean root is still resident; when no root was published, the candidate is admitted because it has no history requiring overdelete. This preserves the single Boolean-root representation and reduced the benchmark to 8.058 ms against 4.023 ms, a 2.00x ratio. The remaining difference is not attributed to complete-output reconstruction or component-wide reset.

The 10,000-state mutation matrix localizes the remaining cost. Empty frontiers measured 0.911 us for product and 0.935 us for established execution. A support- only duplicate input measured 2.178 us and 2.138 us respectively. The semantic one-edge mutation measured 7.930 ms and 3.992 ms. Product overhead is therefore confined to affected WFS maintenance; idle dispatch and non-boundary input maintenance are at parity.

Product gamma role publication uses one complete representation per role. Published assumption, direct and derived extents are the only complete authorities. A revision borrows those extents and records sparse absolute final assignments. Reads consult the assignment first and the published extent otherwise. Prepared publication contains only the assignment journals; after frontier, role, component and record-transaction validation, commit applies them without fallible work. Derived output membership remains a projection of direct > 0 or derived > 0 and is never retained as another extent. Returning a complete stable-revision result may materialize that projection transiently. Dropping either a revision session or prepared publication cannot mutate published state.

flowchart LR
    Input[Signed base and assumption deltas] --> Eval[Product-time rule evaluation]
    Published[Published role extents and operator state] --> Overlay[Read-only published plus sparse overlay]
    Input --> Overlay
    Overlay --> Eval
    Eval --> RoleJournal[Sparse absolute role assignments]
    Eval --> OperatorJournal[Sparse operator journals]
    Eval --> MultiJoinJournal[Sparse multiway-join journals]
    RoleJournal --> Validate[Frontier, role, component, support and record validation]
    OperatorJournal --> Validate
    MultiJoinJournal --> Validate
    Validate --> Commit[Infallible atomic publication]
    Commit --> Published
    Published --> Rebase[Completed-epoch replacement builder]
    OperatorJournal --> Rebase
    MultiJoinJournal --> Rebase
    Rebase -->|every completed epoch, touched identities only| Validate

The sparse role journal removes transaction-local clones of all nine role extents and makes cancellation canonical: a final weight equal to its published weight leaves no assignment. Relation identities are canonicalized before an assumption correction enters either the shadow input or its journal. The full reasoning suite measured 938 passing tests with one skipped, and strict clippy completed without warnings. On the same 10,000-state Criterion workload, the product path measured 8.117 ms in an adjacent run where the established path measured 4.124 ms, a 1.97x ratio. This result does not establish a latency improvement over the earlier 7.930 ms product measurement. Sampling attributes the dominant remaining time to product gamma assumption-delta staging and antijoin evaluation, not journal publication.

Operator publication subsequently adopted the same representation rule. Evaluation already produced a sparse validated operator journal, but finish cloned the complete operator state, applied that journal and compacted every temporal profile before every publication. Ordinary revisions now publish the journal directly. An intermediate implementation prepared a fully compacted replacement every thirty-second operator generation. Sparse completed-epoch closure described below supersedes that global replacement. The record-native antijoin window also projects retained profiles for touched keys directly instead of constructing and then rereading a temporary semantic antijoin state.

On the 10,000-state one-edge insertion, this reduced product time from 8.009 ms to 4.035 ms. The adjacent established measurement was 4.178 ms, making product 1.04x faster for that insertion. Retraction measured 3.705 ms product versus 3.380 ms established, making product 1.10x slower. Thirty-two alternating insertions and retractions, including periodic compactions, measured 131.11 ms product versus 118.36 ms established, a 1.11x total-time ratio. With the thirty-second-generation compaction interval, this became 126.28 ms product versus 117.74 ms established, a 1.07x total-time ratio. The established engine remains the default until broader mutation shapes pass their gates.

The standalone sustained-mutation probe uses the same fixture as the Criterion latency benchmark and reports allocator-retained, peak and percentile data. At 10,000 retained states and 1,024 alternating semantic mutations, established execution measured 3.895 ms p50, 5.386 ms p95, 5.555 ms p99 and 5.856 ms maximum, with 6.35 MB final retained heap and 10.36 MB mutation peak. Product execution measured 3.902 ms p50, 5.305 ms p95, 8.303 ms p99 and 9.053 ms maximum. Product is therefore 1.00x at p50, 0.99x at p95, 1.49x at p99 and 1.55x at the compaction maximum; it remains 1.04x in final retained memory and 1.65x in mutation peak. At this intermediate stage, retained-memory growth was bounded, but prepared compaction still required a complete replacement and remained the tail-latency and transient-memory target.

The same 1,024-mutation probe over 5,000 independent two-node negation cycles alternates an exit edge whose deletion must remove an unsupported coordinate cycle. After candidate-causal overdelete, established execution measured 3.879 ms p50, 4.095 ms p95, 5.316 ms p99 and 5.614 ms maximum. Product execution measured 3.978 ms p50, 5.417 ms p95, 8.281 ms p99 and 9.345 ms maximum. Product is 1.03x at p50, 1.32x at p95, 1.56x at p99 and 1.66x at the compaction maximum. Final retained and mutation peak ratios remain 1.04x and 1.65x. Unsupported-cycle correction is therefore near parity outside prepared compaction, with the same tail target as the self-loop workload.

A Boolean gamma output can remain present while its derivational support changes. This occurs when retracting an exit from a negation cycle: the obsolete coordinate assumption can immediately replace the removed base derivation, so settled gamma membership alone reports no boundary crossing. Overdelete now seeds the affected truth and falsehood regions from both settled membership changes and touched gamma candidates. Candidate touches also grow the region during the overdelete closure, allowing a support change hidden behind an alternative derivation to reach the coordinate cycle that depends on it. Candidate multiplicity remains internal to gamma; the coordinate scheduler still publishes one Boolean root. This removes unsupported cycles without introducing a second coordinate representation. An exhaustive differential over all 512 directed graphs on three nodes and every one of their nine possible single-edge toggles agrees with fresh well-founded evaluation.

Checkpoint payloads remain physical-engine independent. Restore now accepts an explicit well-founded engine internally, reconstructs Product role and coordinate state from semantic inputs and the (truth, possible) pair, and requires that reconstruction to reproduce the checkpoint pair. A dedicated restore-then-retract differential and forty alternating exit mutations spanning repeated completed-epoch closures agree with fresh evaluation. Checkpoint encoding, input-authority validation and gamma reconstruction are owned by the well-founded checkpoint module. Mutation preparation remains in the component orchestrator. Both modules extend the same component state; the split introduces neither a checkpoint-side state copy nor an engine-specific semantic representation. Published membership queries, tuple projection and retained-state metrics are owned by a read-model module over that same component state. Observation cannot prepare, validate or commit a revision; mutation orchestration does not encode the public true/undefined projection. Prepared component variants, validation, sealing, transaction metrics and the infallible validated commit are owned by a component-transaction module. Evaluation produces that typed journal but cannot publish it directly; checkpoint and read-model code cannot construct a validated commit. All three modules borrow the same component state and introduce no parallel authority. Product and Established well-founded evaluation are separate engine drivers. Each driver owns its revision sessions and returns a validated evaluation artifact containing the candidate semantic pair, prepared gamma state and revision count. Engine dispatch, oracle comparison, output projection and component-transaction assembly remain engine-neutral orchestration. Semantic tuple, record-identity and encoded-catalog translation is isolated in a stateless record-codec module shared by oracle auditing and checkpoint recovery. The codec cannot inspect or mutate component, scheduler, engine or frontier state; callers remain responsible for transactional record interning. External support staging returns one of three typed input revisions: unchanged, support-only or membership-changing. Only the membership-changing variant carries a semantic gamma correction. Input staging validates tuple shape, support non-negativity and record identity but cannot evaluate an engine or publish assignments; the coordinator selects those later phases explicitly. Fresh Established oracle preparation is isolated from both engine drivers. It owns complete-base materialization, encoded-catalog evaluation and conversion back to record coordinates, then returns an immutable base/pair observation. Audit comparison and refusal remain in engine-neutral orchestration before transaction sealing. Checked record-extent algebra and public output projection are stateless modules. Product’s sparse coordinate deltas and Established’s complete pair difference converge on the same true/undefined sink representation. In audit builds, sparse Product projection must equal complete pair differencing before the coordinator can construct a component transaction. The materialized-model relation-name view is the ordered union of base relation names and circuit-produced relation names. It is derived from the authoritative base store and component state, not a separately updated registry. Consequently a head that is empty at construction and first becomes nonempty after a mutation is visible to columnar projection immediately. The 63-frontier Gray-code mutation differential now explicitly constructs the Product engine. A mixed acyclic/positive-recursive, two-relation component also produces the same terminal model when four signed changes are submitted in one batch or in three different sequential orders. The full reasoning suite measured 974 passing tests with one skipped, and strict clippy completed without warnings.

MaintenanceMode::PreferCircuits and MaintenanceMode::RequireCircuits select the qualified Product engine for admitted well-founded components. MaintenanceMode::Established remains the process default and disables circuits. MaintenanceMode::PreferEstablishedWfsCircuits and MaintenanceMode::RequireEstablishedWfsCircuits retain all admitted incremental operators while selecting Established well-founded evaluation as an operational escape hatch. PreferUnauditedProductCircuits and RequireUnauditedProductCircuits remain explicit route-pinning names for qualification code. Mutation and restore-then-mutate differentials exercise both engines through the query engine. Runtime circuit selection requires OXC_ENABLE_INCREMENTAL_CIRCUITS; OXC_USE_ESTABLISHED_WFS_INCREMENTAL_CIRCUITS then selects the Established well-founded escape hatch. Construction and checkpoint recovery resolve the same maintenance policy. OXC_ENABLE_AUDITED_PRODUCT_INCREMENTAL_CIRCUITS selects MaintenanceMode::PreferAuditedProductCircuits when ordinary incremental circuits are enabled. Audited Product computes the complete candidate pair and compares it with fresh Established WFS evaluation before publication on every effective mutation. A mismatch fails the transaction before circuit, record, frontier or checkpoint state changes. Audit choice is not persisted, so the same checkpoint restores under Product, audited Product or Established policy. OXC_REQUIRE_UNAUDITED_PRODUCT_INCREMENTAL_CIRCUITS and OXC_REQUIRE_AUDITED_PRODUCT_INCREMENTAL_CIRCUITS each imply circuit and their named Product policy, and convert any admission fallback into an error. Product integration and canary gates use these fail-closed selectors so Established fallback cannot produce a false pass. When both Product enable flags or both Product require flags are set, the audited policy takes precedence. For non-required modes, the Established escape-hatch flag overrides both Product enable flags. Product-specific require flags override the escape hatch, so a fail-closed qualification job cannot silently test Established instead.

flowchart TD
    F[Runtime maintenance flags] --> R{Circuits required?}
    R -->|Product-specific require| RP[Required Product policy]
    R -->|ordinary require plus escape| RE[Required circuits with Established WFS]
    R -->|ordinary require| RU[Required circuits with Product WFS]
    R -->|no| I{Circuits enabled?}
    I -->|no| D[Established whole-program maintainer]
    I -->|yes, escape| PE[Preferred circuits with Established WFS]
    I -->|yes, audited| PA[Preferred circuits with audited Product WFS]
    I -->|yes| PU[Preferred circuits with Product WFS]
    RE --> C[Shared circuit compiler and non-WFS operators]
    RU --> C
    PE --> C
    PA --> C
    PU --> C

Runtime discovery first captures these values in a named flag record, then converts that record to one maintenance mode. No positional Boolean API exists, so adding or reordering a selector cannot exchange audited and unaudited policy without a field-name change. Production maintainer coverage verifies that raw, audited and Established policies survive full rebuild; raw and audited policies also restore from the same engine-neutral checkpoint and retain their selected engine across subsequent mutations. The internal engine discriminator uses the same explicit names: Established, UnauditedProduct, and AuditedProduct. A bare Product engine state cannot conceal whether an independent publication oracle is active. Live engine state represents audit policy inside the Product rule-state variant; Established state cannot carry Product audit policy, and no parallel boolean can drift from the selected physical engine. The default-routing benchmark uses one exit assertion against 10,000 retained well-founded rows and 10 measured mutations. Product p50 is 49,583 nanoseconds, versus 4,214,542 nanoseconds for the Established-WFS circuit and 37,685,791 nanoseconds for the whole-program Established maintainer: 85.00 and 760.05 times faster, respectively. Product retained heap is 4,581,046 bytes versus 5,046,722 for the Established-WFS circuit. Its 22,847,444-byte construction peak remains 2.31 times the whole-program Established peak; construction cost remains a separate optimization target. A scoped divergence injection fires after Product preparation and before validation. The audited mutation is refused while the program frontier, record arena metrics, derived output and checkpoint bytes remain identical to their pre-mutation values, directly gating the rollback boundary.

Defeasibility elaborated into ordinary strict support, blocking and public-head rules requires no defeasibility-specific incremental operator. Its positive and negative dependencies enter the same component planner, stratifier and well-founded circuit as handwritten strict rules. Artifacts that retain executable defeasibility metadata remain outside Product admission and use the established compatibility path. Model maintenance does not imply explanation or proof-tag parity; provenance remains a separate publication contract. A well-founded SCC may contain outputs that participate only in positive recursion while another output is read under in-SCC negation. Gamma assumptions are supplied only for relations whose rewritten rules actually read an $assumed:: shadow. Gamma construction records that output-to-shadow map once and refuses a rewritten read without a corresponding physical input. Product therefore ignores assumption corrections for a positive-only SCC output instead of requiring a nonexistent shadow input. A direct insert/retract/reinsert differential and the compiled-defeasibility runtime suite pin this mixed SCC shape against fresh Established evaluation. The runtime proof-tag reader consumes only the inert compilation carrier. It computes a strict-only least fixed point by excluding default-support producers: surviving tuples in that fixed point are ; other surviving compiled defeat conclusions are +∂. This preserves partial provenance through strict recursion instead of treating a populated strict support relation as a definite proof. For an absent public tuple, a populated default-support relation identifies defeated-default cause C6 and −∂. Evaluation and incremental maintenance never consult this metadata, so explanation cannot alter circuit admission, state or publication. An end-to-end compiled-artifact differential exercises a recursive defeat target whose attacker reads that target. Audited Product remains on the recursive circuit while a Gray-cycle history visits all four eligibility/exclusion base states, including exclusion without eligibility, in both insertion and retraction directions. The public coordinate traverses unblocked, undefined, reinstated and removed states. Each effective WFS mutation is compared with Established evaluation before publication, and a fresh-store assertion separately gates the initially-empty public projection. The artifact carries both emitted events and non-executable defeat-compilation provenance through binary encoding and runtime loading; the test rejects either legacy executable defeat metadata or missing compiled provenance. A separate three-subject mutation matrix retains simultaneous true, undefined and absent states. Mutating one subject preserves the other subjects’ public coordinates while every effective mutation remains audited against Established evaluation. A second compiled-artifact scenario covers an acyclic exception-to-exception: a default attacker blocks a default target, a strict rule defeats that attacker, and retraction reinstates the attacker. The complete mutation cycle stays on the acyclic circuit and pins its two-valued semantics separately from recursive well-founded undefinedness. The corresponding emitted-rule benchmark retains 10,000 eligible subjects and alternates one exclusion for 128 mutations. Established measured 33.470 ms p50; Product measured 0.296 ms p50, a 113.0-fold improvement. Product construction peak was 72.26 MB versus 54.81 MB, 1.32 times Established. Final retained heap was 29.09 MB versus 20.51 MB, 1.42 times Established. Product mutation peak was 29.22 MB versus 30.23 MB Established. The result isolates steady mutation benefit from the explicit cost of retaining derivative state. A post-modularization rerun with the same 10,000-subject, 128-mutation workload measured 0.284 ms Product versus 34.333 ms Established at p50, a 121.0-fold improvement. Product construction peak remained 72.26 MB, final retained heap 29.09 MB and mutation peak 29.22 MB. The transaction, engine-driver, input, oracle, codec and projection boundaries therefore introduced no measured hot- path or retained-state regression. Benchmark-only logical-state telemetry attributes the retained difference. Both engines retain 70,000 profiled rows, 30,000 temporal-trace entries, 20,000 coordinate candidates and the same 10,000 value records. Product additionally retains 40,000 gamma-assumption rows and 60,000 gamma-derived rows; direct and output rows are zero. These 100,000 record-keyed Boolean summaries account for the 8.58 MB heap difference. They are required incremental read state. The initial published representation carried general signed Z-set weights despite a validated set invariant, identifying a representation mismatch. Published Product gamma assumption, direct and derived summaries now use relation-indexed record sets. Signed weights exist only in the prepared journal; validation restricts terminal assignments to zero or one before infallible set publication. Domain publication remains a distinct weighted-extent patch and does not share the summary journal. On the same compiled-defeat workload, Product final retained heap fell from 29.09 MB to 27.62 MB, reducing the ratio to Established from 1.42 to 1.35. Product p50 fell from 0.280 ms to 0.212 ms; the stronger published representation therefore reduced both memory and lookup cost without changing logical row counts. The same compiled-artifact scenario checkpoints the undefined state, restores the Product circuit from the durable semantic payload, then removes and reasserts the exclusion. The restored route remains recursive and both post-restore frontiers pass the Established audit. Recovery comparison treats an empty relation entry and its canonical absence as the same semantic extent; it still compares every populated relation plus the shared translation, world-assumption and metric-resolution frame exactly. A two-module differential assembles the artifact through workspace symbol collection and the combined defeat-plane pass. The attacker imports and reads a target in another module; qualified eligibility and exclusion mutations still traverse true, undefined and reinstated target states on audited Product. This gates qualified internal relation identity across elaboration, planning, inter-component flow and runtime publication without a cross-module circuit special case.

The end-to-end wfs_recursion conformance ontology passes through the compiled CLI with both runtime flags enabled: its two language-level assertions pass and its twelve-mutation scenario produces exactly the two declared winning positions. The generated reasoning conformance replay also passes every supported vector and retract/reassert mutation against the established engine.

Completed-epoch rebase now publishes profile state and multiway-join state as one validated transaction. Previously, periodic profile compaction left multiway-join temporal tries unbounded. Rebase integrates every terminal’s closed history into at most one cell at the next epoch frontier, preserves the semantic or interned trie representation, and refuses overflow or negative support before publication. This operation is intentionally not exposed as generic product-lattice compaction: inner coordinates from an earlier outer epoch are incomparable with the next outer epoch until the earlier epoch is closed. A product-time multiway-join test verifies that three retained cells collapse to one without changing integrated support.

The completed-epoch builder traverses prepared trace levels directly and merges sparse absolute assignments with immutable published profiles. It does not clone a complete RecordWfsGammaCircuitState. A differential covering operators, joins, antijoins, distinct, count and factorized aggregate state, including cancellation and untouched keys, matches the former clone-then-compact implementation.

After this change, the 10,000-state, 1,024-mutation self-loop probe measured 3.823 ms established versus 3.880 ms product at p50, 4.009 ms versus 4.128 ms at p95, 4.142 ms versus 6.792 ms at p99, and 4.496 ms versus 6.935 ms maximum. Final retained heap remained 6.35 MB established versus 6.59 MB product. Mutation peak fell from 17.09 MB before the direct builder to 12.46 MB, or 1.20x the 10.36 MB established peak instead of 1.65x. The two-cycle workload measured 3.774 ms versus 3.921 ms at p50, 4.046 ms versus 4.163 ms at p95, 4.127 ms versus 6.908 ms at p99 and 4.373 ms versus 7.105 ms maximum, with the same 1.20x peak-memory ratio. These measurements preceded sparse completed-epoch closure.

The next compaction form is sparse completed-epoch closure. For a logical identity x changed in a closed outer epoch e, let H_x contain its retained cells and committed correction. Closure replaces H_x with ((e + 1, 0), sum(H_x)), deleting the identity when the sum is zero. The completed-epoch contract excludes later input at an outer coordinate less than or equal to e; therefore (e + 1, 0) is less than or equal to every future evaluation time and all future reads observe the same integrated weight. An identity untouched in epoch e was last closed at an earlier (k + 1, 0), which is also less than or equal to every future time, so it does not require restamping. Bilinear operators have emitted the closed epoch’s corrections before their retained arrangements are rewritten.

Closure granularity is the retained logical identity: operator row, arranged key and tuple, antijoin support key, distinct row, aggregate group, or multiway-join atom key. The current aggregate journal closes the complete touched group; subprofile-granular aggregate journals remain a scaling refinement. Preparation visits only identities named by the sparse journal, performs checked summation and emits absolute deletions plus at most one frontier assignment. Published state remains the sole complete authority; commit remains an infallible journal application. This changes periodic O(S + H) time and O(S) transient space into sparse journal work. Each immutable trace batch is a packed ordered page tree with 256-entry leaves and fanout-32 branches. Closure seek-scans selected keys, path-copies only intersecting leaves and their ancestors, and shares disjoint subtrees by identity. A 100,000-key test closing one identity copies at most 512 entries and 16 nodes while sharing more than 300 unrelated nodes. Keys spanning leaves and multi-value keys retain ordered semantics. This is not a lazy global frontier: no hidden normalization metadata enters reads, checkpoints or timestamp comparison.

The implementation applies this closure to operator rows, join-side key/tuple identities, antijoin support keys, distinct rows, count and factorized-aggregate groups, and semantic or interned multiway-join atom keys. Ordered multiway-join rounds close against their evolving prepared overlay, so repeated changes to one key retain correct support and live-key metadata without materializing a complete trie. The production transaction now has one representation: sparse profile journals plus sparse multiway-join journals. The complete rebase exists only as a differential test oracle. Every Product stage must return exactly one journal for every admitted multiway-join node. Validation rejects missing, extra, wrong-arity or state-incompatible journals before profile preparation, and repeats the same exact-set check when sealing the transaction. This invariant is active in raw Product builds; it does not depend on the test-only semantic shadow. Fault injection removes one journal and proves rejection before publication followed by a successful identical retry. The retained admission evidence also owns the exact physical node set for binary joins, scalar and correlated aggregates, and factorized aggregates. These kernels always produce a journal, including an empty correction, so raw Product rejects a missing or extra family journal before preparing retained state. Fault injection covers each family and a valid identical retry. Antijoin, semijoin and distinct journals remain sparse by contract: they are absent when neither input contributes a correction, and every present journal is still validated by retained-state preparation. Their backend contract returns one typed result containing output plus an explicit optional retained correction. Shared evaluation owns journal-map insertion for semantic, record-backed ordinary and Product modes. Missing map entries are therefore a represented no-correction outcome rather than an implicit backend side effect. Binary join uses the corresponding mandatory-journal result: every backend returns output and its complete bilateral arrangement correction as one value, and shared evaluation inserts it exactly once. Product’s admitted-node check is therefore a defensive validation of a structurally total kernel result rather than the mechanism that couples output to retained state. Scalar, correlated-count and factorized aggregate kernels use the same mandatory-journal result. Output and the complete touched-group correction cannot be returned independently, and shared evaluation is the only journal map writer. All total profile-state families therefore have one ownership model across ordinary and Product logical time.

With sparse closure, the 10,000-state, 1,024-mutation self-loop probe measured 3.724 ms established versus 3.373 ms product at p50, 4.684 ms versus 4.657 ms at p95, 5.200 ms versus 4.798 ms at p99, and 5.379 ms versus 4.967 ms maximum. Product final retained heap was 6.59 MB versus 6.35 MB established, while mutation peak was 10.36 MB for both engines: 1.04x retained and approximately 1.00x peak. The two-cycle workload measured 3.829 ms versus 3.834 ms at p50, 4.037 ms versus 3.968 ms at p95, 4.122 ms versus 4.033 ms at p99, and 4.970 ms versus 4.420 ms maximum. Its Product mutation peak was 10.84 MB versus 10.37 MB, a 1.05x ratio. The periodic tail and its 1.65x peak-memory regression are absent in these sustained runs.

Fixed-delta scaling is not yet achieved. With 128 mutations of one self-loop, 1,000 retained states measured 0.402 ms established and 0.332 ms product at p50, while 50,000 retained states measured 21.17 ms and 22.27 ms. The Product path therefore remains approximately linear in retained state despite sparse operator publication. Inspection identifies complete coordinate/domain work: ordinary mutation projects the full truth/possible pair and falsehood, duplicates complete gamma outputs and assumptions, constructs a complete next pair, performs complete-state equality checks, and differences complete old and new outputs.

The delta-native coordinate design keeps published domain plus scheduler truth and falsehood as the sole semantic authorities. A mutation returns sparse domain, truth and falsehood corrections. Truth output is the truth correction; undefined output is computed only over records touched by any of the three corrections using D and not T and not F before and after the overlaid update. Gamma assumptions and outputs use read overlays for affected records. Complete pair projection remains available for checkpoints and explicit oracle validation, but is not part of ordinary Product publication. Production-local validation checks set weights, disjoint truth/falsehood, coordinate containment in domain, and gamma agreement over touched and affected records.

Affected truth and falsehood selections are owned by one typed region value. Discovery may grow either coordinate explicitly; final publication consumes the mutable region and produces a certified region only after every sparse truth and falsehood correction is contained in its corresponding selection. Subsequent gamma/coordinate agreement checks accept only that certified value. This makes coordinate swapping and unchecked exterior publication structurally difficult, and mirrors Lean’s paired PositiveRegion. The certificate proves sparse-write containment; it does not by itself prove prior/new global exterior agreement or that the runtime worklist computes the regional least fixed point. Audited Product additionally derives prior and Established-target falsehood from their domains and possible coordinates, then rejects any truth or falsehood change outside the certified region before the existing complete-pair equality gate. This directly exercises the first premise of the Lean affected-region theorem during canary execution without adding a complete scan to ordinary Product.

The first delta-native publication slice is complete. Product domain state now publishes checked absolute assignments while Established uses an explicit replacement variant. Initial Product construction and checkpoint reconstruction publish their complete domain as the same checked assignment form. Non-owning extent overlays provide checked point membership and selected-record projection for domain and coordinate reads. Scheduler truth and falsehood point APIs read the retained coordinate authority directly. Ordinary Product sink projection uses sparse simultaneous domain/truth/falsehood corrections and is differentially checked against complete pair differencing. The complete pair is still materialized inside the differentiated driver at this intermediate stage. The reasoning suite measured 980 passing tests with one skipped after these changes.

Coordinate revision now retains four transaction-local Boolean views as sparse absolute assignments over scheduler truth, scheduler falsehood and derived possible reads. Fixed-point rounds validate a complete incoming correction before applying only its touched assignments; they do not clone assignments accumulated by earlier rounds. Sparse set algebra and validation are isolated from driver orchestration. Production agreement checks visit only the affected truth or falsehood region.

On the 128-mutation fixed-delta probe, 1,000 retained self-loops measured 0.398 ms Established versus 0.285 ms Product at p50. At 50,000 retained self-loops they measured 20.65 ms versus 20.76 ms. Product mutation peak was 47.89 MB versus 50.62 MB Established at 50,000 states; final retained heap was 31.95 MB versus 30.74 MB. The local overlay work therefore does not add a retained-state scan, but the approximately 73-fold Product latency increase for a 50-fold state increase confirms that complete pair projection and final pair materialization still dominate fixed-delta scaling.

The completed delta-native cut removes those traversals. Ordinary Product revision reads scheduler truth and falsehood by record, derives possible as domain - falsehood, retains only affected-region rederivation, validates the prepared scheduler’s sparse truth and falsehood corrections, and projects sink changes from the simultaneous sparse domain/truth/falsehood transition. The prepared component no longer retains an unused pair. Complete pair projection remains only for initial construction, checkpoint observation and explicit debug oracles. Tests assert that an ordinary Product mutation does not create one.

Domain revision follows the same rule. Product gamma returns a sparse settled domain correction; the driver reads it through a checked published-plus-delta view, and publication validates and commits sparse absolute assignments. A complete gamma base is built only for Established execution, initial Product construction or an enabled debug oracle. The Boolean read authority, sparse coordinate algebra and driver orchestration are separate modules.

On the same 128-mutation probe after this cut, 1,000 retained self-loops measured 0.401 ms Established versus 0.052 ms Product at p50. At 50,000 retained self-loops they measured 19.30 ms versus 0.117 ms. Product therefore improved 7.7 times and 164 times respectively. Its p50 grew 2.26 times across a 50-fold retained-state increase, rather than approximately linearly. At 50,000 states, Product mutation peak was 33.07 MB versus 50.62 MB Established; final retained heap remained 31.95 MB versus 30.74 MB. A 10,000-node two-cycle correction measured 0.089 ms Product versus 3.628 ms Established, a 41.0-fold improvement. The complete reasoning library reports 932 passing tests with one skipped, and strict all-target Clippy reports no warnings. The end-to-end conformance corpus passes with both incremental-circuit and Product-circuit runtime flags enabled.

A dedicated aggregate probe validates every insert/retract cycle against fresh Established evaluation before timing one changed value in one retained hot group. An unprojected cardinality control with 10,000 values and 128 alternating mutations measured 0.397 ms Product versus 2.929 ms Established at p50; factorized Count measured 0.401 ms versus 3.104 ms. Product retained memory was at parity and mutation peak was 0.62 times Established for both shapes. The unprojected Product control measured 0.189 ms at 1,000 values and 1.222 ms at 50,000 values, while Established measured 0.305 ms and 16.64 ms.

The actual hot-subprofile workload uses projected Sum, forcing the retained group to contain one value profile per input. At 1,000 values, Product measured 11.24 ms versus 5.85 ms Established; at 10,000 values it measured 113.22 ms versus 61.53 ms. Product is 1.84 times slower and scales linearly with group width. Count publication now carries and closes only correction-named input, value and outer subprofiles, but the 50,000-value unprojected control remained 1.217 ms after that change versus 1.222 ms before it. Publication is therefore not the dominant term. Subprofile evaluation and an incrementally maintained aggregate summary are required to remove whole-group work.

After replacing projected Sum value histories with temporal projected-count and exact-sum profiles, the 10,000-value, 16-mutation workload measured 0.350 ms Product versus 2.945 ms Established at p50: Product is 8.42 times faster rather than 1.84 times slower. Product mutation peak was 3.15 MB versus 5.05 MB, while final retained heap was 2.95 MB for both implementations. Product construction peak remained higher at 22.13 MB versus 18.30 MB. The mutation result establishes constant-width scalar-fold work for Sum; it does not cover the pending indexed CountDistinct, Min or Max representation.

Dedicated nonlinear controls use the same 10,000-value, 16-mutation hot group and verify every insert/retract cycle against fresh evaluation. CountDistinct measured 16.03 ms Product versus 12.65 ms Established at p50. Min measured 112.64 ms versus 62.60 ms, and Max measured 113.24 ms versus 59.75 ms. Product is respectively 1.27, 1.80 and 1.90 times slower. These controls pin the remaining whole-value-map scan and provide the acceptance baseline for the ordered indexes below.

After adding a typed CountDistinct fold with authoritative support leaves and a derived temporal presence-count profile, the same CountDistinct workload measured 0.341 ms Product versus 2.787 ms Established at p50: Product is 8.18 times faster. Product mutation peak was 6.17 MB versus 8.07 MB and final retained heap remained at parity, 5.86 MB versus 5.87 MB. The record-backed path resolves only correction-named leaves; resolving the complete retained value map would leave mutation time linear despite the scalar root summary.

Aggregate state is specialized by algebra rather than forcing every aggregate through a complete value multiset scan. Count retains only its temporal row cardinality. Sum and Avg retain temporal projected cardinality and exact sum; these are homomorphic folds over signed input corrections. CountDistinct keeps one support trace per value in an ordered map and places the nonlinear support > 0 boundary at the leaf before summing leaf-presence profiles into a temporal root count. Min and Max are not invertible group folds: they keep ordered value-support leaves and use a persistent augmented tree to derive the temporal winner after retraction. Equal numeric values remain separate semantic value leaves, preserving value identity.

flowchart LR
    Delta[Touched aggregate corrections] --> Kind{Aggregate algebra}
    Kind -->|Count| Cardinality[Temporal cardinality]
    Kind -->|Sum or Avg| Linear[Temporal projected count and exact sum]
    Kind -->|CountDistinct| DistinctLeaf[Value support leaf]
    Kind -->|Min or Max| ExtremaLeaf[Ordered numeric support leaf]
    DistinctLeaf --> DistinctRoot[Temporal presence-count root]
    ExtremaLeaf --> ValueTree[Path-copied augmented value tree]
    DistinctLeaf --> DistinctPatch[Validated touched-value assignments]
    ExtremaLeaf --> ExtremaPatch[Correction-named tree paths]
    Cardinality --> Result[Root result profile]
    Linear --> Result
    DistinctRoot --> Result
    ValueTree --> Result
    Result --> Mobius[Canonical old/new Möbius correction]
    Mobius --> Output[Aggregate output delta]
    Delta --> Journal[Sparse absolute subprofile or root journal]
    DistinctPatch --> Journal
    ExtremaPatch --> Journal
    Journal --> Validate[Complete validation before publication]
    Validate --> Commit[Infallible atomic commit]

Support leaves or scalar fold profiles are the semantic authority. Internal tree metadata is a deterministic function of children and is neither journaled nor serialized. Checkpoints encode scalar profiles or canonical ordered value support leaves and rebuild metadata. CountDistinct computes a presence-profile correction from only the old and new touched leaf and applies it to the derived root count. A touched extrema leaf path-copies only its search path and recomputes ancestors using the existing lattice join-closure and Möbius operators. Count/Sum/Avg target work proportional to correction cells plus affected output outers; CountDistinct/Min/Max add logarithmic value-index work. Enumerating every affected outer remains necessary because each is an observable output.

The indexed 10,000-value, 16-mutation controls measured 0.403 ms Product versus 3.115 ms Established for Min and 0.393 ms versus 3.027 ms for Max at p50. Product is respectively 7.73 and 7.71 times faster. Final retained heap was 9.96 MB for both implementations. Product mutation peak was 10.27 MB versus 12.17 MB for Established. Product construction peak remained higher at 26.77 MB versus 22.95 MB. Common integral extrema keys use an inline i64; non-integral and out-of-range keys retain exact rational ordering. Tree nodes share keys and unchanged subtrees through reference-counted immutable paths. This removes the previous retained-state premium without weakening exact numeric semantics.

A 2026-07-23 current-head acyclic projected-Sum control retained 10,000 values, retracted 100 values per mutation, and sampled 100 target operations. Circuit p50 was 1.076 ms versus 283.431 ms Established, a 263.5-fold speedup. Circuit retained heap was 1.389 MB versus 1.601 MB, and mutation peak was 4.593 MB versus 5.806 MB. Circuit construction peak remained higher at 10.346 MB versus 5.002 MB. The result confirms the optimized aggregate mutation path after integration with the current base; the 2.07-fold construction peak remains a routing gate. A phase-attributed rerun localized the 10.347 MB peak to initial record-join preparation. Heap remained near 9.0 MB through profile-state commit and fell to 4.205 MB when the prepared overlay released, before reaching 1.391 MB retained. The remaining construction premium is transient ownership overlap at the record-join/profile publication boundary rather than a second retained aggregate authority. Initial publication, record interning, checkpoint restoration and complete epoch rebasing build a balanced tree directly from canonical key order. Point mutations alone use path-copy insertion. Bulk construction removes transient persistent paths but does not remove the measured construction-peak premium; allocation-phase attribution remains required for that peak.

Production-gate rerun and remaining factorized fold

A 2026-07-21 optimized rerun used 10,000 retained values or states and 128 alternating mutations. Every benchmark asserts the selected circuit route and validates each mutation cycle against fresh established evaluation.

WorkloadProduct/circuit p50Established p50SpeedupProduct retained / establishedProduct mutation peak / established
WFS independent self-loops0.068 ms3.749 ms54.8x1.04x0.66x
WFS independent two-cycles0.099 ms3.937 ms40.0x1.04x0.66x
Projected Sum0.391 ms2.794 ms7.15x1.00x0.62x
Projected CountDistinct0.394 ms2.774 ms7.05x1.00x0.76x
Projected Min0.434 ms2.807 ms6.47x1.00x0.84x
Projected Max0.443 ms2.787 ms6.29x1.00x0.84x
Factorized projected Sum0.406 ms2.755 ms6.79x0.99x0.62x
Factorized projected CountDistinct0.400 ms2.776 ms6.94x1.00x0.76x
Factorized projected Min0.553 ms3.496 ms6.32x1.00x0.85x
Factorized projected Max0.477 ms3.439 ms7.21x1.00x0.85x

After separating count change derivation, fold maintenance and output differentiation, the same Projected Sum shape measured 0.396 ms Product versus 2.833 ms Established at p50, preserving the 7.15x speedup. Product retained heap was 0.998x, mutation peak was 0.407x and construction peak was 0.945x Established. The module boundaries therefore introduced no measured latency or memory regression.

An ordinary recursive upper-stratum retraction of 100 facts measured 0.504 ms for the circuit and 10.434 ms for established maintenance, a 20.7x speedup. Its retained heap ratio was 1.35x; construction peak was 3.64x and mutation peak was 0.95x. The WFS Product controls had a 1.02x construction-peak ratio.

The initial factorized result identified a complete-value-map scan. Live tagged factor projection state now contains only None, Linear, Distinct, and Extrema. The checkpoint DTO retains generic legacy leaves solely for decoding and canonical reconstruction. Factorized Sum and Avg retain temporal projected count plus exact sum and the factor product consumes that scalar result. A 10,000-value rerun reduced Product p50 from 59.072 ms to 0.406 ms while established maintenance measured 2.755 ms. Product retained heap was 0.99x, mutation peak was 0.62x, and construction peak was 1.21x established. Nonlinear factor projections retain authoritative leaves; they do not coexist with a scalar fold.

Factorized CountDistinct reuses the ordinary temporal presence fold. Each touched value computes one zero-crossing correction against retained support; the factor output reads the resulting scalar presence profile and does not scan retained values. Checkpoints encode the authoritative support leaves and rebuild the presence profile from the immutable aggregate kind. Record evaluation resolves only values named by the correction. At 10,000 values and 128 alternating mutations, the first indexed implementation measured 1.802 ms p50 against 3.061 ms established, a 1.70x speedup. Atomic publication still cloned the retained record-key map before applying one leaf patch. Publication now prepares absolute assignments only for correction-named leaves, validates them without mutating published state, and installs the sparse patch during the infallible commit phase. Epoch closure rebases only those assignments. The same workload then measured 0.400 ms Product against 2.776 ms established, a 6.94x speedup. Product retained memory was 0.997 times established, mutation peak was 0.764 times established, and construction peak was 1.204 times established.

Factorized Min and Max reuse the ordinary persistent temporal extrema index. Canonical projections contain a path-copied augmented tree; signed WFS coordinates retain an explicit delta until they combine with canonical state. The record-backed evaluator interns only correction-named values and resolves only temporal winners for output. It does not convert the retained record-key index to a semantic value map. Prepared epoch assignments carry the extrema keys named by the correction, so frontier closure rebases only those paths. Tree nodes retain a derived subtree cell count, allowing transaction metrics to observe retained size without enumerating support leaves. The 10,000-value, 128-mutation controls above validate every insertion and retraction against fresh established evaluation. Product retained memory was 0.998 times established, mutation peak was 0.846 times established, and construction peak was 1.149 times established for both orientations.

Checkpoint payload version 14 introduces the tagged factor-projection field. Versions 12 and 13 retain the factor value map as a frozen decode-only field. Restoration uses immutable plan metadata to migrate Sum and Avg maps to Linear, CountDistinct maps to Distinct, Min and Max maps to Extrema, and non-value factors to None before validation or publication. Version 14 always emits the frozen field empty. Projection variants are mutually exclusive by construction in semantic state, record state, epoch rebasing, and checkpoint restoration.

Checkpoint envelope version 15 introduced source-bound physical-plan identity. Version 16 additionally commits each operator’s local identity stratum separately from its global scheduling stratum. The circuit operator schema remains version 13, so module, relation and node identities remain stable. Restore accepts envelope versions 12 through 14 by recomputing their canonical version-2 plan identity and version 15 by recomputing its source-bound version-3 identity. It validates the unchanged operator graph and state, then re-emits version 16. This preserves existing checkpoints, prevents physical components from detaching from their exact lowered rules, and makes schedule rebasing explicit rather than an implicit identity exception.

Removing the generic live leaf variant did not regress the indexed paths. A post-change 10,000-value, 128-mutation run measured Product versus Established p50 at 0.412 versus 2.838 ms for factorized Sum, 0.421 versus 2.872 ms for CountDistinct, 0.444 versus 2.927 ms for Min, and 0.440 versus 2.871 ms for Max. Product final retained-memory ratios were 0.995, 0.997, 0.998 and 0.998; mutation-peak ratios were 0.623, 0.764, 0.846 and 0.846. Construction-peak ratios remained 1.210, 1.204, 1.149 and 1.149 respectively.

Phase-attributed heap probes locate the premium in initial seed application. At 10,000 values, seed application peaked at 25.09 MB Product versus 21.25 MB Established; plan construction used 21.7 KB in both paths and instance construction used 37.0 KB versus 41.9 KB. At 1,000 values the seed-application gap was 0.383 MB, confirming linear transient cost of about 383 bytes per seeded value. Retained state remained at parity. The remaining source is inside seed evaluation or publication rather than plan or instance construction.

Fine-grained preparation boundaries narrow this further. Before record-native extrema evaluation begins, the 10,000-value Product path holds 19.94 MB versus 16.10 MB Established: the complete 3.83 MB premium is already present. Extrema change validation then adds approximately 4.65 MB in both paths. The remaining construction premium is therefore upstream product-time scan, join or recursive preparation state, not the extrema index or its semantic-to-record conversion. The aggregate benchmark exposes these boundaries through OXC_AGGREGATE_PREPARATION_PHASES=1.

Embedded Product corrections now intern impulse tuples at classification and carry RecordId profiles through recursive preparation; direct semantic and interned assignment deltas share one validation kernel. This removes a second semantic tuple authority but leaves the 10,000-value construction peak unchanged at 27.04 MB. Raw input-tuple duplication is therefore not the peak source. Derived binding, join or recursive preparation profiles remain the linear transient candidates.

Initial Product observation previously cloned the complete settled domain only to compute possible = domain - falsehood, while retaining the same domain delta for publication. It now projects that difference through a checked published-plus-delta view and constructs only the required possible extent. The 10,000-value factorized-Min construction peak remained 29.37 MB versus 25.55 MB Established. The clone was a duplicate authority but was allocated after the earlier preparation peak; removing it narrows the remaining source without changing the measured maximum.

Product revision construction also cloned the immutable base batch into the domain, possible and truth role sessions before executing any role. Sessions now borrow one base authority. At 10,000 values, revision-session current heap fell from 9.82 MB to 4.07 MB. Initially creating a mutable shadow batch while each role executed lowered the domain-role peak from 26.72 MB to 22.89 MB but left the overall peak unchanged. Empty assumption deltas now evaluate the borrowed base directly; a mutable copy is created only when shadow-assumption facts must be appended. Domain, possible and truth role peaks consequently fell to 20.97, 23.22 and 25.48 MB. The complete factorized-Min construction peak fell from 29.37 MB to 27.45 MB, 1.074 times the 25.55 MB Established peak. Remaining overhead is accumulated staged derivative state retained for atomic three-role commit, not input-batch ownership.

This representation advances the checkpoint payload to version 13. Version 12 stores aggregate input, value and outer histories without aggregate-kind metadata; Sum and Avg therefore depend on the complete value map for recovery. The version-12 DTO remains frozen and decodable. Restore binds it to the live physical plan and structurally validates the old aggregate indexes. A delegated profiled checkpoint may omit materialized operator rows, so version-12 Sum/Avg value histories are folded once into exact scalar timelines during restore and then discarded. Version 13 always emits a tagged fold payload and rejects a payload variant inconsistent with the plan. Versions older than 12 remain rejected. Migration is canonical validation and reconstruction before publication; a failure cannot partially install state.

Generated aggregate mutation streams compare all six admitted numeric aggregate kinds with the fresh fold oracle after every simultaneous and partitioned batch, including incomparable ProductTime cells, empty and undefined projections, outer multiplicity and nonnumeric admission boundaries. This gate also exercises factorized Count, CountDistinct, Sum, Avg, Min and Max in the product-time well-founded circuit across initial publication, an independent factor insertion, checkpoint restoration and a value-factor retraction. The non-factorized six-kind matrix crosses the same restore-then-retract boundary. An additional Product differential requires both Filter and Map inside a well-founded component and crosses insertion, restore and retraction. This gate found a cardinality-neutral replacement defect: projected values changed while the net row count stayed constant, so retained outers were not scheduled and the aggregate output remained stale. A value-profile correction now schedules those outers independently of row-count change in both semantic and record paths. The regression pins replacement of one supported value by another and the resulting old/new output crossing.

The well-founded candidate path has the analogous provenance obligation. In the graph {0 -> 2, 1 -> 0}, replacing 0 -> 2 with 0 -> 1 leaves the cardinality of the rule head unchanged but changes the model to the undefined cycle 0 <-> 1. Summing the insertion and retraction before candidate selection erased both the derived correction and its candidate identity, leaving win(0) stale. Product gamma now stages mixed-sign input as negative then positive micro-batches at one logical outer coordinate. Settled corrections remain additive; candidate records form a set union. The enclosing role advances once and the complete operation remains one validated atomic transaction. An exhaustive differential covers every graph on three nodes, every unordered pair of toggled edges, one batched mutation and both sequential orders: 55,296 Product transitions agree with fresh well-founded evaluation. The minimal replacement also passes after Product checkpoint reconstruction, with live and restored checkpoints converging byte-for-byte. A mixed-sign batch spanning the base, p, and q relations of one mutual-negation component agrees with fresh evaluation for both true and undefined projections. The checkpoint differential also replaces one aggregate input with another in one cardinality-neutral batch for Count, CountDistinct, Sum, Avg, Min and Max. Both ordinary and factorized aggregate plans agree with fresh well-founded evaluation, and live and restored Product states converge after the replacement and a subsequent retraction. An integrated mixed-component differential seeds a positively recursive two-node cycle from one direct fact, restores a checkpoint, then retracts the cycle’s last external support. Both live and restored Product states remove the unsupported cycle and agree with fresh evaluation. This gate exposed two representations of an empty well-founded relation: absent in the checkpoint and present with an empty extent after gamma reconstruction. WellFoundedPair canonicalizes empty extents to absence at construction, so checkpoint equality and runtime equality use the same representation by construction. Failure atomicity is engine-independent at the program boundary. Established and Product gates force a zero-round convergence failure, negative support and signed-weight overflow after a valid publication. Each failure preserves the program frontier, record-arena metrics and checkpoint bytes exactly. This covers nested-revision termination and checked input-weight failure without a Product-specific recovery path. WFS checkpoint restoration treats the supplied base catalog as authoritative. The immutable program plan classifies each dependency by provenance before restoration. External dependencies must equal the base catalog at exact positive support weights. Dependencies produced by an earlier stratum must contain any direct base support; their derived support is validated by the complete restored model because it is not external base authority. These checks run before interning checkpoint rows or rebuilding either gamma engine. A Product gate rejects independently corrupted truth and possible coordinates and also a byte-valid checkpoint paired with a different external base snapshot; an unchanged checkpoint reconstructs to identical bytes. Support multiplicity is retained independently from Boolean membership in both engines. Adding or removing one of two identical WFS input supports emits no derived delta and performs no WFS revision; removing the final support performs exactly one revision. The same boundary holds after checkpoint restoration, and live and restored checkpoints converge after the final deletion. Checkpoint ordering is semantic rather than allocator-dependent. Multi-join tries freeze resolved values in value order, then re-establish destination-key order after catalog or record identifiers are rebound during restoration. Recovery audits construct the oracle through MaintenanceMode::Established with the same explicit evaluation inputs as the restored circuit; no parallel catalog assembly can omit the evaluation-clock relation. Whole-program composition is gated for both engines across an acyclic lower stratum, a WFS component and an acyclic upper consumer. After checkpoint restoration, one batch retracts both a lower-stratum seed and a WFS base edge; the lower relation, true and undefined WFS projections and upper visible relation all agree with fresh evaluation, and live and restored checkpoints converge. Direct facts on a WFS head remain a separate support authority from rule-derived truth. Both engines checkpoint a tuple supported simultaneously by a direct fact and a rule, remove the direct support without removing truth, then remove the derivation and retract truth. Live and restored outputs agree after each boundary and converge to identical checkpoints. A two-relation mutual-negation component gates p, q and both undefined projections in one transaction clock. Both engines restore after initial direct head and seed publication, then process a simultaneous seed/direct-head retraction followed by a four-relation mixed-sign replacement. Every projection agrees with fresh evaluation after each frontier and restored checkpoints converge with live state. Product gamma is independently gated before stable revision. One persistent gamma state walks all 512 directed graphs on three nodes in Gray-code order; for every base graph it walks all eight fixed WFS assumptions in Gray-code order. All 4,096 Γ_E(I) results equal the direct rule oracle, while base and assumption histories both change incrementally and each base frontier is validated before publication. Every integrated WFS fresh-evaluation assertion now reconstructs the published pair from retained coordinates before comparing semantic output. Pair construction requires T to be a subset of U; domain validation requires U to be a subset of D_E. Because U = D_E - F, these checks also require T and F to be disjoint. The exhaustive graph, partition, restore, aggregate and mixed-stratum differentials therefore gate K3 invariants at every asserted frontier rather than only comparing visible sinks. The recursive gamma variant adds a positive transitive rule after the antimonotone seed. One persistent Product state walks all 256 combinations of two-node move and link graphs and all four fixed assumptions: 1,024 evaluations agree with a direct seed-plus-least-positive-closure oracle. This independently gates the positive recursive component inside each gamma role. The production maintainer boundary explicitly selects Product mode, verifies that the recursive program carrier owns a Product WFS engine, round-trips the projection checkpoint, and applies exit insertion, retraction and reassertion. At every mutation the live Product route, restored Product route, Established circuit and fresh evaluator publish the same complete read model; live and restored projection checkpoints converge. The repository ontology conformance replay constructs retained Established, ordinary circuit and require-Product circuit maintainers for every admitted package, then compares all three after mutation and checkpoint restoration, with fresh Established evaluation as an additional oracle. On the gamma-bound head it admitted all 32 eligible packages and replayed 188 mutations: nine seeded-input mutations, 179 synthesized initially-empty-input mutations and 25 atomic multi- input replacements. No package fell back or failed to build; both recursive packages retained recursive routing. Product construction and checkpoint restore are fail-closed, so Established fallback cannot satisfy this gate. The combined oxc-reasoning, oxc-runtime and oxc-driver nextest surface passes 2,947 tests with six skipped. The temporal-conjunction corpus entry has explicit positive and exclusion goldens, so the general corpus gate and the incremental differential both validate that fixture rather than accepting a positive package with no semantic assertion. Product-authored projection checkpoints restore directly in Established mode, and Established-authored projection checkpoints restore directly in Product mode. Both backends emit identical checkpoint bytes before either switch, and each switched instance follows the same subsequent mutation stream. Engine selection is therefore runtime policy, not persisted semantic state, providing rollback and rollout paths without checkpoint migration.

Audited Product restoration independently evaluates Established WFS from the checkpoint’s validated base support before reconstructing Product physical state. The restored truth/possible pair must equal that oracle before the program can become readable. This closes the interval between checkpoint publication and the first audited mutation: a deterministic Product defect cannot validate its own prior checkpoint and then serve it without an independent comparison.

Audited Product also compares every maintained domain, truth and possible gamma role against an independent stability_step evaluation at the role’s current assumption. Empty and absent relations are canonicalized before comparison. Mismatch aborts the transaction before coordinate publication; a fault-injection test verifies that the frontier, model, record arena and checkpoint remain unchanged. One opaque audit policy is selected for the complete revision and is required by every production domain, truth and possible revision method, so a new call site cannot omit the selected audit accidentally. Ordinary Product does not construct the oracle.

Product rule-circuit construction also classifies every physical operator into one of eight semantic obligations: empty, unit, input, positive relational, existence, set boundary, aggregate or publication. The Rust match is exhaustive, so adding a physical operator cannot compile until its obligation is assigned. Admission retains both that theorem family and an exact physical-kernel tag: empty, singleton, scan, delayed scan, join, multijoin, filter, map, binding projection, tuple projection, union, antijoin, semijoin, distinct, aggregate, factorized aggregate, correlated count or sink. A new variant therefore cannot hide inside an existing broad family, and review evidence can name the concrete kernel present at each plan node. Aggregate admission is fail-closed at construction; collection and other unsupported aggregate semantics cannot survive until mutation execution. This classification is the index for per-operator refinement work, not itself a proof that an implementation satisfies its assigned obligation. The six-kind aggregate fragment is one exhaustive circuit-IR predicate shared by rule compilation, Product admission, physical dispatch and checkpoint validation. These boundaries cannot independently grow incompatible kind lists; adding a protocol kind makes the closed classification fail to compile until it is explicitly admitted or refused. Canonical-plan validation also requires aggregate group variables to be strictly ordered and an optional result variable to be fresh relative to the group. Generic, factorized and correlated-count nodes share this binding invariant. Malformed extraction metadata cannot receive Product admission or survive checkpoint plan validation. Admission evidence is issued only for a structurally canonical plan whose identifier recomputes from its complete operator, schedule and metadata commitment. Every session rechecks that canonical identity as well as the operator-obligation map. A stale identifier therefore cannot authorize an operator mutation merely because the old and new operators belong to the same semantic family. The same per-node admission evidence owns Generic Join arity together with its physical-kernel and semantic-obligation classification. Product rule state no longer retains a second node-to-arity map. Every temporal trie journal is validated against the admitted node and arity before publication; missing state is refused during circuit construction, while a missing journal, an extra journal or a shape mismatch refuses the transaction. Construction also compares the admission-owned node sets with the independently allocated antijoin/semijoin, distinct, scalar/correlated aggregate and factorized- aggregate state maps. A missing or extra retained-state owner therefore fails before the circuit can accept input. Fault tests remove and add a distinct-state owner and require construction-boundary refusal. Join, existence, distinct, scalar/correlated aggregate and factorized-aggregate retained state use one closed family-to-kernel classification. Construction and journal validation consume that classification rather than repeating kernel lists and diagnostic labels at each boundary.

DifferentialOperators.lean defines the signed-relation target for seven of those eight families. Empty and unit have constant-transition laws; input and publication use identity change. Selection and projection commute with change; union is linear; join uses the full bilinear derivative including the change-by-change term. A three-input join uses all seven nonempty combinations of retained and changed input support, followed by arbitrary binding selection and collision-consolidating projection. This is the multilinear derivative required by a three-atom multiway join. Antijoin and semijoin have separate theorems for remaining supported, losing last support, gaining first support and remaining unsupported. Distinct emits exactly zero, one or minus one according to its old and new zero crossings, so changes that alter support magnitude without altering membership emit zero. These theorems prove the algebra the physical operators must implement. They do not prove the Rust methods implement it; the node-local semantic audit checks that concrete correspondence during tests. Lean also emits a generated differential-operator layer containing concrete signed transitions for empty, unit, input, selection, collision projection, bilateral union, bilinear join, antijoin and semijoin support boundaries, a cyclic ternary multiway join, distinct zero crossings and publication. Executable finite selection, projection, join and existence-support definitions have proved equality to the canonical denotations before their results are emitted. Rust applies each pair of endpoints to a retained production circuit and compares its emitted delta with Lean. The test-only Product node audit independently compares the record backend with this scalar production backend at every physical node. Together these gates form a finite cross-language chain from the canonical operator semantics through scalar execution to Product record execution; they do not constitute universal Rust refinement proofs. The multiway vector joins binary atoms (x, y), (y, z) and (z, x) whose rows have deliberately different orders, uses variable order [x, y, z], and projects x. It changes all three inputs simultaneously with non-unit signed support. Its expected delta is computed by Lean’s generic binding-selection and projection theorem over the proved seven-term derivative, then replayed through the retained worst-case-optimal multiway-join kernel. This proves the generic Lean denotation and its correspondence to Rust for this finite nontrivial fixture. Arbitrary Rust plans remain covered by production differentials and node-local semantic auditing rather than a universal refinement theorem. The same Lean layer computes every one of 512 Boolean support states for three cyclic binary atoms over three rows each. Rust traverses those states in forward and reverse Gray-code order through all six atom orders. Each plan derives the required canonical first-occurrence variable order and exercises projections to each variable, two ordered variable pairs, a constant and a repeated variable. All 43,008 retained frontiers compare both emitted change and retained output with Lean. This exhausts that finite binding domain and catches plan-order, projection-collision and insertion/retraction history errors; it does not quantify over unbounded relations or arbitrary compiled atom shapes. Binary join uses a separate Lean-generated radix-three matrix with duplicate keys on both sides. Six projections retain a left value, right value, ordered pair, collision-consolidated key, constant, or repeated left value. Rust runs each projection with both physical operand orientations through forward and reverse reflected histories. All 17,496 frontiers compare emitted change and retained output with Lean, covering bilateral non-unit support, projection collisions and both retained arrangements. Antijoin and semijoin additionally use a Lean-generated radix-three matrix over three left rows and three right rows. Two right rows share one key, so the 729 support assignments distinguish one witness, duplicate witnesses, last-witness removal and support changes that do not cross zero. Rust traverses forward and reverse reflected histories through both retained production operators. All 2,916 frontiers compare emitted change and retained output with Lean. The reflected-radix traversal and its completeness and one-row/one-unit adjacency checks are shared with factor-summary conformance rather than reimplemented per operator family. Composition, binary join, multiway join and existence matrices use one retained replay driver for state-space completeness, emitted signed changes and retained sink equality. Matrix modules own only plan construction, input encoding and Lean-output decoding; adding a matrix cannot silently omit one of the common checks. The generated wire schema is isolated from replay and plan construction, so a schema revision does not enlarge the operator harness and a matrix implementation cannot redefine its deserialized contract locally. The same generated layer contains an explicitly composed join-antijoin-filter-project-distinct plan. Lean proves its executable finite definition equal to the corresponding canonical operator composition, then computes both output rows for all 256 Boolean assignments of four left rows, two join supports and two blocker supports. Rust traverses every assignment in forward and reverse Gray-code histories. Every frontier changes one input fact and must produce the exact Lean-derived signed output change as well as the exact retained output. This checks 512 retained composition frontiers without assuming that locally correct operator deltas necessarily compose correctly. StratifiedAggregate.lean models an aggregate result as a deterministic guard fixed by the completed lower stratum. It proves guarded compiled reduct evaluation and gamma equal the normal-program semantics of the selected grounding. This keeps aggregation outside the growing same-stratum fixpoint. AggregateDenotation.lean then defines the six admitted folds over consolidated nonnegative support. Count, sum and average preserve multiplicity; count-distinct, minimum and maximum use positive presence. Average is exact rational division. Its closed denotational tag contains those six folds plus refusal; the generated wire layer maps it to surface/runtime names, so the Reasoning layer does not import Syntax merely to classify physical folds. Count and sum return zero on empty input, while average and extrema return no result. Unsupported collection, set-collection, string-join and percentile kinds have no denotation. The Accepts predicate is the concrete completed- stratum guard after rule-specific group extraction and result binding. Scalar, factorized, semantic and record-backed aggregate outputs use one Rust result-binding function. It extends the canonical outer/group binding only when the result variable is absent; an already-bound result fails with an inconsistent-binding error instead of being overwritten. Canonical plans require the same freshness statically, so construction and execution enforce one binding invariant. Group extraction likewise uses one binding-access operation for canonical semantic bindings and borrowed record bindings. A representation- parity test uses a record whose physical column order differs from variable order, requires both representations to produce the same canonical group and the same present or absent projected value, and requires the same unbound-group- variable refusal. Projection absence contributes to row cardinality but not to the projected value multiset, matching Established aggregate evaluation.

The aggregate denotation is executable. Lean emits a separate generated vector layer covering every admitted kind, exact rational inputs, duplicates, empty inputs, bare cardinality, a refused kind, and factorized Cartesian products with empty factors and exact scaling. A Rust replay invokes the production fold_aggregate method and the incremental aggregate_result_from_support method, then compares their normalized exact results with the Lean-computed value. A third replay compares combine_factorized_fold with results that Lean computes by denotationally expanding the Cartesian product; the Rust path never performs that expansion. Each factorized vector also defines a second endpoint. The incremental node initializes the first endpoint, applies the signed factor differences, and must emit the Lean-computed old/new correction while retaining the exact second-endpoint cardinality and value support for every factor. Each transition runs in one changed two-column correlated group beside an unchanged group, with distinct non-unit outer supports. The gate checks group-key extraction, multiplicity-preserving result binding, and isolation as well as empty-factor activation, simultaneous factor changes, extrema winner retraction, and distinct-support replacement. All replays share one tagged Rust schema loader. CI re-emits and diffs the committed artifact, so changing either denotation or vector inputs cannot silently preserve stale expectations. This establishes the scalar and retained- support fold correspondence and a finite cross-language gate for factor-summary combination, retained-summary transitions, finite multi-column correlated group extraction, group isolation, and multiplicity-preserving numeric result binding. FactorizedAggregate.lean proves the general binary algebra beneath summary combination: Cartesian cardinality multiplies, a projection owned by one factor scales its exact sum by the other factor’s cardinality, and its active projected average is invariant under nonempty scaling. Its active projected values are preserved when the other factor is active or empty when it is not. The canonical retained projected-factor summary is also defined in Lean as cardinality plus signed support per projected value. Lean proves that integrating an arbitrary signed relation change is exactly pointwise summary addition, and that multiplying this summary by an independent factor’s cardinality equals summarizing the expanded Cartesian relation. These theorems apply to arbitrary finite relations and integer supports rather than the generated fixture domain. Universal correspondence from Rust’s retained factor-summary methods to this denotation remains open; generated histories, node-local auditing and full-model differentials cover that runtime boundary operationally.

The generated aggregate layer also exhausts retained factor-summary histories for count, count-distinct, sum, average, minimum and maximum. Each kind has two three-row factors; the projected factor contains values -1, 2 and 2, so the state space includes duplicate-value support as well as extrema and sign changes. Every row has support zero, one or two. Lean expands the Cartesian product and computes every result for all 729 radix-three factor-support assignments. Rust traverses those assignments in forward and reverse reflected radix-three Gray-code order through the real factorized node, checking the signed output change, retained factor cardinalities and retained value support at every frontier. Adjacent states change exactly one row by one, so the histories cover zero crossings and multiplicity-only changes. This adds 8,748 retained transitions without using the runtime summary-combination algorithm as its own oracle.

The same 8,748 transitions also pass independently through the semantic profile backend and the EvaluationRowId record profile entry used by Product evaluation. Inputs use session-local row identities, outputs are materialized through the production resolver, and retained journals publish persistent arena identities. Both emitted deltas must equal the Lean result. After every atomic publication, the complete record state is resolved and must equal the semantic profile state. This covers first-use projection publication, duplicate-value insertion and retraction, extrema replacement, and record interning across long-lived histories. Distinct-value patching preserves untouched values and explicitly publishes values introduced when a factor changes from no projection to a distinct projection. Extrema-index equality is defined by orientation and its ordered key-to-support relation; persistent tree shape is an internal indexing choice and cannot make semantically equal retained states compare unequal. The corrected first-use publication changes persisted circuit semantics, so the derivation semantic epoch advances and older circuit checkpoints are rebuilt rather than trusted with incomplete distinct-value support. The prior canonical-row test adapter, its separate extrema implementation, and its partial record-group decoder were removed. Factorized Product evaluation now has one record implementation for every aggregate kind; the exhaustive history gate tests that implementation directly. Semantic and record evaluation also call one representation-generic retained transition kernel. That kernel selects affected outer bindings, prepares distinct zero-crossing corrections, merges cardinality and value support, canonicalizes extrema, and validates every temporal prefix. Only row identity resolution and output emission remain backend-specific. A change to retained factor semantics therefore cannot update one backend while bypassing the other. Ordinary projected aggregates follow the same single-backend rule. Their semantic/record parity and zero-cardinality replacement tests now construct session-local evaluation rows, invoke the production record evaluator, and materialize its output through the production resolver. The canonical-row record adapter, its separate record-extrema group evaluator, and the partial count-group decoder were removed. The remaining extrema module contains only the exact semantic key and shared index-error translation used by production folds. Join and distinct record evaluation follow the same ownership rule. Production and differential tests both intern session-local EvaluationRowId values, execute the retained record evaluator, and materialize results only at the comparison boundary. The canonical-row join and distinct adapters, their record-arrangement conversion, and their duplicate retained-join emitter were removed. Product-time, published/staged/correction cross terms, persistent journal identity and distinct zero crossings are therefore tested against the same implementations used by maintained programs. Ordinary iteration time and Product gamma time instantiate one generic record state view. Scan, multijoin, filter, map, projection, sink, join, distinct, count and factorized aggregate routing therefore cannot diverge by adding or changing a method in only one engine. A typed mode supplies the logical time, retained antijoin support and antijoin transition because scalar exclusion support and Product-time temporal exclusion have different state algebras. Ordinary profiling hooks are also mode policy; they do not alter evaluation. Canonical bindings and transaction-local record bindings also implement one binding-access contract. Filter comparison, definedness, expression-input admission, map evaluation and binding projection call one representation- generic kernel. Tuple projection uses the same contract for constants, variables and unbound-variable failures. Record comparisons retain borrowed values, and expression evaluation materializes a compact binding at most once, so this correspondence does not require tuple-wide conversion on the comparison path. Antijoin retains one intentional mode boundary. Ordinary scalar support uses the four sealed-support transitions; Product time uses temporal support and Möbius differentiation over the logical-time join closure. Product semantic and record evaluation both call the same key/row-generic temporal differentiator. Ordinary and Product record emission also use one canonical positional binding-order constructor, so their tuple identities cannot diverge. Multiway joins instantiate one generic trie evaluator with semantic or interned key codecs. Record-backed execution has one EvaluationRowId adapter for both ordinary and Product logical time. The prior canonical-row record adapter and its duplicate output codecs were test-only and are removed; tests now invoke the production adapter and resolve its session-local output directly.

A post-audit-mode sustained probe uses 10,000 states, alternates one edge between two destinations for 128 mutations, and verifies that the final derived model returns to its initial state. Ordinary Product measured 0.106 ms p50 versus 4.446 ms Established, a 41.8-times speedup. Product final retained heap was 1.038 times Established, mutation peak was 0.662 times Established, and construction peak was 0.989 times Established. The explicit audited policy does not add complete-pair materialization or Established evaluation to ordinary Product.

Product is suitable for explicit gated use only when all of the following hold:

  • gamma parity for every fixed assumption under signed base and assumption histories;
  • T intersect F = empty, T subset U subset D_E, complement correctness and equivalence between fixed points of C_E and stable revision;
  • the affected-region and worklist theorems are conditional; the runtime admits a regional revision only after checking prior/new exterior agreement, final regional exterior stability, gamma-target containment, monotone candidate emission, and scheduler quiescence;
  • last-support deletion of unsupported positive and coordinate cycles;
  • exact direct-head fact handling, nested-time termination, checked weights, transaction atomicity and checkpoint reconstruction from semantic state;
  • differentials over odd and even negation loops, self loops, mutually recursive relations, exit insertion and deletion, alternative support, simultaneous domain/base changes, every false/undefined/true transition, mutation-order permutations and restore-then-mutate streams; and
  • fixed-delta scaling over independent game regions demonstrating work proportional to the affected region rather than the retained component.

An operator-refinement matrix compiles one well-founded program whose gamma plan contains every admitted semantic obligation: empty, unit, input, positive relational, existence, set boundary, aggregate and publication. It traverses all 243 support assignments for five input tuples at multiplicities zero, one and two in both forward and reverse order. Each history is checkpointed and restored halfway through before continuing. Consecutive assignments include support-only changes, zero crossings, corrections of magnitude two and mixed- relation batches. Product output is compared with fresh Established evaluation after every frontier. The matrix also requires both engines to initialize predicate-free rules from an empty first batch. This gate exposed and now pins two initialization boundaries: an initial empty batch is a real first well-founded revision, and Product empty staging evaluates acyclic components as well as advancing recursive ones. Once an acyclic component has completed its initial singleton stage, an empty accepted delta skips it; this preserves the steady-state sparse path. The matrix’s exact manifest contains 14 kernels: empty, singleton, scan, join, multijoin, filter, map, tuple projection, union, antijoin, distinct, aggregate and sink, plus semijoin through nested double negation. Delayed scan, binding projection, factorized aggregate and correlated count are not mislabeled as matrix coverage. Separate Product tests assert each exact kernel before executing delayed recursive feedback insertion and retraction, modal diamond projection and retraction, all six factorized numeric mutation histories, or recursive correlated-count replacement. The complete Product-focused surface constructs every admitted physical kernel through the exact manifest before evaluation.

Every Product rule-circuit stage in the Rust test build also runs the semantic profile backend from the same published and transaction-local retained state. The audit compares every physical node’s signed output, settled sinks, candidate sinks and the resulting retained operator families before the record state can continue. Multiway-join journals are applied to cloned published tries after every transaction-local prior round, then compared as resolved semantic key/time state; equal stage output cannot conceal a divergent retained trie that would affect a later mutation. A test-only per-instance fault removes the record journal, verifies rejection at this boundary, then disables the fault and confirms the same unpublished input succeeds. Antijoin traces compare by materialized key/time support, not trace-batch segmentation. The canonical record decoder is generic over logical time and antijoin-support representation so this audit uses the same decoder as scalar checkpoint tests rather than maintaining a second Product shadow representation. Product-time extrema corrections are decoded symmetrically with their existing encoder. The audit is test-only and adds no production mutation work.

The runtime checks and differentials establish a fail-closed gated path, not default semantic authority. CompiledGamma.lean defines the two-input reduct operator directly over compiled relational rules: positive literals read the growing reduct state while negative literals read the fixed gamma assumption. The kernel proves this operator equal to the ground normal-program reduct step, and proves their least-fixed-point gamma results and signed gamma transitions equal. This covers relational atoms and deterministic comparison/compute guards. It does not cover aggregates or prove that the Rust physical operators implement the compiled denotation.

DifferentialOperators.lean also defines unary, binary and three-input exact-derivative refinement boundaries. It proves that exact stateful derivatives compose through both sequential nodes and binary fan-in, including simultaneous changes from both upstream branches, and through direct three-input fan-in. The three-input contract includes all seven nonempty combinations of changed and retained inputs. These compositions reconstruct the operator’s new retained output. This closes structural composition of unary, binary and three-input nodes at the signed-relation denotation level. Identity, union, selection, projection, Cartesian product, and join have explicit exact-derivative witnesses; product and join include the simultaneous change-change term. Antijoin and semijoin have complete four-case support-transition derivatives, and distinct has a complete zero-crossing derivative; all three satisfy the same compositional contract. Lean-computed transition vectors replay these denotations through retained production circuits. The concrete unary replay includes a conditional Map whose many-to-one result binding is projected and consolidated, covering expression evaluation, result-variable insertion and downstream collision handling across a signed mutation. Every operator vector is also expressed as actual CompiledRule input: constants, identity, selection, projection, map, union, binary and multiway join, antijoin, double-negation semijoin, set boundary and publication. The compiler-selected graph must emit the Lean-derived membership zero-crossing rather than the raw Z-set correction. This checks lowering and execution together while preserving the distinction between internal multiplicity and published set semantics. It does not prove that every Rust physical method satisfies an exact-derivative premise or that the concrete physical plan denotes the compiled reduct.

The same file proves the full arbitrary-arity support derivative. For any finite family of aligned input relations, the derivative is the sum over every nonempty subset of changed inputs, multiplied by retained support from the complement. The kernel proves this powerset expansion equals the exact product change, then lifts it through binding selection and collision-consolidating projection. The multiway-join algebra is therefore not limited to the three-input instance. Correspondence from the Rust Generic Join trie and its temporal journals to this denotation remains a physical-method obligation. An independent retained temporal differential exercises the concrete trie and journal implementation at every arity from two through eight. Its histories include simultaneous all-input changes from support one to support two, forcing every nonempty derivative subset to contribute. Expected output is computed only as the difference of the before/after scalar support products. The test also covers zero crossings, mixed zero/non-unit support, reactivation and complete retraction; it does not reuse trie evaluation as its oracle.

Aggregate denotation has a separate Lean-computed vector layer for all six admitted numeric kinds, empty groups, duplicate projections, exact rationals, factorized transitions and exhaustive retained factor histories. Rust checks those vectors at the Established fold, incremental support fold, factorized summary and record-profile boundaries. A further replay builds actual compiled aggregate rules, applies them through retained CircuitInstance state and compares their published result tuples with the Lean values. Factorized vectors also build real independent-factor rules, require the compiler to select FactorizedAggregate, and compare the retained before-to-after publication delta with Lean’s Cartesian denotation. Count vectors additionally compile through the positive-recursive-body path, require CorrelatedCount rather than the generic aggregate operator, and compare its published tuples with Lean’s empty and nonempty count denotation. This connects source-rule lowering to the concrete aggregate operators and publication path; Product mutation matrices independently compare the same physical kernels with fresh Established evaluation.

The remaining default-routing gap is therefore the concrete correspondence from the Rust physical methods to the signed operator denotations, the concrete physical plan’s correspondence with the compiled reduct operator. Canonical plan admission now derives binding-variable sets along the physical operator graph. Aggregate group variables must occur in both the outer and folded input; factorized groups must occur in every factor; projected values must occur in the selected folded input; and correlated-count outer and input declarations must exactly equal their derived physical bindings. Malformed extraction metadata is therefore refused before completed-stratum state construction. Finite exhaustive graphs, ontology replay, audited execution, and fresh Established comparison detect divergence but do not prove that correspondence for every program. Product and Established construction now share one additional translation-input gate. The actual WellFoundedEvaluationPlan rewrite is built once from the retained component rules; its canonical module commitment must equal the module commitment carried by every acyclic or positive-recursive gamma physical component. Each physical component also retains its sorted multiset of canonical compiled-rule commitments. Their combined multiset must equal the rewritten rule multiset exactly, so an omitted, duplicated, or substituted rule is refused without recompiling the physical plan or retaining another rule copy. These commitments are part of CircuitPlanId; checkpoint validation therefore cannot detach them from the operator graph. The same checker independently derives rewritten rule-head identities and arities and requires their exact equality with the acyclic sink and positive-recursive output relation set; a correct source hash cannot conceal a missing, extra or renamed physical output. Rewritten bodies independently yield their complete relation dependency identities and arities; these must equal the union of physical Scan, DelayedScan and Generic Join inputs. Faults that rename a physical input or output while preserving the source commitment are refused. Rule-head projection is also checked without recompilation. The validator walks each sink through its set boundary and union to the connected rule producers, then recomputes each producer node identity from the exact rewritten rule, physical stratum, canonical output path and actual operator. Every rewritten rule must match exactly one connected Project or Generic Join producer for its output relation, every producer must be matched, and its head slots must equal the compiled rule head. A detached producer, wrong relation, wrong constant, reordered variable or substituted projection is refused before state construction. Candidate lookup is indexed by output relation, so this validation is linear when relations have one producer and does not recompile rule bodies. Every embedded acyclic, nested-recursive and recursive-SCC plan must also retain its canonical identity. Operator identities recursively commit body inputs, columns, join keys, predicates, expressions, aggregate configuration and projections, so any detached or mutated physical body is refused generically. This proves source/plan integrity, not that the compiler selected the denotationally correct operator graph. The physical-plan refinement obligation above remains open. Successful source validation returns opaque ValidatedGammaCircuitPlan evidence. Production well-founded gamma-state construction accepts that evidence rather than a raw gamma plan, so neither Established nor Product construction can depend on a caller remembering a separate validation step. Lower-level circuit constructors remain available to focused kernel tests but are not the whole-program production seam. Product rollout does not claim a universal proof that every Rust physical plan refines its denotation. Instead, admission is fail-closed and the release gate combines validated source-to-plan identity, Lean vectors, exhaustive finite kernels, generated retained differentials, ontology replay, checkpoint continuation, and an independently selectable Established oracle. Product is the well-founded engine under ordinary circuit enablement. Established remains both the process default when circuits are disabled and an explicit well-founded escape hatch when circuits are enabled.

The generated whole-program reasoning layer provides a direct executable bridge for a finite source fragment. Lean computes true and undefined extents for 12 programs covering recursion, negation, joins, comparisons, modal stripping, grouped count/sum/min/max, empty aggregates, and two well-founded cycles. Rust rebuilds each emitted program and evaluates it through fresh evaluation plus cold and retained RequireEstablishedWfsCircuits, RequireCircuits, and RequireAuditedProductCircuits programs receiving their facts through empty input, forward insertion/retraction, full reassertion, and reverse retraction/insertion. Lean independently computes true and undefined extents at all 148 frontiers. Required admission makes fallback impossible. Well-founded vectors select Established gamma through the explicit escape-hatch mode and Product gamma through ordinary and audited circuit modes. At the first full-input frontier each retained program is checkpointed, restored with the matching EDB and policy identity, and then continues the same trace. All three incremental policies must match at every frontier; fresh and cold incremental evaluation must match the final model. This closes static and forward/reverse mutation correspondence for those fixtures. It does not prove universal physical-method correspondence or every arbitrary mutation order; those claims remain outside the rollout evidence.

The batched Lean differential broadens cold-construction coverage beyond those fixed fixtures. A deterministic generator emits 1,600 range-restricted programs: 700 relational compositions and 300 each for ordered comparisons, recursion-through-negation, and grouped aggregates. The same generated program is evaluated by Lean, fresh Established evaluation, fail-closed ordinary incremental maintenance, and fail-closed Product maintenance seeded with its facts as EDB state. All Rust paths must reproduce Lean’s true and undefined extents. Incremental routes use RequireCircuits and RequireUnauditedProductCircuits, so an admission failure or Established fallback fails the gate. This tests many source-program compositions directly against Lean. Every 700-program relational batch reserves one mandatory case for a four-predicate cyclic join projected to one variable; the remaining 699 cases retain deterministic feature generation. This extends direct Product differential coverage beyond the proved and exhaustively enumerated three-input join without relying on probabilistic feature selection. Each program then follows five retained EDB frontiers: empty, full, one rule-read-fact retraction, full reassertion, and empty. Both incremental routes checkpoint and restore at the first full frontier; all 16,000 retained route models are compared with Lean’s true and undefined extents. This broadens mutation-history evidence but does not replace the universal physical-method correspondence argument.

DynamicGamma.lean isolates the history algebra from compiler correctness. It proves that exact signed differences between consecutive semantic gamma results telescope to the final semantic result. CompiledGamma.lean connects that algebra to the compiled relational denotation and its grounded semantic gamma; it deliberately stops before the Rust physical plan. A persistent Product differential walks every two-node edge-support assignment with support weights zero, one and two, including simultaneous corrections with weights from minus two through two, and checks every fixed assumption against semantic gamma. This establishes the signed-set algebra and exercises non-Boolean support histories; it does not replace the remaining per-operator compiler refinement argument.

Product domain ownership

Established gamma retains its evaluated domain because no subordinate rule state exposes equivalent support. Product gamma does not retain a second domain map. Its domain role’s direct and derived record sets are the canonical support authority; published domain membership is their Boolean union.

Product domain role
  +-- direct records  ----+
  +-- derived records ----+--> borrowed Boolean union
                                  +--> tuple-local mutation reads
                                  +--> sparse coordinate projection
                                  +--> optional complete-pair observation

The union view supplies checked point membership to temporal overlays, coordinate differentiation and sink projection. It allocates no retained state and does not scan untouched records. Checkpoint and public full-model reads may materialize the union transiently. Established domain replacement remains variant-local; Product transactions cannot publish an independent domain map.

Record identity lifetime

RecordId is process-local storage identity, not checkpoint or semantic identity. The arena may contain records created by committed evaluation but no longer reachable from retained operator state. No component reclaims or remaps records independently. Reclamation uses the semantic program-checkpoint boundary:

published ProgramProjection
        |
        +--> encode semantic component state and catalogs
        +--> restore off-path into a fresh RecordArena
        +--> validate program/catalog agreement
        +--> restore projection-owned relation indexes
        '--> atomic whole-snapshot swap

The old program remains published until every restore and validation step succeeds. Compaction runs before a subsequent user transaction, never after that transaction commits. It is considered only after at least 4,096 new record identities and two-times growth from the last compact arena. This amortizes reconstruction while bounding append-only garbage. The threshold is ephemeral policy state and is not part of semantic checkpoint identity.

The fixed-delta benchmark asserts before timing that inserting the exit edge publishes win(0) and retracting it removes win(0) under both well-founded engines. Three adjacent 20-sample runs over 10,000 retained self-loop states and 32 alternating mutations measured Product at 1.775 ms, 1.743 ms and 1.739 ms. The corresponding Established-WFS circuit measurements were 135.26 ms, 128.20 ms and 139.87 ms. The last adjacent comparison is 80.4 times faster for Product. This result covers one tuple-local mutation shape; it does not replace the cross-shape latency, retained-memory and peak-memory gates for default routing.

Phase E — persistence and scale

Canonical projection checkpoints contain authoritative base/derived catalogs, component derivative state, arrangements, scheduler profiles and published-batch identity. Delegated input, sink and evaluator projections are omitted. Compatibility binds tenant, fork, module commitment, physical program-plan commitment, circuit schema version, derivation semantic epoch, execution configuration, circuit frontier, durable generation and event-prefix commitment. Restore validates the complete envelope off-path, replays the suffix and audits against full evaluation before atomic snapshot publication.

Temporal record ownership

The whole-program record arena is the sole owner of decoded temporal tuples. Temporal relation support, producer provenance, pair indexes, NAF correlation indexes and durable multiway-join keys retain record identities. They do not retain Tuple, Value or canonical tuple bytes. Interval vectors and producer addresses remain temporal-trace state because they are not derivable from membership alone. Transaction-local semantic working sets may decode values, but they are discarded before publication and checkpointing.

flowchart LR
    B[Semantic temporal batch] --> RT[One program record transaction]
    RT --> T[RecordId temporal trace journal]
    RT --> P[RecordId pair and NAF journals]
    RT --> M[Record-key multiway journals]
    T --> V[Validate support and transaction token]
    P --> V
    M --> V
    V --> A[Publish record suffix]
    A --> C[Commit temporal indexes and trace]
    C --> F[Publish frontier and output]
    C --> W[Resolve records into semantic checkpoint DTO]
    W --> R[Restore into one fresh record transaction]

Every prepared temporal journal carries the record-transaction token that created its provisional identities. Validation rejects a journal paired with another transaction. All fallible interning, support arithmetic and shape validation precede publication. The arena suffix publishes first; subsequent trace and component commits are prevalidated assignment-only operations. A failure therefore preserves arena length, temporal state, component indexes, frontier and checkpoint bytes.

Runtime identities never enter checkpoint compatibility. The temporal wire payload remains relation name, canonical semantic tuple and coalesced interval support. Encoding resolves record identities through the live arena; restore interns semantic rows through one fresh program transaction and reconstructs every trace and index against those identities. Checkpoint bytes consequently remain independent of allocation order and existing checkpoint versions remain readable without a schema migration.

The conversion gate requires all of the following:

  • direct support, derived many-to-one provenance, pair indexes, NAF indexes and multiway source vectors reuse the same record identity for an equal tuple;
  • every retained temporal record identity resolves through the program arena;
  • a failed or stale prepare after provisional interning leaves arena length, trace, indexes, frontier and checkpoint bytes unchanged;
  • forced digest collisions remain payload-distinct through temporal provenance and index lookup;
  • checkpoint bytes are independent of local record-allocation order, restoration may assign different local identities, and the next mutation remains equal;
  • last-support deletion removes temporal provenance and index references without leaving an unresolved identity; and
  • retained, construction-peak and mutation-peak measurements report record counts alongside trace contribution and index-reference counts.

The native metric-temporal fragment admits acyclic unary box_minus, box_plus, diamond_minus, or diamond_plus rules whose operands are predicates and whose heads project bound operand variables or constants. Interval-preserving positive rules of the form H(projection) :- P(pattern) are trace-native components, so aliases and filtered projections may occur before, after, or between temporal operators without crossing a membership-only boundary. Reordering, repeated-variable and constant selection, many-to-one projection, chains, fanout, multiple producers of one head, and direct facts on derived heads share one trace. Acyclic binary since and until rules over two predicate operands use keyed arrangements in the same trace and admit crossed operand shapes, Cartesian joins, constants, repeated variables, and projected heads. Positive recursive SCCs composed of interval-preserving projections and unary or binary temporal rules retain producer/source contribution cells across frontiers. Growth follows a sparse changed-tuple frontier. Support shrink uses causal overdelete, exact-source rederivation, and then the same sparse frontier; old cyclic support cannot justify itself across a retraction. Binary recursive rules build transaction-local reverse dependencies from contribution provenance. Those indexes contain addresses, not support, and cannot become a second relation authority. Exact least-fixpoint reconstruction remains the construction, checkpoint-recovery, and differential oracle path. Each rule remains in a typed ProgramComponent, not a parallel maintainer. A positive recursive temporal SCC may contain a positive relational join of arbitrary arity. Its interval image is the intersection of every source support. Two-predicate matching shares the neutral binary trace infrastructure used by since and until. Larger joins use the canonical WCOJ atom order and atom-ordered source provenance. Acyclic joins retain one WCOJ trie per atom; recursive settlement creates one transaction-local WCOJ working set per producer and applies round deltas to it. The working set is discarded before publication and therefore cannot become a second relation authority. NAF, and analytical-provider components in a temporal-connected program fall back before circuit state is created. Comparison, compute, aggregate, and modal atoms inside a metric-temporal program are language-level OE1351 refusals in the established evaluator; circuit admission does not define an independent semantics for them. Explicit single-frame modal rules use the relational lowering above. Modal companions, nested modal operands and federation truth carriers remain outside that fragment.

recursive frontier tuples
          |
          v
transaction-local WCOJ working set
  +-- one trie per ordered atom port
  +-- apply current-round membership corrections
  `-- restrict each changed port to changed tuples
          |
          v
affected ordered source vectors
          |
          v
intersect support from the shared temporal trace
          |
          v
producer/source assignments -> next recursive frontier
          |
          `-- discard working set before atomic publication

Metric-temporal maintenance requires an interval-valued component boundary; ordinary FactDelta is insufficient because equal tuple membership can carry a different valid-time support. Temporal components therefore consume signed interval changes keyed by (relation, tuple). One program-local relation trace owns direct support and producer-tagged derived support. A consumer reads the producer’s trace entry directly; it does not retain a private input copy. The effective relation is the coalesced union of direct and producer contributions. Tuple membership is only the projection support != empty, never a second authority.

One nonrecursive temporal stratum may contain independent temporal and ordinary positive rules. Admission selects a unary/binary temporal, projection, pair join, or multiway join component per rule; it does not require every rule in the stratum to share one operator family. All components still read and write the same trace transaction and publish one frontier.

Producer identity is independent of component scheduling. An acyclic component owns one canonical producer identity; a recursive SCC owns one per rule. The trace rejects duplicate identities during construction, and plan commitments bind each identity explicitly. Component vector position is never provenance.

durable commit with valid-time events
                 |
                 v
 canonical event-to-EDB contribution projection
                 |
                 v
 coalesced PRE/POST support for touched tuples
                 |
                 v
 signed TemporalFactDelta(PRE - POST, POST - PRE)
                 |
                 v
 shared direct + producer-tagged relation trace
                 |
                 v
 compiled tuple constraints and projection
      |-- injective: support by projected tuple
      `-- many-to-one: projected tuple -> source contributions
                 |
      +----------+-----------+
      |                      |
      v                      v
 projection/unary image  keyed pair or multiway WCOJ
 identity or box/diamond since/until or relational intersection
      |                      |
      +----------+-----------+
                 v
 old/new support difference
                 |
                 v
 temporal worklist to fixpoint
                 |
      +----------+-----------+
      |                      |
      v                      v
 synthesized catalog   membership projection
 interval view         (support is non-empty)

Unary operators recompute only tuples whose operand support changed and emit new_image - old_image plus old_image - new_image. Binary operators arrange both operands by their shared binding key and revisit only keys touched on either side. Arbitrary-arity acyclic joins retain one membership-only WCOJ trie per ordered atom port. A transaction stages trie corrections, restricts each changed port to each changed tuple, and evaluates affected bindings against both prior and staged roots. The union is the exact candidate set whose atom-ordered contribution cells may have changed. Recursive multiway growth uses the same changed-port restriction over a transaction-local trie working set updated between rounds. Recursive deletion indexes every ordered source in existing contribution provenance, clears dependent cells causally, rederives exact addresses, then applies the same sparse frontier. Ordinary relational conjunction uses pairwise interval-set intersection; it does not impersonate a temporal operator. Recursive deletion seeds invalidation from both effective-support shrink and direct-support shrink. The latter is required when obsolete cyclic support masks removal of the last direct seed. Each invalidated (producer, output, source) cell is cleared exactly, dependent cells are invalidated transitively, and all cleared cells are rederived against the overdeleted trace before sparse forward settlement. Multi-source invalidation uses a transaction-local reverse dependency index over old ordered provenance; growth uses final staged operand tuples. The established finite-materializability guard remains the publication gate. Checkpoints encode direct coalesced support and reconstructible plans, not expanded time points or derived indexes. The shared interval algebra supplies exact set difference over finite and infinite endpoints so operator deltas never use sentinels or timeline-width iteration.

Before recursive settlement, the trace classifies every staged direct-support assignment by exact interval difference. If old \\ new is empty for every touched cell, the mutation is monotone: a sparse relation/tuple worklist recomputes only producer contributions reachable from changed operands. Unary rules visit only changed source tuples; binary rules build transaction-local key indexes and enumerate only pairs touching a changed operand. Any direct support shrink or replacement enters causal overdelete/rederive. This boundary is semantic, not heuristic: retained cyclic support is safe during growth, while shrink must remove causal descendants before testing which cells can be rederived. Both paths write the same producer/source cells in the shared trace. An SCC whose input relations have no effective support change creates no settlement session; a two-SCC differential pins one additive mutation to one observed recursive settlement.

staged direct support
        |
        v
old \\ new empty for every touched direct cell?
   | yes                            | no
   v                                v
sparse changed-tuple worklist   seed direct/effective shrink roots
   |                                |
   |                    +----------- causal overdelete ----------+
   |                    | clear producer/output/source cell       |
   |                    | enqueue dependent output tuple          |
   |                    +------------------------------------------+
   |                                |
   |                         rederive cleared cells
   |                                |
   |                     sparse changed-tuple worklist
   |                                |
   +---------------+----------------+
                   v
          shared trace changed?
             | yes       | no
             '--- next   v
                      finite guard
                           |
                           v
                  atomic publication

ProjectionChanges bundles membership retractions, membership assertions and temporal support changes into one commit argument. A maintainer either validates and publishes all three or publishes none. A maintainer without an interval-valued component refuses a nonempty temporal delta before changing state; runtime then discards that projection and rebuilds from durable events. For every temporal operand, preparation derives the expected empty/nonempty carrier transitions from old and new interval support and requires exact equality with the effective tuple-membership delta. This rejects a commit that supplies only one representation while retaining support-only changes whose carrier remains present.

Unary publication uses the program transaction boundary. One sparse trace transaction stages direct assignments, walks components in dependency order, and propagates only relations whose effective support changed. Preparation computes next operator images and empty/nonempty membership transitions without mutating live state. Validation precedes an assignment-only commit of the trace, base membership, frontier, durable commit identity, and read-model generation. The read model synthesizes intervals from the trace; direct facts and every producer on the same head are coalesced rather than overwritten.

Tuple projection is compiled once. Runtime projection performs only arity, constant, and repeated-column checks followed by indexed column moves. An injective projection stores one support set per output tuple. A many-to-one projection retains source-keyed contribution cells under each output tuple so changing or retracting one source cannot erase another. The coalesced output is computed from those cells and is not retained as a second authority. Projected output tuples, rather than physical operand tuples, enter the downstream temporal worklist. Unary projection admission increments the derivation semantic epoch to 10; native binary temporal admission increments it to 11; trace-native relational projection admission increments it to 12; recursive unary and binary temporal admission increments it to 13; trace-native two-predicate relational join admission increments it to 14. Arbitrary-arity acyclic relational join admission increments it to 15. Trace-native temporal predicate exclusion admission increments it to 16.

Program checkpoint version 9 persists the direct relation trace once and recomputes producer images topologically on restore. Plan-only component DTOs cannot duplicate fanout operands. Component-owned relations are omitted from catalog sidecars because content-addressed catalog bytes deliberately omit evaluation metadata. Restore rejects unordered relations or tuples, malformed or non-coalesced intervals, support without positive direct membership, plan or scope mismatch, and noncanonical encodings. Version-3 unary checkpoints migrate their sole input map into the shared trace; version-4 unary checkpoints, version-5 binary checkpoints, version-6 projection checkpoints, version-7 recursive checkpoints, version-8 join checkpoints, and older non-temporal versions remain decodable. Projection producers, acyclic binary join indexes, recursive binary pair contributions, acyclic multiway tries, reverse dependency indexes, and all other recursive producer images are rebuilt rather than persisted. The trace is the sole live and persisted interval authority; the catalog is a synthesized read view.

Binary components retain reconstructible join key -> tuple indexes for each operand. Support remains exclusively in the shared trace. A transaction stages index membership by join key from the final effective support, unions old and staged-new counterpart buckets, and recomputes each affected tuple pair once. Pending producer support has one output-partitioned representation; it is not duplicated in source and output indexes. Typed pair provenance prevents one Cartesian or projected collision from erasing another. Both operand changes, index assignments, pair contributions, downstream support, tuple membership, and the frontier publish through one program journal. An infinite-tail since or until image refuses the journal before any index or trace assignment commits.

Temporal binary operators and ordinary relational joins instantiate one generic trace-pair component. The component owns retained operand indexes and the common prepare/commit journal; its operator owns tuple matching, projection, and support algebra. Since/until compute a metric image, while relational conjunction computes interval intersection. This division prevents duplicated index publication logic without conflating their semantics.

Closed-world predicate exclusion uses the same pair indexes but evaluates one left contribution against the union of every matching right witness. The operator computes left support \\ coalesce(right witness support) and retains only unary left provenance. A right change invalidates the left bucket for its join key; unrelated keys are not visited. Right support is read from the shared trace and is never copied into the index.

changed left tuples ---------+
                             v
                       affected left keys ---> staged pair index
                             ^                         |
changed right tuples --------+                         v
                                  matching right tuples from final membership
                                                   |
left support from trace ---------------------------+-- union/coalesce
                                                   |
                                                   v
                                      interval difference
                                                   |
                                                   v
                                  unary source contribution
                                                   |
                                                   v
                                      shared temporal trace

The pair specialization admits one positive predicate and one predicate NAF. The generalized conjunctive component admits arbitrary positive predicate and metric support expressions with multiple predicate NAF lookups. Multiple local existential witnesses contribute their support union. A program with NAF over a relation whose effective per-concept world is open remains on the established engine because absence is not definite falsity. NAF over an explicitly closed relation remains circuit-admissible even when the package default or unrelated concepts are open. Positive and modal operators do not read the world map. Non-predicate negands and recursion through NAF remain explicit refusals.

The established temporal materializer uses the same existential semantics. Variables local to a negated predicate are not ground-input requirements; every tuple extending the current positive binding contributes to one coalesced witness-support union. Treating an unbound local variable as an empty ground lookup would incorrectly derive through an existing witness and is a differential-oracle defect, not an admissible circuit difference.

Temporal rule compilation has one conjunctive normal form. It assigns stable source ports to predicate leaves, retains metric operators as support-expression nodes, separates positive conjuncts from predicate-NAF lookups, and records one head projection. The same normal form owns the flattened positive atom vector consumed by WCOJ, every relation identity used by support lookup, and each NAF correlation projection into positive source columns. The executable operator retains only resolution-dependent metric windows and source ports; it cannot retain divergent relation or correlation metadata. Positive variables use first-occurrence WCOJ order. Each NAF records correlated variables in positive order and local existentials in negand-source order. A head variable must be positively bound; a NAF-local variable cannot become a head binding.

Support evaluators exchange module-qualified RelationId values. Conversion to catalog relation names occurs only at the temporal-trace adapter boundary; operator interfaces cannot silently alias equal strings from different plan identities. The trace owns one canonical name-to-RelationId registry built from component plans and refuses conflicting module or arity identities before loading catalog state. Retained direct-support partitions and producer-output ownership are keyed by those typed identities. String keys exist only on external catalog, mutation, and checkpoint adapters and are resolved through the registry before retained state changes.

compiled rule body
      |
      v
temporal conjunctive normal form
  +-- positive support expressions
  |     +-- predicate source ports
  |     +-- unary metric image
  |     `-- binary metric image
  +-- predicate NAF lookups
  |     +-- correlated variables
  |     +-- positive-source correlation projection
  |     +-- predicate relation identity
  |     `-- local existentials
  +-- positive WCOJ variable order
  +-- complete source order
  `-- head projection
      |
      +---- projection / unary / binary / join / exclusion specialization
      `---- generalized conjunctive component

Existing specialized plans consume this normal form rather than reparsing rule shapes independently. The generalized component may therefore replace those specializations without changing source identity, variable order, projection, or NAF correlation semantics. WCOJ accepts fixed correlated bindings and descends only matching trie prefixes when a negand changes; it does not retain a second positive-match relation.

flowchart LR
    Delta[effective support changes] --> Pos[staged positive WCOJ tries]
    Delta --> Neg[staged correlation-to-witness indexes]
    Neg --> Keys[affected correlation keys]
    Keys --> Old[restricted published WCOJ descent]
    Keys --> New[restricted staged WCOJ descent]
    Pos --> Changed[positive-port affected matches]
    Old --> Affected[deduplicated positive source vectors]
    New --> Affected
    Changed --> Affected
    Affected --> Eval[normal-form support evaluator]
    Neg --> Eval
    Trace[shared temporal support trace] --> Eval
    Eval --> Journal[one atomic producer/index journal]
    Journal --> Trace

The NAF arrangement stores exactly correlation key -> witness tuples. Existential bindings are not retained as another index level because execution only observes their support union. Positive tries store membership and keys; all interval support remains in the shared trace. Published witness buckets are borrowed during support evaluation rather than cloned per candidate; only a staged overlay materializes its changed witness view. The prepared journal validates the positive port count and NAF index count before publication.

Generated generalized-conjunction coverage runs 128 deterministic multi-key frontiers spanning positive and NAF membership insertion, deletion and support replacement. Every frontier is compared with fresh full evaluation, and each stream continues through checkpoint restoration. Recursive lower-stratum NAF coverage adds 96 generated graph frontiers spanning seed, edge and blocker membership and support changes, including cycles and checkpoint continuation.

Positive recursive temporal SCCs may read predicate NAF from completed lower strata. They use the same normal form, support evaluator, source-vector provenance, and transaction-local WCOJ/NAF arrangements. The lower relation is frozen during each SCC fixpoint. Positive-support growth and NAF-support shrink use sparse frontier settlement. Positive-support shrink, NAF-support growth, or mixed replacement refreshes the affected conjunctive producer, seeds changed output identities into causal overdelete/rederive, and resumes sparse frontier settlement. Cyclic self-support is removed before rederivation. Neither path invokes Established, clears the whole SCC, or duplicates interval support. The transaction-local reverse adjacency owns each contribution address once; per-input keys retain only integer indexes into that table, so an arbitrary-arity source vector is not copied for every incident input port. One producer-aligned settlement workspace lazily owns both plain multiway WCOJ state and generalized-conjunctive WCOJ/NAF state. Producer ordinal has one workspace authority rather than parallel vectors. Conjunctive input refresh is a separate transaction-local snapshot: one prior WCOJ/NAF working set advances atomically to the input-current snapshot, enumerates the union of prior and current affected source vectors, evaluates their current support, and is then discarded. A removed positive tuple probes only the prior roots, an inserted tuple probes only the staged roots, and a tuple absent from both probes neither. NAF correlation refresh descends one root set when positive membership did not change; identical prior/current WCOJ descent cannot produce another candidate. Causal overdelete changes recursive membership after that point, so the settlement workspace is built lazily from the post-overdelete trace rather than relabeling the input-current WCOJ as settlement-current. Both snapshots are discarded before publication and neither becomes retained relation authority.

flowchart LR
    Prior[prior committed trace] --> Refresh[refresh working set]
    Input[input-current trace] --> Refresh
    Refresh --> Candidates[old and new source vectors]
    Candidates --> Assign[assign current producer support]
    Assign --> Overdelete[causal overdelete]
    Overdelete --> Settle[post-overdelete settlement workspace]
    Settle --> Rederive[exact rederive and sparse frontier]
    Rederive --> Publish[atomic trace publication]
    Refresh -.discard.-> Drop1[transaction-local]
    Settle -.discard.-> Drop2[transaction-local]

Metric binary provenance is a typed left/right pair. Relational-join provenance is an atom-ordered source vector, including for two-atom joins. The distinct representation preserves aliased atom ports and extends to arbitrary arity without reinterpreting a metric contribution key. Both remain source keys under the same producer/output contribution map.

Ordinary and interval-valued join compilers call one canonical metadata builder for ordered atom ports, relation identities, columns, and first-occurrence variable order. The temporal compiler cannot drift from the ordinary WCOJ binding layout while its retained support representation remains independent. The interval join plan stores that atom vector and variable order directly; binary execution is a specialization over the first two atom ports, not a second left/right plan representation.

Binary temporal semantics are a stateless operator containing only the compiled tuple projection, window, and since/until operation. The acyclic component wraps that operator with retained operand indexes. A recursive producer stores only the stateless operator. Settlement constructs transaction-local pair indexes, multiway trie working sets, or reverse-dependency indexes from the staged trace and contribution provenance. Recursive state therefore cannot accidentally publish or checkpoint an acyclic index as a second membership authority.

Recursive binary differential coverage includes since and until, nonzero windows, two-round tuple propagation, support-only replacement, pair-collision survival, edge and seed retraction, removal of an unfounded cycle, checkpoint continuation, and iteration-bound failure followed by a successful retry at the same frontier. Every accepted result is compared with fresh Established evaluation; each test asserts the temporal circuit route so fallback cannot satisfy the oracle accidentally.

Recursive three-predicate coverage checks initial least-fixpoint construction, checkpoint reconstruction, causal edge retraction, exact interval support, and fresh-evaluator parity. It asserts the temporal circuit route. A 100-node, five-sample recursive multiway replacement measured circuit p50 1.596 ms and p95 2.003 ms versus full-evaluation p50 31.872 ms and p95 37.558 ms, a 19.97-fold p50 speedup. A 1,000-node control was terminated after two minutes in full evaluation and is not reported.

Acyclic relational-join coverage compares initial construction, support-only replacement, checkpoint restoration, and Cartesian many-to-one pair removal with fresh Established evaluation. Three-predicate coverage additionally checks checkpoint reconstruction, final tuple retraction, and survival of a colliding alternative contribution. Every case asserts the temporal circuit route.

The arbitrary-arity benchmark retains recent(x), anchor(x), and guard(x) arrangements and alternates valid-time support for base tuples. At 1,000 rows and one mutation, ten release samples measured p50 0.713 ms for the circuit and 21.737 ms for full evaluation, a 30.50-fold speedup. At 10,000 rows, one mutation measured 4.007 ms versus 1,876.521 ms, a 468.34-fold speedup; 100 simultaneous mutations measured 5.201 ms versus 1,885.681 ms, a 362.60-fold speedup. The circuit retained 30,000 trie keys and 10,000 producer cells. These are latency measurements, not retained-memory replacement evidence.

The predicate-exclusion benchmark retains aligned base(x) and excluded(x) arrangements and changes only excluded support. On an Apple M5 Pro, 1,000 rows and one mutation measured circuit p50 0.006 ms versus established p50 26.171 ms, a 4,052.51-fold speedup. One hundred simultaneous mutations measured 0.228 ms versus 27.636 ms, a 121.06-fold speedup. At 10,000 rows and one mutation, five samples measured 0.005 ms versus 3,790.854 ms, a 758,170.83-fold speedup; five samples expose scale but do not establish production percentiles. The established materializer scans negand tuples for every positive binding in this workload. A 100,000-row control was terminated without reporting a result. The circuit retains two operand arrangements and one producer contribution per left tuple.

Acyclic temporal transactions do not construct the recursive finite-materialization guard. Its data-extent calculation scans all direct support and is required only when a recursive component can extend endpoints; computing it for acyclic projection, join, or exclusion would turn sparse mutation latency into a function of total temporal state.

The native support-only benchmark retains 100,000 independently interval-stamped input tuples and alternates one tuple between two disjoint intervals. Each circuit sample applies one identified TemporalFactDelta; each control sample clones the base catalog and runs the established full temporal evaluation. Ten release samples on an Apple M5 Pro measured a one-rule circuit at p50 1,709 ns and p95 45,084 ns versus full-evaluation p50 217,735,333 ns and p95 223,634,792 ns: 127,405 times faster at p50. A four-rule chain measured circuit p50 4,917 ns and p95 48,333 ns versus full-evaluation p50 566,281,542 ns and p95 588,555,625 ns: 115,168 times faster at p50. This isolates admitted unary support-only shapes; it does not estimate binary, recursive, event-scan delta assembly, or new-identity fallback costs.

A 20-sample many-to-one control retained 100,000 two-column input tuples, projected groups of 100 sources to one output, chained that output through a second unary temporal rule, and changed one source interval per sample. Circuit p50 was 16,917 ns and p95 24,250 ns versus full-evaluation p50 147,272,375 ns and p95 149,163,916 ns: 8,706 times faster at p50. This measures source provenance aggregation and downstream support propagation, not membership-only short-circuiting.

A 100-sample binary since control retained 1,000 aligned tuples in each operand and changed one left support per sample. Circuit p50 was 2,834 ns and p95 4,792 ns versus full-evaluation p50 39,562,292 ns and p95 40,140,792 ns: 13,960 times faster at p50. Changing all 1,000 left supports in each transaction measured circuit p50 3,135,042 ns and p95 4,201,125 ns versus full-evaluation p50 40,652,958 ns and p95 42,024,084 ns: 13.0 times faster at p50. Both runs retained 2,000 arranged tuples, 2,000 join-key buckets, 1,000 producer cells, and 1,000 sink tuples. The full evaluator’s current binary materializer uses a nested operand join, so a 100,000-row control is quadratic and was not a practical comparison; the circuit uses shared-variable keyed arrangements.

A ten-sample acyclic relational-join control retained 1,000 aligned tuples in the temporal input and ordinary anchor relation, then changed one input support per sample. Circuit p50 was 0.247 ms and p95 0.288 ms versus full-evaluation p50 46.350 ms and p95 49.262 ms: 188 times faster at p50. The circuit retained 2,000 arranged tuples, 2,000 join-key buckets, and 2,000 producer cells across the temporal image and join. A 100,000-row control did not complete one bounded comparison within 90 seconds because the established temporal materializer uses a nested relational join; it was terminated and is not reported as a speedup.

A 100-sample recursive identity control retained 10,000 base tuples and the same 10,000-tuple recursive closure, then changed one base interval per sample. The exact affected-SCC rebuild measured circuit p50 94.274 ms and p95 96.698 ms versus Established p50 22.136 ms and p95 23.214 ms: the first recursive backend was 4.26 times slower. After retained causal deletion replaced mutation-time reconstruction, the same 100-sample shape measured circuit p50 1.351 ms and p95 1.411 ms versus Established p50 22.404 ms and p95 25.559 ms: 16.59 times faster at p50. Construction and checkpoint recovery retain exact reconstruction as an independent correctness path.

For monotone recursive growth, 100 insertions into a retained 10,000-row recursive identity workload measured circuit p50 1.302 ms and p95 1.331 ms versus Established p50 21.718 ms and p95 22.117 ms: 16.68 times faster. A 20-insertion 100,000-row run measured circuit p50 14.249 ms and p95 14.545 ms versus Established p50 258.070 ms and p95 268.001 ms: 18.11 times faster. Both runs add one new direct tuple per frontier and compare the final model with fresh evaluation. Growth and shrink now use the same retained contribution representation; only their settlement algorithms differ.

A 20-sample recursive binary chain control retained one seed, 99 temporal edges, and a 100-tuple closure, then changed the seed interval. Exact SCC reconstruction originally measured circuit p50 16.546 ms versus Established p50 14.759 ms. Retained causal deletion with a transaction-local binary reverse dependency index measured circuit p50 0.723 ms and p95 0.800 ms versus Established p50 15.357 ms and p95 15.559 ms: 21.25 times faster at p50. The circuit retained 100 producer cells and 100 sink tuples.

A three-sample 1,000-tuple chain measured circuit p50 1,776.563 ms and p95 1,798.802 ms under exact reconstruction versus Established p50 9,943.131 ms and p95 10,013.046 ms. Retained causal deletion subsequently measured circuit p50 8.282 ms and p95 8.662 ms versus Established p50 10,064.654 ms and p95 10,085.062 ms: 1,215 times faster at p50. The retained result has only three samples and is a scaling signal, not a production latency gate.

A five-sample recursive relational-chain control routes a [0,0] diamond_plus seed into an ordinary recursive two-predicate join and alternates the seed support. At 100 nodes, circuit p50 was 1.111 ms versus Established p50 12.785 ms: 11.51 times faster. At 1,000 nodes, circuit p50 was 8.101 ms versus Established p50 10,704.844 ms: 1,321 times faster. Both runs compare the final model with fresh evaluation. Five samples are sufficient to expose scaling, not to establish a production latency percentile.

Temporal deltas are support differences, not raw event intervals. For every touched (relation, tuple), runtime projects both the pre-commit and post-commit live-event sets through the same concept and relation closure used by full catalog seeding. It coalesces all supporting intervals, then emits PRE - POST and POST - PRE. This preserves overlap supported by duplicate assertions and handles a replacement assertion atomically. The materialized model cannot supply pre-commit base support because its interval catalog may already contain derived support; durable base events or retained base-support state are the only authorities.

The evaluation clock is an explicit retained input. Compilation rewrites every today() in one rule to one fresh variable and prepends the reserved singleton scan $evaluation_clock(day). The rewrite is idempotent and refreshes every enclosing NAF projection mask. Runtime captures one day per operation and installs, refreshes and checkpoints the singleton through the same catalog and transaction carriers as user facts. User deltas cannot write reserved input relations.

runtime operation captures EvaluationInputs(day)
                    |
                    v
       normalize today() -> clock variable
                    |
                    v
    canonical $evaluation_clock(Date(day)) singleton
                    |
        +-----------+-----------+
        |                       |
        v                       v
 Established off-path     circuit signed delta
 rebuild on rollover      -Date(old), +Date(new)
        |                       |
        +-----------+-----------+
                    v
       one published materialized model
                    |
                    v
 checkpoint identity binds evaluation day

Clock changes for ordinary rules are signed circuit transactions. An enumerate-derived rule reads the clock while materializing its frozen EDB, so a clock change rebuilds that EDB and the circuit off-path under the same captured day before publication. Warm columnar state remains the sole frozen model representation; its maintainer reads the clock singleton from that columnar model rather than retaining a duplicate day field. Aggregate subpipelines admit predicate scans, comparisons, computed bindings and predicate negation. Negation lowers through the same sealed antijoin constructor used by an outer rule body, so schedule metadata, relation arity, join keys and nonmonotone boundaries cannot diverge between the two contexts. Nested aggregates, modal atoms and temporal atoms inside an aggregate remain typed whole-program refusals. Supported well-founded components select Product, audited Product or Established gamma through explicit maintenance policy. Unsupported constructs are typed compile refusals; no rule is silently omitted.

Verification

The generated Lean differential evaluates 1,600 cold programs and 16,000 retained incremental-route frontiers. Every history retracts a fact read by an enabled rule and crosses checkpoint restoration; ordinary incremental and Product must match Lean’s true and undefined extents at every frontier. The authoritative differential passed in 88.44 seconds. The complete oxc-reasoning suite passed 1,302 tests with one skipped in 105.90 seconds; strict all-target, all-feature Clippy and formatting passed. Fixed-vector and generated-history replay use one shared K3 model comparator; conformance paths cannot independently drift on truth or undefined diagnostics.

A post-correspondence bounded rollout probe retained 10,000 independent game states and alternated one edge for 128 mutations. Product measured 0.063 ms p50 versus 3.899 ms Established, a 62.0-times speedup. Product final retained heap was 0.900 times Established and mutation peak was 0.476 times Established. Product construction peak remained 1.372 times Established; construction is therefore still a rollout optimization target, not a retained-state regression.

Program flow transfers each accepted profile into its final consumer. The record-backed Product evaluator consumes that owned profile directly; only the borrowed kernel-test convenience path clones it. This removes a redundant full profile copy without adding a second retained representation. Positive-recursive components likewise consume the Product-time profile and move its signed buckets into recursive logical time. Component outputs needed by later strata remain profiles in program flow; terminal settled and candidate outputs are consolidated directly into record extents as each component finishes. The revision driver therefore receives its native extent representation instead of retaining and subsequently converting a second whole-program profile map. The rule-session transaction retains physical operator journals, but it does not retain cumulative copies of settled and candidate output profiles: production propagation consumes each stage result immediately. Cumulative prepared-result accessors remain test-only for kernel assertions and are absent from release construction. Product program flow is representation-generic. Production interns external semantic facts once at the program boundary, then carries record identities through acyclic components, positive-recursive components and downstream strata. Gamma-assumption changes enter the same flow as record profiles under their compiled shadow-relation identity. They are no longer decoded to tuples, rebuilt as semantic profiles and re-interned before evaluation. The test-only physical-method audit resolves record inputs back to semantic tuples so the independent semantic backend still checks the production record path. In bounded 10,000-value construction probes, factorized-min Product peak heap fell from 29,118,641 to 23,945,202 bytes and edge-replacement Product peak heap fell from 35,206,998 to 27,353,955 bytes. Removing the production output mirrors first reduced edge replacement from 33,944,177 to 32,355,519 bytes; record-native flow accounts for the remaining reduction. Factorized-min Product construction is 0.875 times the corresponding 27,349,240-byte Established peak. Edge replacement still retains three staged gamma-role journals during atomic construction and remains 1.095 times its 24,990,650-byte Established peak.

Product temporal antijoin preparation reads retained state through a validated key-window view. The view checks physical key metadata once, then materializes one affected key while differentiating it; it does not copy the complete affected retained window. Left corrections are also the sole source for their arrangement journal instead of being accumulated into a parallel assignment map. These are representation and lifetime changes: the same antijoin derivative, temporal support trace and atomic journal remain authoritative. On the 10,000-game, 128-mutation edge-replacement control, Product construction peak fell from 27,353,955 to 23,535,563 bytes and is 0.942 times the 24,975,690-byte Established peak. Product p50 was 0.060 ms versus 3.907 ms Established, a 65.4-times speedup. Final retained heap was 4,184,481 bytes versus 4,651,381 bytes Established, and mutation peak was 4,622,199 bytes versus 9,725,441 bytes. The 10,000-value factorized-Min control measured a 23,938,898-byte Product construction peak versus 27,346,584 bytes Established, with 0.937 ms Product p50 versus 3.196 ms Established. Streaming retained antijoin windows therefore removes the remaining measured Product construction premium on both bounded controls without adding another state representation.

Every deterministic and generated mutation stream compares the integrated circuit output with a fresh full evaluation after every frontier. Gates also cover batch permutation, cancellation, checkpoint/restore, module commitment changes, arithmetic overflow, operator failure and the circuit kill switch. Integrated WFS and Product differentials live in a dedicated program-test module; generic program transactions, recursive strata and flow tests remain in the parent module. Ledger, catalog and record-resolution fixtures remain shared from one parent definition rather than copied between suites.

The admitted replacement matrix also constructs an Established maintainer for the same rules, initial EDB and mutations. Join, direct-head overlap, antijoin, comparison filters, computed map/projection, individual constants, repeated predicate variables, constant heads, all numeric aggregates, bare-count existence, constant aggregate projections, aggregate-local filter/map pipelines, aggregate-local predicate negation, factorized outer-only negation, unary/binary/conditional/builtin scalar expressions, compact date arithmetic, text indirection, undefined-expression row removal, union support, physical multiway join, generated mixed acyclic mutations and named recursive support transitions compare Established, circuit and fresh evaluation after every frontier. Each circuit assertion pins its selected route, and each Established assertion pins the disabled-circuit route. Product-specific mutation and restore coverage includes constant heads, constant recursive arguments, repeated predicate variables, compact dates, text indirection, symbolic identity, conditional partial expressions, undefined-row removal, exact rounding and calendar builtins. Nested zone-aware and wall-clock chains cover instant parsing, time-zone validation and conversion, DST-aware zoned-day shifts, plain-datetime shifts, civil date/time recomposition and wall-to-zone conversion across mutation and checkpoint restoration. A loud scalar type error is also required to preserve the prior frontier, model and byte-identical checkpoint. Product aggregate coverage includes bare-count existence transitions and a filtered/computed projected Sum over exact rational inputs across checkpoint restoration. Predicate-free compute, empty-body constants, global negation, leading numeric aggregation and computed-prefix joins additionally compare scalar circuit, program circuit and fresh evaluation. Checkpoint restoration occurs both while global negation is blocked and while constant support is visible. A recursive constant base rule exercises singleton emission inside nested-time evaluation. The Established delta-seeded inserter requires a leading predicate; its route selection therefore uses full recomputation for predicate-free prefixes rather than evaluating a different body order. Scalar NAF coverage includes positive and double-negated comparisons, defined and undefined computations, aggregate-local scalar negation, mutation and checkpoint restoration. Division by zero pins the distinction between DoesNotSatisfy and ordinary Boolean inversion. Modal coverage includes diamond witnesses, universal box support, open-world missing successors, dead-end vacuity, accessibility insertion and retraction, proposition insertion and retraction, simultaneous edge/proposition changes, last-payload-domain removal, nullary payloads and checkpoint restoration. Nested predicate NAF coverage checks multiple existential witnesses, removal of one versus the last witness, different correlation keys, aggregate-local existence without witness multiplication, mutation and checkpoint restoration. Catalog comparisons remove empty relation entries because Established retains those storage entries while circuit projections omit them; tuple contents and relation semantics must otherwise be exact. Circuit admission first runs the Established full-program open-world taint guard. It then reduces a top-level NAF chain ending at an effectively open predicate to the same constant as Established: odd depth is false and removes the rule; even depth is true and removes the atom. Other producers of the same head and downstream positive consumers remain in the circuit. The same reduction applies to conjunction-level atoms inside a numeric aggregate body. An odd-depth atom becomes an explicit typed relational zero; even depth removes the identity atom. This preserves empty-input Count, CountDistinct, Sum, Avg, Min, Max and bare-existence behavior without aggregate-specific retained state. Other embedded open-world NAF shapes still force a typed whole-program refusal. Build and checkpoint restoration use the same normalized execution rules under the original accepted program’s module identity. Distinct source programs that normalize to the same operators therefore cannot share checkpoints, and changing effective world assumptions rejects an incompatible checkpoint. Retained plans use a canonical statistics-free body order: every base relation has the same neutral structural cost and derived relations remain unknown. Full evaluation may choose a fresh SIP order from current relation sizes, but mutable catalog cardinalities cannot change a retained plan commitment between construction and checkpoint restoration. Source-corpus recovery after asserting into an initially empty join input is the regression gate for this boundary. Negative admission controls pin fallback for remaining open-world NAF shapes and nonpositive relation weights. Positive, modal and explicitly closed-NAF programs admit catalogs containing unrelated open-world concepts. Any positive input support is one live logical fact in both engines. Record-backed base projection retains its exact integer weight, so construction, mutation and checkpoint restoration preserve the Established catalog rather than collapsing publication to weight one. Valid-time metadata is retained by the projection even when the rule program has no temporal operator: temporal-only durable commits update the pass-through sidecar without changing circuit membership, and checkpoint restoration reconstructs the same metadata. Production route controls require the circuit route for the supported explicit modal and metric-temporal fragments. Language-invalid temporal mixtures, non-numeric aggregates and nested aggregates retain defensive typed whole-program refusal coverage at the internal compiler boundary; they are not production source-program fallback cases. Aggregate-local negation instead requires the circuit route and compares all six numeric folds, bare existence, existential blocker support, atomic blocker replacement and checkpoint restoration with Established and fresh evaluation.

Inline-set values are canonicalized to sorted unique values when rules compile; catalog seeding repeats that canonicalization defensively for older decoded rules. Seeding replaces the reserved relation with its exact content-addressed extent instead of trusting preexisting state. Individual references enter the catalog identity space at this boundary, so inline membership and ordinary fact bindings use the same representation. The content-addressed relation name, stored values and seeded relation therefore describe one set by construction. Duplicate literals cannot create a non-set weight, change admission according to rule order or force a catalog fallback. Differentials exercise duplicate and individual inline-set values through Established, ordinary circuit and Product routes across retraction and assertion.

The compiled ontology corpus runs three independent maintainers from the same elaborated rules and EDB: Established, Established-WFS circuit preference, and Product-WFS circuit preference. Every admitted package compares all three with fresh evaluation after initialization and after mutating every compiled input relation through retraction, repeated absent retraction, reassertion and duplicate assertion. The semantic comparison includes tuple weights, explicit valid-time support, metric resolution and every program-observed world assumption. Temporal packages additionally replace and restore support without changing membership. Every admitted plan checkpoints after its first input sequence, restores under the same required route and continues the remaining stream. Multi-input packages also execute one atomic cross-relation replacement and restoration. The current corpus admits 32 packages and exercises 188 input sequences, including nine seeded inputs, plus 25 atomic multi-input sequences; one package has no compiled rule input. The identity-equality package proves the diagonal result, then retracts and reasserts the concept extent through Established, circuit, Product and fresh evaluation. Both recursive corpus packages route through the recursive circuit under Product. The same differential runner accepts an explicit external corpus root without changing engine semantics. Corpus discovery, artifact resolution, semantic comparison, differential-session ownership, mutation scenarios and orchestration are separate test modules. The session alone owns the three maintainers, EDB, generation and checkpoint lifecycle, so a scenario cannot advance one engine without the others. External artifacts mutate seeded tuples only because compiled terms do not retain enough source type information to synthesize valid values. The Sharpe ontology corpus at revision 9937171e7e contains 17 manifests: 11 build under the current compiler, require RecursiveCircuit and pass reversible mutation parity; six fail source compilation before maintenance selection. Its seven authoritative ontology test packages also pass 868 source tests. Release mutation timing is workload-dependent. Sharpe core is 14.96 times faster than Established, and the five authoritative packages other than market-calendar are approximately 21.9 times faster in aggregate. The market-calendar $arm sequence previously remained 3.85 times slower after transient-key optimization because calendar-roll lowering produced approximately nine million distinct Cartesian record pairs before a semantic equality filter. Generic compiled-rule variable aliasing is forbidden: semantic equality can equate different numeric and temporal representations, while join identity is structural.

Equality normalization therefore runs during elaboration, before type evidence is erased. It uses a second variable-sort analysis that does not propagate a sort through the equality being removed. Only two variables with independent domain-individual evidence are unified; numeric, temporal, reflective, unknown and conflicting sorts retain their comparison. Nested scopes are excluded. An unrelated computed binding may coexist with normalization, while a compute that reads or writes an aliased variable blocks it. Surface rule identity is captured first, identical predicate conjuncts created by the alias collapse to one, and ordinary alpha canonicalization runs afterward. Predicate deduplication is restricted to predicates touching the new alias; unrelated duplicate conjuncts remain unchanged.

flowchart LR
    S[Resolved rule with type guards] --> P[Independent sort evidence]
    P -->|both domain individuals| U[Unify variables and remove equality]
    P -->|otherwise| F[Retain semantic equality filter]
    U --> A[Alpha canonicalization]
    F --> A
    A --> W[Shared rule artifact]
    W --> E[Established evaluator]
    W --> C[Incremental circuit compiler]

The shared lowering prevents engine-specific semantic drift. The measured market-calendar sequence is 0.338 seconds circuit versus 1.690 seconds Established, a 5.00-times speedup; the preceding circuit run was 21.600 seconds. Across all 11 buildable Sharpe ontology packages, aggregate mutation time is 1.375 seconds circuit versus 11.698 seconds Established, an 8.51-times speedup, with no route fallback or differential mismatch. Circuit construction remains 2.89 times Established across that corpus and is tracked separately from steady-state mutation latency.

The primary circuit selector uses require-circuit mode: catalog or compiler refusal fails construction rather than silently substituting Established. Prefer-circuit mode remains the production rollout policy where correctness fallback is required.

Admission completeness is defined at the production source boundary, not by the set of defensive errors exposed by the scalar circuit compiler. Every source-valid rule program that Established can evaluate is a circuit-parity obligation when every stored catalog fact has positive support. Raw temporal and modal atoms are dispatched to program components before scalar compilation; today() is normalized to the explicit evaluation-clock relation; collection aggregates, temporal well-founded recursion, modal companions, and temporal mixtures with scalar or aggregate operators are rejected by the language or Established before circuit admission. Their internal compiler errors protect layering invariants and are not evidence of a supported-program fallback. Negative catalog weights remain a distinct state-compatibility refusal because they do not denote set-valued source facts. Catalog admission preserves that distinction as typed evidence: nonpositive support and a residual open-world negation observation cannot collapse into one generic fallback reason. Signed catalog admission requires exact weight transitions through scans, joins, distinct support, publication and checkpoint recovery; filtering negative weights at the circuit input would not match the Established weighted evaluation semantics. Tests of internal refusal shapes must identify themselves as defensive-boundary tests; production parity tests must enter through source elaboration or the whole-program selector and assert the selected route.

Required negative and transition coverage includes:

  • unsupported atom, producer and mixed-program refusal with no circuit state;
  • duplicate batch idempotence, conflicting duplicate, frontier gap and stale plan rejection;
  • bilateral and aliased join deltas in one batch;
  • multiple rule support, cancellation, support overshoot and zero crossings;
  • antijoin left-only, right-only and simultaneous zero-crossing changes;
  • every aggregate under insert, retract, value replacement and empty-group transitions;
  • recursive cycle, diamond, alternative-support, last-support and retract/reassert streams;
  • failure after durable append followed by retry, checkpoint replay and full rebuild; and
  • module hot swap with concurrent appended batches.

Tests assert route evidence so an oracle cannot pass while silently using full recomputation. The kill switch routes the whole program through the full evaluator without reading or mutating circuit state.

Steady-state benchmarks time delta application only. Parsing, compilation, process startup, state cloning and destruction are reported separately. Each result records total model size, affected frontier size, emitted delta size, arrangement probes, retained-state bytes and mutation-to-visible-frontier latency.

The evaluation-input benchmark isolates dependency selectivity: copy(x) materializes a large clock-independent relation while evaluation_day(d) alone depends on today(). It alternates two evaluation days after both maintainers are constructed. On an Apple M5 Pro with an optimized build, 100,000 retained rows and 100 rollovers measured 12.292 us circuit p50 versus 100.701 ms Established p50 (8,192x). At 1,000,000 rows and 20 rollovers it measured 14.625 us versus 1.234 s (84,385x). The circuit work is proportional to the one affected clock-dependent output; Established reevaluates the million-row unaffected copy. Reproduce with:

cargo bench -p oxc-reasoning --bench incremental_evaluation_inputs \
  --features bench-support -- --rows 1000000 --samples 20

Existing work to reuse

The current arrangement implementation supplies canonical join-key construction, bucket probing and decode-sharing techniques. It must be refactored behind a signed persistent API; its set-membership and caller-managed eviction semantics cannot be copied unchanged.

The current lockstep mutation tests supply the per-frontier full-recompute oracle, route evidence, kill-switch pattern and transitive-closure workloads. The current delete/rederive maintainer remains a differential and performance baseline.

Conservative dependency analysis supplies the required refusal discipline: unknown producers or operators force fallback. Negation memoization supplies referenced-variable projection tests, but persistent antijoin requires signed right-support counts rather than cached Boolean verdicts.

RFD 0086 — Admission-point checks: honest verdicts over undetermined membership

  • State: committed. The decision is fixed; implementation has landed in part on a branch and not yet on the default branch — the honesty gate (Slice 1) is implemented in pull request #1826 and is the base the remaining slices build on. Per-slice status is marked under Staging, and every transcript in this record says which binary produced it.
  • Opened: 2026-07-23
  • Decides: two user-facing semantics for a check whose violation body reads membership in a computed concept (an iff/shape-defined concept, or an asserted descendant of one). (a) Admission scoping — a membership-gated check is judged at the admission point of the data it governs (the valid-time instant a write asserts from, or the all-times view for an unqualified write), not against a single global snapshot; each check has a firing closure naming which writes and events can trigger it, so a write outside every closure runs no check discharge. (b) The honesty gate — when a membership-gated check’s verdict for a tuple hinges on membership the well-founded fixpoint leaves undetermined (three-valued Can — a negation/defeat standoff or an open-world absence), the check does not silently pass and does not fire as a false accusation; the outcome is a first-class inconclusive verdict whose disposition is decided per check, derived from the check’s declared severity: a blocking check refuses the write (fail-closed, OE1430), and a reporting check reports at undefined grade. A point-free judgment of membership that varies by valid-time point is refused as having no single honest answer. That refusal is unimplemented and deliberately carries no diagnostic code here; the slice that implements it allocates one.
  • Affects: the runtime write gate and on-demand check surfaces (oxc-runtime); the check-rule registry (RFD 0081) — the runtime honesty gate enters as an audited inventory row; the well-founded-semantics mechanization (spec/lean/Argon/Reasoning/Checks.lean). No grammar change and no Core-IR extent representation change — the gate reads the existing three-valued model, and the disposition comes from a severity the modeler already writes.
  • Builds on: RFD 0025 (check discharge; K3 “fire on is only”), RFD 0017 / RFD 0070 (defined-concept membership), RFD 0063 (the bitemporal read point), RFD 0081 (the check-rule registry), and the defeat plane’s undefined-by-decision cycles (RFD 0028).

In plain language

Argon lets you declare a check — a rule that says “this must never be true; if it is, refuse the write, or at least report it.” Separately, Argon lets you declare a concept whose membership is computed rather than typed in: an Adult is a Person iff age >= 18; an employee has badge access iff a chain of rules says so. Membership in such a concept is derived from evidence.

The problem this RFD closes: what does a check mean when its body asks “is x a member of computed concept C?” and the answer is not yet decided? Argon’s semantics are three-valued and open-world. A membership question can come back undetermined — the evidence is absent, or the classifying rules sit in a negation/defeat standoff that the well-founded semantics resolves to undefined (Can), not to true or false. Two naive behaviors are both wrong:

  • treat undetermined as “not a member” and fire the check → a false accusation (“you have no badge access, therefore you violated the badge-required rule”) when the truth is simply unknown;
  • treat undetermined as “member, no problem” and let the write commit → the system admitted a write whose legality it could not actually decide.

Today’s Argon does the second thing, silently. (See Context — this is measured, not assumed.) Admission-point checks make a third choice: undetermined is a first-class verdict, and each check declares what to do with it. A blocking check refuses the write, loudly, and tells the modeler to establish the classifying evidence or break the standoff. A reporting check reports it at undefined grade without blocking. Which of the two a check gets is read off the severity the modeler already declared — there is no new annotation to learn. That much is implemented and executed below.

The second half is when membership is judged. A computed concept’s extent changes over valid time (someone becomes an Adult on a birthday; evidence carries a validity window). A membership-gated check must therefore be judged at the write’s own valid-time point — the instant the write asserts from — not against the union of every valid time at once. When a modeler asks such a check on the all-times view and the membership genuinely varies by point, there is no single honest answer, and that is refused too, pointing the modeler at a specific point to ask instead.

Everything else — checks whose bodies touch no computed membership — is untouched. They keep the classic commit-time behavior exactly.


When an undecided membership stops you, and when it does not

The whole behaviour change lands at one moment: a write. Nothing about it is a build-time or check-time refusal, and none is proposed.

MomentTodayUnder this record
ox checkpassesunchanged — passes
ox buildwrites the artifact, warning about undefined headsunchanged — the program is admitted, which is what lets the check run over an undecided read at all (transcript in Context)
A write whose blocking check hinges on an undecided membershipcommits, degrading the violation to an information-grade noterefused (OE1430); nothing commits
A write whose check is Warning / Info / #[observe]commits, reportsunchanged — commits, reports
A read (query, derive)unaffectedunchanged — this is a write-path gate

Exactly one cell moves. Three properties bound it:

  • The declared severity chooses. Refusing versus reporting is decided by the word already on the check (D3). There is no new switch, and no global policy.
  • Only what the write introduces. A tuple already undecided before the write cannot block it — the gate differences pre against post, so a standoff elsewhere never wedges an unrelated write (D3, delta discipline).
  • The refusal is atomic. It is judged against a throwaway post-state overlay, so on refusal nothing has reached the committed store.

What this deliberately cannot give you: advance warning. Whether a check becomes undecidable depends on the data, not the program, so no amount of build-time analysis can tell a modeler “this check will stand off.” The design accepts that and spends its guarantee on the write instead — a loud refusal naming the check, the subject and the undecided membership, in place of a silent commit under a constraint nobody evaluated.

Question — what a modeler cannot state today

The distilled index of the examples below. Each row is one thing a modeler wants to state; LEFT is the closest the default branch gets, verified against the re-run probe in the next section; RIGHT is what the design gives, tagged with implementation status.

Wants to sayClosest on the default branchWhat the design gives
Refuse a write whose governing check cannot be decided.Inexpressible — the write commits. The Severity::Error violation over undetermined membership degrades to an observe note (☞ [undefined]) with no gating effect. Measured; transcript below.Implemented. A blocking check refuses the write, OE1430. Executed transcript below.
Report an admitted write whose governing check was undecidable, distinguishably from a clean pass.Already expressible — a reporting-severity check over undetermined membership emits an ☞ [undefined]-graded row naming the check, and a clean pass emits nothing. An earlier revision of this record claimed this was inexpressible; that claim was wrong and is corrected here.Implemented as the reporting half of the severity-derived default. The change is that the row now has a decided disposition behind it rather than being the only possible outcome.
Discharge this check exactly when the data it governs is written, at that data’s valid time.Whole-store commit-time re-litigation only — a dated write is judged against the union of all valid times, and every check re-litigates on every write.Design, not yet implemented. Per-check firing closures plus admission-point (point-scoped) discharge (D2).

Context — the probe, re-executed

The soundness claim above is measured, not assumed, and it was re-measured for this revision against a compiler built from the current default branch. Every transcript in this record appears in this section, and each is verbatim output from that binary, or from the honesty-gate branch’s binary where a transcript is labelled as such. No transcript in this record is reconstructed from memory. Artifact paths are shown package-relative for readability; the runs used an absolute path, and where a long diagnostic is cut the cut is marked .

What changed since the first revision. The re-run reproduced the defect, but three surface details in the original transcript no longer hold and are corrected here: source files carry the .ar extension (the original probe was written .ox); diagnostics and mutation names now render module-qualified (badge::badge_required, not badge_required); and the hiring write emits three events rather than one, because this probe’s inserted individual carries two mutable fields and each field assignment is its own event (the original inserted a fieldless individual). Event counts in these transcripts are a function of the program, not of the defect. One structural detail also changed: a content-carrying file may no longer open with mod NAME; to name itself — a module takes its name from its file stem, and mod NAME; now means only “a sibling NAME.ar exists”. The probe is therefore laid out as a package.

The program

A package with ox.toml naming src/root.ar as its entry. src/root.ar holds nothing but mod badge; mod report; mod strict;. The three modules are the three exhibits: badge carries the undetermined case, strict the two definite cases, report the reporting-severity case. All were built and run as one package; per-module sources are shown where each exhibit is discussed.

badge puts a computed classification in a negation cycle so that the well-founded semantics genuinely cannot settle it. keys_a (“has badge access”) holds by default for an employee, unless conflict_a; conflict_a holds exactly when keys_b does; keys_b holds by default unless conflict_b; and conflict_b reads keys_a back. No head directly defeats itself, so the defeat graph is acyclic and the program is admitted — but the rule-dependency graph cycles through negation, so neither head has a stable two-valued warrant and both surface undefined for every employee. This is the documented way to construct a genuinely inconclusive membership; it is not a contrived error state.

// src/badge.ar
use std::core::type;

pub type Person { mut employee: Bool, mut payrolled: Bool }
pub type Employee <: Person iff { self.employee == true };
pub type Payrolled <: Person iff { self.payrolled == true };

#[default]
pub derive keys_a(p) :- Employee(p);

#[defeats(keys_a(p))]
pub derive conflict_a(p) :- keys_b(p);

#[default]
pub derive keys_b(p) :- Employee(p);

#[defeats(keys_b(p))]
pub derive conflict_b(p) :- keys_a(p);

pub derive on_roster(p) :- Employee(p);

pub derive in_payroll(p) :- Payrolled(p);

pub query roster() -> on_roster;

pub query badge_holders() -> keys_a;

pub check badge_required(p: Employee) :-
    Employee(p), not keys_a(p)
    => Diagnostic {
        severity: Severity::Error,
        code:     "Badge::E001",
        message:  format!("employee {} has no badge access", p),
    };

pub check roster_member_is_payrolled(p: Employee) :-
    on_roster(p), not in_payroll(p)
    => Diagnostic {
        severity: Severity::Error,
        code:     "Payroll::E004",
        message:  format!("roster member {} is absent from payroll", p),
    };

pub mutate hire_bob() { insert Person { employee: true, payrolled: true }; }

The compiler admits the program — the first thing worth reporting, because it means the check really does get to run over an undetermined read rather than being refused beforehand. It warns, at length, that the two heads are undefined:

$ ox build .
workspace: elaborated 4 module(s) into one artifact
  ⚠ warning: OW0740: the public heads `badge::keys_a`, `badge::keys_b` form a
  │ mutual-defeat cycle: each head's live attacker derives, in turn, from that
  │ same head through its positive support (a rule-dependency negative cycle
  │ through defeat). This is NOT the identity cycle of two heads directly
  │ defeating each other (refused earlier by OE0718) — the defeat graph here
  │ is acyclic. Such a cycle has no stable two-valued warrant, so under the
  │ well-founded semantics these heads are UNDEFINED (neither definitely true
  │ nor definitely false) and surface as `Can` rows rather than a definite
  │ `Is`/`Not`. This is the canonical semantics, not an error — no evaluation
  │ order changes it. The warning names the mutually-defeating heads so an
  │ author expecting a definite verdict knows they read `Can`. To get a
  │ … (the remainder of the warning explains how to break the cycle)

wrote ./target/root.oxbin (54 events, 47184 bytes)

What the default branch does with an undetermined read — verbatim

Running the hiring write, and enumerating the declared queries with their four-valued row tags:

$ ox query target/root.oxbin --mutate badge::hire_bob --with-truth4
Badge::E001

  ☞ Badge::E001: [undefined] employee #i1 has no badge access [#i1]
  help: fired by check `badge::badge_required`

ran badge::hire_bob: 1 ops, 3 events emitted
query badge::badge_holders: 1 row(s)
  [Can] (#i1)
query badge::roster: 1 row(s)
  [Is] (#i1)
query report::clearances: 0 row(s)
query report::roll: 0 row(s)

Reading it. badge_holders enumerates keys_a, and its one row is tagged [Can] — the undetermined verdict, confirmed at the read surface rather than inferred. badge_required is declared Severity::Error, and its body not keys_a(p) is therefore also undetermined for #i1 — not definitely true. Under RFD 0025 D4 (“fire on is only”) an undefined violation does not fire, so it never reaches the D2 delta guard that a Severity::Error check blocks on (D3: severity drives blocking, and only a firing violation guards); it is delivered instead through the observe channel (RFD 0025 D6) — the residue the well-founded-semantics mechanization names undefinedObservations. So the violation surfaces as one [undefined]-graded note ( is the observe channel) — and the mutation commits (3 events emitted).

That is the hole. The check governs exactly this write; its verdict for this employee hinges on a membership the fixpoint left undetermined; and there were two honest options — refuse the write, or report the undecided verdict at a grade that gates — and it took neither. It admitted the write, and the Severity::Error the modeler wrote had no gating effect on the undetermined tuple.

The same check, the three determinacy cases, side by side

The claim needs a control: the gate is not simply absent. src/strict.ar is the same check shape and the same severity over a classifier that does settle — has_badge follows from a plain computed Cleared, with no cycle:

// src/strict.ar
use std::core::type;

pub type Worker { mut employee: Bool, mut cleared: Bool }
pub type Staff <: Worker iff { self.employee == true };
pub type Cleared <: Worker iff { self.cleared == true };

pub derive has_badge(p) :- Cleared(p);

pub check staff_badge_required(p: Staff) :-
    Staff(p), not has_badge(p)
    => Diagnostic {
        severity: Severity::Error,
        code:     "Badge::E002",
        message:  format!("employee {} has no badge access", p),
    };

pub mutate hire_unbadged() { insert Worker { employee: true, cleared: false }; }
pub mutate hire_badged()   { insert Worker { employee: true, cleared: true }; }

A definite violation is refused, on the default branch, today:

$ ox query target/root.oxbin --mutate strict::hire_unbadged --extent strict::Staff
Badge::E002

  × Badge::E002: employee #i1 has no badge access [#i1]
  help: fired by check `strict::staff_badge_required`

Error:   × mutation `strict::hire_unbadged` rejected by check guard: 1 new
  │ violation(s) at `Severity::Error`

A definite non-violation commits, as it should:

$ ox query target/root.oxbin --mutate strict::hire_badged --extent strict::Staff
ran strict::hire_badged: 1 ops, 3 events emitted
extent(strict::Staff): 1 individual(s)
  #i1

So the write gate works and refuses on the error severity. Hold severity, check shape, and write shape fixed, vary only whether the classifier settles, and the third case — the badge module’s transcript above — silently commits. The defect is precisely and only the undetermined case; nothing about the gate’s plumbing is at fault.

The honesty gate on the same program — verbatim

The surviving half of this record is implemented on the honesty-gate branch (pull request #1826). Built from that branch and run against the identical package:

$ ox query target/root.oxbin --mutate badge::hire_bob --with-truth4
OE1430

  × OE1430: check `badge::badge_required` cannot be discharged for #i1: its
  │ firing verdict depends on membership `badge::keys_a` left undetermined
  │ (`Can`) by the well-founded semantics [#i1]
  help: fired by check `badge::badge_required`

Error:   × mutation `badge::hire_bob` rejected by check guard: 1 new violation(s) at
  │ `Severity::Error`

The refusal names the check, the subject, and the membership that could not be decided. It arrives through the same write-gate rejection channel as the definite violation above — the outer “rejected by check guard” line is identical in both — so a caller already handling a refused write needs no new error path, and assert rejects(OE1430) pins it in a negative-enforcement test. The write commits nothing — the command exits on the refusal, so the query enumeration that follows it in the default-branch transcript does not run at all.

The two definite cases are byte-identical to their default-branch transcripts on this branch. The gate changes exactly one of the three outcomes.

Blast radius — what one silently-admitted write becomes

The asymmetry between refusing and admitting is the whole argument, and it is not a rhetorical one. Follow the default-branch transcript above in three steps.

Step 1 — the write nobody decided is admitted. 3 events emitted. The only trace is the ☞ [undefined] note. It is printed, so it is not invisible at a terminal — but it is not an error and does not gate. The command exits 0 (measured), so a batch importer that checks exit status sees a clean run.

Step 2 — the undecided question is laundered into a definite fact. In the same transcript, in adjacent stanzas:

query badge::badge_holders: 1 row(s)
  [Can] (#i1)
query badge::roster: 1 row(s)
  [Is] (#i1)

badge_holders is still honest — [Can], undetermined. But on_roster derives from Employee alone, and #i1 is now a definite Employee, so the roster row is [Is]. Nothing downstream of on_roster can tell that this individual entered the population through an undecided gate. The undetermined verdict did not propagate; it was dropped at the admission boundary, and everything past that boundary reads a definite fact.

Step 3 — a later check is now confidently wrong. roster_member_is_payrolled is a second Severity::Error check over the same roster. Its body reads nothing undetermined — on_roster(#i1) is the [Is] row above, and in_payroll follows from the payrolled field the write asserted directly — so it renders a confident verdict, and in the transcript that verdict is a pass: it emits nothing at all. The only Severity::Error verdict the system now states about #i1 with any gating force is a pass. An operator reading the run sees one advisory note and one clean blocking check, and concludes the hire is compliant. The badge question was never answered; it was skipped, and the next check inherited the skip as a pass.

Extend those three steps to a running deployment and the cost is not local: payroll accruals fan out from Employee on the next period close; headcount and access-audit checks count an admitted population; every derived roster lists the hire. Unwinding it is not a delete, because the store is event-sourced — the hire event is durable history, and the accruals and roster memberships derived from it are their own committed facts with their own valid-time extents. Correction means issuing compensating events and reconciling everything that read the bad state in between, at a cost that scales with how long the bad event sat unnoticed. Refusing at the gate costs one rejected write and a fix to the evidence.

The failure-direction doctrine. The two directions of being wrong are not symmetric, which is why the default is fail-closed. A wrong admission is silent, compounds in durable history, and is read as settled by every consumer. A wrong refusal is loud at the write and fixable at the source: supply the classifying evidence, break the standoff so the concept settles, or lower the check’s severity to say the undetermined case is not a stop. Argon already applies this doctrine elsewhere — the open-world lints refuse to let a mutual-defeat cycle pass unremarked (OW0740 in the probe is one), the defeat plane names an undecided head undefined by decision rather than resolving it false, typed views refuse a silent coercion across a view boundary. A check reading undetermined membership was the last place the language still guessed silently.


Decision

Terms used below. A membership-gated check is a check whose violation body consumes membership in an iff/shape-defined concept — directly (x : C, not x : C, or the predicate spelling C(x)) or through the nominal upward closure (an asserted <:-descendant of a defined concept is membership-dependent). The admission point of a write is its valid-time instant (an operation-level at/since), else the all-times view. A standoff is a membership verdict that the well-founded semantics leaves undefined at a point — a negation/defeat cycle, or open-world absence of evidence.

D1 — The three verdicts, never collapsed

For each candidate violation tuple, discharge of a membership-gated check distinguishes three outcomes, and never folds one into another:

VerdictConditionMeaning
Definite violationbody holds under wfTrue (is)fires exactly as a classic check (RFD 0025 D4)
Definite non-violationbody definitely false at the pointdoes not fire
Inconclusivebody’s firing verdict hinges on a membership atom that is Can (undefined) at the pointfirst-class outcome — disposed of by the honesty gate (D3)

Inconclusive is a set of violation tuples, not of membership atoms: the head tuples that fire under a possible reading of the body (each positive atom needs only to be well-founded possible; each negated atom needs only that its subject is not well-founded true) minus the tuples that fire under the definite (is-only) reading. The membership-atom residue wfPossible ∖ wfTrue is what drives a tuple into that difference, lifted to the check head — the driver of the verdict, not the level at which the verdict is defined. The two forbidden collapses are: “undetermined ⇒ not a member ⇒ fire” (the false accusation) and “undetermined ⇒ silent pass” (the admitted-undecidable write, the Context exhibit).

Inconclusive is not the observe-channel residue, and the difference is the whole soundness content of the gate. The residue an Error check degrades to the observe channel today is the difference against the two-valued pass, whose positive atoms are read against well-founded truth; it is a strict subset of the inconclusive set. A tuple whose firing hinges on a positively undetermined membership fires under the possible reading but never enters the two-valued pass at all — so it carries no observe note, and a gate enforced over the observe residue would let it through silently. The mechanization states that containment (undefinedObservations ⊆ inconclusive) precisely so the two are not conflated: the gate is enforced over the inconclusive set.

D2 — Admission scoping (the firing closure)

A membership-gated check is judged at the admission point of the write under judgment, over the facts and rules visible at that point — not against a single committed base snapshot. The discharge reuses the existing point-parameterized read seam (the bitemporal read point of RFD 0063); it introduces no parallel evaluator.

Each membership-gated check has a firing closure: the transitive set of relations, concepts, and rule heads whose change can alter the check’s firing verdict, computed through the compiled rule bodies (the check body’s atoms, plus everything those atoms derive from, plus the membership classifiers’ cones). A write is judged against a check iff the write lands inside that check’s closure.

Write shapeWhat is judged
write inside a check’s firing closurethat check discharges at the write’s admission point
write outside every firing closureno membership-gated discharge runs (zero admission work)
operation-qualified write (at t)judged at point t; no all-times view is materialized
unqualified writejudged at the all-times view (subject to D4)

Scoping matters for two reasons. Precision: a module with no membership-gated check pays nothing; a write untouched by any check’s closure pays nothing. Honesty of time: a valid-timed write is judged against membership as of its own instant, so “was this employee an adult when the contract was signed” is asked at the signing date, not against the union of all dates.

Standpoint scoping is part of both the view and the check’s identity: a standpoint-scoped pub check discharges only over its standpoint’s view at the admission point, and its inconclusive-tuple bookkeeping is keyed by (check path, standpoint) — a scoped check’s inconclusive tuple never collides with a base check’s, and a sibling standpoint’s dated evidence cannot make a base check point-variable.

D3 — The honesty gate (disposition of an inconclusive verdict)

Only inconclusive tuples the judged write newly introduces are dispositioned — the same delta discipline the classic guard applies to violations. A tuple already inconclusive over the pre-state does not wedge an unrelated write.

The disposition of a new inconclusive tuple is decided per check, derived from the check’s declared severity. This table is implemented, and the two rows a transcript can exercise today are executed in the Context section.

Check severity / markerDefault disposition of a new inconclusive tuple
Severity::Error (blocking)block — refuse the write, OE1430
Severity::Warning / Severity::Inforeport at undefined grade; never blocks
#[observe] on any severityreport at undefined grade; never blocks (RFD 0025 D3 — #[observe] on an Error check opts out of guarding)
=> emit (emission check)pass — an emission needs a definite firing, so inconclusive tuples are simply not emitted

The refusal contract. A block disposition that fires returns a classified refusal (OE1430) naming the check, the undetermined membership, and the subject, and assert rejects(OE1430) pins it in a negative-enforcement test — as the executed transcript in the Context section shows.

Rationale for per-check over a global default (measured in “Why disposition is per check”). A global fail-closed default (Alternatives (a)) is sound but too blunt: it converts every modelling gap in a long-lived open-world knowledge base into a hard write-stop, including cases the modeler has consciously accepted. A global pass default is the status-quo hole. Deriving the disposition from the declared severity keeps the sound default for the checks that must gate, keeps the reporting behavior for the checks that must not, and states the choice at the constraint itself — in a word the modeler was already required to write, so the disposition cannot drift away from the severity it belongs to.

D4 — Point-free honesty

Status: unimplemented. No refusal for this exists in the compiler on the default branch or on the honesty-gate branch; the gate as implemented judges at the all-times view unconditionally. Everything in this subsection is intended behavior, not measured, and no transcript for it appears in this record because none could be produced honestly.

This subsection names no diagnostic code, on purpose. An earlier revision reserved one by writing it here, and the default branch then allocated that number to an unrelated refusal — a code named only in a design record is a reservation nothing enforces, so the next entry allocated from the same range takes it. The slice that implements this refusal allocates its own code and records it in the diagnostic catalogue, which is the only place a code is owned.

A point-free (all-times) judgment of membership that varies by valid-time point is refused: the all-times view admits every valid time at once, so point-variable membership has no single honest verdict. Point-variability has three sources over the check’s membership cone:

  1. committed evidence on a cone relation/concept/field carrying a bounded (non-all-time) valid-time window;
  2. a rule-validity window on a cone rule clause (RFD 0056);
  3. a clock-reading classifier (a cone rule reads the wall clock, e.g. today()).

A concrete point stays exact: judging at point t pins any clock reading to t’s own civil day, so a clock-reading classifier is decidable at the point. The refusal fires on three surfaces — the write path (an unqualified write landing in a point-variable check’s closure), the check-debug surface (ox derive <CheckName>), and the on-demand stored-check ask (one point-variable check refuses the whole ask, naming the check) — and is classified so assert rejects(...) observes it under whichever code the implementing slice allocates. The remedy the diagnostic teaches: ask the check at a specific valid-time point (at <t>).

D5 — Aggregated membership

The three-valued lifting (D1) is top-level over the body’s atoms; a membership atom nested inside an aggregate body is not lifted there. The reasoner already refuses to fold an aggregate over a relation carrying undefined atoms (OE1332 AggregateOverUndefined, pre-existing). Under admission, when that refusal lands on a membership-gated relation, it is re-registered as OE1430 so an aggregated standoff refuses in the same classified register as a top-level standoff, rather than surfacing as a generic reasoning error. It never folds undefined-as-false, never fires, never silently passes.

D6 — Registry integration contract (RFD 0081)

This feature does not introduce a parallel dispatch mechanism. It integrates with the check-rule registry exactly as that RFD prescribes — its one rule is a row in the one auditable table, tagged by axis and wiring. Because the disposition is read from the declared severity, there is no new declaration surface to validate statically, so this feature contributes no driver-wired row:

  • The runtime honesty gate itself (the write-gate discharge that emits OE1430, and later the point-free refusal) enters as an inline INVENTORY row, not a driver-dispatched rule. This is exactly the registry’s category for runtime backstops (its inventory row for the program-global tuple-target resolver, OE0266): placement is semantic — the gate runs inside the write path over the post-state overlay, where the three-valued model exists — and cannot move into the check-pass walk without recomputing the model. The inventory row makes the gate visible in the same table (its codes, its axis-of-reach, the write-gate site) without pretending it is statically enumerable.

The contract in one line: the registry owns “where every check-pass rule runs”; admission adds one audited inventory row and invents no dispatch table of its own.

D7 — Resolution-pass boundary (non-goal)

This RFD’s discharge is a consumer of name resolution, never a rewriter of it. The prior implementation lineage of this feature (Alternatives (b)) died in a three-way collision in which admission, a typed-views lane, and a scope-tracking lane each rewrote per-scope name/severity state in the same resolution surface. To foreclose that, the boundary is stated as a hard non-goal:

  • Reads, never rewrites. Admission reads resolved concept identities, the resolved <: ancestry (for the upward membership closure), resolved rule-head identities (for the firing closure), and the resolved standpoint owner of each scoped clause. It never rewrites a resolution verdict, never re-keys the resolver’s per-scope state, and never introduces a second scope-identity carrier alongside the resolver’s own.
  • Severity is read from the declaration, not dispatched through resolution. The block/report disposition (D3) is computed from the check’s declared severity — present on the check declaration — and from nothing the resolution pass owns. Admission adds no “severity-dispatch” rewrite to the resolver.
  • One identity key, and it is admission-local. The inconclusive-tuple delta index is keyed by (check path, standpoint) and lives entirely in the write gate. It is not a new per-scope identity type shared with resolution or with any other lane; if a future lane needs a shared per-check identity carrier, that is a separate reconciliation (Open questions), not a dependency of this RFD.

Rationale

Two load-bearing choices above are argued from the same probe: why the disposition is per check rather than one engine-wide policy, and why the judgment is scoped to an admission point rather than a whole-store commit-time pass.

Why disposition is per check, not one global policy

Two checks in the same program can want opposite treatment of an equally undetermined verdict, and this is measurable rather than asserted. src/report.ar is the counterpart to badge: the identical negation-cycle construction, so the classifier is equally undetermined, but the check is Severity::Warning because it states a vetting standard rather than a legal bound — blocking every engagement while a clearance objection is unresolved would stop the business.

// src/report.ar
use std::core::type;

pub type Contractor { mut engaged: Bool }
pub type Engaged <: Contractor iff { self.engaged == true };

#[default]
pub derive cleared_a(p) :- Engaged(p);

#[defeats(cleared_a(p))]
pub derive objection_a(p) :- cleared_b(p);

#[default]
pub derive cleared_b(p) :- Engaged(p);

#[defeats(cleared_b(p))]
pub derive objection_b(p) :- cleared_a(p);

pub derive engagement_roll(p) :- Engaged(p);

pub query roll() -> engagement_roll;

pub query clearances() -> cleared_a;

pub check clearance_notice(p: Engaged) :-
    Engaged(p), not cleared_a(p)
    => Diagnostic {
        severity: Severity::Warning,
        code:     "Vetting::W010",
        message:  format!("contractor {} may lack a security clearance", p),
    };

pub mutate engage_dana() { insert Contractor { engaged: true }; }

On the default branch the two checks are indistinguishable. badge_required (a hard bound that must block) and clearance_notice (a standard that must not) produce the same shape of outcome over equally undetermined membership — an ☞ [undefined] note, and a committed write:

$ ox query target/root.oxbin --mutate report::engage_dana --with-truth4
Vetting::W010

  ☞ Vetting::W010: [undefined] contractor #i1 may lack a security clearance
  │ [#i1]
  help: fired by check `report::clearance_notice`

ran report::engage_dana: 1 ops, 2 events emitted
query badge::badge_holders: 0 row(s)
query badge::roster: 0 row(s)
query report::clearances: 1 row(s)
  [Can] (#i1)
query report::roll: 1 row(s)
  [Is] (#i1)

That is what “one global policy” looks like in practice: it is already in force, it is fail-open, and it is right for clearance_notice and wrong for badge_required. Flipping it to a global fail-closed would invert the error — right for the hard bound, and a business-stopping refusal for the vetting standard. Neither setting can serve both, because the two checks differ in what a non-decision costs, and that is a property of the constraint, not of the engine.

The severity-derived default resolves exactly this, and it is implemented. On the honesty-gate branch, the same two writes, on the same package, diverge with no annotation anywhere: badge::hire_bob refuses with OE1430 (transcript above), while report::engage_dana produces output byte-identical to the default-branch transcript just quoted — it commits, and reports at undefined grade. The distinction comes from the declared severity alone.

Where the severity-derived default stops, and this record stops with it. A third case exists: a check that is a hard bound and has a cohort the team has consciously decided to admit, mid-migration. Severity cannot say that, because severity is one word for the whole check, and a per-check disposition cannot condition on which classifier is undetermined or on which subject. Deciding that finer surface is deliberately out of scope here — it is the subject of the in-body determinedness-test record (design record; its number is allocated when the file lands, so this cites the record rather than a number that does not yet resolve), whose implementation stacks on this record’s honesty-gate branch. What this record decides is the default that such a surface deviates from, and that default is the severity above.

Why admission-point scoping matters

Honesty about the verdict is one half; the other is honesty about which data, at which time, a check is judged over. Two mis-scoping failures follow from a whole-store, commit-time discharge. Wrong time: a valid-timed write (“this filer’s return, as filed on 2025-04-15”) is judged against the union of every valid time at once, so a membership that changed during the year has no single truth across that union and the check either picks one arbitrarily or throws; the honest question is “was this filer a dependent as of the filing date”. Wrong reach: a commit-time whole-store pass re-litigates every check on every write, so a write in an unrelated module pays for — and can be blocked by — a check whose governed data it never touched.

D2’s firing closures and admission-point discharge address both: judged only against writes inside a check’s closure, and at each such write’s own valid-time instant. This half of the design is not implemented — the honesty gate judges at the all-times view — so the transcripts above exercise the verdict axis only.


Worked examples

The Context section carries the executed exhibits. These examples restate the same mechanisms in a domain a reader is likelier to recognize — a tax filing — and cover the case the probe cannot: the unimplemented time axis.

None of the source below has been run: it is written against a schema that exists only in this record, and Example 4 additionally describes unimplemented behavior. Every block is therefore fenced argon,ignore and every outcome is stated as intended, paired with the executed transcript from the Context section that supports it, or marked as unsupported where none exists.

Example 1 — a statutory hard bound (blocking is the only defensible default)

Scenario. A tax authority publishes a deduction cap: a filer who counts as a dependent for the year may not take the standalone standard deduction above a capped amount. “Dependent” is not typed in — it is computed by section rules from claimed-support and residency evidence, exactly the kind of classification an auditor expects the system to derive, not accept on assertion. The cap is a hard legal bound; a deduction that exceeds it for a dependent filer is a defect the return cannot carry.

pub type Filer { standardDeduction: Money }
pub type DependentFiler <: Filer iff { claimedAsDependent(self) && residencyMonths(self) >= 6 }

pub check dependent_deduction_cap(f: DependentFiler) :-
    DependentFiler(f), f.standardDeduction > money(1_250)
    => Diagnostic {
        severity: Severity::Error,
        code:     "Tax::E1099",
        message:  format!("dependent filer {} exceeds the capped standard deduction", f),
    };

Meaning. The check forbids a computed DependentFiler from carrying a deduction over the cap. Input. Three writes, each recording a filer’s return. What happens, and why:

  1. Definite member, over cap. Residency and support evidence definitely establish DependentFiler(f); the deduction is money(1_400). The body fires under is; the write is refused as a classic violation (Tax::E1099). Unchanged from today.

  2. Definite non-member. The filer is definitely independent (independent residency established). The body does not fire; the write commits. Unchanged.

  3. Undetermined membership. Support evidence is present, but residency is in a standoff — two dated assertions the fixpoint cannot reconcile at the write’s valid-time point — so DependentFiler(f) is Can, and the deduction is money(1_400). Today (Context) this commits silently with an [undefined] note. Under this RFD the check is Severity::Error: admitting a deduction whose legality was never decided is the exposure the invariant exists to prevent, so the new inconclusive tuple refuses the write (OE1430) — blocking is the only defensible default for a hard legal bound. The refusal names the undetermined concept and the subject and points the modeler at the evidence to establish or the standoff to break.

All three outcomes are intended for this program, which was not run. Case 3 is supported by executed evidence in the Context section: the honesty-gate branch refuses the structurally identical badge_required write with OE1430, and the transcript there is verbatim. An earlier revision of this record quoted a refusal render with a RFD 0025 D2 — new violation(s) created header and a subject #i2827; that string does not match what the branch emits and has been replaced by the actual transcript in the Context section.

If the modeler’s answer is instead that an unresolved-residency filer should be reported rather than refused, that is a reporting severity — Example 2.

Example 2 — the reporting middle (a worklist, not a write-stop)

Scenario. Finance requires that every payment carry a cost center for attribution. Cost-center assignment is a computed classification (Attributed — an iff concept deciding, from vendor and project evidence, which cost center a payment rolls up to). This is a data-quality standard, not a legal bound: finance wants a worklist of un-attributed payments, but blocking payroll because an attribution is momentarily undetermined would be operationally unacceptable.

pub type Payment { amount: Money }
pub type Attributed <: Payment iff { exists c. costCenter(self, c) }

pub check payment_needs_cost_center(p: Payment) :-
    Payment(p), not Attributed(p)
    => Diagnostic {
        severity: Severity::Warning,
        code:     "Fin::W2050",
        message:  format!("payment {} has no cost center", p),
    };

Meaning. A Severity::Warning check flags any payment lacking a cost center. Input. A payroll batch in which one payment’s attribution is undetermined (the vendor-to-cost-center evidence is in a standoff at the write’s point). What happens: the payment commits; the undetermined membership appears in the check’s report at undefined grade — distinct from a definite “no cost center” and from a clean pass. Why: a reporting-severity check never blocks (D3); its job is to hand the ops team a worklist, and undetermined entries belong on that worklist marked as undecided (chase the evidence) rather than definitely missing (assign one). Payroll runs; the worklist grows by one honest, correctly-graded item.

This one is executed, in a different domain: the Context section’s report::clearance_notice is the same shape — a Severity::Warning check over an equally undetermined classifier — and its transcript shows exactly this outcome (☞ Vetting::W010: [undefined] …, then ran report::engage_dana). On the honesty-gate branch that output is byte-identical to the default branch’s, which is the point: the reporting disposition is not new behavior, it is the behavior that becomes a decision once the blocking disposition exists beside it.

Example 3 — defeat-plane interplay — folded into the Context probe

This example previously restated the negation-cycle program that the Context section now builds, runs, and quotes end to end. It is cut rather than duplicated. The one point worth keeping separate is the agreement between two honesty features: the defeat plane already names such a cycle undefined by decision (OW0740, quoted in the Context build transcript) instead of quietly resolving it false, and the honesty gate carries that same refusal-to-guess across the check boundary. An undecided head stays undecided everywhere, and no consumer may collapse it — which is exactly what the default branch’s [Can] membership row beside its [Is] roster row shows going wrong today.

Example 4 — admission over valid time — INTENDED, UNIMPLEMENTED

Nothing in this example has been executed, and no evidence supports it beyond the design argument: the point-free refusal does not exist in the compiler on any branch and has no allocated diagnostic code, and the honesty gate as implemented judges at the all-times view unconditionally, so the refusal described here does not happen today. Read it as the design’s stated intent for the time axis, not as behavior.

Scenario. A filer’s residency changed mid-year: independent through the spring, dependent from the summer. An auditor asks whether Example 1’s deduction cap was satisfied on the return as filed. Because residency membership varies across valid time, the answer depends entirely on which instant the question is asked at.

// dated to the filing instant — a single, honest verdict:
insert Filer { standardDeduction: money(1_400) } at 2025-04-15;

Meaning. A write dated at 2025-04-15 judges DependentFiler membership as of that day. Input / what happens:

  • the dated write is judged at 2025-04-15 — one honest verdict (member or not on that day), dispositioned by Example 1’s rules;
  • an unqualified write whose firing closure includes the point-variable residency evidence is refused: the all-times view admits every valid time at once, and point-variable membership has no single honest answer there. Why: rather than silently pick a valid time or fold the variation away, the gate refuses and the diagnostic teaches the remedy — ask the check at a specific point (at <date>). This is the time-axis twin of D3’s verdict honesty: D3 refuses to guess what the membership is; D4 refuses to guess when it is being asked.

An honest gap — retroactive amendment (deferred, see Open questions)

Scenario. A filer’s return was admitted last year with a definite DependentFiler non-membership. This year an amendment retracts one of the residency assertions the classification rested on — and the retraction pushes that already-admitted filer’s membership into a standoff, making a verdict that was once definite now undetermined for data that is already committed history.

D3 dispositions only inconclusive tuples the judged write newly introduces; a tuple already inconclusive over the pre-state does not wedge an unrelated write. But the amendment write is not unrelated — it is the write that created the new inconclusiveness, over data admitted earlier. Whether the gate should refuse the amendment (treating the newly-introduced inconclusiveness as this write’s to dispose), report it, or admit it and re-open the earlier admission for review, this RFD does not decide. It is recorded as an Open question rather than resolved by invented semantics.


Alternatives considered

(a) Global fail-closed default, no per-check policy. Undetermined always refuses, everywhere, with no per-check override. Sound and simple. Rejected as the sole mechanism: in a long-lived open-world knowledge base, undetermined membership is a routine transient (evidence not yet arrived), and a global stop converts every such gap into a hard write failure with no modeler recourse short of editing the check. This RFD keeps fail-closed as the default for blocking checks and lets a check opt out by declaring a reporting severity, so the sound behavior is the default and the alternative is visible at the declaration. (A global pass default is the status-quo soundness hole and is not reconsidered.)

(b) The dropped prior implementation lineage. An earlier, unlanded implementation of this feature carried the same core semantics (three verdicts, admission-point discharge, point-free honesty) but grew a second, richer verdict machine on top: a dedicated per-check identity type, a severity-dispatched interception layer with a separate “hinge”/“standoff” representation, and a total membership-reach walk that propagated through defeat and analytical edges and composed with the defeat plane rather than refusing it. That shape collided with two sibling lanes — a typed-views lane and a per-scope-discipline lane — because all three independently represented per-scope/per-check verdict state and the well-founded-undefined boundary in the same resolution surface, producing an irreconcilable set of failures that could not be landed piecemeal. This RFD neutrally records that shape as a considered alternative and does not adopt it: it keeps the verdict representation minimal and admission-local (one (check path, standpoint) delta key, the possible-minus-definite lifting, and a declared refusal for the defeat-plane composition rather than a total reach walk), and states the resolution-pass boundary (D7) precisely so the collision cannot recur. Whether a shared per-check identity carrier is eventually warranted is an open question, deferred to a cross-lane reconciliation, not a dependency here.

(c) Fire on undetermined (treat Can as a member for blocking). Rejected: this is the false-accusation collapse — it refuses a write for a violation that is not established, only possible, indistinguishable to the modeler from a real violation.

(d) A per-check disposition attribute (#[undetermined(block | pass | report)] on the declaration). An explicit per-check word for what an inconclusive verdict does, overriding the severity-derived default. Rejected: the attribute sits on the declaration, so — exactly like severity — it is one word for the whole check and cannot condition on which classifier is undetermined or on which subject. The case that motivates a finer surface is a blocking check with a consciously admitted cohort mid-migration, and a declaration-level word cannot name that cohort; it can only widen the admission to every undetermined tuple the check sees, which is the status-quo silent pass with an annotation on it. The successor shape is a determinedness test written in the check body, where a predicate over the subject is expressible — the in-body determinedness-test record (design record). This record decides only the default such a surface deviates from.

(e) A new severity level (Severity::Undetermined). Rejected: undetermined is a property of a tuple’s verdict at a point, not of the check; a check has one severity and many tuples, some definite and some undetermined. A fourth severity would force the modeler to classify the check by an outcome only some of its tuples have, so the undetermined tuples’ disposition is derived from the existing severity (D3) rather than becoming a severity of its own.


Consequences

  • New surface — none. This record changes no grammar. The behavior change is carried entirely by the Severity a modeler already writes on the check, so there is no new keyword, attribute, or body form to learn, and no existing source file needs an edit to opt in.
  • Diagnostics. OE1430 (standoff refusal at the write gate) — exists, in the diagnostic catalogue and emitted, on the honesty-gate branch. The point-free refusal (point-variable check judged point-free) — does not exist on any branch, and holds no allocated code. The aggregated-standoff re-registration OE1332 → OE1430 (D5) is likewise unimplemented.
  • Behavior change for existing modules. A membership-gated Severity::Error check whose verdict stands off today admits the write silently; under the honesty gate the same write refuses (OE1430) — the two verbatim transcripts of that difference are in the Context section. A module accepting that risk lowers the check’s severity to Warning/Info or marks it #[observe], which is what the emitted diagnostic’s help text tells the modeler to do. A module with no membership-gated check is byte-identical in check behavior and pays no admission work.
  • Write-path cost. Admission-point discharge and firing-closure computation run only for writes landing inside a gated check’s closure (D2); everything else is untouched.
  • Mechanization. The three-valued surface D1 rests on is formalized in spec/lean/Argon/Reasoning/Checks.lean on the honesty-gate branch (pull request #1826), not yet on the default branch: the possible reading (bodyHoldsPossible, wfViolationsPossible), inconclusive as its difference against the definite set wfViolationsK3, the containments that make the three verdicts a partition, the strictly-weaker standing of the observe residue (undefinedObservations ⊆ inconclusive), and the no-collapse partition theorem itself. This record points at those definitions and does not restate them; where record and mechanization differ, the mechanization governs. What remains unformalized is the point dimension: the mechanized sets are taken over one global well-founded model, so D2’s admission-point discharge and D4’s point-variability refusal have no Lean statement.

Open questions

  • Shared per-check identity carrier. D7 keeps the delta key admission-local. If a future typed-views or scope-discipline lane needs a shared per-check identity type, is that a cross-lane reconciliation RFD, and does it subsume this key? (Deferred; not a dependency here.)
  • Audit surface for deliberate lenience. A reporting severity on a membership-gated check is a decision to admit undecidable writes, and it reads identically to a reporting severity chosen for any other reason. Should a build surface an inventory — a count, or a lint — so a reviewer sees every path that admits an undecided verdict in one place? Not decided here.
  • Disposition for a check whose severity is inherited rather than written. RFD 0025 D5 makes severity: a required field and records no free-standing default; the one way a check carries a severity it did not write is the amendment from RFD 0026 — a check implementing a trait member whose signature pins a severity may omit the field and inherit the pin (a divergent restatement is OE0676). Does the undetermined disposition track the inherited pin, or is such a check’s undetermined disposition always report until the implementor states a severity of its own? (This RFD assumes it tracks the inherited severity.)
  • Point-variability of a rule-validity window vs. dated evidence. D4 targets the full firing closure for dated evidence but only the firing-pattern names for a clock-reading cone. Is the rule-validity-window source (D4.2) closer to the dated-evidence case or the clock case for closure targeting?
  • On-demand whole-ask refusal granularity. D4 refuses the whole on-demand check_violations ask when one check is point-variable. Should it instead return per-check results with the point-variable ones marked, rather than refusing the batch?
  • Retroactive amendment re-introducing inconclusiveness (Worked examples, gap). An amend/retract write can push an already-admitted tuple from a definite verdict into a standoff — the amendment is the write that newly introduces the inconclusiveness, but over data admitted earlier. D3’s delta discipline dispositions inconclusiveness a write newly introduces, which names the amendment as the disposing write; but whether the honest outcome is to refuse the amendment, report it, or admit it and re-open the earlier admission for review is undecided. This RFD does not invent that semantics; it flags it for a follow-up decision.

Staging

Slices are independently reviewable; each leaves the tree green and changes no behavior for a module with no membership-gated check. Status is marked per slice.

  • Slice 1 — DONE. The three-valued verdict plus fail-closed blocking, unqualified only: the membership-gated partition, the possible-minus-definite lifting (D1), and the write-gate honesty gate for Severity::Error checks at the all-times view (OE1430). No admission-point selection yet (all-times only), no point-free gate. Closes the hole measured in Context for the common case. Registry: the runtime gate lands as the inventory row (D6). Implemented on the honesty-gate branch (pull request #1826), including the reporting-severity path; transcripts in the Context section.
  • Slice 2 — NOT STARTED. Admission-point discharge and firing closures. Point-parameterized discharge (D2) over the bitemporal read seam; the firing-closure computation and write-targeting; standpoint scoping and the (check path, standpoint) delta key.
  • Slice 3 — NOT STARTED. Point-free honesty: the point-variability detector (three sources), the refusal (code allocated by this slice) on the write / debug / on-demand surfaces, and the concrete-point clock pin.
  • Slice 4 — NOT STARTED. Aggregated membership: the OE1332 → OE1430 re-registration (D5) and the defeat-plane-composition refusal.
  • Lean obligation — MET on the honesty-gate branch, not yet on the default branch (paired with Slice 1). spec/lean/Argon/Reasoning/Checks.lean on that branch adds the possible reading of a check body (bodyHoldsPossible — positive atoms against wfPossible, negated subjects merely ∉ wfTrue), its firing set wfViolationsPossible, and inconclusive := wfViolationsPossible ∖ wfViolationsK3 — the violation-tuple-level set of D1, with the membership-atom case (wfPossible ∖ wfTrue lifted to the check head) recorded on the definition rather than being its statement. Discharging the no-collapse obligation: wfViolationsK3_subset_possible (definite violations are contained in the possible ones), wfViolations_subset_possible and undefinedObservations_subset_inconclusive (the observe residue is part of the gate’s subject, never the whole), and honesty_partition, which states that the definite violations, the inconclusive tuples and the complement of the possible firing set cover every candidate head tuple, and that the inconclusive set is disjoint from each of the other two — the two forbidden collapses of D1, plus exhaustiveness. Before that branch the file formalized only wfViolations over wfTrue and the negation-driven undefinedObservations residue; neither a possible reading nor an inconclusive set existed, which is why an implementation enforcing over the observe residue alone would have missed positively undetermined membership. Still unformalized, and not part of this obligation: any point-scoped statement — the mechanized sets are over one global model, so D2 and D4 have no Lean counterpart. Per the Lean-first rule for substrate semantics the mechanization, not this record, is the authority on those definitions.

Verification (per slice)

  • A module with no iff/shape-defined membership in any check body is byte-identical in check behavior and pays no admission work (negative control).
  • The Context section’s program refuses OE1430 on the hiring write under Slice 1, and a reporting-severity check over equally undetermined membership still commits and still reports at undefined grade. Both are executed and quoted above; both are pinned by runtime tests on the honesty-gate branch, alongside the two definite cases as controls.
  • The refusal reaches assert rejects(OE1430) through the same rejection channel as a classic violation (pinned on that branch).
  • Remaining, for slices not started: admission-point discharge judged at a dated write’s own valid-time instant, and a write outside every firing closure running no discharge; point-variable point-free → refused on the write, check-debug, and on-demand surfaces; aggregated standoff OE1332 → OE1430.

RFD 0087 — Fixpoint typed views

  • State: discussion
  • State detail: not uniformly in discussion — the three-valued standoff policy’s aggregate arm (D3) is the one piece still undecided. Its read-surface arm states the shipped four-valued envelope contract, and the other decisions below are settled as written; a reader should not take the aggregate arm as carried along with them.
  • Opened: 2026-07-23
  • Decides: that a typed navigation view — a declared concept field whose value is a set computed by walking a relation (f: [T] from Rel.endpoint) — may have a standoff-capable element type T — a shape-clause concept (iff Shape(self, …)) whose membership is a derived rule head the well-founded fixpoint can leave undefined — and that such a view is evaluated inside that fixpoint rather than materialized store-locally at seed time. The trigger is the ability to stand off, NOT a derived extent generally: a braced refinement (iff { P }) also has a derived extent but can never stand off, and is out of scope in both directions (neither lifted nor refused). Settles four things: (1) the single pin-resolution ladder that keys every concept-pinned field read from the base variable’s accumulated concept pins; (2) the raw + gate cell split that keeps a fixpoint-decided extent live and incrementally maintainable; (3) the three-valued (K3) standoff policy at a view boundary — what a view surfaces when an element’s membership is UNDETERMINED (the heart of this RFD; see D3, whose read-surface arm states the shipped four-valued envelope contract and whose aggregate arm is the one piece still undecided) — and, as the carve-out that keeps that policy additive rather than an override, that absence of evidence is excluded from the undefined region only under a closed world: under the closed-world default an unasserted fact is definitely-false and is settled by the reference manual’s membership rule, so the region is the well-founded standoff alone; under an open world the reference manual reads the same absence as unknown, it is undetermined, and it rides the undefined companion alongside a standoff; (4) the explicit contract this feature holds with the per-scope resolution pass, with mapped relation subsumption (RFD 0084), with the incremental defeat/maintenance plane, and with the parallel admission-point-checks RFD on the shared three-valued surface.
  • Numbering: 0086 is the admission-point-checks record this RFD coordinates with, and it is now on the default branch (#1822) — the index runs 0085 → 0086 → 0087 with no gap. 0085 on the default branch is the incremental-rule-circuit-compiler record; the instance-relations lane (#1818) still heads its draft 0085 and owes itself a renumber — that lane’s collision to settle, not this record’s.
  • Surface policy: no new grammar. The view spelling f: [T] from Rel.endpoint [where P] (RFD 0005 §5.6) is unchanged; what changes is which evaluation plane a view lowers to, decided by construction from T’s ancestry and the presence of a where filter. No user-visible mode selector. The observable surface does change, and the claim must not be read wider than the grammar — the three new observables and the one source break are under Consequences.
  • Affects: navigation-view lowering (Module::load in oxc-runtime/src/module_load.rs, plus oxc-instantiate’s lower/rule_ref.rs / lower/ctx.rs); the store-local view materialization on the read path (oxc-runtime/src/catalog_seed.rs, oxc-runtime/src/index.rs::nav_catalog_keeps_value); the concept-keyed field-projection contract (RFD 0005); the well-founded evaluator surface (oxc-reasoning); the K3 $undefined:: companion and check-discharge K3 pass; DERIVATION_SEMANTIC_EPOCH and the RFD 0072 projection-cache derivation commitment; the Lean substrate (Argon.TypeSystem.Soundness.FieldView, Argon.Reasoning.Datalog.WellFounded). Additive on the artifact wire; a lift is a derivation-semantic change and rides an epoch bump.

In plain language — what changes and why

A navigation view is a concept field whose value is not stored but computed by walking a relation:

f: [T] from Rel.endpoint          // f(self) = { x : Rel(… self … x …) and x is a T }
f: [T] from Rel.endpoint where P  // additionally narrowed by predicate P

self.f reads as the set of T-typed things reachable from self across Rel. Deciding which things belong needs two facts: (a) Rel relates self to a candidate, and (b) the candidate is a T.

Today that second fact must be knowable at catalog-seed time. When T is an ordinary declared concept, membership is a seed-time fact and the view materializes locally on the read path — fine. Two other kinds of T have a derived extent, where members are auto-classified rather than inserted, and the seed-time pass has to evaluate that classification early, on each candidate, before the fixpoint has reached its verdict. Only one of the two can actually be undefined when it is evaluated, and the distinction is what the lift keys on:

  • Shape-clause concepts (iff Shape(self, …), RFD 0070) are standoff-capable. Membership is a derived rule head, so recursion through negation can leave it neither definitely true nor definitely false. This is the kind the lift and its refusal are for.
  • Braced refinements (iff { P }) are not. The body is a predicate over the individual’s own fields and cannot call a rule — OE0660 rejects any other form — so P is always two-valued. A referenced field with no recorded value makes membership definitely FALSE under the open-world reading: an information absence, not a standoff. Such a concept has no undefined region, so a view over it has none to surface.

For the standoff-capable kind, that early evaluation is exactly where honesty breaks, in two distinct ways (see “Today’s behavior on main”): a well-founded standoff on a shape/rule concept makes the store-local read fail closed — the view is not navigable at all — while an open-world absence is silently coerced to a two-valued no. Either way a genuinely UNDETERMINED membership has nowhere to live at the boundary.

The change. A filter-free view over a standoff-capable element type stops materializing store-locally. It lowers, by construction, to a rule that runs in the same well-founded fixpoint as the membership rules:

$field::<C>::<f>(self, x) :- <C>(self), <Rel>(… self … x …), <T>(x).

Now the view’s element membership <T>(x) is decided by the same fixpoint that decides everything else — including the K3 undefined region. A candidate whose T-membership is undefined is neither silently kept nor silently dropped; it surfaces as undefined at the view boundary (D3). The reader sees the standoff instead of a fabricated verdict.

Two mechanisms make this work without a second copy of the data. The pin ladder (D1) decides, when a rule/mutate/test body reads t.f, which single field cell that read keys — same-named fields on different concepts are distinct cells, so the ladder must pick the right one from the concepts t is pinned to. The raw + gate cell split (D2) keeps a fixpoint-decided extent live: the raw walked extent is seeded extent-blind and maintainable by delta, while a per-fixpoint gate clause re-applies the membership test, so the view stays correct as commits arrive without recomputing the walk from scratch.


Question — what could not be expressed (before / after)

Before: this could not be expressed. Now: it can. The distilled index of the examples below. Each row is one concept a modeler wants to state; LEFT is the closest today’s main gets (verified in “Today’s behavior on main”); RIGHT is the proposed spelling (argon,ignore).

Wants to sayClosest todayNow
A field whose elements are exactly the Dependents the fixpoint decides.Expressible only as a stored dependents flag (a cache with no invalidation — stale on the next Supports/age/residency edit) or N hand-copied re-derivations of the membership rules (which drift against each other); never navigation of the one canonical concept.dependents: [Dependent] from Supports.supported
An aggregate (count/ratio) that is undefined when any contributing membership is undetermined.Inexpressible — for a shape/rule element type the store-local read of a fixpoint-dependent membership fails closed: the view, the count, and the --extent query all refuse when a member stands off (run-verified exhibit), so the aggregate is not computable at all — there is no two-valued scalar and no undefined region. (The silently-wrong twin — a fabricated Ok(false) scalar, a false PASS — is the field-refinement/open-world case, not the standoff.)Undefined by construction — the standoff rides $undefined:: and an aggregate over a non-empty undefined region is itself undefined (D3).
The worklist of individuals whose classification is in standoff.Inexpressible — a shape/rule classification that stands off is not navigable at all: the store-local read fails closed, so the contested individuals cannot even be listed. (A field-refinement standoff would instead be silently coerced to Ok(false) and dropped — but a braced iff cannot itself stand off, OE0660.)The view’s undefined region — $undefined::$field::…, surfaced as a review queue (D3).
Navigation that stays correct when another module’s classification rules change.Expressible only as a hand-copied re-derivation of the imported rules, which silently fails to track the authority when it tightens a test (and a live store-local view over a shape/rule concept fails closed on any standoff — not navigable at all).The view names the concept, not the rules — eligibleParticipants: [EligibleEmployee] from Enrollment.employee.

Motivation — a worked example the reader can feel

This RFD extends a principle the project has already committed to at three layers. Where an answer is not known, Argon refuses rather than guesses (the build refusals of the OE family); it warns rather than stays silent (the OW family); and it reports undefined rather than coerces (the defeat plane’s undefined-by-decision, where a standoff surfaces as a distinct value instead of being forced true or false). Navigation is the one place that principle has not yet reached: today, walking a relation to a computed concept must fabricate a two-valued membership verdict at read time. Typed views close that gap — a walk to an iff/shape concept can say “undetermined here” and surface it, exactly as the other three layers already do for their own decisions.

A tax model. Person is declared. Dependent and QualifyingChild are computed concepts — membership decided by rules, not asserted:

type Person { ... }

// A qualifying child is decided by age, relationship, residency, support tests —
// a classification, not a stored flag. (RFD 0070 shape/iff clause: the `iff`
// names ONE shape derive; the tests conjoin inside that derive's body, where
// conjunction is `,`.)
pub derive qualifyingChildShape(p: Person) :-
    relatedToTaxpayer(p), underAgeLimit(p), sharesResidence(p);
type QualifyingChild <: Person iff qualifyingChildShape(self);

// A dependent is a qualifying child OR a qualifying relative (another rule).
// Same-head clauses union, so the disjunction is two clauses of one shape.
pub derive dependentShape(p: Person) :- qualifyingChild(p);
pub derive dependentShape(p: Person) :- qualifyingRelative(p);
type Dependent <: Person iff dependentShape(self);

rel Supports(supporter: Person, supported: Person);

type Taxpayer <: Person {
    // The navigation view we want to write: the taxpayer's dependents,
    // reached by walking Supports, filtered to those the fixpoint decides
    // are Dependents.
    dependents: [Dependent] from Supports.supported
}

taxpayer.dependents must be the supported people who are Dependents. Dependent membership is fixpoint-decided: it can hinge on a chain of rules that themselves reference other computed concepts, and — critically — some person’s dependent-status can be genuinely UNDETERMINED (e.g. a residency test that stands off in a negation cycle, or a support fact that is open-world absent for the read-point). A downstream check counts dependents to compute an exemption:

check ExemptionCount { count { d in self.dependents } == self.claimedDependents }

Why membership-dependence matters here, concretely:

  • If the view forces undetermined membership to true, an ineligible person is counted, the exemption is overstated, and the check passes on a fabricated extent — a Can-to-Is collapse.
  • If it forces undetermined to false, a person whose eligibility is genuinely open is dropped, the exemption is understated, and the check passes anyway on a different fabricated extent — a Can-to-Not collapse.
  • The only honest answer is: the view’s extent is itself three-valued at that element, and a consumer that cannot tolerate the standoff must be told, not silently served a two-valued number.

Store-local materialization on today’s main (below) evaluates Dependent membership at seed time. That is the early-verdict trap: it cannot represent the standoff, so — Dependent being a shape/rule concept — it fails closed on it, and the store-local read (view, count, extent) refuses (run-verified exhibit below). This RFD moves the decision into the fixpoint, where the standoff is a first-class value.

The staleness ledger — what the two workarounds cost

Without this feature a modeler who needs taxpayer.dependents has exactly two options today, and both rot. Walk each concretely.

(a) Denormalize the classification into a stored field. Assert a stored Dependent flag (or a stored dependents list) alongside the facts. It is correct the instant it is written. Then a support worksheet is edited — a Supports fact is added, or an age is corrected, or a residency fact flips. Dependent membership is now a function of facts that changed, but the stored row does not know that: it was written by a human (or a one-shot job) and there is no derivation edge from the support fact to the stored flag. The stored Dependent row is now wrong, and every view, credit, and count keyed on it is silently wrong in lockstep. The retraction is a manual obligation with no owner — the fact that justified the row is gone, but the row outlives it. Denormalized classification is a cache with no invalidation.

(b) Hand re-derive the membership rules at each call site. Skip the stored flag; instead, every consumer that needs dependents re-encodes the membership test inline. The Child Tax Credit’s body writes related and underAge and residency; months later the Earned Income Credit’s author writes related and residency — and forgets the age test. Both check bodies are green. Both compute a “dependents” roster. They disagree, and nothing catches it, because there is no single concept they are both obligated to agree with — each is a private re-derivation. This is the identical silent-omission rot that moving rule bodies onto relation-value application removed for rule bodies: one canonical definition, consumed everywhere, instead of N drifting hand copies. Typed views bring the same discipline to navigation — the roster is the concept, walked, not a copy of the concept’s rules pasted at each site.

The feature collapses both failure modes: the roster is a live derivation over the one canonical membership concept, so an edit propagates (no stale cache) and there is exactly one definition to agree with (no drift).

Why the decision must live inside the fixpoint, not after it

A tempting cheaper design is a post-processing filter: run the fixpoint to its verdict, then walk the relation and keep the members two-valued. That fails on mutual recursion between classification and the aggregate a view feeds.

Concretely: a household’s income is the sum over its dependents’ contributions — an aggregate over the dependents view. And a dependency test (a gross-income or support test) references household income. So Dependent membership and the aggregate over dependents depend on each other. A post-processing filter cannot express this: the membership verdict it needs is a function of a value (household income) that is only known once membership is already decided — the filter would have to run after a quantity it is an input to.

Evaluated inside the well-founded fixpoint, the mutual dependence is well-defined. Household income and each dependency verdict are assigned in one fixpoint under K3: where the recursion is productive, each settles to a definite value; where it forms a genuine cycle with no grounding, the cycle resolves to undefined rather than looping or silently picking an evaluation order that biases the answer. K3 is precisely what turns “circular” into “well-defined with an undefined region” — and that undefined region is the standoff this RFD surfaces at the view boundary.


Context — Today’s behavior on main (verified)

Verified against oxc-runtime/src/catalog_seed.rs (the RFD 0005 §5.6 navigation-view projection block, ~L903–1099) and oxc-runtime/src/index.rs::nav_catalog_keeps_value (~L251), re-checked on the default-branch tip 81dfe28dc. (The run-verified exhibit below was executed at an earlier default-branch tip, eac0442a0; each section states the tip its claims were checked against.)

  • A navigation view is carried on the wire as PropertyDeclBody.from_relation: Option<(Vec<String>, String)> (the relation path + endpoint) plus from_filter: Option<CborValue> (the optional where term). oxc-ast carries the surface; Argon.TypeSystem.Soundness.FieldView is the Lean reader of the same clause (RFD 0005 §5.6).
  • Materialization is store-local, at read-model seed time. For each live concept declaration and each field carrying from_relation, catalog_seed resolves the relation’s catalog key, computes the holder/value endpoint positions (binary: the sole other end; arity >= 3: the unique end <:-comparable with the declaring concept, else a LOUD build-bypass refusal OE1376), reads the relation’s subsumption-closed extent from the catalog, and projects each kept (holder, value) row into $field::<f> and the concept-keyed companions $field::<C>::<f> (RFD 0005 concept-keying, keyed by blake3(decl_concept :: field) so same-named fields on distinct concepts do not alias).
  • The element-type filter is evaluated eagerly, per candidate, at seed time. nav_catalog_keeps_value applies the iof T selection (only a strict narrowing T <: E, T != E is a real filter) and the where term. concept_admits_individual at this site admits iff-derived membership — i.e. a narrowing view onto a defined element type is permitted today, and its membership is decided by evaluating the refinement predicate on each candidate at seed time.
  • The honesty limit — two distinct failure modes, both verified end-to-end (see the run-verified exhibit below). The boundary is two-valued, but it does NOT collapse a well-founded standoff to a silent Ok(false). Two cases, established by running ox built from origin/main:
    • Field-refinement iff { … } element type (no standoff possible). Membership is always two-valued. A candidate that is definitely-false or open-world-unknown (a refinement field never asserted) is coerced to Ok(false) and silently excluded — the view drops the row and a count over it is silently short (index.rs: “a legitimate Ok(false) — the predicate is definitely false, or a missing-field open-world-unknown — still excludes”). A braced iff body cannot call a rule (OE0660), so it can never itself stand off; the dishonesty here is an open-world-unknown collapsed to false, not a standoff coerced to false.
    • Shape-derived / rule element type (genuine standoff). Membership is fixpoint-dependent. concept_admits_individual returns Truth4::Can for such a concept — “the faithful value is Can, never a fabricated Not” (classify.rs) — and the store-local read path surfaces that Can as a loud runtime refusal, not a silent drop: the navigation-view read, the count, and the extent query all fail closed (“membership in <T> is rule-derived (fixpoint-dependent) and is undetermined for the store-local classifier”). So a computed concept whose membership stands off is not navigable/countable on the store-local plane at all today. Either way there is no representation, at the view boundary, of a candidate whose fixpoint membership is undefined: the standoff case fails closed and the open-world case is silently coerced. The lift gives the plane an undefined region to place a standoff into.
  • No fixpoint view exists today. Every consumer (wire/display, mutate-body reads, rule/query/check bodies, and a classifier consuming the view through exists { p in self.f }) reads the store-local $field::… relation seeded above. There is no synthesized fixpoint rule for a view and no raw + gate split; the walk is recomputed at each read-model rebuild keyed on the storage write-generation pair (oxc-runtime AGENTS.md model_cache contract).

Which element types the lift covers — the two iff kinds, side by side

Both programs below declare a view over an iff element type. Both element types have a derived extent. Only the first is touched by this record, and the difference is whether the fixpoint can leave membership undefined.

Covered — a shape clause can stand off.

pub derive qualifying(p) :- ... ;                 // a rule, so a rule head
pub type Dependent <: Person iff qualifying(self);

pub type Household <: Person {
    dependents: [Dependent] from Supports.supported          // LIFTED (D1-D3)
    // minorDependents: [Dependent] from Supports.supported where under13(supported)
    //                                                        REFUSED, OE0683 (D7)
}

qualifying is a rule head, so Dependent(x) is decided in the well-founded fixpoint and recursion through negation can leave it undefined. The filter-free view lifts and surfaces that undefined region; the where-filtered variant is refused, because a filtered view stays store-local and would have to guess.

Not covered — a braced refinement cannot.

pub type Adult <: Person iff { self.age >= 18 };  // a predicate over own fields

pub type Household <: Person {
    adults: [Adult] from Supports.supported                  // UNCHANGED
    over21s: [Adult] from Supports.supported where senior(supported)  // ACCEPTED
}

Adult’s body cannot call a rule — OE0660 rejects any other form — so the predicate is always two-valued and Adult has no undefined region. Both views keep their store-local materialization: no lift, and no refusal. A person whose age is unrecorded is definitely not an Adult under the open-world reading, which is an information absence and is excluded, exactly as the reference manual says.

Why the distinction is load-bearing. Keying the lift and the refusal on “derived extent” instead of “can stand off” makes OE0683 refuse the second program’s over21s — a view that builds and serves correctly today — for a hazard it cannot exhibit. The narrower trigger is one variant narrower and costs nothing: it is DefinedByShape alone.

The limit this RFD removes: a filter-free view over a standoff-capable element type cannot be evaluated honestly on the store-local plane, because the plane has no undefined region to place a standoff into.

Run-verified exhibit (end-to-end, not unit-test-level)

Built from origin/main @ eac0442a0; source → ox buildox derive/ox query. Dependent is a computed concept via the shape clause (iff qualifyingChild(self), the clause pointing at a pub derive); qualifyingChild sits in a mutual-defeat cycle through defeat (acyclic defeat graph), undefined (Can) for everyone a strict clause misses.

pub type Person;
pub type Taxpayer <: Person;

#[default]
pub derive qualifyingChild(p) :- Person(p);
#[defeats (qualifyingChild(p))]
pub derive rivalContests(p) :- rivalClaim(p);
#[default]
pub derive rivalClaim(p) :- Person(p);
#[defeats (rivalClaim(p))]
pub derive claimSettled(p) :- qualifyingChild(p);

pub type Tiebroken <: Person;                 // a strict clause settles these
pub derive qualifyingChild(p) :- Tiebroken(p);

pub type Dependent <: Person iff qualifyingChild(self);   // computed concept

pub rel Supports(supporter: Person, mut supported: Person) [0..*] [0..*];

pub type Household <: Taxpayer {
    dependents: [Dependent] from Supports.supported       // the navigation view
}

pub derive dependentsOf(h, d)   :- Household(h), d in h.dependents;
pub derive dependentCount(h, n) :- Household(h), n = count(d for d in h.dependents);

Fixture: alice is a Household supporting bob and dave; bob is Tiebroken (settled), dave is not.

Membership genuinely stands off — bob definite, dave undefined (OW0740 warns at build):

$ ox derive qualifyingChild --explain
  +Δ (bob)                                   # definite (Is)
  1 undefined tuple(s) [well-founded standoff — Can]
  Can (dave)                                 # undefined

Every store-local read of the computed concept refuses — the nav-view read, the count, and the extent query all fail identically:

$ ox derive dependentsOf     # `d in h.dependents`
$ ox derive dependentCount
$ ox query --extent Dependent
Error: membership in `Dependent` is rule-derived (fixpoint-dependent) and is
  undetermined for the store-local classifier — … never evaluated
  mid-classification; read it through the reasoner or classify the individual
  directly

Contrast the field-refinement twin (iff { self.qualified == true }, dave.qualified unset): the view materializes and silently drops davedependentCount reports 1, and ox query --extent Dependent --with-truth4 shows bob only with no undefined region (an open-world-unknown collapsed to definite-false, not a standoff).

Both are the honesty limit above: the standoff case fails closed; the open-world case is silently coerced. This RFD replaces both with a served definite extent plus a surfaced undefined region (D3).


What this enables

Workflows a modeler faces today and cannot express honestly. Each opens with the situation, gives the code the lift enables, works one undefined case where it matters, then states why the store-local plane (see “Today’s behavior on main”) forces a stale, wrong, or dishonest answer. Each carries one concept: a standoff reaching a scalar aggregate, where the harm is a magnitude — a credit over- or understated (1) — and where it is directional — a compliance ratio whose failure flips to a pass (2); navigation over mapped parent relations (3); one view serving two consumers opposite verdicts (4); the undefined region as a review queue (5); propagation outward through a second navigation hop (6); composition with valid-time reads (7); and the cross-module governance boundary (8). All code is proposed/illustrative — nothing here is implemented.

The shared dichotomy, stated once — and not restated below. For a shape/rule element type the store-local read of a standoff member fails closed: the read refuses, so the roster is not navigable at all. The field-refinement/open-world twin does the opposite — it silently coerces the absence to a definite false and drops the member. Neither is a representation of an undetermined membership. Scenarios 1, 2, 4, 5 and 6 all hit the fails-closed arm. Their closings tag the arm in two words and then say only what is new at that site — which consumer surfaces the refusal blocks, and what the stale workaround costs there. The tag is repeated because a reader landing mid-section needs it; the argument behind it is not. Scenarios 3 and 7 turn on different limits (a seed-time plane cannot decide a fixpoint-defined classification at all; it cannot express a valid-time slice at all), and 8 states its own distinction.

1. Qualifying-child navigation on a tax return

A preparer builds a return. Whether a supported person is a qualifying child is not a checkbox: it is the conjunction of the relationship, age, residency, and support tests, and any one of them can turn on facts entered elsewhere in the engagement (a second household, a support worksheet). The preparer wants to write the roster of qualifying children once and have every credit computation read the concept the statute defines, not a snapshot that goes stale the moment a worksheet changes.

// tax module (model as in Motivation): QualifyingChild <: Person is iff-decided.
type Taxpayer <: Person {
    // The roster, typed by the COMPUTED concept, walked over Supports.
    qualifyingChildren: [QualifyingChild] from Supports.supported
}

// A dozen credits key on this one roster. The Child Tax Credit counts it.
check ChildTaxCredit {
    self.claimedCtc == count { c in self.qualifyingChildren } * perChildAmount
}

The preparer navigates alice.qualifyingChildren; the credit check counts it.

Worked undefined case: a child dave lives part-year in two households and the residency test stands off in a defeasible cycle — each household’s claim defeats the other absent a tie-breaker fact the preparer has not yet entered, so QualifyingChild(dave) is undefined (∈ J ∖ I). The view serves the definite children and rides dave on $undefined::$field::Taxpayer::qualifyingChildren; the CTC count sees a non-empty undefined region and is itself undefined (D3). The return does not compute a credit that silently omits or silently includes a contested child — it surfaces that a tie-breaker fact is missing, the one actionable state for the preparer.

Why today fails — fails-closed arm. Blocked surfaces: the nav-view read, the credit’s count, and the --extent query (run-verified exhibit). The roster is not navigable or countable at all — a hard expressiveness blocker, not a silently wrong number. Workaround cost here: a stored roster flag goes stale on the next support-worksheet edit, and hand re-deriving the tests inside each credit sets the dozen credits drifting against each other.

2. Nondiscrimination roster feeding a coverage-ratio test

A retirement plan must pass a coverage test: the fraction of non-highly- compensated employees benefiting must be at least a statutory multiple of the highly-compensated fraction. Eligibility is decided by enrollment rules — hours of service, entry dates, a waiting period that can itself hinge on a rehire fact. An auditor recomputing the test needs the roster to be the eligibility concept, and needs an undetermined member to make the ratio undefined rather than silently drop out and flip a fail into a pass.

pub derive eligibleEmployeeShape(e: Employee) :-
    completedServiceHours(e), pastEntryDate(e), not excludedClass(e);
type EligibleEmployee <: Employee iff eligibleEmployeeShape(self);

type Plan {
    eligibleParticipants: [EligibleEmployee] from Enrollment.employee
}

check CoverageRatio {
    benefitingFraction(self.eligibleParticipants, nonHighlyComp)
        >= statutoryFloor
           * benefitingFraction(self.eligibleParticipants, highlyComp)
}

Worked undefined case: a rehired employee’s waiting-period credit stands off — the rehire-bridging rule and the break-in-service rule form a negation cycle pending a determination the plan administrator has not entered — so EligibleEmployee(x) is undefined. The ratio aggregates over a set with a non-empty undefined region, and per D3 the ratio is itself undefined: the test does not discharge on a two-valued roster. The auditor sees the test cannot be certified until the one member is resolved. Aggregate-goes-undefined is exactly the signal the auditor needs.

Why today fails, and what is distinct here: the harm is directional, not merely inaccurate. Dropping an undetermined member shrinks the roster, which raises the benefiting fraction, which turns a coverage failure into a pass — and a plan qualification defect discovered on a later audit invalidates the intervening years that were already relied upon. That is the field-refinement/open-world arm, and it is the reason the lift must not reintroduce silent exclusion. Today EligibleEmployee is a shape/rule concept, so it lands on the fails-closed arm instead. Blocked surfaces: the roster read, the ratio’s count, and the --extent query (run-verified exhibit) — the ratio cannot be computed at all, so the test is inexpressible rather than silently wrong. Workaround cost here: denormalization goes stale on any hours or rehire edit.

3. Statutory cross-reference over a mapped parent relation

A statute allows a deduction for “items of the kind described in section X.” The modeler expresses section-X as a computed classification DeductibleItem <: IncomeItem, and income items arrive as specialized child relations (WagesUSA, DividendUSD, …) that subsume into IncomeItem under mapped relation subsumption (RFD 0084, merged) — a child pins a constant end such as currency or source. The return must navigate “the deductible items” across the parent relation and see the child facts through their mapped images.

rel IncomeItem(p: Person, amount: Money, c: Currency);
// Every filler is keyed by the PARENT end it fills (RFD 0084 — the positional
// filler list was removed); `c = USD` pins a parent end to a constant.
rel WagesUSA(p: Person, amount: Money) <: IncomeItem(p = p, amount = amount, c = USD);

type DeductibleItem <: IncomeItem iff describedInSectionX(self);

type Return <: Person {
    // Walks the subsumption-closed parent extent → sees WagesUSA as its φ-image.
    deductibleItems: [DeductibleItem] from IncomeItem.p
}

Worked case (D5): a WagesUSA(alice, 1000) fact contributes the φ-image IncomeItem(alice, 1000, USD). The view walks the subsumption-closed parent extent, so the section-X classification runs over the parent frame with the constant-pinned USD end visible to it (RFD 0084 forbids dropped child ends, so the frame is information-complete). If describedInSectionX stands off on one item, that item rides $undefined::, and the deduction total is undefined rather than silently smaller.

Why today fails: no store-local plane can decide a section-X membership that is itself fixpoint-defined and simultaneously honor the standoff — the seed-time pass has no undefined region (“Today’s behavior”). Hand re-derivation cannot even see the φ-images without re-implementing mapped subsumption per query, and would still collapse the standoff to a two-valued deduction.

4. Authorized signers on an account (the case for consumer-decides)

An account’s authorized signers are decided by a delegation chain — a power of attorney, a corporate resolution — each with an effective window and each revocable. Getting membership wrong in either direction is a concrete harm: eager inclusion of an undetermined signer authorizes access that may not exist; eager exclusion locks out a signer whose authority is merely unconfirmed at read time. The two consumers of the roster — an authorization gate and a display roster — want opposite fail directions, which is why the view itself must not pre-decide.

type AuthorizedSigner <: Person iff hasActiveDelegation(self, account);

type Account {
    signers: [AuthorizedSigner] from Delegation.grantee
}

Worked case: a delegation whose revocation is contested stands off, so AuthorizedSigner(p) is undefined. The display roster shows the definite signers and marks p as a standoff for a reviewer to follow up. An authorization gate reading the same view via exists { s in account.signers } evaluates the existential in WFS: an all-undefined witness set yields undefined (D3), and the gate’s own policy — fail-closed for access — denies. That existential’s undefined verdict shares E4’s prerequisite — undefined-ness propagating through a rule body join, the engine-level lift this record inherits rather than owns (D3) — so the gate half of this scenario is intended behaviour awaiting that lift; the display half, which reads the definite roster plus the distinguishable standoff, needs only this record’s slices. Neither outcome required the view to pre-decide to include p (authorizing access) or drop p (silently locking out and hiding the contest). One view, two honest consumers, opposite treatments of the same standoff.

Why today fails — fails-closed arm; the roster read refuses. Distinct here is whose need goes unmet: there is no store-local extent to serve either consumer, so the two correct and opposite treatments of one undetermined member cannot both be built off one roster — which is precisely the surface-honest, consumer-decides policy this RFD adopts (D3).

5. The undefined region as an auditor’s worklist (defeasible classification)

A classification is often a default with exceptions: an employee is a ProtectedClass participant by default, unless a documented exclusion applies; a taxpayer filing status defaults to one value unless a rebutting fact is on file. This is the defeasible plane — a #[default] rule that a #[defeats] exception can override. When the default rule and the exception both fire and neither is grounded over the other, the classification is in standoff: the defeat plane holds it undefined by decision rather than guessing a winner.

A view typed by such a defeasible concept surfaces exactly the individuals whose membership is in standoff — and that is not a nuisance, it is the deliverable.

// Membership defaults true, but a documented exclusion rebuts it.
type CoveredParticipant <: Employee iff enrolled(self);
#[default] derive covered(e: Employee)             :- CoveredParticipant(e);
#[defeats(covered(e))] derive excluded(e: Employee) :- documentedExclusion(e);

type Plan {
    // The roster typed by the defeasible concept.
    coveredRoster: [CoveredParticipant] from Enrollment.employee
}

Worked undefined case: for most employees the default holds or a clean exclusion rebuts it — definite in or definite out. For three employees an exclusion form is on file and an enrollment record contradicts it, and no rule grounds one over the other, so CoveredParticipant stands off on those three. The view serves the definite roster and rides exactly those three on the undefined companion. An auditor reading the roster does not get a wrong list that quietly picked a side; they get a review queue — “resolve the membership of these three” — which is the one actionable output. The undefined region is the worklist.

Why today fails — fails-closed arm (rule-derived through defeat). Blocked surfaces: the roster read and the extent query. Distinct here is that the refusal destroys the deliverable itself — the three contested employees cannot be listed, so there is no roster to hand the auditor and no worklist to build from it. The defeat plane’s whole point (undefined-by-decision instead of a guess) is unreachable the moment the classification is navigated store-locally.

6. Undefinedness propagates through a navigation chain

Views compose: taxpayer.qualifyingChildren is a set of children, and each child has .educationExpenses. A credit sums education expenses across the qualifying children. When one child’s membership is undetermined, honesty must propagate through the chain — the credit total is undefined, not silently computed over the children who happen to be definite.

// Person (declared in Motivation) carries a stored expense field:
//   type Person { educationExpenses: Money, ... }

type Taxpayer <: Person {
    qualifyingChildren: [QualifyingChild] from Supports.supported
}

// Education credit sums a nested navigation: over each qualifying child, the
// child's education expenses.
check EducationCredit {
    self.claimedEducationCredit
        == sum { c.educationExpenses for c in self.qualifyingChildren }
}

Worked undefined case: bob and carol are definite qualifying children; dave’s residency test stands off, so QualifyingChild(dave) is undefined. The outer view self.qualifyingChildren has a non-empty undefined region (dave). The sum aggregates over a set one of whose members is undetermined — and dave’s expenses could change the total — so the sum is itself undefined (D3), and the credit check does not discharge on a number computed over { bob, carol } alone. The undefinedness at the membership boundary flows outward through the nested read to the aggregate: a chain is only as definite as its least definite link.

Why today fails — fails-closed arm. Blocked surfaces: the nested navigation, the sum, and the extent query. Distinct here is that there is no boundary at which the undefinedness could enter the computation — the store-local plane has no undefined region, so the chain has nothing to propagate along and the credit is not computable at all rather than silently partial.

7. “Who was eligible when the test ran?” — views over a computed concept read as-of a valid time

Compliance tests are asked as of a date: “who was an eligible participant when the coverage test ran in March?” Argon is bitemporal — facts carry valid time, and a read can be taken as-of a valid instant. Membership is a computed concept, so the as-of question is “what was the fixpoint verdict over the facts valid at that instant?” A typed view composes with valid-time reads: the roster read as-of March is the extent the membership rules decide over the March-valid facts, standoffs included.

pub derive eligibleEmployeeShape(e: Employee) :-
    completedServiceHours(e), pastEntryDate(e);
type EligibleEmployee <: Employee iff eligibleEmployeeShape(self);

type Plan {
    eligibleParticipants: [EligibleEmployee] from Enrollment.employee
}

Worked case: a service-hours record is later corrected — the correction is valid from April, but as-of March the uncorrected hours were on file. Read as-of March, self.eligibleParticipants is the membership the rules decide over the facts valid in March; an employee whose March eligibility turned on a fact that was itself in standoff in March rides the undefined companion of the as-of read. The auditor reconstructing the March test sees the roster the engine would have served in March — definite members and the March standoffs — not today’s membership retro-applied.

Why today fails: seed-time materialization bakes a single two-valued extent at read-model build; it has no notion of “the membership verdict over a different valid-time slice,” and it cannot carry a standoff into the as-of answer. A view evaluated inside the fixpoint inherits the fixpoint’s valid-time discipline for free — the walk and the membership test run over the same as-of frame.

8. Cross-module: rules evolve in one module, navigation stays correct in another

A governance boundary: module eligibility owns the classification rules; module reporting navigates them. reporting should not — and by import discipline cannot — re-encode eligibility’s tests; it declares a view typed by the imported concept and walks it. When eligibility tightens a test (adds a new exclusion, adjusts an age limit), reporting’s view is correct on the next build with no edit, because the view is the imported concept walked, not a copy of its rules.

// module eligibility
pub derive eligibleEmployeeShape(e: Employee) :-
    completedServiceHours(e), pastEntryDate(e), not excludedClass(e);
pub type EligibleEmployee <: Employee iff eligibleEmployeeShape(self);

// module reporting  (imports EligibleEmployee, Enrollment)
type Plan {
    eligibleParticipants: [EligibleEmployee] from Enrollment.employee
}

Worked case: eligibility later adds not leasedEmployee(e) to the eligibleEmployeeShape body that defines EligibleEmployee. reporting is not touched; its next build walks the tightened concept and the roster shrinks correctly. If instead reporting had hand-copied the three original tests (workaround (b) above), the tightening would silently not apply — reporting’s roster would keep counting leased employees, drifting from the authority that owns the definition, with no error. The runtime and the build-plane validator consume the checker’s resolved reference to the imported concept (D4), so the cross-module walk keys the same membership the checker accepted — no re-resolution that could go false-ambiguous or silently miss.

Why today fails: not that cross-module navigation is impossible today, but that the only stale-proof cross-module option — a live view over the imported concept — is exactly the standoff-capable walk this RFD lifts; store-local materialization of a shape/rule concept fails closed on any standoff in the imported concept (verified honesty limit) — the read refuses — so reporting cannot both track the authority’s rules and honor a standoff the authority’s fixpoint produces.


Decision — the design (D1–D9)

D1 — The pin-resolution ladder (one ladder, keying every concept-pinned field read)

When a rule / mutate / test body reads t.field, the base variable t may be pinned to one or more concepts by the atoms in scope (C(t), iof(t, C), a declared relation endpoint whose sort is a concept). Same-named fields on distinct declaring concepts are distinct cells (each property id hashes the declaring concept’s qualified path — the RFD 0005 concept-keying, verified in oxc-runtime AGENTS.md). A single ladder maps (pin set, field name) to one cell decision. It is layered so file-local answers short-circuit and only genuinely cross-module or ambiguous cases fall through to the workspace <:-ancestry walk:

LevelConditionOutcome
L1 — head-declaredrule head parameter declares the receiver’s concept, and that concept’s file-local chain declares the fieldKeyed(head). An unanswerable chain (cross-module, or chain not fully local) trusts the static annotation → Keyed(head)
L2 — single body pinexactly one pin in scopeKeyed(pin); the runtime resolves the declaring ancestor on its side, so a cross-module single pin stays answerable
L3 — body-pin evidencecollect provable declarers; a pin the file-local oracle cannot answer falls back to the workspace <:-ancestry walkunique most-specific declarer → Keyed(that declarer's declaration-qualified path); two-plus declarers with no containing chain → Incomparable (D3/refusal); zero declarers → Ambiguous (refusal)

The keyed cell always uses the declaration-qualified path, never the bare name, even when the field’s only file-local identity is that cell. The empty pin set is the only early exit (field reads stay bare-named); a size-1 pin set still routes through the full winner classification, so a defined singleton correctly classifies as a gated bare read (D2) rather than serving an unmaterialized keyed read.

The winner’s kind selects the read shape:

  • Plain (ordinary-concept) winner → root the read at its concept-qualified stored relation.
  • Defined (iff/shape) winner → the winner’s keyed stored-slot plane is fixpoint-materialized (extent not seedable), so a rooted stored read would join an empty relation. The read stays the bare relation gated by the rule’s own membership atom. A lifted view at such a winner roots at the winner’s fixpoint-synthesized key; a WFS-dependent declarer’s unlifted-view cell is raw+gate materialized at load (D2) so the rooted read is live, never vacuously empty.
  • Incomparable pins → the read refuses (see D3 / refusal codes). Rules with no field read over the base stay legal — multiple classification is not itself an error.

D2 — Raw + gate cells (keeping a fixpoint-decided extent live)

The problem: a concept whose membership is decided inside the fixpoint has no seed-time extent, so an extent-gated seeding would leave the keyed field relation permanently empty while the wire serves the read. The fix seeds the field candidate row in two forms and defers the membership decision to the fixpoint:

  • Raw cell$rawfield::<C>::<f> seeded extent-blind: every fixpoint-decided concept whose canonical declarer for the field is the walked concept body takes the candidate row with no membership gate (a holder asserted at a descendant is a candidate member of every chain concept by upward closure). Which concepts receive a raw cell is a recorded load-time fact, never a seed-side re-derivation, so the seed pass and the read pass cannot disagree.
  • Gate clause — synthesized at Module::load, one fixpoint rule: $field::<C>::<f>(x, v) :- $rawfield::<C>::<f>(x, v), <C>(x). Membership is decided INSIDE the fixpoint here. The raw cell’s own gated name is rule-only — never seeded — or EDB rows would mix into a fixpoint head.

This split is what makes a fixpoint-decided extent incrementally maintainable: the raw cell is an ordinary EDB projection maintainable by delta as facts arrive; the gate is an ordinary fixpoint rule re-evaluated by the same incremental circuits that maintain every membership rule. See D6 for the maintenance contract.

What the split costs, measured. The raw cell is a persisted second extent, and because it is seeded extent-blind it grows with the walked extent rather than the member extent. So a view whose relation walks many candidates to few members pays proportionally the most, and the cost lands in artifact size rather than in time. Measured on the runtime slice at 40,000 walked candidates: the artifact grows 5.7%–7.3% against the same program with a plain element type (+2.6 MB at 200 members, +2.7 MB at 20,000).

Build time moves the other way, and against the expectation that motivated this split’s scrutiny. Store-local materialization filters at seed time by classifying each candidate individually; the lifted form seeds blind and lets the fixpoint decide membership with one set-at-a-time join. The join wins, and it wins wider as the member extent grows: at those 40,000 candidates, ox build is 2.0× faster lifted at 200 members and 3.5× faster at 20,000, because the store-local path scales with the member count (6.75s → 11.82s) while the lifted path is flat (3.38s → 3.42s). Derived-read time shows no difference at that size. A view over a computed concept is therefore cheaper to build than the denormalized workaround it replaces, which is the comparison a modeller actually faces.

These are from-scratch build figures and say nothing about rebuild cost, which is the open question D6 carries: whether the raw cell participates in touched-set narrowing or is re-seeded whole on every rebuild.

D3 — The K3 standoff policy at the view boundary (the decision)

WFS evaluation is three-valued (Kleene / K3, oxc-reasoning executor/aft.rs): a tuple is definitely-true (I), definitely-false, or undefined (J ∖ I — a well-founded standoff: recursion through negation that the alternating fixpoint pins neither true nor false), read back as the MetaValue K3 assignment (WellFounded.lean). The engine already materializes that region as a per-head companion relationR carries I, $undefined::R carries J ∖ I — and the companion namespace is threaded through the reasoner: the companion-name helper on the query-engine facade (oxc-reasoning/src/query_engine.rs), the stratification boundary that keeps ordinary NAF from reading it as definite (oxc-reasoning/src/compile/stratify.rs, and the OE1365 refusal that guards the unsound case), and the catalog’s exclusion of the namespace from ordinary per-relation treatment (oxc-reasoning/src/catalog/mod.rs).

Absence of evidence is definitely-false only under a closed world. Under the closed-world default an unasserted fact is definitely-false to the derive plane, and a refinement predicate over an unrecorded field is settled by the reference manual’s membership rule (below), not by the fixpoint — so for a closed-world element type the region above is the well-founded standoff, exactly. Under an open world the manual reads the same absence as unknown (spec/reference/src/semantics/world-assumptions.md: “if a fact isn’t derivable, it’s unknown”), and it is undetermined rather than false. That is the shipped meaning of undetermined, not an inference: the check-plane determinedness operator’s undetermined region is the union of the well-founded standoff region with open-world absence of unrefuted evidence, and its mechanized denotation is parameterized by the world, with the standoff-only reading recovered as the closed-world instance (undeterminedAtom and undeterminedAtom_closed_iff_standoff, spec/lean/Argon/Reasoning/Checks.lean).

D3’s region therefore tracks the element type’s world: a standoff alone for a closed-world element type, and a standoff plus an unrefuted-absent candidate for an open-world one. The widening does not change what the derive-plane companion records — $undefined::R carries fixpoint outcomes only, so open-world absence has no companion of its own there, and the derive plane refuses a negation that would read such an absence as definite rather than evaluating it three-valued (OE1367, the open-world twin of OE1365). The check plane carries the union in its own separate region. Which carrier serves an open-world element type at a view boundary is an open question below, not a decision this record makes.

At a view boundary this MUST NOT collapse to a two-valued verdict in either direction. The governing principle, stated here and held in common with the admission-point-checks record (RFD 0086), which states it in its own words as the two forbidden collapses of its three-verdict decision (D1) and as its failure-direction doctrine — that doctrine cites the view boundary’s refusal to coerce as existing precedent, so the agreement runs both ways:

Undetermined never silently coerces to true or false. Each construct that observes an undetermined value declares its own policy for it.

The view construct’s declared policy — surface-honest, consumer-decides:

Element T-membership at xView extent of self.fSurfacing
definitely-true (∈ I)x is in the extentordinary row on $field::<C>::<f>
definitely-falsex is not in the extentabsent — a real exclusion, not a standoff
undefined (∈ J ∖ I)x rides the undefined companion $undefined::$field::<C>::<f>surfaced LOUDLY at every store-local surface; not dropped, not included

Concretely, per consumer:

  • Set-materialization / wire / display read of self.fthis half of the policy is the shipped read surface, not a proposal. Every derived read is already an epistemic envelope (compiler/crates/oxc-mcp/src/convert.rs): each row carries its four-valued tag (Is / Not / Can / Both) beside a sibling hidden object counting the undetermined (can), over-determined (both) and determined-false (not) rows. A head’s WFS-undefined companion tuples ride that envelope as Can rows by default (derive_classical_envelope / derive_tagged_envelope), and hidden.can is that companion’s own count — a projection of the companion, not an independent computation. The human fail-closed projection is opt-in (project_k3); under it the Can rows drop from the row list while hidden is still reported — the crate’s stated contract is “never a silent collapse”, and no path collapses undetermined to false or over-determined to true. The reference manual carries the same contract as surface law (spec/reference/src/semantics/reasoning.md, the envelope paragraph: drop-but-count). What this RFD adds for a view is only the source of the region — the synthesized $field::<C>::<f> head’s companion — never a new wire field, marker, or projection mode.

  • Aggregate / count over self.f (the exemption check in Motivation) — the one arm of D3 that is not already shipped behaviour, and it invents no new discipline. An aggregate whose input set has a non-empty undefined region does not return a fabricated two-valued scalar. It defers to the aggregation-over-undefined discipline that already ships rather than stating a view-specific rule: a fold over a relation whose $undefined:: companion is non-empty is refused whole-relation, loudly, instead of treating undefined as false (ReasoningError::AggregateOverUndefined in oxc-reasoning/src/error.rs, raised from oxc-reasoning/src/executor/aggregate.rs; the diagnostic is OE1332, whose catalog text states the conservative whole-relation scope and names three-valued aggregate intervals as the designed follow-up). So the semantics is: the aggregate is itself undefined at that read unless the undefined region provably cannot change the result — the same conservatism the shipped guard already applies, reached once a lifted view gives the fold an undefined input. This is the per-view policy’s whole point — it lets the standoff propagate to the consumer that can adjudicate it, rather than the view pre-deciding. What approving D3 leaves undecided is confined to this arm: how much partial definiteness the boundary should exploit before declaring the aggregate undefined (Open question 1, the three-valued-interval track). Every other arm of D3 describes behaviour on the default branch.

    Consumer-surface reality — what each aggregate consumer kind experiences. “The aggregate is itself undefined” is the semantics; the concrete surface a modeler meets differs by consumer, because the plane has no undefined-valued scalar. The three consumer kinds:

    • check (three-valued discharge). A check whose body aggregates over a view with a non-empty undefined region does not pass or fail on a fabricated number — it discharges three-valuedly: the check verdict is itself undefined and surfaces through the K3 check-discharge pass. This is the honest, non-refusing consumer.
    • Scalar derive binding the aggregate (OE1332, the runtime refusal raised while ox build maintains the read model). A value derive such as pub derive dependentCount(h, n) :- Household(h), n = count(d for d in h.dependents) binds the count into a scalar n. A scalar cannot hold “undefined”, so the undefined propagates as a refusal: the pre-existing aggregate-over-undefined discipline (OE1332, the #250/#134 track D3 cites) refuses at build rather than folding undefined to false. This is not a new refusal introduced by the lift — it is the existing derive-plane guard, reached once the lifted view gives the fold an undefined input. The steer is to move the consumer to a check (which discharges three-valuedly), or to read the view’s regions directly (definite rows + the undefined companion) and aggregate an intermediate that is itself definite.
    • Direct view read (definite rows + undefined region). A consumer that reads self.f directly (no aggregation) gets the definite-true extent plus the distinguishable $undefined::$field::<C>::<f> region — it never refuses; it inspects the standoff itself.

    The run-verified OE1332 render for the scalar-derive case is in the worked examples (E3), which states the draft it was captured on and cites it.

  • Classifier consuming the view via exists { p in self.f }: a bare exists conjunct over a lifted view forces the classifier onto the rule plane (a fixpoint view is not locally decidable). The existential is evaluated in WFS: undefined membership contributes undefined to the existential per K3, not a silent false. A genuine view↔classifier negation cycle resolves as undefined per WFS — neither a refusal nor false.

  • where-filtered views (see D7) are out of scope for the lift and stay store-local; a filtered view over a standoff-capable target is refused at build, fail-closed, rather than served a two-valued extent it cannot honestly compute. Over a braced refinement it is accepted.

Relationship to the reference manual — D3 composes with it; no chapter needs amending. The two rules answer different questions, and both are needed.

  • The reference manual decides membership.iff-derived membership is three-valued: success requires positive evidence that the predicate holds. A value whose predicate evaluates to unknown does not satisfy the refinement … Under CWA, unknown collapses to false”; “unknown means information absence — a referenced field with no recorded value” (spec/reference/src/declarations/types-and-refinement/refinement.md:24, the Three-valued membership under OWA paragraph). That rule decides whether an element is a member. A view’s element filter inherits it unchanged; this RFD neither restates nor narrows it.
  • D3 decides observability at a view boundary, for the elements the fixpoint leaves undefined. Its two halves land on opposite sides of the manual’s rule and neither contradicts it. Not included is exactly the manual’s verdict — an undetermined element is not in the served extent. Not dropped addresses a question the membership rule does not reach: whether the excluded region must stay readable. D3 says it must, and rides it on the $undefined:: companion. D3 is therefore additive.

The consequence is concrete, and it is conditioned on the world: under the closed-world default a braced-refinement element type over an unasserted field behaves under D3 exactly as the manual says it behaves today. The predicate is unknown, the manual collapses unknown to false under CWA, the value does not satisfy the refinement, it is not a member, and the fixpoint produces no undefined tuple for it — a definite empty extent and a definite count of zero, with no undefined region. D3 changes nothing there. Under an open world the manual does not collapse the unknown, so the same candidate is undetermined and D3 places it in the undefined region rather than excluding it; D3 is still additive there, because it is again following the manual’s verdict on the absence rather than overriding it. What D3 governs is the case the membership rule does not speak to: an element whose membership is decided by rules that recur through negation, where the alternating fixpoint pins the atom neither true nor false.

The truth-values chapter’s set-projection sentence is likewise consistent. “Set / list / record projections: Can-valued cells are omitted from the result set” (spec/reference/src/semantics/truth-values.md:31, K3 fail-closed projection) is scoped to the fail-closed projection of a single-standpoint query to a non-Truth4 type. D3 does not fold undefined elements into the returned set — it routes them to a name-distinguishable companion — so that projection still omits them, and the reasoning chapter already states the obligation the projection sentence leaves implicit: an undefined tuple “surfaces as a Can row, by default — never silently omitted, because an omitted standoff is byte-indistinguishable from a definite Not”, and the fail-closed projection “drops Can rows from the row list but still reports them in hidden.can — drop-but-count, never a silent collapse” (spec/reference/src/semantics/reasoning.md). Drop-but-count is D3’s own discipline one layer down, already surface law and already shipped.

Prerequisite this record inherits rather than owns. spec/reference/src/semantics/reasoning.md states that “A rule body still joins only against definite rows — an undefined atom does not fire downstream rules; propagating undefined-ness through body joins is the engine-level three-valued lift, tracked separately.” The classifier clause above (exists { p in self.f }, worked example E4) and any consumer that joins a lifted view’s undefined region into a further body depend on that lift. No slice in the Staging plan allocates it; it is a prerequisite, and the record does not own it. The two places that show such a consumer — worked example E4 and the authorized-signers scenario’s authorization gate — are marked at the point of use as intended behaviour awaiting the lift, not as output of the slices staged here.

Two build-time refusals are NOT standoffs and stay build errors:

  • An unproductive positive view cycle (a view provably empty for every store — an unseeded cycle or acyclic dead-end) is a modeling defect that should be refused at build. This refusal is net-new analysis, not an edge-addition to an existing check. There is no check-plane productivity / positive-cycle graph on the tree for a view-element edge to be added to — established while building the check-plane slice (#1838). The elaborator (oxc-instantiate) does carry one rule-dependency graph — the defeat lint’s positive / negative / defeat edge graph with a Tarjan strongly-connected- component pass (oxc-instantiate/src/lower/defeat_lint.rs, the emitter of OW0740) — but it judges no productivity or emptiness and carries no view-element edges, and stratification lives in the reasoner’s compile step, not as a build refusal for a provably-empty view. Landing this refusal therefore requires building a view-element dependency graph in the check plane from scratch — a distinct, explicitly-staged increment (staged after the runtime slice; see the Staging plan), not an extension of a pre-existing check. Reserved-not-wired (diagnostic OE0684) until that slice lands; whether to build it or defer it is an owner decision (Open questions).
  • A view whose lift would be ambiguous or incomparable at a field read (D1) refuses at build (the pin ladder’s Incomparable / Ambiguous).

Rejected boundary policies (see Alternatives): dropping undefined (Can-to-Not), including undefined (Can-to-Is), and a global refuse-on-any- undefined (kills the honest-propagation case the Motivation needs). The chosen policy is surface-honest: the definite extent is always available, the undefined region is always distinguishable, and the consumer declares whether a standoff blocks it — which is exactly the commit-time twin the admission RFD owns for writes (D7).

D4 — Resolution contract (non-goal: rewriting the resolution pass)

The pin ladder consumes the per-scope resolution results; it does not rewrite the resolution pass. The failure this contract forecloses is two coexisting scope models in lower/rule_ref.rs — a second scope-tracking design standing alongside the per-scope reading/declarer oracles. Two models can disagree at a position on whether a name reads as a binder or as a relation application, and the pin ladder’s choice of cell keys on exactly that judgment, so a field read’s canonical cell would depend on which model answered rather than on the program. This RFD’s contract:

  • The ladder’s classifier reads the declarer / chain-locality / ancestry oracles the resolution pass already exposes per scope. A name reads as a binder at a position iff a binder for it is in scope at that position; a relation-application analysis is judged per scope. There is exactly one scope model in rule_ref.rs.
  • The ladder adds no new resolution semantics. Where it needs a workspace <:-ancestry answer a file-local oracle cannot give (cross-module pin fallback), it calls the existing workspace-walk classifier, mapping its outcome into the cell lattice — it does not re-resolve names.
  • Cross-module: the runtime and the build-plane view validator consume the same resolved reference the checker accepted — never a re-resolution, which could go ambiguous (false refusal) or silently miss (under-refusal). This is the import-parity invariant (D7 test obligation).

D5 — Interaction with mapped relation subsumption (RFD 0084)

RFD 0084 (merged) made <: on relations an explicit argument-mapped form: a child contributes, for each parent end, a mapped image φ of its tuple (rename / cast / constant pin), and subsumption is mapped-tuple-inclusion (the image of the child under φ), not positional tuple-inclusion.

A view walks a relation’s subsumption-closed extent (verified: catalog_seed reads the closed extent so a projection includes subsumed children). Therefore a view walking a parent relation sees the φ-images of member facts, not the child tuples:

  • The value the view projects at the walked endpoint is φ’s filler for that parent end — a renamed child end, a cast child end, or a constant pin (e.g. WagesUSA(p, amount) <: IncomeItem(p, amount, c = USD) contributes IncomeItem(p, amount, USD); a view over IncomeItem.c sees USD).
  • The element-type test <T>(x) and the holder-endpoint resolution run over the parent frame (the φ-image). RFD 0084 D1 forbids dropped child ends, so the parent frame is information-complete — the holder endpoint is always present in the image, and the arity >= 3 holder resolution (verified in catalog_seed) keeps its uniqueness precondition.
  • No new mechanism: the view consumes the one canonical resolved-mapping structure RFD 0084 already threads through every plane. This RFD states the interaction; it does not re-lower subsumption.

D6 — Maintenance contract (compatibility with the incremental strict model)

The incremental-maintenance plane — defeat-plane compilation (RFD 0083) and the incremental rule circuit compiler (RFD 0085) — makes the strict model survive commits by delta. The raw + gate split is stated compatibly, without deep integration:

  • The raw cell $rawfield::<C>::<f> is an ordinary EDB projection: it is maintained by delta as backing-relation facts arrive/retract, by the same circuits that maintain any seeded relation. No walk recomputation.
  • The gate clause is an ordinary strict WFS rule: it is re-evaluated by the same incremental fixpoint circuits (project / map / union) that maintain every membership rule. A change to <C>(x)’s extent flows to $field::<C>::<f> through the gate, incrementally.
  • The undefined companion $undefined::$field::<C>::<f> is maintained as the companion of any fixpoint relation — no view-specific maintenance path.

This RFD does not specify the incremental circuit internals; it commits only to the shape (raw = delta-maintainable EDB, gate = fixpoint rule) so that a lifted view imposes no new full-recompute obligation on the maintenance plane.

D7 — Filtered views stay store-local and refuse at build (fail-closed)

The non-goal boundary of the lift. Stated as its own decision because the rest of the record cross-references it by number.

What a where filter can say today. In f: [T] from Rel.endpoint where P, the only variable the clause binds is the PROJECTED ENDPOINT, named endpoint. Each form below was run against the checker rather than read off the grammar: a type test (where endpoint : T2), a rule call (where somePredicate(endpoint)) and a field predicate (where endpoint.age < 13) all check. Every filtered example in this record has been rewritten to that spelling — the earlier examples wrote where somePredicate(self), which does not parse into anything the checker accepts, so a reader copying one hit a refusal on a form the record presented as working. That is a correction to the examples, not a statement that the endpoint spelling is the right long-term surface.

The holder is unreachable — a known gap, tracked. The correction above does not settle what the clause OUGHT to bind. self is the spelling a modeller reaches for, and whether it should work here is genuinely open: the reference manual’s only definition of the keyword makes self the enclosing declaration’s subject (which for a field is the HOLDER), while the elaborator’s own lowering of this very clause renames the endpoint binder to self (making it the projected ELEMENT). The examples that used to appear here read as the second; the synthesized lift rule earlier in this record names the holder self, which reads as the first. Separately from that ambiguity, no spelling at all reaches the holder: the relation’s other endpoints are not bound either, so a condition relating a projected element to the holder’s own state (“the dependents under this taxpayer’s own cap”) cannot be written. Both the ambiguity and the missing capability are recorded in issue 2007, which also states why widening the scope is entangled with narrowing this decision.

  • Filtered views stay store-local. A where-filtered view whose element type or filter type-test target is standoff-capable is not lifted; it is refused at build, fail-closed. A filtered view over a braced refinement is accepted and unchanged — it has no standoff to mishandle, so refusing it would reject a program that materializes store-local and serves correctly today. The where machinery (OWA-unknown exclusion, value predicates over the field bag) is not hosted by the fixpoint lowering, and a store-local read of a fixpoint-dependent membership can only fail loud at runtime — check-green / runtime-dead is never admitted.

D8 — One shared principle with the admission-point-checks record, and no shared mechanism

  • No shared new dispatch structure with the admission RFD. Both this RFD and the parallel admission-point-checks RFD touch three-valued surfaces. They share exactly one principle (the boxed statement in D3): undetermined never silently coerces, and each construct declares its policy. They do not share a new dispatch mechanism. The view’s policy (D3: surface-honest at the read boundary) and admission’s policy (its own per-check disposition, derived from the check’s declared severity and its observe marker, judged at the admission point of the write under judgment) are independent. Coupling the two into one severity-dispatch substrate is rejected for a stated technical reason (Alternatives): severity is a property of a check declaration, and a view read boundary has no check and therefore no severity to dispatch on, so a shared substrate would have to invent one for the read side — and would then make neither surface changeable without the other. This RFD keeps them decoupled deliberately.

D9 — The lift is a derivation-semantic change: epoch bump plus projection-cache commitment

Lifting a view changes what the derivation means for the same source and the same artifact, not merely how it is computed: a filter-free view over a standoff-capable element type served a store-local two-valued extent before the lift, and serves a fixpoint extent plus an undefined region after it.

So the lift MUST bump DERIVATION_SEMANTIC_EPOCH, and the change MUST be entered in the projection-cache derivation_commitment of RFD 0072. The failure this prevents is specific rather than hygienic: the projection cache is keyed on a derivation-input commitment, so without the epoch entry a cache populated before the lift stays live afterwards and serves the pre-lift model — and because the binary is the same on both sides, no same-binary differential arm catches it. The full statement of the obligation, including what a stale cache would serve, is under Consequences.

Restated non-goals

  • The resolution pass is not rewritten (D4). Relation subsumption is not re-lowered (D5). The incremental circuits are not modified (D6).

Worked examples with I/O

Model as in Motivation. alice is a Taxpayer; Supports(alice, bob), Supports(alice, carol), Supports(alice, dave).

E1 — definite membership

bob satisfies every QualifyingChild test (all definite-true). carol fails the age test (definite-false).

query alice.dependents
→ { bob }                        // carol excluded — a real exclusion, not a standoff

Lowering: $field::Taxpayer::dependents(alice, x) :- Taxpayer(alice), Supports(alice, x), Dependent(x). Dependent(bob)I; Dependent(carol) definite-false; dave below.

E2 — undetermined membership surfaces, does not silently resolve

dave’s sharesResidence test stands off in a negation cycle → Dependent(dave) is undefined (∈ J ∖ I).

query alice.dependents
→ { bob }                        // definite extent
   standoff: { dave }            // $undefined::$field::Taxpayer::dependents — surfaced, not dropped

dave is neither in the returned set (would be Can-to-Is) nor silently gone (would be Can-to-Not). The read reports the standoff region.

E3 — aggregate over a view with a standoff (both consumer surfaces)

The same count-over-dependents aggregate meets two different concrete surfaces depending on the consumer kind (D3 “Consumer-surface reality”). E3a is the check that discharges three-valuedly; E3b is the scalar derive that refuses.

E3a — the check (three-valued discharge, works).

check ExemptionCount { count { d in self.dependents } == self.claimedDependents }

With E2’s store: the input set has definite { bob } and undefined { dave }. count is not served a fabricated scalar — it is undefined at this read (the undefined element could change the count from 1 to 2), so the check verdict is undefined and surfaces per the K3 check-discharge pass rather than passing on count == 1. Under E1’s store (no undefined region) count { d in self.dependents } == 1 and the check discharges normally.

E3b — the scalar derive (OE1332, the runtime refusal raised while ox build maintains the read model).

A value derive binding the same count into a scalar cannot hold “undefined”. The undefined region propagates as a refusal — the pre-existing aggregate-over-undefined guard (OE1332), not a new refusal introduced by the lift:

pub derive dependentCount(h, n) :- Household(h), n = count(d for d in h.dependents);

Run-verified on the runtime implementation draft, #1839 (the model is this RFD’s run-verified exhibit — bob a definite Dependent, dave undefined — with the scalar dependentCount derive added); ox build:

Error:   × projection cache: strict-model derivation: operation failed: incremental
  │ read-model maintainer: OE1332: aggregate folds over
  │ `$field::Household::dependents`, which has well-founded-undefined atoms
  │ (recursion-through-negation left part of its extent neither true nor
  │ false). Folding would silently treat `undefined` as false — refused rather
  │ than computing a biased result. Aggregate over a definite predicate:
  │ stratify `$field::Household::dependents` so it has a settled extent, or
  │ interpose an intermediate `pub derive` that is itself definite (#250
  │ tracks three-valued aggregate intervals).

The steer: move the consumer to a check (E3a, discharges three-valuedly), or read the view’s regions directly (E2 — definite rows plus the undefined companion). A friendlier teaching render specific to the aggregate-over-a-view’s- undefined-region collision is a recorded cheap follow-up (Open questions), not allocated here.

E4 — classifier consuming the view (undefined case blocked on a prerequisite)

type Sponsor <: Taxpayer iff exists { d in self.dependents };

The bare exists over the lifted view re-homes Sponsor to the rule plane. For alice under E1, Sponsor(alice) is definite-true (bob witnesses). Under a store where alice supports only dave (E2’s undefined), Sponsor(alice) is undefined — the existential over an all-undefined witness set is undefined per K3, not false.

The definite half is deliverable; the undefined half is not, and this example states intended behaviour for it rather than what the stack produces. Runtime slice A synthesizes the classifier re-homing clause, which is what puts Sponsor on the rule plane, so the alice/bob reading above follows from this record’s own slices. The dave reading does not: for a standoff in self.dependents to make Sponsor(alice) undefined rather than leave the head underived, undefined-ness must propagate through the classifier rule’s body join, and the reasoning chapter restricts a rule body to joining “only against definite rows — an undefined atom does not fire downstream rules; propagating undefined-ness through body joins is the engine-level three-valued lift, tracked separately” (spec/reference/src/semantics/reasoning.md:57). That lift is the prerequisite this record inherits rather than owns (D3) and no slice in the Staging plan allocates it. Until it lands, the re-homed classifier body sees only the view’s definite rows, so the standoff does not reach Sponsor’s head. Nothing else in this record turns on the lift — D3’s read-surface arm and its aggregate arm both read the view head’s own $undefined:: companion directly, and the Lean obligations of Staging step 1 are statements about that head’s regions — which is why the lift stays out of scope and this example carries the marker instead.

E5 — filtered view over a standoff-capable target refuses at build

type Taxpayer <: Person {
    // where-filtered AND standoff-capable element type → not liftable
    minorDependents: [Dependent] from Supports.supported where under13(supported)
}
→ build error (fail-closed): a `where`-filtered navigation view whose element
   type has a shape-derived membership cannot be evaluated store-locally without
   a possible loud runtime failure; refused at `ox check` / `ox build`.

// The same filter over a BRACED-refinement element type is ACCEPTED:
type Adult <: Person iff { self.age >= 18 };
type Taxpayer <: Person {
    adults: [Adult] from Supports.supported where over21(supported)
}
→ accepted, unchanged. `Adult`'s predicate cannot call a rule (`OE0660`), so it
   is two-valued and never stands off; there is nothing for the refusal to
   protect against.

E6 — imported backing relation / element type

Supports declared in module family, Dependent in module tax, both imported into the module declaring Taxpayer. The runtime and the build-plane view validator both consume the checker’s resolved references for Supports and Dependent — no re-resolution. A cross-module pin that the file-local oracle cannot answer falls to the workspace <:-ancestry walk (D1 L3); the result keys the same cell the checker accepted, so there is no false-ambiguous refusal and no under-refusal.


Rationale — the decision table

Per-decision status, because the record’s single State: discussion header cannot express it. The state model is binary — discussion means not committed, committed means decided and binding — and this record is genuinely mixed: one arm of one decision is open and the rest are settled as written. The header stays discussion because the record as a whole is not binding until that arm closes; this column says which rows are waiting on it.

#DecisionStatusRationale
D1One pin-resolution ladder keys every concept-pinned field read; winner kind (plain / defined / incomparable) selects read shapesettledsame-named fields on distinct concepts are distinct cells; a single layered ladder is auditable and short-circuits file-local
D2Raw (extent-blind EDB) + gate (fixpoint rule) split for a fixpoint-decided extentsettledkeeps a no-seed-extent view live and delta-maintainable; membership decided inside the fixpoint, not at seed time
D3View K3 policy = surface-honest, consumer-decides: definite extent served, undefined region rides $undefined:: and is surfaced loudly, never silently included or dropped. Absence of evidence is excluded from that region only under a closed world: under the closed-world default an unasserted fact is definitely-false to the derive plane and a refinement predicate over an unrecorded field is settled by the reference manual’s membership rule, so the region is the well-founded standoff (recursion through negation) alone; under an open world the manual reads the same absence as unknown, it is undetermined, and it rides the companion alongside a standoffread-surface arm settled; aggregate arm OPENthe only policy that avoids both Can-to-Is and Can-to-Not while letting the consumer adjudicate the standoff; the world-conditioned carve-out is what makes the policy additive to the manual’s membership rule rather than an override of it — under either world D3 takes the manual’s verdict on an absence and adds only the obligation that an undetermined region stay readable
D4The ladder consumes per-scope resolution; it does not rewrite the pass — one scope model in rule_ref.rssettledtwo coexisting scope models can disagree on whether a name is a binder or a relation application, and the ladder’s cell choice keys on that judgment
D5A view over a subsumed-parent relation walks φ-images (RFD 0084); no re-lowering of subsumptionsettledmapped-tuple-inclusion is the merged semantics; D1-no-dropped-ends keeps the parent frame information-complete
D6Raw = delta-maintainable EDB, gate = ordinary fixpoint rule — compatible with the incremental strict model, no new recompute obligationsettledstates the contract without deep-integrating the maintenance circuits
D7Filtered views over a STANDOFF-CAPABLE element type refuse at build (fail-closed); lift is filter-free only; a braced refinement is neither lifted nor refusedsettledthe where machinery is not fixpoint-hosted; never admit check-green / runtime-dead
D8Shared with admission RFD: one principle (undetermined never silently coerces), independent policies, no shared dispatch structuresettleda view read has no check and so no severity to dispatch on; a shared substrate would have to invent one, and would leave neither boundary changeable alone
D9Lift is a derivation-semantic change: bump DERIVATION_SEMANTIC_EPOCH, enter the change in the RFD 0072 projection-cache derivation commitmentsettleda stale projection cache could otherwise serve a pre-lift model no same-binary differential catches (RFD 0072 D5)

Diagnostics

These are the allocated code numbers, reconciled with the check-plane slices. Each row is anchored to the implementation lane that carries it rather than to a pull request, so the row survives that pull request merging or closing; none of the four is emitted on the default branch yet.

Coordination held across the three lanes drawing from these ranges — this record, the admission-point-checks lane, and the instance-relations lane (OE0740 / OE0741 for its refusals, plus one warning) — so no code double-allocates. Two range facts, checked against the default branch: the OE026x range is allocated through OE0268, so a fresh allocation there now starts at OE0269; and the OW074x range is allocated through OW0744 (OW0742 duplicate-fact-in-file, OW0743 declaration-shadows-import, OW0744 check-not-exhaustive-over-undetermined), so OW0742, which the instance-relations lane’s warning claim named, is taken and the next free warning slot is OW0745 — a matter for that lane, not this record. This record’s own four codes are unallocated on the default branch and uncontested.

CodeLaneCondition
OE0683filtered-view build refusal (D7)a where-filtered navigation view whose element type or filter type-test target has a SHAPE-DERIVED membership — refused at build, fail-closed. A braced refinement (iff { P }) is not a trigger: it cannot stand off
OE0684 (reserved — not wired)view-element dependency graph (D3; Staging plan step 6)an unproductive positive view cycle (provably empty for every store). Reserved-not-wired: the productivity / positive-cycle graph this refusal was assumed to extend does not exist on the tree, so landing it is net-new machinery, staged separately
OE0685check-plane pin resolution (D1)an imperative projection (x.field in a mutate/test body) whose bare field name resolves to multiple distinct canonical cells with nothing determining the slot — the ladder’s Incomparable
OE0686check-plane pin resolution (D1)a rule-body field projection (t.field in derive/query/check) whose base is pinned to several distinct concepts with no head-parameter concept to resolve the slot — the ladder’s Ambiguous / cross-module incomparable

Range fact, checked against the default branch: no code in the OE068x range is emitted or reserved anywhere on the tree. This record claims exactly OE0683OE0686 as tabulated above; OE0680, OE0681 and OE0682 are not claimed by it, so an older citation naming one of those does not designate any condition in this record.

The view boundary’s runtime honesty is not a refusal: an undefined element rides the $undefined:: companion and is surfaced (D3), reusing the existing K3 $undefined:: machinery and check-discharge K3 pass — no new runtime refusal code. The commit-time twin (an undetermined membership observed at a write’s admission point) is the admission-point-checks record’s construct with its own policy and its own code — OE1430, named by that record, which is now on the default branch with the code allocated and live in the catalogue, alongside OE1431 for the determinedness operator’s plane boundary. Both slots are that lane’s; this RFD does not allocate them and does not contest them.


Alternatives considered

  • Store-local materialization for every element type (status quo). Keep evaluating membership at seed time. Rejected for standoff-capable element types: the seed-time plane has no undefined region, so for a shape/rule element type a standoff fails closed (the store-local read refuses — oxc-runtime/src/classify.rs returns Can; the refusal is RuntimeError::MembershipFixpointDependent (oxc-runtime/src/lib.rs, cited by symbol — the line moves with the tip); run-verified exhibit) and only a field-refinement open-world-unknown is silently coerced to Ok(false) (verified in index.rs) — neither can represent the standoff. Retained for ordinary element types and for filtered views (D7) — this RFD narrows, it does not replace.
  • Eager coercion of undetermined to a two-valued verdict at the boundary. Either include undefined (Can-to-Is) or drop it (Can-to-Not). Both fabricate an extent; the Motivation shows each produces a different wrong tax answer that passes checks. Rejected — this is the exact dishonesty the RFD exists to remove.
  • Global refuse-on-any-undefined at the view boundary. Refuse any read whose extent has a non-empty undefined region. Rejected: it kills the honest- propagation case (E3/E4), where the consumer is the right adjudicator; a standoff is a runtime value, not a modeling defect (unlike the productive-cycle refusal, which is).
  • A shared undefined→severity dispatch substrate with the admission RFD. Fold the view’s read-boundary standoff and admission’s write-boundary severity decision into one dispatch mechanism. Rejected on the mismatch of what the two boundaries have to dispatch on: severity is a property of a check declaration, so the write boundary always has one to consult, while a view read has no check and no severity — a shared substrate must therefore invent a synthetic severity for every read, and the invented value, not the modeler’s, would decide what the read surfaces. Coupling also removes independent evolution: a change to either boundary’s policy would have to be made in the shared mechanism and re-validated against the other, so neither can ship or change alone. This RFD keeps the principle shared and the policies + dispatch independent (D8).
  • Implementing the three surfaces as one combined change rather than re-deriving each against main. The pin ladder, the raw + gate cells and the K3 boundary can be built together with a per-scope resolution rewrite and a coupled severity dispatch, on a derivation-semantic epoch numbered off a floor other than the default branch’s. Rejected because each of the three couplings is a design this record decides against on its own technical merits: a second scope model in the resolution file makes a field read’s cell depend on which model answered (D4), a coupled severity dispatch forces a synthetic severity onto a read boundary that has none (D8, above), and an epoch numbered off any floor but the default branch’s cannot be checked against the projection-cache derivation commitment that actually ships (D9). A combined change would carry all three; this record takes the epoch bump off the default branch’s floor and keeps the other two contracts as stated.

Consequences

  • Derivation-semantic epoch bump and the projection-cache commitment (D9) — the downstream obligation this record carries. Lifting a view changes what the derivation means for the same source and the same artifact, not just how it is computed: a filter-free view over a standoff-capable element type served a store-local two-valued extent before the lift and serves a fixpoint extent plus an undefined region after it. So the lift is a derivation-semantic change and MUST bump DERIVATION_SEMANTIC_EPOCH, and the change MUST be entered in the projection-cache derivation_commitment of RFD 0072. The failure this prevents is specific: the projection cache is keyed on a derivation-input commitment, so without the epoch entry a cache populated before the lift stays valid by its own key and a strict read is served a pre-lift model — and because both sides run the same binary, no same-binary differential detects it (RFD 0072 D5). The obligation therefore has two halves, and neither discharges the other: the epoch bump invalidates pre-lift caches, and the cache-seeded ≡ recomputed differential arm over a lifted-view fixture proves the post-lift cache agrees with recomputation. Both land in the same slice (Staging plan step 5). Whether the bump composes cleanly with the derivation_commitment state on the default branch is confirmed before that slice lands (Open questions).
  • Artifact and wire. Additive. The lift adds synthesized relation heads ($rawfield::<C>::<f>, $field::<C>::<f> and its $undefined:: companion) and no new wire field, marker, or projection mode — a view’s undefined region reaches a consumer through the four-valued envelope every other derived read already uses (D3).
  • Observable surface — no new grammar, three new observables. The view spelling is unchanged and there is no user-visible mode selector, but the observable surface does change in three ways, and the no-new-grammar claim must not be read wider than the grammar: a filter-free view over a standoff-capable element type becomes buildable where it is not today; its where-filtered variant becomes a coded build refusal; and a field read gains a new observable, the undefined region. A view over a braced refinement is unaffected in every direction.
  • Source compatibility — one break, at the filtered case. A narrowing view onto a defined element type is permitted today, its membership decided by evaluating the refinement predicate per candidate at seed time (“Today’s behavior”). Under D7 the where-filtered member of that family becomes a build refusal (OE0683, E5), so source that carries a filtered view over a standoff-capable element type stops building and must drop the filter or narrow the element type to a non-computed concept. Unfiltered views over ordinary and braced-refinement element types keep store-local materialization unchanged and pay nothing.
  • Maintenance cost. None new in kind: the raw cell is a delta-maintained EDB projection and the gate is an ordinary fixpoint rule, so a lifted view imposes no full-recompute obligation on the incremental plane (D6).
  • Diagnostics. Four codes, none emitted on the default branch: OE0683 (filtered-view refusal) and OE0685/OE0686 (pin incomparability) land with the check-plane slice; OE0684 stays reserved-not-wired until — and unless — the net-new view-element dependency graph is built, which is an open owner decision. The runtime boundary adds no refusal code of its own.
  • Mechanization owed. The substrate statements come first (Staging plan step 1): a navView_extent_wf lemma and a navView_no_silent_coerce corollary against Argon.Reasoning.Datalog.WellFounded, homed either in a new Argon/Reasoning/Datalog/NavView.lean or as an extension of FieldView.lean. Until they exist, no runtime slice may land ahead of them.
  • Reference manual. Net-new chapter text, no amendment. The refinement chapter’s membership rule and the truth-values chapter’s projection rule both stand as written, and the reasoning chapter’s drop-but-count envelope contract is the surface D3 reuses rather than replaces (D3, Staging plan step 7).
  • Not delivered here. Propagating undefined-ness through a rule body join is the engine-level three-valued lift; this record inherits it as a prerequisite, owns it nowhere, and stages no slice for it (D3). Two documented consumers — the classifier of worked example E4 and the authorization gate of the authorized-signers scenario — state intended behaviour that waits on that lift, and are marked as such at the point of use.

Open questions

  1. Aggregate-over-undefined granularity (D3/E3). The three-valued-interval track (#250/#134) can sometimes prove an undefined element cannot change an aggregate result (e.g. count >= 1 when the definite region is already non-empty). How much of that partial-definiteness should the view boundary exploit before declaring the aggregate undefined? Conservative default: undefined whenever the undefined region is non-empty and could change the result.
  2. Wire shape of the surfaced standoff (D3) — SETTLED by what ships; only the typed accessor is still open. The undefined region rides the $undefined::$field::<C>::<f> companion relation and is read through the normal derived-query surface. Nothing new is needed on the wire: the four-valued envelope already carries a head’s undefined companion tuples as Can rows and already counts them in hidden.can (compiler/crates/oxc-mcp/src/convert.rs; D3), so a lifted view’s region reaches a consumer through the same surface every other derived read uses. The companion is distinguishable by name, which satisfies D3’s “surfaced loudly, machine-readable” guarantee.
    • (a) Canonical surface — SETTLED. The $undefined::$field::<C>::<f> companion is the canonical definition of a view’s undefined region; both the hidden.can count and the truth-4 / --explain Can block are projections of that same companion, not independent computations — so the surfaces cannot drift semantically.
    • (b) Typed SDK accessor — OPEN (no code, no allocation now). Generated SDKs should grow a typed per-view accessor for the undefined region so clients bind to an API rather than hardcoding the $undefined::$field::… naming convention. Staged with whatever slice first teaches the SDK generator about views; not required for the runtime slice.
    • (c) Wire-contract pin — PROPOSED. The companion’s served shape is a compatibility surface. A pin (nav_view_lift_fixpoint) asserts the companion’s name shape and served row shape for the exhibit view Household::dependents, so a future change to the companion breaks a test rather than a client.
  3. Final diagnostic allocation. Which concrete codes the in-flight lanes take from the free block — settle jointly before any implementation slice. The admission-point-checks lane is no longer among them: its record has landed on the default branch naming OE1430 for the write-boundary refusal, so the joint settlement is now between this record’s four codes and the instance-relations lane’s, whose warning still owes itself a fresh number (see Diagnostics).
  4. Nested views (self.f.g where f and g are both lifted). The design composes in principle (each is a fixpoint relation), but the productivity / cycle analysis over composed view-element edges needs a stated bound. Note the view-element dependency graph this bound is stated over is itself net-new (see D3 / the OE0684 staging item) — the elaborator’s only rule-dependency graph today is the defeat lint’s, which judges no productivity and carries no view-element edges — so this is a design obligation of that net-new slice, not a tweak to an existing analysis.
  5. Build vs. defer the unproductive-view-cycle refusal (OE0684). The refusal requires a net-new view-element dependency graph in the check plane (D3; the check-plane slice, pull request #1838, found no productivity graph to extend). Owner decision: build it as its own staged slice (Staging plan step 6) or defer it and leave OE0684 reserved-not-wired. Until settled, an unproductive positive view cycle is not caught at build.
  6. Epoch composition with the projection cache (D9). Confirm the lift’s epoch bump composes cleanly with the RFD 0072 derivation_commitment state on current main before the runtime slice lands.
  7. Friendlier teaching render for aggregate-over-a-view’s-undefined-region (cheap follow-up, not allocated). A scalar derive counting over a lifted view with a non-empty undefined region refuses with the generic aggregate-over-undefined guard OE1332 (D3 E3b, run-verified). OE1332 names the synthesized $field::<C>::<f> relation, which is correct but not oriented to a modeler thinking in terms of “my count over self.dependents.” A dedicated, view-aware teaching render (possibly its own code later) that names the view and points at the check / direct-read alternatives would be a cheap ergonomic win. Open — do not allocate a code now.
  8. Carrier for an open-world element type’s undefined region (D3). D3’s undefined region is world-conditioned, so a view whose element type is marked #[world(open)] has an undetermined candidate that is not a fixpoint standoff: it is an unrefuted absence. The derive-plane companion $undefined::$field::<C>::<f> records fixpoint outcomes only and cannot carry it; the derive plane’s present answer to an open-world absence under negation is a refusal (OE1367), and the check plane keeps the union in a separate region of its own. Three candidates, none decided here: restrict the lift to closed-world element types and refuse an open-world one fail-closed (smallest, consistent with D7’s treatment of the filtered case); widen the derive-plane companion to carry open-world absence, which is the general three-valued open-world propagation OE1367 currently stands in for and is far larger than this record; or serve the view’s region as the union computed at the boundary, which needs its own soundness argument against the companion’s stated meaning. Settle before the runtime K3-boundary slice (Staging plan step 4), since that slice is what fixes the region’s carrier.

Staging plan

Land Lean-first, then check plane, then runtime slices — never a runtime slice ahead of its substrate obligation.

  1. Lean (substrate first). Extend Argon.TypeSystem.Soundness.FieldView (already the reader of the from_relation clause, RFD 0005 §5.6) with the standoff-capable element-type case, and state the view-in-WFS semantics against Argon.Reasoning.Datalog.WellFounded: a lifted view’s extent is the wfTrue slice of $field::<C>::<f> and its undefined region is the wfPossible ∖ wfTrue slice (the K3 boundary of D3). New statement plan: a navView_extent_wf lemma (definite/undefined regions coincide with the fixpoint’s I / J ∖ I at the view head) and a navView_no_silent_coerce corollary (an undefined element is in neither the definite-true nor the definite-false region). Home: a new Argon/Reasoning/Datalog/NavView.lean importing WellFounded + FieldView, or an extension of FieldView.lean — decide at slice time.
  2. Check plane. The pin ladder (D1) as the single cell-resolution discipline in lower/rule_ref.rs consuming the per-scope oracles (D4); the lift predicate (filter_free && element_membership_dependent) shared by lowering and runtime; the build refusals OE0683 (filtered fail-closed) and OE0685/OE0686 (pin incomparability). The unproductive-view-cycle refusal (OE0684) is not in this slice — it is net-new productivity-graph machinery, staged separately (step 6). Import-parity: the build-plane view validator consumes the checker’s resolved references (D4).
  3. Runtime slice A — lowering. Module::load synthesizes the $field::<C>::<f> fixpoint rule, the raw+gate gate clause (D2), and the classifier re-homing clause for a lifted exists; records the raw-gated-cell load-time fact.
  4. Runtime slice B — seeding + K3 boundary. catalog_seed emits the extent-blind $rawfield:: cells (D2) and excludes them from the ordinary concept-keyed companion seed; the read path surfaces the $undefined:: companion at the view boundary (D3) instead of the seed-time nav_catalog_keeps_value two-valued drop for lifted views (store-local materialization stays for unlifted views).
  5. Runtime slice C — epoch + cache. Bump DERIVATION_SEMANTIC_EPOCH; enter the change in the RFD 0072 projection-cache derivation_commitment; add the cache-seeded ≡ recomputed differential arm for a lifted-view fixture (D9).
  6. Check plane (net-new) — unproductive-view-cycle refusal (OE0684). Build a view-element dependency graph in the check plane and refuse a provably-empty positive view cycle. Explicitly staged here, after the runtime slice, because the check-plane slice (pull request #1838) established this is net-new productivity analysis — the elaborator’s only rule-dependency graph, the defeat lint’s, judges no productivity and carries no view-element edges, so there is nothing to extend — not an edge-addition to an existing check (D3). Owner decision: build vs. defer; until built, OE0684 stays reserved-not-wired.
  7. Book. Document the standoff-capable view surface and the standoff boundary in the reference once the runtime slices are green. Net-new chapter text, not an amendment: the refinement chapter’s membership rule and the truth-values chapter’s projection rule both stand as written (D3 composes with them, see D3), and the reasoning chapter’s drop-but-count envelope contract is the surface D3 reuses rather than replaces.

Index rows

README table row:

| [0087](0087-fixpoint-typed-views.md) | Fixpoint typed views: a typed navigation view (`f: [T] from Rel.endpoint`) whose element type `T` is standoff-capable (a shape-clause concept whose membership is a rule head the well-founded fixpoint can leave undefined; a braced `iff { P }` refinement cannot stand off and is out of scope in both directions) is evaluated IN that fixpoint rather than materialized store-locally — a single pin-resolution ladder keys every concept-pinned field read, a raw (extent-blind EDB) + gate (fixpoint rule) split keeps a fixpoint-decided extent live and delta-maintainable, and the K3 standoff at the view boundary is surface-honest (definite extent served, undefined region rides `$undefined::` and surfaces loudly — never silently included or dropped, the consumer adjudicates); the ladder CONSUMES the per-scope resolution pass (no rewrite, one scope model), a view over a subsumed-parent relation walks the φ-images of RFD 0084, filtered views over a standoff-capable element type refuse fail-closed, and it shares ONE principle with the admission-point-checks RFD (undetermined never silently coerces; each construct declares its own policy) with independent policies and no shared dispatch structure; rides a derivation-semantic epoch bump. The standoff policy's aggregate arm is the one piece still undecided | discussion |

SUMMARY list row:

- [0087 — Fixpoint typed views: standoff-capable navigation views evaluated inside the well-founded fixpoint](0087-fixpoint-typed-views.md)

RFD 0090 — In-body determinedness tests: a check-plane operator over the undetermined verdict

  • State: committed (owner decision, 2026-07-24) — adopted over the declaration-level disposition attribute #[undetermined(block | pass | report)], the rival surface this record considered and rejected (its design, and the full comparison against it, are recorded here — D4, the comparison table, and the before/after table; its implementation was drafted in pull request #1840, closed unmerged with the branch preserved). That attribute is the superseded alternative for the disposition surface; the two designs share one foundation — the honesty gate of RFD 0086 slice 1 (pull request #1826), which makes undetermined a first-class verdict — and that foundation ships regardless of this decision. Implementation of this RFD is open, not merged: pull request #1848, stacked on the honesty-gate base.
  • Opened: 2026-07-24
  • Decides: that the per-check disposition of an undetermined membership verdict is expressed in the check body, by a determinedness test — a body atom undetermined(A) (and its dual determined(A)) that reads the converged model and holds iff the membership atom A’s verdict is not definite there — a well-founded standoff, or, for an open-world concept, an unrefuted absence of evidence (D1) — rather than by a declaration-level attribute. The operator is check-plane-only: checks are post-fixpoint consumers, so reading Can-ness in a check body is sound (D2); the same operator in a derive/query body is truth-value reflection feeding the fixpoint (circular, unsound under the well-founded semantics) and is refused (D2, OE1431). A check body that uses a determinedness operator yet leaves some membership atom’s Can-ness routed to no clause — the check’s firing verdict can still be Can on that path — raises an exhaustiveness warning (OW0744, D3, armed per-body: the analysis runs only on checks that use the operator, and then holds every membership atom of that check to explicit disposition) and falls through to RFD 0086 slice 1’s severity-derived default. An operator-free check body is not armed and never warns: it takes the same severity-derived default as a check under RFD 0086 slice 1 alone, silently.
  • Affects: the check-declaration grammar and body vocabulary (a new body-atom form); the parser, lowering, and check-discharge planes (oxc-syntax, oxc-check, oxc-runtime); the check-rule registry (RFD 0081) — a new static well-formedness row (the plane-boundary refusal) and integration with the existing honesty-gate inventory row; the well-founded-semantics mechanization (spec/lean/Argon/Reasoning/Checks.lean). No Core-IR extent representation change — the operator reads the existing three-valued model, exactly as RFD 0086’s gate does.
  • Builds on: RFD 0086 (the honesty gate — the shared base; read it first), RFD 0025 (check discharge; K3 “fire on is only”), RFD 0017 / RFD 0070 (defined-concept membership), RFD 0081 (the check-rule registry), RFD 0028 (the defeat plane’s undefined-by-decision cycles).

In plain language

Argon’s membership questions are three-valued. “Is x a member of computed concept C?” can come back yes, no, or undetermined — the evidence is absent, or the classifying rules sit in a negation/defeat standoff the well-founded semantics resolves to undefined (Can), not to true or false. RFD 0086 establishes that a check whose verdict hinges on an undetermined membership must not guess: undetermined is a first-class verdict, and each check decides what to do with it. That much is shared base and not in question here.

The question this record settles is how the modeler expresses that decision. One answer is a declaration-level attribute — #[undetermined(block | pass | report)] written above the check, one policy for the whole check. This record considered that surface, rejected it (D4), and answers differently: with an operator in the check body. You write undetermined(A) (or its dual determined(A)) as an ordinary body atom, and the check body itself branches on it. Because a check runs after the fixpoint has converged, asking “is this membership undetermined?” in a check body is the same sound read the RFD 0086 gate already performs internally — the operator just names it in the surface.

Why put it in the body rather than on the declaration? Because a body can express things an attribute cannot. A statutory tie-breaker ladder — “if residency is undetermined, fall back to the domicile test; if both are undetermined, block” — is a cascade of conditions the single-policy attribute cannot state. A data-quality check can route undetermined memberships to a report head while definite violations block, in one check. The attribute forces one disposition per check; the body operator lets one check carry several, each guarded by which membership is undetermined.

The cost is real and this RFD does not hide it (see Maintainability). The attribute is nearly free in the compiler — one recognized word, one lookup. The body operator is a new expression form threaded through parse, lowering, and discharge, plus a new coverage analysis that warns when a check leaves some undetermined path unhandled. The two designs also read differently to a human: the attribute is trivially grep-able for governance (“show me every deliberately-lenient check”); the body operator recovers that only by making the compiler report each check’s inferred disposition. The body operator buys expressiveness at a maintainability price. This RFD lays both out; the trade was settled for the body operator (see State).


The shared base (not in question)

Both designs stand on RFD 0086 slice 1 (pull request #1826):

  1. The three verdicts, never collapsed (RFD 0086 D1). For each candidate violation tuple, discharge distinguishes definite violation (body holds under wfTrue — fire on is), definite non-violation, and inconclusive (the firing verdict hinges on a membership atom that is Can). Inconclusive is exactly wfPossible ∖ wfTrue lifted to the check head — mechanized as bodyHoldsPossible ∖ bodyHoldsK3 in spec/lean/Argon/Reasoning/Checks.lean on the slice-1 branch.
  2. The default disposition, severity-derived (RFD 0086 D3 defaults column). An unhandled inconclusive tuple blocks on Severity::Error (OE1430), reports at undefined grade on Warning/Info or #[observe], and is not emitted for an => emit check. This RFD reuses that default exactly — it is what an unhandled path in a body falls through to (D3).
  3. Admission-point discharge, point-free honesty, registry integration (RFD 0086 D2/D4/D6/D7). Unchanged and orthogonal; this RFD is a consumer of the same converged model at the same admission point.

The only thing this RFD replaces is the surface for per-check disposition — the declaration-level attribute. Everything else is shared.


Design

Terms are RFD 0086’s. A membership-gated check is a check whose violation body consumes membership in an iff/shape-defined concept. A standoff is a membership verdict the well-founded semantics leaves undefined (Can) at a point. A below ranges over membership atoms of the check body — x : C, not x : C, or the predicate spelling C(x) over a computed concept.

D1 — The determinedness test as check-body syntax

A determinedness test is a body atom over a membership atom A of the check. It reads the converged three-valued model and its truth is fixed by A’s verdict at the discharge point:

Body atomHolds iffReads
undetermined(A)A’s verdict is not definite at the pointthe concept’s undetermined region (below)
determined(A)A’s verdict is definitely true or definitely falsethe complement of that region

Two sources of indefiniteness, and the operator reads both. A verdict is left indefinite by a well-founded standoff or by absence of evidence under an open world, and the two are independent:

Evidence about AClosed world (#[world(closed)], the default)Open world (#[world(open)])
derived / asserteddetermined (definitely true)determined (definitely true)
explicitly refuted (pub not_fact)determined (definitely false)determined (definitely false — a refutation is present evidence, not absence)
well-founded standoff, unrefutedundeterminedundetermined
absentdetermined (definitely false — absence is evidence of absence)undetermined (absence is unknown)

This is exactly the classification the four-valued assert [not] derivable verdict computes over the same model (§6.9 / RFD 0045; RFD 0010 for the refutation carrier). Two surfaces reading one model must not disagree about which subjects it decides, so the four inputs — the extent, the well-founded undefined companion, the refutation extent, and the concept’s world — are read once per surface against the same rule rather than one surface re-deriving a narrower one. A denotation over wfPossible ∖ wfTrue alone would be the standoff row only, and would report open-world absence as decided — the opposite of the world-assumption semantics.

Under an open world the undetermined region is therefore every subject that is neither an established member nor refuted, with no narrower domain: that is the manual’s reading of absence, and the assertion surface applies it to any tuple. Two consequences are worth stating rather than discovering:

  • The region an implementation materializes is bounded by the individuals that exist in the model read. That bound is not a semantic narrowing — every binding a body can produce comes from an extent or a declared individual, so no subject outside it is addressable — but it does mean an undetermined(C(x)) whose subject is bound by nothing else in the body ranges over the whole population.
  • Argument shape: the atom takes a membership atom over ONE subject (D5). For a wider tuple the open-world region would be a product of that population, which is neither a set worth materializing nor a shape a membership atom has, so the compiler refuses it rather than silently answering the standoff-only question.

determined(A) ≡ not undetermined(A); both spellings ship so a body reads naturally either way (determined(residency) in a positive guard, undetermined(...) in a fallback). The atom is a predicate over a membership atom, not over an arbitrary term — its argument is syntactically a membership atom of the same check body (D5 constrains the argument set).

Spelling — candidates recorded, one chosen. Three surfaces were considered against the existing body vocabulary (the predicate spelling C(x), the type-test x : C, the reflection intrinsics iof/extent/implements — all function-call-shaped atoms over the model):

  1. undetermined(A) / determined(A) — an intrinsic-predicate atom, sibling to iof/extent/implements (function-call atoms that read the model in a body). Chosen. It reuses the exact vocabulary word the rejected declaration-level attribute spends (undetermined), so a reader who knows one knows the other, and the two designs’ surfaces are directly comparable — which is the point, since one supersedes the other. It is grep-able (\bundetermined\() and composes as an ordinary conjunct.
  2. A is undetermined / A is determined — a postfix modal spelling on the reserved is keyword (reserved today, no active grammar rule). Reads as prose but introduces a second is-form and a state-word lexicon (undetermined, determined) in operator position; rejected to avoid a bespoke postfix grammar for one operator.
  3. A? postfix / possible(A) — rejected: ? collides with optionality (T?, Truth4Of<T>), and possible names wfPossible (true-or-undefined), not the Can-only residue this operator needs.

The chosen form is used throughout below, fenced argon,ignore.

D2 — The load-bearing boundary: check-plane-only (soundness)

This operator is legal only in a check body. In a derive/query body it is refused (OE1431). That free-slot check has since been carried out: the code space immediately around the honesty gate proved collision-prone exactly as anticipated, and the allocated codes are the ones recorded in “New-syntax inventory” below.

The soundness argument is the whole RFD, so it is stated precisely.

A check is a denial rule over the well-founded model (RFD 0025): it is a consumer that runs after the WFS fixpoint has converged and contributes no facts back into it (Cat3 — observers, not derivers). At that point the three-valued model is a fixed object: every atom’s verdict is settled as wfTrue, wfFalse (∉ wfPossible), or Can (wfPossible ∖ wfTrue). Reading Can-ness here is reading a converged value — exactly the read RFD 0086’s honesty gate performs internally to compute bodyHoldsPossible ∖ bodyHoldsK3. The operator surfaces that read; it adds no new access to the model, only a name for one the gate already uses.

In a derive body the same read is truth-value reflection feeding the fixpoint. A derive rule contributes to the model; a body atom that branches on whether some atom is currently undefined lets the rule’s own output depend on the in-progress undefinedness of the model it is helping to compute. That is a non-monotone self-reference the well-founded semantics has no fixpoint for — the classic reason WFS forbids reading Can as a value inside the very fixpoint that produces it. undetermined(A) inside a derive would let a rule fire because A is not yet decided and thereby change what A is decided to be: circular, and unsound. So the operator is refused outside the check plane, structurally, not by convention.

What “structurally” obliges of the implementation. Three properties, each of which a narrower refusal would have failed:

  1. One enforcement point, not one per caller. The refusal fires at the entry of the rule-body reference-resolution pass — the only place the operator’s intermediate becomes an executable read of the undetermined region — and that pass takes the body’s plane as a REQUIRED argument. A body lowering added later cannot admit the operator by forgetting to wire a refusal, because there is no plane value meaning “unstated”. This covers the standpoint bridge body, whose rules derive into a target standpoint and so contribute to the fixpoint.
  2. At any container depth. The refusal searches the whole atom, so a spelling nested inside a negation, an aggregate, a modal, or a temporal atom is refused as the top-level spelling is. not determined(A) matters in particular: the operator’s own lowering already spells determined as a negation, so the written not makes it doubly negated — semantically identical to the undetermined(A) one spelling away, and equally refused.
  3. The names are reserved, not merely captured. Recognition claims a single-segment undetermined(...) / determined(...) call before name resolution runs, so a user declaration of either name would be silently unreachable and its own call sites refused with a message about membership atoms — describing a program the author never wrote. Both names therefore join the reserved-intrinsic list, alongside the identifier-lexed reflection intrinsics, and such a declaration is refused at the declaration (OE0704).

Semantic anchor (Lean). The K3 “definitely” modal this operator exposes is confined to consumers by construction. spec/lean/Argon/Reasoning/Checks.lean (on the RFD 0086 slice-1 branch) already partitions atoms into the wfTrue region and the wfPossible ∖ wfTrue region and defines bodyHoldsPossible/bodyHoldsK3 over the converged wfPossible/wfTrue of P.ground. undetermined(A) denotes membership of A’s ground instance in wfPossible P.ground ∖ wfTrue P.ground — a predicate over the converged model, well-defined precisely because the check plane reads P.ground after it exists. There is no corresponding well-defined denotation inside the fixpoint’s own iteration, which is the mechanized statement of why the derive-plane use has no meaning to give it.

D3 — Exhaustiveness over undetermined membership (a coverage warning)

A check body that branches on undetermined(...) can leave a hole: some membership atom’s Can-ness routes to no clause, so the check’s firing verdict is still Can on that path. A three-valued coverage analysis over the check body detects it and warns (OW0744):

not exhaustive over undetermined membership in C; the default applies.

When the analysis is armed (the firing rule, stated precisely). OW0744 fires only on partial handling — a body that uses a determinedness operator yet leaves a Can path unrouted. The precise rule:

  1. Arming is per-body. The coverage analysis runs on a check declaration iff some clause of that check contains undetermined(...) or determined(...). A check body that uses no determinedness operator is not armed and never warns — its Can disposition is the RFD 0086 slice-1 severity-derived default, which is a deliberate, sound fallback, not an exhaustiveness defect (see the consistency decision below and Example 4).
  2. Once armed, every membership atom is in scope. For an armed check, the analysis considers every membership atom M the body depends on — not only the ones an operator names. Using a determinedness operator anywhere in a body is the modeler signalling intent to dispose of undecidability explicitly; the analysis then holds the whole check to that intent. Starting a case analysis on one membership and staying blind to another standoff-capable membership in the same check is exactly the hole OW0744 catches.
  3. The uncovered-path test. For each membership atom M of an armed check, warn (once, naming M) if the conservative analysis finds — or cannot disprove — a feasible body path where (a) M is Can, (b) that uncertainty leaves the check’s overall firing verdict Can, and (c) no clause routes that path to a definite outcome: fire, don’t-fire, an explicit report clause, or an explicit acceptance of the default (a clause routing undetermined(M) to the check’s own default disposition — e.g. undetermined(M) => block on an Error check — which silences the warning by making the fallthrough intentional and visible). Mere occurrence of undetermined(M)/determined(M) does not establish coverage; the guard must cover the relevant Can path.

The analysis is syntactic and conservative (see Maintainability — this is the identified rot-prone hot spot, deliberately kept dumb). When unsure it warns — it never proves coverage it cannot see. Warning-only, so a conservatism bug never blocks a build.

Why per-body and not per-atom: per-atom arming would make OW0744 nearly vacuous (guarding undetermined(M) already routes M’s only interesting Can state, so the atom you guarded is covered by construction) and would forbid the flagship cross-atom warning of Example 3 — the useful signal is precisely “you handled one membership’s undecidability but left another’s unrouted in the same check.” The apparent cost of per-body — adding a guard for M can surface a warning about a sibling N — is the intended behavior: it reminds the modeler that N is also standoff-capable and undisposed, at the price of one clause. The blast radius is bounded to checks that already opted into determinedness handling (a small minority), which is categorically unlike warning on the whole operator-free corpus.

The default for an unhandled path is RFD 0086 slice 1’s severity-derived behavior, exactly (the shared base, item 2): an uncovered Can path on a Severity::Error check blocks (OE1430); on a Warning/Info/#[observe] check it reports at undefined grade; on an => emit check it is not emitted. This is a consistency decision, spelled out so the two designs agree on the fallback: whatever RFD 0086 slice 1 does for a check with no disposition handling, this design does for an un-handled path of an armed check — and, identically, for an un-armed (operator-free) check as a whole. An operator-free body here is behaviourally a plain RFD 0086 slice-1 check: silent, default-dispositioned. A body that handles every path never reaches the default and never warns; the warning is exactly the signal “you began disposing of undecidability here and did not finish, so slice-1 behavior takes over on this path.”

Firing-rule candidates considered (recorded, one chosen).

  • Warn only on partial handling, per-body arming. Chosen (the rule above). Fires on armed-but-incomplete bodies; operator-free bodies silent. Consistent with RFD 0086 slice 1’s silent default, low-noise (bounded to opt-in checks), preserves the cross-atom Example 3 signal.
  • Warn always (the literal reading of the uncovered-path test with no arming gate). Rejected. Taken literally — “some membership atom’s Can-ness routes to no clause → warn” — this fires on every operator-free check over any computed/defeasible membership, because such a check has, by definition, no routing clause. That warn-spams the entire existing corpus, all of which correctly relies on the sound severity-derived default: measured over the in-repo .ar corpus, of 18 non-fuzz check declarations only ~3 consume genuinely computed or defeasible membership whose verdict can be Can (and a dumb syntactic pass that cannot prove a base-marker type Can-free would flag up to ~5) — and every one of them would warn despite being correct. It also breaks the RFD 0086 symmetry (a plain slice-1 check is silent; an operator-free body here is the same thing) and inverts the failure-direction doctrine: a missed warning here is low-cost (the default is sound and loud where it matters — Error blocks the build), while a spurious warning on every correct check is pure noise that trains modelers to ignore the diagnostic.
  • Per-atom arming (analyse only the atoms an operator names). Rejected. Makes OW0744 nearly vacuous and forbids the cross-atom Example 3 warning; see “Why per-body and not per-atom” above.
  • Tiered: partial-handling default plus an opt-in strict lint level flagging every implicit fallback. Rejected for now. The house lint machinery (#[allow/warn/deny], [lints], -A/-W/-D) controls the level of an emitted diagnostic (suppress or promote), not an allow-by-default “pedantic” tier that turns a normally-silent condition into a warning. Introducing a strict “flag-every-implicit-fallback” lint is a separable future addition if demand is demonstrated; it must not change OW0744’s default firing rule, which stays the partial-handling rule above.

D4 — Relationship to the declaration-level disposition attribute (decided: Option A)

The rival surface is a declaration-level attribute #[undetermined(block | pass | report)], written above a check, fixing one disposition for that whole check. Two options for how this operator relates to that attribute were recorded. The owner decided for Option A on 2026-07-24: this RFD replaces the attribute; the attribute never lands. Option B is preserved below as the considered-and-rejected alternative.

Option A — this RFD replaces the attribute. The body operator is the one obvious way to dispose of an undetermined verdict; the #[undetermined(...)] attribute is removed (never lands past its draft, pull request #1840). The attribute’s one advantage — grep-able per-check governance (“enumerate every deliberately-lenient check”) — is recovered by making the inferred per-check disposition a reportable surface: the D3 exhaustiveness analysis already computes, for each check, what happens on each undetermined path, so the compiler can emit a per-check disposition summary (a build-time inventory, or ox surface) that a reviewer reads and greps exactly as they would the attribute. Governance moves from a source token to a derived, always-accurate report.

Option B — the attribute is kept as sugar. #[undetermined(block | pass | report)] remains, desugaring to a single body clause: #[undetermined(block)] becomes an implicit trailing undetermined(A) => block over the check’s membership atoms; pass/report likewise. The attribute is then the ergonomic shorthand for the whole-check case, and the body operator is the general form for the cases the attribute cannot express (ladders, mixed routing). One surface desugars to the other; both are documented, one is canonical.

The trade: Option A is one-obvious-way (no two surfaces to drift, no desugar to maintain) at the cost of removing a near-zero-cost, maximally-grep-able governance token and leaning on a report to recover it. Option B keeps the cheap common case cheap and readable but carries two surfaces and a desugar, re-introducing the drift risk the single-surface discipline (RFD 0084’s maintainability doctrine) exists to avoid. The decision is the one recorded above: Option A.

D5 — The argument set and interaction with defeat / aggregation

  • Argument set. undetermined(A)/determined(A) take a membership atom A over ONE subject that occurs in the same check body. The static well-formedness rule (D6) refuses a determinedness test over a non-membership atom (a plain relation tuple whose undefinedness is not a computed-membership standoff), over a wider tuple (D1: the open-world region of a k-column predicate would be a k-fold product of the population), or over a term not present in the body — it is not a general “is this atom undefined” operator, it is a guard over the check’s own membership atoms.
  • Defeat plane. A membership atom left Can by a mutual-defeat cycle (RFD 0028; OW0740) is exactly the case undetermined(A) names — the two honesty features agree (RFD 0086’s defeat-plane interplay example). The operator reads the converged post-defeat model; it does not compose with or re-run the defeat plane.
  • Aggregation. A membership atom nested inside an aggregate body is not lifted (RFD 0086 D5); a determinedness test’s argument is a top-level membership atom of the check body. An aggregated standoff re-registers as OE1430 per RFD 0086 D5 unchanged — the body operator does not reach inside aggregates.

D6 — Registry integration (RFD 0081)

No parallel dispatch. Two rows:

  • Static well-formedness of the determinedness test — a new rule validating that undetermined(...)/determined(...) appears only in a check body (the D2 plane-boundary refusal, OE1431), over a membership-atom argument of that body (D5), enters as a driver-wired BODY-axis row (it is a body-walk predicate over rule bodies, the registry’s Axis-1 category).
  • The runtime honesty gate (the discharge that emits OE1430, later the point-free refusal, and now evaluates the body’s determinedness tests against the converged model) is the existing inline INVENTORY row from RFD 0086 D6, extended — not a new row. The gate already runs inside the write path over the post-state overlay where the three-valued model exists; evaluating undetermined(A) there is the same read it already performs, so the operator adds discharge logic to that inventoried site without inventing a dispatch table.

The exhaustiveness analysis (D3) is a static BODY-axis pass emitting OW0744; it is a warning-only sibling of the well-formedness row, reusing the same body walk. It is armed per-body (D3): the pass short-circuits on any check whose clauses contain no undetermined/determined operator, so the common operator-free check never enters the coverage analysis at all.


Comparison — the declaration-level disposition attribute vs. this design

DimensionDeclaration-level disposition attribute (#[undetermined(...)])This RFD — in-body determinedness test
ExpressivenessOne disposition per check (block | pass | report). Cannot express a tie-breaker ladder or mixed routing (undetermined→report while definite→block in one check).Full-body branching. Ladders, per-membership routing, mixed dispositions in one check. Superset of the attribute.
Compiler costNear-zero: one recognized word, one static well-formedness row, a lookup at discharge.New body-atom form through parse → lower → discharge; a new conservative coverage analysis (OW0744); a plane-boundary refusal (OE1431). Materially larger.
User-code readabilityTrivial for the whole-check case: one line above the check, self-documenting.Ladders read naturally in the body; the whole-check case is wordier than one attribute line (mitigated by Option B sugar).
Auditability / governanceMaximally grep-able: grep '#\[undetermined(pass)\]' enumerates every lenient check directly from source.Recovered indirectly: a per-check inferred-disposition report (Option A) or the retained attribute-as-sugar (Option B). Not a single source token by default.
MigrationNone — additive over slice 1.None — additive over slice 1. Both are pure additions on the shared honesty-gate base.
Soundness boundaryNo new operator, so no plane-boundary risk.Requires the check-plane-only invariant (D2) as a standing rule; refusal (OE1431) enforces it.
Implementation statusDrafted — pull request #1840 (slice 2 over the slice-1 base #1826); closed unmerged after the 2026-07-24 adoption of this RFD, branch preserved.Implemented, open (not merged) — pull request #1848, stacked on the slice-1 base #1826.

Maintainability

The owner asked for this first-class and honest. The body operator costs more to maintain than the attribute; this section states where, and how each cost is contained.

New expression form through the pipeline. undetermined(...)/determined(...) is a new body-atom form that touches parse (a new atom production), lowering (a new Core-IR body-atom variant reading the three-valued model), and discharge (the gate evaluates it). Contrast the attribute: a recognized word and a lookup, no new expression form. Mitigation: one single check-lowering path through the check-rule registry (RFD 0081) — the operator lowers at exactly one site, and the BODY-axis well-formedness row (D6) is the only recognizer. No second surface lowers it.

The check-plane-only boundary as a standing invariant. D2 is a permanent rule: the operator must never become legal in a derive/query body, on pain of unsoundness. An invariant a future contributor could erode by “just allowing it in derives too.” Mitigation: one refusal gate (OE1431, the D6 BODY-axis row) is the sole authority on where the operator is legal, and a drift-style test pins that no other surface lowers the operator — the same discipline RFD 0084 uses to keep one canonical surface. The soundness argument (D2) is recorded in the RFD as the permanent why, so the re-litigation “why not derives?” has a standing answer rather than being re-derived each time.

The exhaustiveness analysis is the rot-prone hot spot. A three-valued coverage analysis over branching bodies is the kind of pass that accretes special cases and silently starts lying (claiming coverage it cannot actually prove). Mitigation: keep it syntactic and conservative — warn-when-unsure, never clever (D3), and armed per-body so it only ever runs on checks that opted into determinedness handling (operator-free checks — the whole existing corpus — never enter it, bounding the blast radius). It never proves coverage it cannot see; a false “not exhaustive” warning costs the modeler one explicit clause, a false “exhaustive” (silence) would hide a default fallthrough — so the analysis is biased to warn. It is warning-only (OW0744), so a conservatism bug never blocks a build; the worst case is a spurious warning, fixed by an explicit clause. This is the deliberate ceiling on the pass’s ambition.

Long-term scope-creep pressure. “Can we test determinedness in derives? in queries? over arbitrary atoms?” will recur. The RFD pre-answers: never, with the D2 soundness argument as the permanent reason. A determinedness read inside the fixpoint is circular under WFS; that does not change with implementation effort, so the answer is stable and the RFD is the citation.

User-code maintainability — both directions. The body operator makes cascading policies readable where the attribute cannot express them at all (a statutory ladder is one legible block instead of an inexpressible intent split across checks). The attribute makes governance readable — one grep enumerates every lenient check — where the body operator recovers that only via a report (Option A) or the retained sugar (Option B). Neither dominates; the trade is expressiveness-in-the-body vs. governance-in-a-token.

Contrast, stated plainly. The attribute’s compiler-side maintenance cost is near zero and it carries no standing soundness invariant. This design’s cost is a new expression form, a standing plane-boundary invariant, and a conservative coverage pass. That cost buys expressiveness the attribute cannot reach. The 2026-07-24 decision paid it: the expressiveness was judged worth the maintenance because each cost above is contained by a stated mitigation — one lowering site, one refusal gate plus a drift-style test, and a deliberately dumb warning-only coverage pass — while the expressiveness gap is not closable at any price, the attribute having no spelling for a ladder or mixed routing at all. The governance the attribute gave up is recovered by the inferred-disposition report (D4 Option A).


Worked examples

Proposed syntax is fenced argon,ignore; no outputs are fabricated. The shared base (RFD 0086 slice 1, pull request #1826) provides the three-valued verdict these bodies read.

Example 1 — the statutory tie-breaker ladder (inexpressible under the attribute)

Scenario. A residency determination has a statutory fallback ladder: decide residency by the primary-residence test; if that is undetermined, apply the domicile test; if both are undetermined, the return cannot be filed and the write is blocked. The attribute cannot state a ladder — it has one disposition for the whole check. The body operator states it directly.

pub type Filer;
pub type ResidentByHome    <: Filer iff { primaryResidenceMonths(self) >= 6 };
pub type ResidentByDomicile <: Filer iff { domicileState(self) == filingState(self) };

pub check residency_required(f: Filer) :-
    Filer(f),
    // primary test undetermined → fall back to domicile
    undetermined(ResidentByHome(f)),
    // domicile also undetermined → no honest verdict: block
    undetermined(ResidentByDomicile(f))
    => Diagnostic {
        severity: Severity::Error,
        code:     "Tax::E1200",
        message:  format!("residency undecidable for {} under both tests; cannot file", f),
    };

Meaning. The check fires (blocks, Severity::Error) exactly when both residency tests are undetermined. A definite resident under either test does not match the body (the guards demand undetermined(...)), so it does not fire. A filer determined by the home test alone is determined(ResidentByHome(f)), so the first guard fails and the ladder passes at the first rung. The cascade — home, then domicile, then block — is one legible body. Under the attribute this intent has no single-disposition spelling; it would have to be split across separate checks with duplicated bodies.

Example 2 — data-quality routing (undetermined reports, definite blocks, one check)

Scenario. Finance wants un-attributed payments on a worklist, but a payment whose cost-center attribution is definitely wrong (attributed to a closed cost center) is a hard error. One check, two dispositions keyed on determinedness.

pub type Payment { amount: Money };
pub type Attributed  <: Payment iff { exists c. costCenter(self, c) };
pub type MisAttributed <: Payment iff { exists c. costCenter(self, c) && closedCenter(c) };

// definite mis-attribution: block
pub check payment_attribution_hard(p: Payment) :-
    Payment(p), determined(MisAttributed(p)), MisAttributed(p)
    => Diagnostic {
        severity: Severity::Error, code: "Fin::E2051",
        message: format!("payment {} attributed to a closed cost center", p) };

// attribution undetermined: report, never block
pub check payment_attribution_report(p: Payment) :-
    Payment(p), undetermined(Attributed(p))
    => Diagnostic {
        severity: Severity::Warning, code: "Fin::W2052",
        message: format!("payment {} has undetermined cost-center attribution", p) };

Meaning. The first check fires only on a definite mis-attribution (guarded by determined(...), so an undetermined attribution never reaches the blocking head). The second routes an undetermined attribution to a Warning report head. Definite violations block; undetermined memberships land on the worklist. The attribute could give each check one disposition, but could not, within a single check, distinguish the definite-block case from the undetermined-report case keyed on the membership’s own determinedness — the body operator makes the routing the body’s job.

Example 3 — the exhaustiveness warning

Scenario. A modeler guards one undetermined path and forgets another.

pub check dependent_deduction_cap(f: Filer) :-
    DependentFiler(f), f.standardDeduction > money(1_250),
    undetermined(ResidentByHome(f)) => /* ... report ... */ ;
    // DependentFiler(f) itself can be `Can` and is not guarded here:
    // OW0744 — "not exhaustive over undetermined membership in DependentFiler;
    //           the default applies" (Severity::Error ⇒ block, per RFD 0086 slice 1)

Meaning. The body handles ResidentByHome’s undetermined case but leaves DependentFiler’s own Can path unrouted. Because the body uses a determinedness operator (undetermined(ResidentByHome(f))), the check is armed, and the analysis then holds every membership atom to explicit disposition — including DependentFiler, which the operator does not name. OW0744 names DependentFiler; that path falls through to RFD 0086 slice 1’s default for a Severity::Error check — block (OE1430). The warning is the signal that slice-1 behavior, not a body clause, governs that path. (This cross-atom warning is exactly what per-atom arming would suppress — see D3.)

Example 4 — the operator-free twin (silent, no warning)

Scenario. The same check without any determinedness operator — the overwhelmingly common shape, and every existing corpus check today. It consumes computed membership (DependentFiler, an iff/derive-defined concept whose verdict can be Can) but never mentions undetermined/determined.

pub check dependent_deduction_cap(f: Filer) :-
    DependentFiler(f), f.standardDeduction > money(1_250)
    => Diagnostic {
        severity: Severity::Error, code: "Tax::E1300",
        message: format!("dependent standard-deduction cap exceeded for {}", f) };

Meaning. No operator ⇒ the check is not armedno OW0744, ever, even though DependentFiler(f) can be Can. The Can path takes RFD 0086 slice 1’s severity-derived default — here, Error ⇒ block (OE1430) — silently and soundly, exactly as a plain RFD 0086 slice-1 check does. This is the twin of Example 3: adding one determinedness operator anywhere in this body flips it from silent-default to armed-and-analysed. The warning is opt-in by using the operator; it never spams a check that relies on the honest default.


New-syntax inventory

FormKindWhere legalMeaning
undetermined(A)body atomcheck body onlyA’s membership verdict is Can (wfPossible ∖ wfTrue) at the point
determined(A)body atomcheck body onlyA’s verdict is definite (wfTrue or ∉ wfPossible); ≡ not undetermined(A)
OE1431refusala determinedness test outside a check body, or over a non-membership argument
OW0744warningan armed check body (one that uses undetermined/determined somewhere) not exhaustive over some membership atom’s undetermined path; the default applies. Operator-free bodies are not armed and do not warn

These codes are allocated, not notional. The block immediately above the honesty gate proved collision-prone as this record warned it would: the default branch landed four unrelated endpoint diagnostics into the next free catalog slots and took the codes with them, one of which was the code the honesty gate had already claimed. All three codes here were therefore re-allocated together, verified free both on the default branch and on every open branch in the repository — checking only the default branch would have moved the collision onto someone else’s unmerged work rather than resolving it. The honesty gate itself is OE1430.


Before / after — expressiveness

Wants to sayUnder the declaration-level disposition attributeUnder this RFD
Statutory tie-breaker ladder: try test A, fall back to B, block if both undetermined.Inexpressible — one disposition per check; must split across checks with duplicated bodies.One check body: undetermined(A(f)), undetermined(B(f)) => block (Example 1).
One check: definite violation blocks, undetermined membership reports.Inexpressible in one check — the attribute sets one disposition for the whole check.Two heads keyed on determined/undetermined (Example 2).
Deliberately lenient on undetermined for one specific membership, strict on others.Whole-check pass only — cannot scope leniency to one membership.Guard the lenient membership with undetermined(A) => report; leave others to default.
Enumerate every deliberately-lenient check from source.grep '#[undetermined(pass)]' — direct.Per-check inferred-disposition report — the decided Option A. (Option B, retaining the attribute as sugar, is rejected with the attribute itself: the owner has decided against that surface and it will not be supported.)

Staging

Each slice leaves the tree green and changes nothing for a module with no determinedness test. All slices sit above RFD 0086 slice 1 (the shared base).

  • Slice 1 — the operator + the plane boundary. Grammar for undetermined(...)/ determined(...); lowering to a Core-IR body-atom reading the three-valued model; the check-plane-only refusal (OE1431) as the RFD 0081 BODY-axis static row; discharge evaluates the atom against the converged model at the honesty-gate inventory site (RFD 0086 D6). Reuses the slice-1 gate; no new dispatch.
  • Slice 2 — the exhaustiveness analysis. The conservative three-valued coverage pass and OW0744, armed per-body (runs only on checks that use the operator; D3); the severity-derived default fallthrough (D3) wired to RFD 0086 slice 1’s behavior for both un-armed checks and armed-but-uncovered paths.
  • Slice 3 — the D4 relationship. Option A, as decided (D4): the attribute does not land, and this slice ships the per-check inferred-disposition report that recovers its governance grep-ability. Sequenced last because nothing above depends on it.
  • Lean obligation (paired with Slice 1): spec/lean/Argon/Reasoning/Checks.lean gains the denotation of undetermined(A) as A ∈ wfPossible P.ground ∖ wfTrue P.ground over the converged ground model, and a lemma that this denotation is well-defined only over P.ground (the mechanized statement of the D2 check-plane boundary — no denotation inside the fixpoint iteration). Extends the slice-1 bodyHoldsPossible/bodyHoldsK3 partition.

Verification (per slice)

  • A module with no determinedness test is byte-identical in check behavior (negative control).
  • undetermined(...)/determined(...) in a derive/query body refuses OE1431; assert rejects(OE1431) pins it.
  • Example 1’s ladder blocks exactly when both tests are undetermined; a filer determined under either test passes.
  • An armed body (one using undetermined/determined) that leaves one membership’s Can path unrouted warns OW0744 and falls through to the RFD 0086 slice-1 default; a fully-covering armed body neither warns nor falls through.
  • An operator-free body over a computed/defeasible membership whose verdict can be Can emits no OW0744 and takes the severity-derived default silently (Example 4) — the per-body arming negative control, and the parity check against an plain RFD 0086 slice-1 check.
  • Arming is per-body: a body that uses the operator on membership A and leaves a sibling membership B’s Can path unrouted warns naming B (Example 3).
  • The drift-style test confirms exactly one surface lowers the operator.

Alternatives considered

(a) The declaration-level disposition attribute (#[undetermined(block | pass | report)] above a check). The competing design (this whole RFD is the contrast). Near-zero compiler cost and maximal governance grep-ability, at the price of one disposition per check — no ladders, no mixed routing. See the comparison table. Rejected here in favour of the in-body operator (see State and D4). RFD 0086 rejects the same surface independently (its Alternative (d), on the ground that a declaration-level word cannot name which classifier or subject is undetermined) and names the in-body determinedness test as the successor shape, deciding only the default that successor deviates from.

(b) A general undefined(atom) operator over any body atom, any plane. Rejected on soundness: outside the check plane it is truth-value reflection into the fixpoint (D2), and over a non-membership atom it names a plain relational absence, not a computed-membership standoff — a different and less meaningful thing. The operator is deliberately narrowed to membership atoms in check bodies.

(c) A fourth severity (Severity::Undetermined). Rejected for the same reason RFD 0086 rejected it (its Alternative (e)): undetermined is a property of a tuple’s verdict at a point, not of the check. A per-tuple body guard is the right granularity; a per-check severity is not.


Open questions

  • Per-atom vs. whole-membership granularity. The operator is defined over a membership atom A. Should it generalize to per-atom determinedness within a compound membership (e.g. the individual conjuncts of an iff body), or stay at whole-membership granularity? Finer granularity is more expressive but sharpens the D3 exhaustiveness analysis’s burden. Left open.
  • Interaction with RFD 0087 view undefined regions. A check over a lifted view can read the view’s undefined region. Does undetermined(A) over a view-lifted membership atom read the view’s undefined region or the base model’s Can? The boundary between this operator and 0087’s undefined-region semantics must be coordinated before either ships a check over a lifted view. (RFD 0087 is pull request #1821; cross-reference at decision time.)
  • The D4 choice (Option A vs. B). Decided 2026-07-24: Option A (see D4).
  • Governance report shape (Option A). The attribute does not land, so the report carries the governance: what is the precise surface of the per-check inferred-disposition report — a build-time inventory, an ox subcommand, or both — so it matches the grep-ability the attribute gave up?
  • Retroactive amendment re-introducing inconclusiveness. Inherited unchanged from RFD 0086 (its Worked-examples gap): an amend/retract that pushes an already-admitted tuple into a standoff. Orthogonal to the disposition surface, so the adoption of the in-body operator neither settles nor changes it.

RFD 0093 — iof denotes membership, whole

  • State: discussion
  • Opened: 2026-07-25
  • Decides: that iof(x, C) denotes an individual’s membership in C, whole — asserted classification, <: closure, predicate-refinement-derived membership, and rule-derived membership alike — and that extent and meta follow it. C(x), x : C, and iof(x, C) become three exact spellings of one question — for a bare or module-qualified concept path; the self::-qualified path is a separate, unfixed defect (issue #1904). Undefined membership remains the third truth value and must not collapse to false.
  • Does not fix — and these are protected from the fix, not overlooked by it: meta(x) == C stays narrower than iof under upward <: closure (the metatype yields <:-minimal classifiers, by design); x :: C and self::-qualified iof stay refused; the self::-qualified type test stays broken, as issue #1904. Detail in Non-goals.
  • What is being asked for is the semantic decision (D1), not a mechanism. Two mechanisms deliver identical semantics and both are live: (a) make $iof rule-derived, through generated bridge rules from each concept’s own membership atom — the form D2–D5 are written against; or (b) rewrite constant type positions at elaboration and materialize the relation once at a stratum boundary above every read. Either way iof, extent, and meta answer full membership, the individual-tier seed scan goes away, extent(t) becomes a projection over $iof, and $meta’s individual tier becomes an antichain over $iof. Where D2–D5 name bridge rules they are describing mechanism (a) concretely enough to be argued about, not ruling out (b).
  • Affects: the reflection materializer (compiler/crates/oxc-runtime/src/standpoint.rs, compiler/crates/oxc-runtime/src/catalog_seed.rs); the rule-body atom-resolution and ad-hoc lowering paths (compiler/crates/oxc-instantiate/src/lower/rule_ref.rs, compiler/crates/oxc-instantiate/src/lower/adhoc.rs); the bare-name type-test lowering it is made to agree with (compiler/crates/oxc-instantiate/src/atom_lower/mod.rs); the extent desugaring (compiler/crates/oxc-instantiate/src/atom_lower/source.rs); the reasoner’s stratification and incremental-maintenance model (compiler/crates/oxc-reasoning/); and the specifying chapter spec/reference/src/declarations/meta-calculus.md. It does not affect the Lean mechanization — see D6.
  • Relates to: RFD 0023 (the reflection intrinsics), RFD 0017 (refinement classification), RFD 0027 (the meta-property plane and the catalog tiers), RFD 0070 (derive-backed classification).

In plain language — what is wrong, and what this restores

Argon lets a modeller define a concept by a rule instead of by assertion:

pub derive IsRegistered(p: Person) :- Registered(p);
pub type Voter <: Person iff IsRegistered(self);

Nobody writes down who the voters are. The reasoner works it out. This is a shape-defined concept (RFD 0070); the shape clause must name a pub derive.

Now read that membership back. The reference manual says x : T is sugar for iof(x, T) — the same question, spelled two ways. They do not agree:

  • Voter(p) finds the voter.
  • p : Voter finds the voter.
  • iof(p, Voter) finds nobody, silently, with no diagnostic.
  • meta(p) == Voter finds nobody, silently. Worse, meta(p) reports that person’s most-specific type as Person — a true statement replaced by a less true one.
  • not iof(p, Voter) matches everybody, including the genuine voter, because the relation it negates over is empty.

Every one of those is executed with real output in Demonstration.

The cause is mechanical. iof reads are served by enumerating a relation named $iof that the runtime builds when it loads the store — before the reasoner runs. A shape-defined concept’s members are decided by the reasoner. The builder cannot see them, so it writes no rows, and $meta, which is computed from the same scan in the same pass, inherits the hole.

The thesis of this record is that the meaning was never in doubt. Three design records and a Lean theorem already define iof as membership including derived membership; the materializer’s own source comment claims the inclusive reading. The narrow behaviour is a hole in one builder, not a narrower definition.

The remedy belongs to the relation, not to the read sites. A fix confined to constant type positions makes iof(x, Voter) and iof(x, t), t == Voter answer differently — the same question, two answers; that shape is recorded and rejected in Alternatives. So the remedy goes where the hole is. The form D2 is written against is a generated rule per concept C saying “if C(x), then x is an instance of C, with the reasoner — which knows the answer — filling the relation in. Every spelling then reads the same complete relation and they agree because they are looking at the same thing. Which pass computes that relation is still open (front matter); what this record asks for is agreement that the relation must be complete.

Two records were opened on this defect and they remain alternatives. The other is RFD 0092 — Membership-plane authority: fail-closed reads of derived membership (pull request #1874), which refuses an asserted-plane read of a derived-membership concept rather than answering it. The two share a diagnosis and differ on the remedy across the whole surface, including meta(x) == C: under the design below meta is derived from $iof like everything else, so that spelling is fixed rather than refused. RFD 0092 is argued at full strength in Alternatives.

Question

Does iof(x, t) mean “x’s membership in t”, or “x appears in the seed-time-materialized $iof relation for t”? These coincide for every concept whose membership is asserted or predicate-refinement-derived, and diverge for a shape-defined concept, where the second answers empty and the first does not. Which is the definition, and which is an implementation?

Context

Two membership planes

The seed plane. When a store is loaded, catalog_seed populates per-type extent relations from committed events — iof assertions plus <: closure — then runs the refinement-honesty pass, which composes each predicate-refined concept’s extent from its parent’s extent intersected with the refinement predicate. That pass exists precisely so derived membership is visible to ordinary reads (seed_base_catalog_in_frame_filtered in compiler/crates/oxc-runtime/src/catalog_seed.rs, the refinement-composition block):

// Refinement composition (refinement-honesty pass): for each
// concept with a refinement_predicate, compute its honest
// extent (parent extent ∩ predicate-true) and populate the
// catalog under the refined name. This lets rule bodies
// mention refined concepts like `Adult(p)` and get the
// semantically-correct set, not the (typically empty) set
// of individuals explicitly iof-asserted as Adult.

The seed plane is not lazy; it goes out of its way.

The rule plane. A shape-defined concept carries no refinement predicate (compiler/crates/oxc-protocol/src/storage.rs:298-308, the DefinedByShape variant: “The concept carries NO refinement_predicate (it is None)”; the invariant is enforced locally in the elaborator, lower_concept_decl in lower/concept.rs). Its membership is a synthesized concept-headed rule evaluated in the reasoner’s well-founded fixpoint. The refinement-honesty pass has nothing to compose, so the concept is absent from the seed plane’s extent map entirely (catalog_seed.rs:840):

// #1859 — a SHAPE-DEFINED concept (`type Thing <: Base iff
// isThing(self)`, RFD 0070) has no static or refinement-honesty extent:
// its membership is decided in the reasoner fixpoint. The
// refinement-honesty pass above classifies only `iff <predicate>`
// (`Defined`) concepts (a shape concept carries NO refinement
// predicate), so a shape concept is absent from `concept_extent_of_
// internal`, and the concept-keyed field-projection loops below would
// emit ZERO `$field::<Thing>::<field>` rows.

and names the failure mode at catalog_seed.rs:852: (checked-clean, silently-wrong).

Seed-time materialization cannot reach the rule plane for a structural reason, not an oversight: the reasoner has not run when the seed runs, and running it there would invert the dependency, because the fixpoint reads the seeded relations.

$iof and $meta are built by one scan, on the seed plane

Both reflection relations are materialized in one function, materialize_reflection_relations (compiler/crates/oxc-runtime/src/standpoint.rs:49), called once, from the seed, before the fixpoint (the tail of seed_base_catalog_in_frame_filtered).

The individual tier scans the per-type extents already in the catalog (the individual-tier loop of materialize_reflection_relations, standpoint.rs:74-110) and, in the same loop, accumulates a per-entity type set that $meta then reduces (standpoint.rs:64-65):

// Also accumulate the per-entity iof type SET (`iof_by_entity`), which
// `$meta` (below) reads to pick each entity's most-specific type(s).

$meta’s individual tier is that set filtered to its <:-minimal elements (the $meta reduction in the same function, standpoint.rs:159-174). This is why meta(x) == C and x :: C are broken by the same mechanism as iof, not by a second one: there is one scan, one source, and one hole. meta(x) == C is therefore a third silently-empty spelling. x :: C would break identically but never reaches evaluation — it is already refused (below), so it is a latent instance rather than a live one.

The catalog tier (the metatype-tier block of the same function, standpoint.rs:186-284) is different in kind, and the difference matters to the design. Its rows come from declarations only:

// Rows derive from DECLARATIONS only
// (catalog-closed, never from the ABox), so NAF over them is
// stratification-safe — the same justification as `$implements`.
// `meta` here is exact (a declaration has exactly one resolved
// introducer per the §3.4 gate), so the `<:`-minimality pass the
// individual tier needs does not apply.

Two further row sets — the reflective-sort extents and the declared type×type X : T rows — feed $iof only, never $meta, because “a type may be declared an instance of several higher-order types, so meta(X) … stays the §3.4 introducer”. The catalog tier is not an antichain over $iof and is not meant to be. D4 respects this; the naive reading does not, and Demonstration measures the discrepancy.

$specializes (the $specializes block of the same function, standpoint.rs:286-370) is the reflexive-transitive closure of the declared <: graph, type-tier only, both columns Value::Name. It is catalog-closed and stratification-safe for the same reason.

extent has no relation of its own

extent(t) is not a stored relation. It desugars to the reserved-head iof atom (lower_source in compiler/crates/oxc-instantiate/src/atom_lower/source.rs):

// RFD 0023 D4 — the explicit extent form `for e in extent(t)`:
// `extent(t)` lowers (expr_lower) to `App(Var("extent"), [t])`. Iterate
// it as the entity column of the `iof` relation restricted to type `t`
// — i.e. the reserved-head atom `iof(binder, t)`.

So extent inherits iof’s hole exactly, and fixing iof fixes extent with no separate work. Its command-line surface already fails closed: ox query --extent over a shape-defined concept refuses with RuntimeError::MembershipFixpointDependent (the RuntimeError variant in compiler/crates/oxc-runtime/src/lib.rs, raised from concept_admits_individual in compiler/crates/oxc-runtime/src/classify.rs and from select_member_impl in compiler/crates/oxc-runtime/src/store_read.rs). That refusal becomes unnecessary under this design and is retired by the slice that derives $iof.

How each read surface is served today

SpellingPathBehaviour over a shape-defined C
C(x)concept atom, rule planecorrect
x : C, C a bare namelowered to the concept atom (atom_to_atom_ir in atom_lower/mod.rs)correct
x : pkg::C, qualifiedrefusedOE0223fail-closed, not silent
iof(x, C)reserved head over $iof, seed planesilently empty
not iof(x, C)samesilently matches everybody
meta(x) == Creserved head over $meta, seed planesilently empty
x :: CrefusedOE0223 in a declared rule; OE0204 ad-hocfail-closed
not (meta(x) == C)refused — unsupported term shapefail-closed
extent(C) in a ruledesugars to iofsilently empty
ox query --extent Cstore-local classifierfail-closed
x is Cnot a membership spelling at all — see belown/a

The bare name is kept honest by an early lowering that never reaches the reflection atom (atom_to_atom_ir in atom_lower/mod.rs):

#![allow(unused)]
fn main() {
return AtomIR::Predicate {
    path: vec![name],
    args: vec![operand],
};
}

The qualified type test is not a silent-empty bug. An earlier revision of this record recorded it as one, and as an open question about which plane it evaluates on. That was wrong, and the correction matters because it removes an item from the defect list rather than adding one. x : pkg::C is refused, by name, at check — before any lowering decision is reached:

OE0223: unresolved type `rfd93q::Voter` in type-test atom (a `c : T` atom
is membership in `T`'s extent; `T` must be a declared concept)

The lowering path behind it is fail-closed too: a qualified path falls through to AtomIR::TypeTest { ty: TypeExpr::Unknown } (the fall-through arm of the same function), which the rule compiler refuses as not-yet-evaluable, so the build aborts rather than deriving an answer. This surface therefore needs a better message, not a new diagnostic and not a semantic decision. The message currently says the type is unresolved, which is misleading when the concept plainly exists and only the spelling is unsupported.

x is C is a different construct entirely. is is a keyword, but it introduces the optional-payload test (is Some / is None) and the reasoning-outcome suffix (is unknown, and the refused is both / is ambiguous / is timeout) — see rule_atom and atom_suffix in compiler/crates/oxc-parser/src/rule_atom.rs. p is Voter does not reach the type-test path at all; it is read as a relation-valued application and refused with OE1386. Open question 1 records the audit this needs; no claim in this record covers it.

The book already contradicts itself

Three sites in one chapter, spec/reference/src/declarations/meta-calculus.md. Two promise full membership:

  • :115 — “The rule-atom form x : T … is syntactic sugar for iof(x, T).”
  • :123 — “extent has the signature extent(t: TypeRef) -> Set<Entity> … Returns the set of entities x such that iof(x, t) holds at the current state.”

One describes the asserted-only relation:

  • :143 — “$iof (entity × type, closed under supertypes; this carries both tiers — the individual-tier rows from pub fact T(x) assertions and the catalog-tier type×type rows from each : T instantiation clause, the latter closed upward over the target type’s <: ancestors)”

The third sentence names two sources, neither of them the rule plane, and it is the only denotational statement the chapter makes about $iof. A reader who takes :115 and :123 at face value and a reader who takes :143 at face value will disagree about what the language does, and both will be citing the specification.

The meaning is already ratified elsewhere

  • spec/rfd/0023-reflective-type.md:89 introduces the intrinsic as the general membership predicate — iof(x: Entity, t: TypeRef) -> Bool // sugar: x : T — with extent(t: TypeRef) -> Set<Entity> at :91, and gives the Lean characterization at :104: x ∈ extentOf u t ↔ x ∈ u ∧ iof x t. No materialization clause appears in the definition.
  • spec/rfd/0017-refinement-classification.md:36 defines derived membership in terms of iof: “Membership is derived: extent(C) = { x : iof(x, parentᵢ) ∧ P(x) }”. If iof meant asserted-only, the equation would be ill-formed.
  • spec/rfd/0070-derive-backed-classification.md:48 settles the shape case: “A shape-defined concept C is an ordinary atom iof(x, C) of the reasoner’s well-founded / AFT fixpoint — uniform with every other ground atom. There is no separate classifier fixpoint”. Line :143 records the biconditional as a proved surface promise: “L5 | parent/body split = surface promise iof(x,C) ↔ iof(x,parent) ∧ shapeBody | PROVED-scratch”.
  • spec/lean/Argon/TypeSystem/Realization.lean:13-15 states the realization biconditional for the refinement case, and realize_iff (:82) proves it.
  • Appendix C already states fixpoint-evaluated membership normatively: OE0258 (spec/reference/src/appendix-c-diagnostic-codes.md:139) — “Membership is the individual’s full classification (asserted iof UNION derived iff, <:-closed …)”; OE0240 (:121) and OE0241 (:122) — “membership evaluated in the fixpoint … including shape-defined members whose extent is never asserted”.

Demonstration

Every command with its output. One package, no mutations. Voter is shape-defined; alice is registered, hence a voter on the rule plane. Adult is an ordinary asserted concept holding bob, as a control.

Row sets, row counts, event counts, diagnostic codes, and message text are reproduced as emitted, and every output below was re-executed against an ox built from the main line this record is written against — none is inherited from an earlier revision. Four things are trimmed for readability and are the only edits: the module-path prefix on queries from a single-file package, the at vt=… wall-clock suffix an ad-hoc read prints, the multi-line help: body under a diagnostic, and the artifact byte size — which varies with the package name and carries no argument.

On the code fences in this record. The package immediately following is the only block fenced as checkable Argon. It checks clean, and that result comes from a local run: the documentation-example gate roots its scan at the reference manual’s source tree (run_check_doc_examples in compiler/xtask/src/doc_examples.rs joins spec/reference/src and walks only that tree), so it executes no fence in this directory. No block below is machine-verified by continuous integration.

Every other relational block anywhere in this record is an excerpt or is pseudo-relational, and carries a bare or ignore-flagged fence on purpose. Do not promote them: they are not self-contained and would not check. Were such a block moved into the reference manual, the gate would run ox check on it exactly as written, and an excerpt does not stand alone.

use std::core::type;
use std::core::rel;

pub type Person;

pub rel Registered(mut p: Person);

pub derive IsRegistered(p: Person) :- Registered(p);

pub type Voter <: Person iff IsRegistered(self);

pub type Adult <: Person;

pub derive ViaConceptAtom(p: Person) :- Voter(p);
pub derive ViaSugar(p: Person) :- p : Voter;
pub derive ViaIof(p: Person) :- iof(p, Voter);
pub derive ViaNegatedIof(p: Person) :- Person(p), not iof(p, Voter);
pub derive ViaMetaEq(p: Person) :- Person(p), meta(p) == Voter;
pub derive ControlAssertedIof(p: Person) :- iof(p, Adult);
pub derive ControlAssertedAtom(p: Person) :- Adult(p);

pub derive Nonminimal(x: Entity, c: TypeRef) :-
    iof(x, c), iof(x, d), specializes(d, c), d != c;
pub derive DerivedMeta(x: Entity, c: TypeRef) :-
    iof(x, c), not Nonminimal(x, c);

pub query q_concept_atom() -> ViaConceptAtom;
pub query q_sugar() -> ViaSugar;
pub query q_iof() -> ViaIof;
pub query q_negated_iof() -> ViaNegatedIof;
pub query q_meta_eq() -> ViaMetaEq;
pub query q_control_iof() -> ControlAssertedIof;
pub query q_control_atom() -> ControlAssertedAtom;
pub query q_nonminimal() -> Nonminimal;
pub query q_derived_meta() -> DerivedMeta;

pub fact Person(alice);
pub fact Registered(alice);
pub fact Person(bob);
pub fact Adult(bob);

ox check reports ok; ox build writes the artifact; ox query enumerates every declared query:

$ ox check .
ok
$ ox build .
wrote ./target/root.oxbin (31 events)
$ ox query .
query q_concept_atom: 1 row(s)
  (#i7582462787275748164)
query q_control_atom: 1 row(s)
  (#i8648907253816210642)
query q_control_iof: 1 row(s)
  (#i8648907253816210642)
query q_derived_meta: 11 row(s)
  (std::core::type, Metatype)
  (Voter, std::core::type)
  (Voter, TypeRef)
  (Adult, std::core::type)
  (Adult, TypeRef)
  (Person, std::core::type)
  (Person, TypeRef)
  (Registered, TypeRef)
  (Metatype, Metatype)
  (#i7582462787275748164, Person)
  (#i8648907253816210642, Adult)
query q_iof: 0 row(s)
query q_meta_eq: 0 row(s)
query q_negated_iof: 2 row(s)
  (#i7582462787275748164)
  (#i8648907253816210642)
query q_nonminimal: 1 row(s)
  (#i8648907253816210642, Person)
query q_sugar: 1 row(s)
  (#i7582462787275748164)

#i7582… is alice; #i8648… is bob.

The defect, four ways. q_concept_atom and q_sugar find alice. q_iof finds nobody. q_meta_eq finds nobody. q_negated_iof finds both people, including the genuine voter. The control is clean in both directions: q_control_iof and q_control_atom both return bob, so iof is not broken — it is broken for exactly one class of concept.

Reading $iof directly shows the missing row:

$ ox query . --eval 'pub derive q(p, t) :- iof(p, t);'
ad-hoc query: 12 row(s)
  (std::core::type, Metatype)
  (Voter, std::core::type)
  (Voter, TypeRef)
  (Person, std::core::type)
  (Person, TypeRef)
  (Registered, TypeRef)
  (Adult, std::core::type)
  (Adult, TypeRef)
  (Metatype, Metatype)
  (#i7582462787275748164, Person)
  (#i8648907253816210642, Person)
  (#i8648907253816210642, Adult)

(#i7582…, Voter) is absent. And $meta gives alice’s most-specific type as Person:

$ ox query . --eval 'pub derive q(x, t) :- meta(x) == t;'
ad-hoc query: 8 row(s)
  (std::core::type, Metatype)
  (Voter, std::core::type)
  (Adult, std::core::type)
  (Person, std::core::type)
  (Registered, std::core::rel)
  (Metatype, Metatype)
  (#i7582462787275748164, Person)
  (#i8648907253816210642, Adult)

The antichain reformulation, measured. q_nonminimal and q_derived_meta are D4’s rules written in ordinary Argon and evaluated by the ordinary reasoner. On the individual tier they reproduce $meta exactly: (alice, Person) and (bob, Adult), the same two rows the built-in $meta gives, with bob’s Person row correctly suppressed as non-minimal. On the catalog tier they do not, in both directions — q_derived_meta emits four TypeRef rows that $meta withholds, and misses (Registered, std::core::rel) which $meta has. That is exactly what that block’s own comment says it should do, and it is why D4 is scoped to the individual tier. This discrepancy was found by running the rules, not by reading them.

$specializes is reflexive, measured. D2’s bridge rules turn on this, so it is read off the relation rather than off the comment:

$ ox query . --eval 'pub derive q(a, b) :- specializes(a, b);'
ad-hoc query: 14 row(s)
  (Voter, Voter)
  (Voter, Person)
  (Adult, Adult)
  (Adult, Person)
  (Person, Person)
  (Registered, Registered)
  (TraitRef, TraitRef)
  (TraitRef, Entity)
  (Entity, Entity)
  (Metatype, Entity)
  (Metatype, Metatype)
  (Metatype, TypeRef)
  (TypeRef, Entity)
  (TypeRef, TypeRef)

(Voter, Voter) is present, so the <:-closure bridge rule subsumes the direct one.

The refusals, confirmed. Each is fail-closed today and stays that way:

$ ox check .          # with `pub derive V(p: Person) :- p : rfd93q::Voter;`
OE0223: unresolved type `rfd93q::Voter` in type-test atom (a `c : T` atom
is membership in `T`'s extent; `T` must be a declared concept)
$ ox build .
Error: 1 error(s); build aborted

$ ox check .          # with `pub derive V(p: Person) :- Person(p), p :: Voter;`
OE0223: unresolved predicate `p :: Voter` in rule body

$ ox check .          # with `pub derive V(p: Person) :- iof(p, self::Voter);`
OE0227: in rule `membership::V`, the path `self::Voter` resolves to nothing —
searched enum constants, metaxis values, declared types, declared relations,
and declared individuals (this module and the workspace); a multi-segment path
is never a variable

$ ox query . --eval 'pub derive q(p) :- Person(p), not (meta(p) == Voter);'
Error: ad-hoc query could not be prepared: unsupported term shape in atom: MetaCall

$ ox query . --eval 'pub derive q(p) :- Person(p), p is Voter;'
Error: OE1386: the relation-valued variable `p` in rule `q` is applied without
pinning which relations it can stand for

$ ox query . --extent Voter
Error: query_extent: membership in `Voter` is rule-derived (fixpoint-dependent)
and is undetermined for the store-local classifier — it is computed by the
reasoner (`select … from Voter(x)` / a `derive` body), never evaluated
mid-classification (#1314); read it through the reasoner or classify the
individual directly

A trap, recorded so no later reader repeats it. The ad-hoc surface (ox query --eval) resolves a bare concept constant unqualified, while the store holds the module-qualified name. So an ad-hoc read naming a concept constant can disagree with a declared rule containing the identical atom:

$ ox query . --eval 'pub derive q(p) :- iof(p, Adult);'
ad-hoc query: 0 row(s)

Adult is an ordinary asserted concept and the declared rule over the same atom returns bob. The zero here is the ad-hoc path failing to resolve the constant, not a membership answer. Every load-bearing number in this record therefore comes from a declared query head. Ad-hoc reads are used only where the type position is a variable (iof(p, t), meta(x) == t), which binds rather than resolves a constant and is therefore safe; those are read as a dump of the relation, never as a membership verdict.

The whole surface, measured. The package above is one shape. The conformance sweep in pull request #1903 evaluates all fifteen spellings against all five shapes by execution. Its table, with this record’s scope marked — bold is a defect, and the fixed/not-fixed split is called out below it:

spellingassertedbraced iff {…}shape clauseupward closureimported shape clause
C(x)membermembermembermembermember
x : Cmembermembermembermembermember
x : mod::Cmembermembermembermembermember
x : self::Cemptyemptyemptyemptyempty
iof(x, C)membermemberemptymemberempty
iof(x, self::C)refusedrefusedrefusedrefusedrefused
iof(x, mod::C)membermemberemptymemberempty
iof(x, t) boundmembermemberemptymemberempty
meta(x) == Cmembermemberemptyempty by specempty
x :: Crefusedrefusedrefusedrefusedrefused
x in extent(C)membermemberemptymemberempty
x in extent(t) boundmembermemberemptymemberempty
P(x), not C(x)non-membernon-membernon-membernon-membernon-member
P(x), not x : Cnon-membernon-membernon-membernon-membernon-member
P(x), not iof(x, C)non-membernon-memberEVERYONEnon-memberEVERYONE

Reading it: member means the spelling returns the same individuals the concept atom C(x) returns; non-member means it returns the complement, which is right for a negated spelling.

What the sweep does and does not establish. Its oracle is the concept atom. So it measures agreement between spellings, and it does that by execution rather than by inspection — which is what makes the disagreements incontrovertible. It does not establish that the concept atom is itself correct; that rests on the ratified meaning cited above, not on the sweep. Read the table as a disagreement census, not as a correctness proof.

  • Two whole columns split, and they are the same column twice. The shape clause, and the shape clause reached through an import. Seven defect cells each. D2 closes all fourteen.
  • One whole row is empty, in every column. x : self::C — including the asserted column, where nothing is derived and there is no hole for D2 to fill. Five cells, issue #1904, not fixed here.
  • empty by spec is not a defect. meta(x) == C under upward closure asks for minimal classifiers, and D4 preserves that; see Non-goals.
  • The braced column is the control. It agrees everywhere the shape-clause column splits, which is what proves the reasoner boundary is the cause rather than the reflection intrinsic.

That control is executed here rather than taken from the table. Membership by a braced refinement is decided by a field value, and a field is only writable through a mutation, so the two individuals are enrolled by a scenario:

pub type Person { mut registered: Bool }
pub type BracedVoter <: Person iff { self.registered == true };

pub mutate enroll(p: Person, r: Bool) {
    insert iof(p, Person);
    update p: Person set { registered = r }
}

pub derive BracedAtom(p: Person) :- BracedVoter(p);
pub derive BracedIof(p: Person) :- iof(p, BracedVoter);
pub derive BracedNegatedIof(p: Person) :- Person(p), not iof(p, BracedVoter);
pub derive BracedMetaEq(p: Person) :- Person(p), meta(p) == BracedVoter;

With alice enrolled registered and bob unregistered:

$ ox run-scenario .
scenario: applied 2 mutation(s) from ./demo.toml
query q_braced_atom: 1 row(s)
  (#i7582462787275748164)
query q_braced_iof: 1 row(s)
  (#i7582462787275748164)
query q_braced_meta_eq: 1 row(s)
  (#i7582462787275748164)
query q_braced_negated_iof: 1 row(s)
  (#i8648907253816210642)

All four agree: the concept atom, the reflection read, and the metatype read all return alice, and the negated read returns exactly bob. Compare the shape-clause package above, where the same four spellings give the member, nobody, nobody, and everybody. The only difference between the two programs is how membership is decided.

A downstream consumer of extent, executed. The direct reflection read is not the only casualty: a rule that joins against an extent derives nothing, one step removed from the read, which is how the hole escapes notice. Over the same vocabulary, with two ballots:

pub type Slip;
pub rel Ballot(mut b: Slip, mut owner: Person);

pub derive CountedBallot(b: Slip) :- Ballot(b, p), p in extent(Voter);
pub derive ControlCountedBallot(b: Slip) :- Ballot(b, p), p in extent(Adult);
pub derive ExtentMember(p: Person) :- p in extent(Voter);
pub derive ControlExtentMember(p: Person) :- p in extent(Adult);
pub derive AtomCountedBallot(b: Slip) :- Ballot(b, p), Voter(p);

pub fact Slip(slip1);
pub fact Slip(slip2);
pub fact Ballot(slip1, alice);
pub fact Ballot(slip2, bob);
$ ox check .
ok
$ ox build .
workspace: elaborated 2 module(s) into one artifact
wrote ./target/root.oxbin (31 events)
$ ox query .
query membership::q_atom_counted: 1 row(s)
  (#i768220427304451167)
query membership::q_control_counted: 1 row(s)
  (#i2503042279537445241)
query membership::q_control_extent: 1 row(s)
  (#i8648907253816210642)
query membership::q_counted: 0 row(s)
query membership::q_extent: 0 row(s)

#i7682… is slip1 (alice’s), #i2503… is slip2 (bob’s). The extent read over the rule-decided concept is empty, so the ballot rule counts nothing; the asserted control counts slip2; and the same ballot rule with the concept atom in place of the extent read finds slip1. The intended answer for q_counted is therefore exactly the row q_atom_counted computes.

The type-parametric rule, executed — the case with no literal-spelling workaround. When the type position is a bound variable rather than a written name, no concept atom can be substituted, because the concept is not known statically. This is the shape of every vocabulary-generic rule: one rule intended to hold for each concept in a family, including concepts declared later, in packages that do not exist yet.

pub derive RoleGeneric(p: Person, role: TypeRef) :-
    specializes(role, Person), iof(p, role);

pub derive RoleFamilyGeneric(p: Person, role: TypeRef, family: TypeRef) :-
    RoleGeneric(p, role), specializes(role, family);

// Only writable because this vocabulary happens to be known statically.
pub derive RoleExpanded(p: Person, role: TypeRef) :-
    specializes(role, Person), role == Voter, Voter(p);
pub derive RoleExpanded(p: Person, role: TypeRef) :-
    specializes(role, Person), role == Adult, Adult(p);
pub derive RoleExpanded(p: Person, role: TypeRef) :-
    specializes(role, Person), role == Person, Person(p);
$ ox check .
ok
$ ox build .
workspace: elaborated 2 module(s) into one artifact
wrote ./target/root.oxbin (23 events)
$ ox query .
query membership::q_role_expanded: 4 row(s)
  (#i7582462787275748164, membership::Person)
  (#i7582462787275748164, membership::Voter)
  (#i8648907253816210642, membership::Person)
  (#i8648907253816210642, membership::Adult)
query membership::q_role_family_generic: 4 row(s)
  (#i7582462787275748164, membership::Person, membership::Person)
  (#i8648907253816210642, membership::Person, membership::Person)
  (#i8648907253816210642, membership::Adult, membership::Person)
  (#i8648907253816210642, membership::Adult, membership::Adult)
query membership::q_role_generic: 3 row(s)
  (#i7582462787275748164, membership::Person)
  (#i8648907253816210642, membership::Person)
  (#i8648907253816210642, membership::Adult)

The generic rule is missing (alice, Voter) — for no reason visible in it, since it names no concept — and the rule one join downstream is missing the two Voter-headed triples that follow. It is correct for the asserted concept and silently incomplete for the rule-decided one, so it under-derives once per rule-decided concept in the vocabulary. The hand expansion is not a workaround: it requires the author to enumerate the vocabulary and cannot be written by library code that receives a type as a value. This is the strongest motivation for D1 — under D1 the generic rule returns q_role_expanded’s four pairs.

The metatype’s narrowness, executed as a control. meta yields <:-minimal classifiers, so a member asserted at a strict subtype is deliberately absent under the supertype; this is the empty by spec cell and it is preserved:

pub type Person;
pub type Adult <: Person;

pub derive AtomPerson(p: Person) :- Person(p);
pub derive IofPerson(p: Person) :- iof(p, Person);
pub derive MetaEqPerson(p: Person) :- Person(p), meta(p) == Person;
pub derive MetaEqAdult(p: Person) :- Person(p), meta(p) == Adult;

pub fact Adult(bob);
$ ox check .
ok
$ ox build .
workspace: elaborated 2 module(s) into one artifact
wrote ./target/root.oxbin (16 events)
$ ox query .
query membership::q_atom_person: 1 row(s)
  (#i8648907253816210642)
query membership::q_iof_person: 1 row(s)
  (#i8648907253816210642)
query membership::q_meta_eq_adult: 1 row(s)
  (#i8648907253816210642)
query membership::q_meta_eq_person: 0 row(s)

Membership in Person holds by both the concept atom and the reflection read; the classifier is Adult; meta(bob) == Person is correctly empty.

The self:: row, executed. It needs no derived membership at all:

pub type Person;
pub fact Person(alice);
pub fact Person(bob);

pub derive ViaAtom(p: Person) :- Person(p);
pub derive ViaBare(p: Person) :- p : Person;
pub derive ViaSelf(p: Person) :- p : self::Person;
$ ox check .
ok
$ ox build .
wrote ./target/root.oxbin (12 events)
$ ox query .
query q_atom: 2 row(s)
  (#i7582462787275748164)
  (#i8648907253816210642)
query q_bare: 2 row(s)
  (#i7582462787275748164)
  (#i8648907253816210642)
query q_self: 0 row(s)

A checked-clean program serving an empty answer to a well-formed question about a plainly asserted concept. That is issue #1904, and D2 does not touch it.

The truth-qualifier precondition, executed. D5 makes this a blocker for slice 4:

pub derive IsRegistered(p) :- holdsRegistration(p, r), Registration(r);
pub type Voter <: Person iff IsRegistered(self);

pub derive DefinitelyVoter(p) :- Voter(p);
pub derive UnknownVoter(p) :- Person(p), Voter(p) is unknown;
$ ox query .
query q_definite: 1 row(s)
  (#i7582462787275748164)
query q_unknown: 1 row(s)
  (#i7582462787275748164)

alice holds a registration, so her membership is definitely true; she is returned by both. bob, definitely not a member, appears in neither. The qualifier is not being evaluated. Issue #1902.

Group axioms are already correct and are not what this record is about. The reported defect that prompted it — partition mis-evaluating a shape-defined cover — was fixed by pull request #1873. With Voter and Abstainer both shape-defined and partition Person { Voter, Abstainer }:

$ ox check .          # alice registered — covered
ok
$ ox check .          # `pub fact Registered(alice);` removed — uncovered
Argon::OE0241: `Person` is declared completely covered, but an instance is
in none of its cover members [alice]

Both directions enforced. This record concerns the reflection spelling a modeller writes by hand.

Decision

D1 — iof denotes membership, whole

iof(x, C) denotes an individual’s membership in C: asserted classification, <: closure, predicate-refinement-derived, and rule-derived alike. extent and meta follow it. C(x), x : C, and iof(x, C) are three exact spellings of one question, and answer identically for every concept and every argument shape, including a bound type variable.

Exact is the operative word and is stronger than the first revision’s claim. It means substitution holds: iof(x, Voter) and iof(x, t), t == Voter denote the same set. Any remedy that does not deliver that is not a remedy for this defect, only for its most visible instance.

Scoped to bare and module-qualified concept paths. The sweep finds a separate defect on self::: p : self::C checks, builds, and silently derives nothing, in every shape including a plainly asserted concept where no derived membership is involved. Nothing in this record touches path resolution, so “three exact spellings of one question” holds for C and for pkg::C and not for self::C until issue #1904 is fixed.

D1 is the decision this record asks for. The originating records already fix this meaning (Context, above). Under D1 the seed-time $iof build is an implementation of it, and an incomplete one. The remedy is to complete the implementation, not to narrow the intrinsic.

D2 — The reflection relation carries derived membership

The individual tier of $iof stops being a seed-time snapshot of the per-type extents and starts carrying every concept’s decided membership, rule-decided included. That is the semantic content of D2 and it is what D1 requires.

The mechanism below is one of two, and is not settled. D2 is written against generated bridge rules because that form is concrete enough to argue about — the stratification obligation in D5 and the antichain in D4 are stated over it. The alternative mechanism — rewriting constant type positions at elaboration and materializing the relation once at a stratum boundary above every read — yields the same answers for every spelling and every argument shape, so nothing in Consequences or Measured scope turns on the choice. Reviewers are asked to settle D1; D2’s mechanism is sequenced with slice 4.

In the bridge-rule form: for each declared concept C the elaborator emits bridge rules into the program:

$iof(x, C)     :- C(x).
$iof(x, super) :- C(x), $specializes(C, super).

The second rule folds in <: closure, reading the same catalog-closed $specializes relation the surface specializes intrinsic reads.

Two corrections to the obvious form of these rules, both read off the tree:

  • $specializes is reflexive (standpoint.rs:286-290: “for each declared concept t, ancestor_concept_ids_including_self(t) yields t itself plus every supertype”), and the executed rows in Demonstration show (Voter, Voter). The second rule therefore already subsumes the first. The first is kept only so a concept absent from the <: graph still bridges; whether that case exists is an implementation detail for the slice, not a semantic question.
  • $specializes’s columns carry the concept name directly (Value::Name), not a wrapped type-reference constructor. The rule joins on the plain name.

The catalog tier stays extensional, unchanged. Its rows are declaration-derived and finite, it is explicitly catalog-closed, and negation over it is already justified as stratification-safe on that ground (the metatype-tier block of materialize_reflection_relations). Deriving it would buy nothing and would put declaration-level rows into a fixpoint that has no need of them.

materialize_reflection_relations’ individual-tier scan (standpoint.rs:74-110) is deleted, along with the iof_by_entity accumulation it feeds.

D3 — extent(t) is a projection over $iof

No change of meaning and no separate work: extent already desugars to the reserved-head iof atom (lower_source), so once $iof is derived, extent(t) is the entity column of $iof restricted to t and is correct for every concept, constant or variable argument alike.

The store-local classifier’s fail-closed refusal (RuntimeError::MembershipFixpointDependent) exists because that surface runs mid-classification and cannot reach the fixpoint. Once the read is served from the derived relation rather than from the classifier, the refusal has nothing left to protect and is retired. Retiring it is part of the slice that derives $iof, not a separate decision — but it is a user-visible widening and is called out as such in Consequences.

D4 — meta is an antichain over $iof, on the individual tier

$meta’s individual tier stops being a second snapshot and becomes:

$nonminimal(x, C) :- $iof(x, C), $iof(x, D), $specializes(D, C), D != C.
$meta(x, C)       :- $iof(x, C), not $nonminimal(x, C).

This is the same <:-minimality the current code computes (the $meta reduction in materialize_reflection_relations), expressed over the derived relation instead of over the seed scan’s accumulated type set. Demonstration evaluates exactly these two rules in ordinary Argon and confirms they reproduce the current individual tier row for row.

Scoped to the individual tier, deliberately. The catalog tier’s $meta is the exact declared introducer, not a minimal element — a type may be declared an instance of several higher-order types, and two catalog row sets feed $iof but not $meta on purpose (the metatype-tier block of materialize_reflection_relations; Context, above). The demonstration measures what an unscoped antichain would get wrong: four spurious rows and one missing one, in a program with three concepts. The catalog tier keeps its existing construction.

D5 — Stratification, and the third truth value

The bridge is positive. $iof(x, C) :- C(x) has no negation, so concept-to-relation-to-concept recursion is monotone: a concept whose rule reads iof of another concept forms an ordinary positive cycle, and the least fixpoint is the intended one. No new stratification obligation arises from the bridge itself.

Negation inside a component is handled by the well-founded semantics, not by refusal. The stratifier already distinguishes the three edge kinds and, on finding negation inside a strongly-connected component, marks that component for well-founded evaluation instead of refusing it (stratify in compiler/crates/oxc-reasoning/src/compile/stratify.rs):

#![allow(unused)]
fn main() {
// Recursion through negation → well-founded semantics.
EdgeKind::Naf => scc_is_wfs[src_scc] = true,
}

So a rule reading not iof(x, C) puts $iof, $nonminimal, $meta, and every concept that bridges into them into one dependency component, evaluated as a single well-founded unit.

This is not new machinery, and the claim that shape membership already works this way is not an analogy. A shape-defined concept’s membership rule is emitted as an ordinary rule declaration (compiler/crates/oxc-instantiate/src/lower/concept.rs, synthesize_shape_membership_rule) and is stratified with everything else; the pipeline has a named pass for shape classifiers caught in negation cycles (shape_classifiers_in_negation_cycles, compiler/crates/oxc-workspace/src/pipeline.rs), and its documentation (the OW0679 block of runtime_rule_failures) records the disposition this design relies on: a shape-defined classifier in a recursion-through-negation cycle “is ALLOWED, not refused”, because “WFS gives the cycle a model (membership surfaces Can)”. It warns (OW0679) rather than refusing. D2 puts the reflection relation into the same regime that already governs the concept it bridges from.

D4’s $meta pair is a weaker case still: $nonminimal does not read $meta, so the two sit in different strata and the negation is ordinary stratified negation, not recursion through it. The demonstration evaluates that pair today, unmodified, with no stratification complaint.

Hard invariant: undefined membership remains the third truth value and must not collapse to false. Where the well-founded semantics leaves iof(x, C) undefined, iof(x, C) is undefined, not iof(x, C) is undefined, and neither derives. A read must not silently substitute false for undefined — that would replace today’s silent over-derivation with a different one, which is the failure class this record exists to close.

The substrate has the representation for this. The well-founded executor materializes the undefined extent as a companion relation rather than losing it (undefined_name in compiler/crates/oxc-reasoning/src/executor/wfs.rs): R holds the definitely-true fragment and $undefined::R holds the rest, explicitly “so a downstream reader … can tell undefined apart from definitely-false”. On the classification side the same distinction is already honoured for exactly this class of concept (concept_membership_local in compiler/crates/oxc-runtime/src/classify.rs), where a rule-headed concept’s undetermined membership reads Truth4::Can and never a fabricated Not.

A bound on that invariant, stated rather than glossed. Three-valued undefined does not yet propagate into derive heads: cross-stratum negation over a well-founded relation is currently refused (OE1365, OE1367; the ReasoningError variants in compiler/crates/oxc-reasoning/src/error.rs, gated at the cross-stratum block of stratify), and the source calls this “the honest interim until three-valued undefined propagates into derive heads”. Aggregation over an undefined extent is likewise refused (OE1332). So today the invariant is upheld by refusal, not by propagation. D2 must not weaken that: where the propagation does not exist, the refusal stays. Making a derived $iof widen the set of programs that hit those refusals is an accepted and expected consequence; making it silently answer them is not.

The surface already has a spelling for the undefined case — is unknown, the well-founded-undefined read (atom_suffix in compiler/crates/oxc-parser/src/rule_atom.rs) — but the reflection atoms do not accept that suffix. Open question 2.

That spelling was broken and is now repaired by RFD 0094. Issue #1902 established that Voter(p) is unknown returned the definitely-true extent because lowering discarded the qualifier. RFD 0094 lowers it to the completed $undefined::Voter companion, orders the consumer after Voter, and refuses recursive reads.

This is not an unrelated ticket. Making membership fixpoint-derived means more concepts can legitimately be undetermined — every shape-defined concept whose defining rule is itself undetermined now has an undefined reflection extent where before it simply had none. The surface for asking “is this membership undetermined?” therefore matters materially more after this change than before.

So issue #1902 is a stated precondition of slice 4, the first behaviour-changing slice — not a follow-up. It was fixed before the read surface widens. The same pressure applies to the refusals themselves: this design pushes more programs into OE1365 / OE1367 territory, so the refusal path becomes a surface users actually meet rather than a corner. It has to be a good refusal — a message that names the undetermined concept and points at the qualifier — and not merely a correct one.

Recursion through aggregation stays refused, by the existing gate OE1317 (the aggregate-cycle arm of stratify): an aggregate edge inside a component is collected and refused, because “recursion through aggregation has no well-defined fixpoint”. The bridge rules introduce no aggregation and do not widen this.

No restriction on variable type positions is required for soundness. A free type position — iof(x, t) with t unbound — is sound under this design; it simply ranges over the whole relation. Any restriction imposed on it is for tractability, because a free type position can merge otherwise-independent dependency components into one large well-founded unit. That is a cost question, not a correctness question, and Open question 3 records what bounds it. This distinction is worth stating plainly because the first revision’s design-gated slice implied the opposite.

D6 — The mechanization requires no change

spec/lean/Argon/MetaCalculus/Reflect.lean:183-184 declares membership as an opaque field on the reflection catalog:

/-- `iof x t = true` iff instance `x` is an instance of type `t`. -/
iof : Ref → Ref → Bool

It is a bare Ref → Ref → Bool with no defining equation and no provenance condition — contrast the sibling specializes, which carries preorder obligations. Whether rows arrive by a seed scan or by a bridge rule is exactly the choice of inhabitant supplied for that field.

extentOf is defined from it (Reflect.lean:209-210):

def extentOf (c : ReflectCatalog) (u : List Ref) (t : Ref) : List Ref :=
  u.filter (fun x => c.iof x t)

and every downstream result — iofRel (:204), mem_extentOf (:214), extentOf_subset (:221), catalogTierIof (:242) and its catalogTierIof_eq_iofRel := rfl (:248-249) — is proved through the field and is provenance-agnostic. Realization.lean’s realize_iff (:82) operates on a different abstraction with no seeding assumption.

So this design requires no Lean change. This inverts which option looks conservative, and the inversion is the point. The rejected alternative (Alternatives, below) would have needed a second catalog field — an asserted-plane membership distinct from membership — and would have had to re-point extentOf and every theorem above it at whichever field the surface now meant. A record that presents itself as the smaller change would have been the one to disturb the mechanization.

“No Lean change” is not “no Lean obligation”. The field’s opacity is what makes existing theorems survive, and it is also why D1’s actual semantic content — that membership includes rule-derived membership — is not stated in the mechanization: nothing in Reflect.lean says which inhabitant is the right one, and the strongest backing for the inclusive reading is RFD 0070’s L5, which that record reproduces as PROVED-scratch rather than as a merged theorem. Mechanizing that bridge — relating the generated membership rules’ output to the iof field’s population, so the inclusive reading is a theorem and not only a surface promise — is deferred, not a non-goal. It is deferred without a named owner and without a slice in this record’s plan; no decision here forecloses it, and no claim here rests on it having happened.

One boundary to respect: AxisRelation.lean’s catalog-closure invariants (:182, :224-227) state that the reflective extents read the catalog only. D2 keeps the catalog tier declaration-derived, so those theorems stand. A design that let rule-derived rows feed the reflective-sort extents would not, and is out of scope.

D7 — The book states the meaning once, and the migration is mechanical

meta-calculus.md:143 is corrected to state the meaning — full membership — with the two-source enumeration retained as an implementation note about what the catalog tier builds. :115 and :123 become true instead of aspirational. The Appendix C entries already stating fixpoint-evaluated membership (OE0258, OE0240, OE0241) are cited, not rewritten. The qualified-path type test gets a better OE0223 message, saying the spelling is unsupported in a membership position rather than that the type is unresolved.

Migration is compiler-locatable. Both halves of the trigger are already computed by the compiler, so no new analysis is needed to find every affected site.

Which concepts carry a shape clause. The elaborator decides this from the concrete syntax and records it as the refinement tag (lower_concept_decl in compiler/crates/oxc-instantiate/src/lower/concept.rs, matching on find_shape_clause), and the runtime exposes the answer as a pair — Module::shape_defined_concept_ids and Module::is_shape_defined (compiler/crates/oxc-runtime/src/schema_module.rs:1820 and :1835).

Which body atoms are reflection heads. iof, meta, and specializes are keyword-lexed, not identifiers (compiler/crates/oxc-syntax/src/generated.rs, the from_keyword table), and the elaborator already tests for them by name at the atom-resolution pass (validate_reflection_carrier in compiler/crates/oxc-instantiate/src/lower/rule_ref.rs):

#![allow(unused)]
fn main() {
if matches!(head, "iof" | "specializes" | "meta")
}

The intersection is therefore mechanical: every rule whose body reads a reflection head with a shape-defined concept in the type position can be enumerated at elaboration. The same site is where the first revision’s prototype keyed its rewrite off the bare head string; used for a warning rather than a silent rewrite, that keying is fit for purpose, because a missed carrier under-warns rather than mis-derives.

A warning ships one release ahead of the behaviour change, naming each such site and what its answer will become. For nearly everyone the edit count is zero: a read that silently derived nothing starts deriving correctly, which needs no source change. Only rules that depend on the empty answer change meaning — in practice a negated read used as a universal, not iof(x, C) standing in for “everybody”. Those are the sites the warning exists to find. Open question 4 records that the warning’s exact trigger set is not yet fixed.

Rationale

The meaning is already ratified, and none of the ratifying sites mentions a materialization. The citations — four design records and a Lean proof — are in The meaning is already ratified elsewhere. What follows from them is the argument here: narrowing the intrinsic to match the materializer would retract a proved surface promise to accommodate a seed-order constraint.

The implementation already agrees and already works to make it true. The refinement-honesty pass in seed_base_catalog_in_frame_filtered exists for no other reason than to make derived membership visible through these surfaces, at real cost. The $iof materializer’s own doc comment claims derived members are included (standpoint.rs:16-28). The minimal-classifier path documents the union explicitly. The inclusive reading is the implementation’s stated intent; shape concepts are the one class it structurally cannot reach.

Fixing the relation is smaller than fixing the readers. There is one producer of $iof and an open-ended set of consumers — rule bodies, ad-hoc bodies, the extent desugaring, $meta, the serving reads. Every remedy that teaches consumers to look elsewhere has to enumerate them and stay enumerated as new ones appear; the first revision’s prototype found this the hard way, keying its rewrite on the head string "iof" at each site so that a new reflection carrier would silently miss it. Deriving the relation fixes every consumer at once, including consumers not yet written, because they all read the same relation and the relation is now right.

The precedent ran twice, both times by routing the read to the plane that knows the answer. Issue #1859 → pull request #1860 fixed field projections over a shape-defined concept by materializing rows extent-blind and gating them on the fixpoint membership atom. Issue #1871 → pull request #1873 fixed group-axiom checks by swapping iof for the concept atom; the rule is now recorded in the source (compiler/crates/oxc-instantiate/src/lower/groupaxiom.rs:48): “Membership is tested with CONCEPT atoms, never iof(...): the $iof reflection relation carries asserted classification only, while a shape-defined member’s extent is derived in the fixpoint — the concept atom covers both planes”. Neither was remedied by refusing the read. D2 generalizes the same move to its source: after it, that comment’s premise is false and the comment can be deleted, because $iof no longer carries asserted classification only.

Nothing pins the current behaviour. No test pins iof / extent / $iof against a shape-defined concept on the read side, so no test expectation has to be revised to accommodate the correct answer. The audit: compiler/crates/oxc-runtime/tests/shape_write_gate.rs is entirely write-and-load refusals (OE0211) and contains no extent read at all; compiler/crates/oxc-driver/tests/group_axiom.rs reads iof only against asserted concepts, and its shape-defined case (shape_defined_parent_covering_guard_fires, :681) exists to record the change away from the iof spelling; compiler/crates/oxc-runtime/tests/materialization_impact_proof.rs:437 (cone_reaches_through_shape_defined_membership) exercises shape membership through a concept atom in a check body and queries extents only for plain concepts; and compiler/crates/oxc-driver/tests/shape_defined_field_projection.rs likewise reads through concept atoms. The two tests that do spell iof(x, …) and extent(…) target a non-shape concept.

Alternatives

Rewrite only the statically-known iof(x, C), and refuse meta(x) == C — REJECTED

This was this record’s own first revision, and it is recorded here as rejected, with its reasoning, rather than quietly replaced.

The proposal: at elaboration, when the type position of a membership read is a constant naming a shape-defined concept, rewrite iof(x, C) to the concept atom C(x), positive and under not. Leave a variable type position alone. Refuse meta(x) == C, because the surface does not survive relationalization as one atom — it splits into a join meta(x, $g) plus a filter $g == C, and rewriting the pair needs a second pass, a duplicated wrapper-recursion walk, and a total occurrence test that the prototype could only discharge by matching a substring of a derived debug rendering.

Its attractions were real: one small site, no source break, no epoch bump, and it matched what the two merged fixes for the same root cause did.

It is rejected because rewriting only the statically-known form breaks substitution. After the rewrite, iof(x, Voter) denotes full membership and iof(x, t), t == Voter denotes asserted-plane membership. Those are the same question, and they would return different sets. iof is not an incidental predicate — the manual sells it as first-class and type-value-polymorphic, and the value-polymorphic use is the reason RFD 0023 D4 introduced the reflection relations at all (0023-reflective-type.md:89-91). A first-class predicate over which equals cannot be substituted is not a predicate a modeller can reason about, and no amount of documentation repairs it. The proposal would have converted a uniform wrong answer into a non-uniform one, which is harder to find and harder to explain.

The refusal of meta(x) == C falls with it, for a different reason: it was adopted because the pair rewrite was fragile, and the pair rewrite is fragile because it is a rewrite. Once $meta is derived from $iof there is no pair to rewrite and nothing to refuse. The spelling is fixed.

Two findings from that revision’s prototype survive as evidence and are kept: the size and fragility of the relationalized-pair rewrite (which is why no design should try it), and the head-string keying of the rewrite sites (which is why per-site remedies do not stay correct).

A second intrinsic exposing the asserted-only plane — CONSIDERED AND DECLINED

Add asserted_iof(x, C) (or similar) so a modeller who genuinely wants the asserted plane can ask for it, with iof freed to mean membership. This looks like a tidy separation of concerns and it was considered on those terms.

It is not adopted. The reasons, in order of weight:

  • It is not load-bearing for the fix. D2 delivers correct iof, extent, and meta without it. It is an addition, and additions to a language surface need their own justification.
  • No concrete need has been produced. Not one use case has been written down that requires distinguishing asserted from derived membership at the read surface.
  • The write path already reaches asserted membership by construction — it writes assertions — and OE0211 refuses precisely where the two planes would diverge, so the place where the distinction has operational meaning is already governed. (OE0211 is narrower than a general plane-divergence gate: it refuses all insert/delete iof on any defined concept or defined-ancestor, while the construct door refuses only shape-defined targets, and it is runtime-only. The point stands for the read surface.)
  • It would cost a reserved name, which iof does not. iof, meta, and specializes are lexed as keywords (compiler/crates/oxc-syntax/src/generated.rs, the from_keyword table), so they can never collide with a user identifier. extent is already the exception — it lexes as an identifier and has to be carried on two allow-lists (compiler/crates/oxc-workspace/src/hygiene.rs, fn is_ambient; compiler/crates/oxc-check/src/references.rs, REFLECTION_INTRINSICS). An asserted_iof would be identifier-lexed too, expanding both gates and taking a plausible user identifier out of circulation, for a facility nobody has asked for.

If a provenance need does appear, it can be served without a new keyword — through the reflection relation surface, an attribute, or a standard-library predicate — and it should be decided then, against the real use case, rather than speculatively now.

Fail-closed refusal everywhere — RFD 0092

The alternative record’s position, at full strength: the two planes are a real architectural fact, and the honest response to a read that cannot be answered on the plane it is issued against is to refuse it, not to silently reroute it. RFD 0092 introduces one authority predicate naming which plane owns a concept’s membership, and a diagnostic refusing an asserted-plane read of a derived-membership concept.

Its case is strong on three counts. It is uniform: a refusal needs no static concept, so it covers the variable and runtime forms that a rewrite cannot reach — which was decisive against the rejected alternative above, and is not decisive against D2, because D2 is also uniform. It is loud: a refused program is strictly better than one that derived the wrong answer, and Argon’s diagnostic culture prefers refusal to silent adaptation. And it is cheap: no stratification argument, no incremental-maintenance work, no epoch bump, no change to what the reasoner computes.

The reasons this record prefers D2:

  • It keeps iof meaning one thing. Under a refusal, iof means asserted-plane membership and x : T means full membership, while the manual documents them as synonyms — a permanent two-meaning split every reader must learn, and a :115-versus-:143 contradiction resolved in favour of the narrower sentence.
  • It needs no source break. A refusal turns programs that check today into programs that do not, and the count is not small: every iof read over a shape-defined concept, correct or not.
  • It retracts a proved surface promise. RFD 0070’s L5 is recorded as proved; a refusal makes the surface narrower than the theorem, and the mechanization would have to be re-pointed to say so (D6).
  • It gives up on the value-polymorphic use RFD 0023 D4 exists to enable, rather than delivering it.

The counter-case is honest and is why this stays a live alternative: D2 is a silent change to what an existing program derives, in a direction no user can have intended but may have built on, whereas a refusal makes the modeller choose. D7’s warning-one-release-ahead is the mitigation, and a mitigation is not the same as not having the problem.

Do nothing

Rejected. The failure is checked-clean and silent — the source’s own words (catalog_seed.rs:852) — and in the negated direction it does not merely under-derive, it over-derives. A wrong answer with no diagnostic is the failure mode Argon’s loud-gate contract exists to exclude.

Move all membership to the rule plane and delete the seed plane’s classification

One plane, so the question cannot arise. Rejected as out of scope: the seed plane’s assertion closure and refinement honesty exist for reasons outside this record — mid-classification write gates read them — and moving them would restructure the write path. D2 does not require it.

Measured scope: what this record closes, and what it does not

The consequences below are counted, not estimated. A conformance sweep evaluates every membership spelling against every program shape by execution and records the answer each gives — five shapes (membership asserted; braced predicate refinement iff { … }; shape clause iff Derive(self); upward <: closure; the shape-clause concept imported across a module boundary) by fifteen spellings, seventy-five cells. It is proposed in pull request #1903.

Nineteen cells are defects. They fall into exactly three groups:

GroupCellsClosed by this record?
The shape-clause shape: iof (bare, qualified, bound type variable), meta ==, extent (constant, bound), and not iof7Yes — D2
The same seven, reached through an import7Yes — D2; the module boundary is not the cause
p : self::C, silently empty in all five shapes5No — issue #1904, a path-resolution defect this design does not touch

So this record closes fourteen of nineteen. The remaining five are an independent defect that survives it: the sweep finds p : self::C empty even in the asserted shape, where no derived membership exists to be missed, which is what establishes it as unrelated to the hole D2 fills.

Fourteen defect cells is not fourteen behaviour changes: the defect count sizes the diagnosis, and a much smaller flip count sizes the migration. The breakdown, and the one honest limit on it, are in Consequences.

The sweep also isolates the cause. The braced-refinement shape agrees on every spelling — a braced refinement is composed on the seed plane before the reasoner runs, so its members reach the reflection relation. Only the shape clause splits. That contrast is the control, and it is why the remedy is aimed at the reasoner boundary and not at the reflection intrinsic in general.

Non-goals — the behaviours that must be protected from the fix

These are the most likely way to over-implement this record. An implementer reading “every spelling agrees” could reasonably try to make them agree too. They must not. Neither is a defect, and neither is closed, softened, or widened by anything here.

  1. meta(x) == C under upward closure is empty, and that is correct. The metatype yields an individual’s <:-minimal classifiers. When a member is asserted at a strict subtype of C, its minimal classifier is that subtype, so C is absent — the individual is a member of C and is not most-specifically classified by it. The sweep records this cell as differing by specification, not as a defect. meta asks a narrower question than iof by design, and D4 preserves that: the antichain is over $iof, so it inherits $iof’s completeness without inheriting its breadth. Making this cell agree would delete the metatype’s reason to exist.
  2. The two refusals stay refusals. x :: C is refused, and iof with a self::-qualified path is refused (OE0227). Neither becomes an answer under this design, in any shape. x :: C is the metatype-equality sugar and is refused before evaluation; widening it is a separate decision with its own surface consequences. The self:: refusal is the fail-closed half of issue #1904 — the reflection intrinsic rejects a path the type test silently accepts, and of the two behaviours the refusal is the correct one. Fixing that issue by making the type test refuse would be consistent with this record; making iof silently accept would not.

Recorded explicitly because silence invites scope creep in both directions.

Consequences

  • Every membership spelling agrees, for a bare or module-qualified concept path. C(x), x : C, iof(x, C), extent(C), and meta(x) == C return the same answer for the same concept, for a constant or a variable type position — with the two non-goals above, and with self:: still excluded pending issue #1904.

  • Programs that silently derived nothing start deriving correctly. No source change needed; this is the intended effect.

  • The behaviour change is two cells, not fourteen. This distinction is the whole migration story and the defect count badly overstates it. Of the fourteen cells this record closes, twelve go from empty to correct: a read that derived nothing starts deriving the right rows. No rule can meaningfully depend on those — an empty relation contributes nothing to a positive body, so there is no behaviour to preserve. Two cells genuinely flip: not iof(x, C) in the shape-clause shape and in the imported shape-clause shape, which today match every individual and will match only genuine non-members. That is the dangerous cell and the only one: a rule that reads as an exclusion but has been silently behaving as a pass-through changes meaning. D7’s warning exists for exactly these two.

    One honest limit on that count: the sweep measures negation over three spellings only — the concept atom, the type test, and iof. A negated extent or metatype read (not (x in extent(C))) is not in the table and would flip the same way. The claim “two measured cells” is exact; the claim “two possible negated reads” is not, and D7’s trigger set (Open question 4) must cover the unmeasured spellings too.

  • ox query --extent over a shape-defined concept stops refusing and starts answering. A widening, not a break, but a visible behaviour change for any tooling that treats the refusal as a signal.

  • $iof moves on more events. It currently moves only on membership assertions; the archived incremental-maintenance model records exactly that (archive/rfd/0079/architecture/07-commit-pipeline-reuse.md:100, .../15-materialization-impact-proof.md:164). A derived $iof moves on any commit that moves a shape concept’s extent, so the impact model must be extended or its proof narrowed. This is work, and it is the largest single cost of this design.

  • A dependency component can get larger. A free type position can merge otherwise-independent components. Sound, but a cost — Open question 3.

  • The catalog tier is untouched, so its stratification-safety justification and the Lean catalog-closure invariants stand.

  • The mechanization is untouched (D6).

  • The seed gets smaller. The individual-tier scan and the iof_by_entity accumulation are deleted under either mechanism.

  • Derivation-semantic epoch. The first behaviour-changing slice bumps it. DERIVATION_SEMANTIC_EPOCH (compiler/crates/oxc-runtime/src/lib.rs) is 10 on the main line today, and pull request #1784 advances it to 11, so the first slice here takes 12 or later. The number is read off whatever the main line carries when the slice lands, never assumed.

Slice plan

Each slice is independently reviewable and independently revertible. Only the slices marked as such change what a program derives.

  1. Executable oracle — pins today’s wrong answers. A fixture per spelling (iof, not iof, meta ==, extent, constant and variable type position, ad-hoc and declared, shape-defined and asserted control) asserting the answers the toolchain gives now, wrong ones included, each annotated with the answer it will give after. No behaviour change; no epoch bump. This lands first so every later slice’s effect is a diff against a recorded baseline rather than a claim.

  2. Canonical concept semantics. One statement, in the specifying chapter and in the mechanization’s prose, of what concept membership means and which relation denotes it — the D1 statement, with the D5 third-truth-value invariant. Documentation and the corrected meta-calculus.md:143. No behaviour change; no epoch bump.

  3. Typed intrinsic and intermediate-representation boundary. Give the reflection reads a typed representation and keep the metatype-equality atom whole through relationalization instead of splitting it into a join plus a filter, so lowering can later decide on it as one thing. This is the slice that makes D4 implementable without the pair rewrite the rejected alternative foundered on. Internal representation only; no epoch bump, subject to the artifact hash moving, which the projection cache already discriminates on (derivation_commitment in compiler/crates/oxc-oxbin/src/projection_cache.rs).

  4. Derived $iof. The bridge rules, the deleted individual-tier scan, the stratification wiring. First user-visible change; epoch bump — 12 or later. extent follows for free (D3), and the --extent refusal is retired here. D7’s warning ships one release before this slice.

    Preconditions, both blocking. (a) Issue #1902 — the is unknown truth qualifier returns definitely-true members — is fixed or the qualifier is refused, per D5: this slice widens the region of legitimately undetermined membership, so the read surface for undetermined membership must be honest before the region grows. (b) Open question 2 is settled: the incremental maintainer’s handling of the undefined extent across commits. Neither is a follow-up.

  5. Derived $meta. The antichain rules, individual tier only. Epoch bump. Kept separate from slice 4 because it is the slice with a measured discrepancy against the catalog tier and should be reviewable against the demonstration’s numbers on its own.

  6. Optimization. Bounds on the free type position, incremental-maintenance work for the widened $iof impact set, and whatever the measurements from slices 4 and 5 justify. No semantic change; no epoch bump. Last, deliberately: the design must be correct before it is fast, and the tractability restrictions D5 declines to impose for soundness belong here or nowhere.

Open questions

  1. Does x is T belong to this surface at all? It does not appear to share the : lowering path: is introduces the optional-payload test and the reasoning-outcome suffix (rule_atom and atom_suffix in compiler/crates/oxc-parser/src/rule_atom.rs), and p is Voter is refused as a relation-valued application (OE1386). No claim in this record covers it. Settled by: an audit of the is suffix grammar against the membership surface, deciding whether x is T should be a membership spelling, a refusal with a message pointing at x : T, or left as it is. Required before any statement about “every membership spelling” is complete.
  2. How does the incremental maintainer preserve the third truth value across commits? D5 makes undefined a real state of the reflection relation. The batch evaluator represents it as a companion relation ($undefined::R, undefined_name in compiler/crates/oxc-reasoning/src/executor/wfs.rs), but the incremental circuits carry signed weights, so a cell moving between undefined and false must not become indistinguishable from one that never moved — and the companion relation must be maintained, not recomputed. Related: whether the reflection atoms should accept the is unknown suffix so the undefined case is readable at all (atom_suffix in compiler/crates/oxc-parser/src/rule_atom.rs); today they do not. Settled by: a statement of how the undefined extent is maintained incrementally, plus a differential test that moves a cell into and out of undefined across commits and compares against full re-derivation. This gates slice 4.
  3. What bounds a free type position? iof(x, t) with t unbound is sound (D5) but can merge otherwise-independent dependency components into one well-founded unit, turning a set of small evaluations into one large one. Settled by: a measurement of component size against a corpus with free type positions, and a decision on whether to bound it — by requiring a $specializes or equality constraint on t, by a cost-based warning, or not at all. This is slice 6 and must not become a soundness argument.
  4. What is the warning’s exact trigger set? D7 says a warning ships one release ahead. The obvious trigger — a reflection head with a shape-defined concept in the type position — over-fires on positive reads that need no edit and under-fires on a variable type position that could range over a shape concept. Settled by: a trigger specification plus a measured false-positive rate against the example corpus, before the warning ships.
  5. Do the bridge rules interact with defeasible or standpoint-scoped membership? The bridge is stated over C(x) unqualified. Whether it should bridge per standpoint, and how it composes with a defeated membership conclusion, is unexamined here. Settled by: an audit against the defeat-plane and standpoint surfaces before slice 4.
  6. Should the mechanized type-test atom gain an evaluation semantics? typeTest and metaEq are recorded as correct-to-fail (spec/lean/Argon/Reasoning/EvalProgram.lean:73-74, :1375-1380), and the surface stays honest only because the bare-name lowering never reaches the atom. D6 shows this design does not force the question, but it does not answer it. Settled by: a decision at the substrate layer, which has precedence over the surface for evaluation semantics.
  7. Who fixes the self::-qualified path, and in which direction? Issue #1904 is five of the nineteen measured defect cells and is explicitly out of this record’s scope, but it bounds this record’s headline claim (D1) until it is resolved. The direction is not obvious: the type test accepts the path and silently answers nothing, the reflection intrinsic refuses it (OE0227), and those cannot both be right. Settled by: a ruling on whether self:: resolves or is refused, applied to both spellings at once. Independent of slices 1–6, but the “three exact spellings” claim is not fully true until it lands.
  8. What does #[world(open)] change? Under an open world an empty membership answer may be unknown rather than wrong. The OE0240 and OE0241 entries record the open-world softening as designed and not wired. Settled by: a ruling on whether shape-defined non-membership under an open world reads as Not or as Can, and whether D5’s third truth value and the open-world unknown are the same state or two.

References

  • RFD 0017 — Refinement classification: where (primitive) vs iff (defined) (0017-refinement-classification.md)
  • RFD 0023 — Reflective TypeRef (0023-reflective-type.md)
  • RFD 0027 — The meta-property plane (0027-meta-property-plane.md) — the catalog tiers the $iof / $meta metatype rows implement
  • RFD 0070 — Derive-backed classification (0070-derive-backed-classification.md)
  • RFD 0092 — Membership-plane authority: fail-closed reads of derived membership (the alternative record; pull request #1874)
  • Pull request #1903 — the membership-spelling conformance sweep: five program shapes by fifteen spellings, seventy-five cells, each evaluated by execution. The source of every count in Measured scope.
  • Issue #1902is unknown returns definitely-true members. A blocking precondition of slice 4 (D5).
  • Issue #1904 — the self::-qualified type test silently derives nothing. Five of the nineteen measured defect cells; not closed by this record, and it bounds D1.
  • spec/lean/Argon/MetaCalculus/Reflect.lean — the opaque membership field and extentOf
  • spec/lean/Argon/TypeSystem/Realization.lean — the realization biconditional
  • spec/lean/Argon/Reasoning/EvalProgram.lean — the correct-to-fail atom ledger
  • Issue #1859 → pull request #1860 — field read on a shape-defined-typed parameter
  • Issue #1871 → pull request #1873 — group-axiom checks evaluate membership in the fixpoint

RFD 0094: Rule-body reads of a completed undefined companion

State: committed

Question

How may a rule body select tuples that the well-founded model leaves unknown without observing a relation before its fixpoint converges?

Context

The evaluator stores a head R as two disjoint extents: definitely true tuples in R, and well-founded-undefined tuples in $undefined::R. The read plane already exposes both. The surface R(args) is unknown was parsed but discarded, which silently changed the question to definite membership in R.

Companion names are generated, not rule heads. Without an explicit dependency, the stratifier may schedule a companion consumer before the component that produces it. Incremental maintenance also cannot use a delta path that omits the whole-program companion-producing pass.

Decision

R(arguments) is unknown lowers to a positive read of $undefined::<resolved R>. The dependency graph treats that companion read as a strict completion dependency on R: a consumer in another component is scheduled after R converges. A read in the same recursive component is refused with OE1441; no in-progress undefined extent is observable.

is not unknown, payload-binding is unknown(x), and path-headed x is unknown remain refused. They require, respectively, a finite complement domain, payload semantics, or a predicate identity. Multi-valued both / ambiguous / timeout outcomes remain OE1349.

Programs containing a companion read use full recomputation for mutation maintenance. This preserves incremental/full-recompute equality until the incremental circuit represents companion production directly.

Rationale

The lower-component restriction gives the qualifier its literal set meaning using the existing WFS result. It neither collapses unknown to false nor exposes an intermediate approximation. A dedicated dependency kind distinguishes this completion requirement from aggregation, so recursive use is not mislabeled as OE1317.

Alternatives

Continuing to refuse all uses preserves safety but leaves an already-materialized extent inaccessible to rules. Treating the companion as an ordinary base relation can evaluate too early. General recursive truth-value reflection needs pair-valued recursive semantics and remains outside this decision.

Consequences

The derivation semantic epoch advances because accepted artifacts and their materialized projections change. Parser, elaboration, stratification, evaluator, runtime, and maintainer tests must cover definite, false, unknown, recursive refusal, and mutation parity.

Open questions

Native incremental companion maintenance and recursive truth-value reflection remain separate work.