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 onmain) 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; theState: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) toarchive/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
| # | Title | State |
|---|---|---|
| 0001 | ID architecture | discussion |
| 0002 | #[comptime] attribute | discussion |
| 0003 | Reasoner backend dispatch | discussion |
| 0004 | pub fact declarations | discussion |
| 0005 | Relation subsumption | committed |
| 0006 | Field mutability via mut | discussion |
| 0007 | Missing-value semantics under OWA | discussion |
| 0008 | Standpoint-sheaf equivalence proof roadmap | committed (Path A landed) |
| 0009 | std::mlt library scope | discussion |
| 0010 | Negative facts / strong negation | discussion |
| 0011 | Aggregate semantics under OWA | discussion |
| 0013 | Toolchain distribution | accepted — partially implemented |
| 0014 | Runtime serving surface | discussion |
| 0015 | mutate body surface: EdgeQL-shaped, set-semantic | committed |
| 0016 | Numeric tower: exact by default | committed |
| 0017 | Refinement classification: where (primitive) vs iff (defined) | committed |
| 0018 | Production reasoner: the incremental DBSP engine | accepted — partially implemented |
| 0019 | Mutation write-path correctness: construction, identity, read-your-writes, exact values | accepted — partially implemented |
| 0020 | The runtime data engine: a composable query + reasoning pipeline | accepted — partially implemented |
| 0021 | The reasoner execution engine (as built): joins, optimizer, factorization, BYODS, incrementality discipline | committed |
| 0022 | Package-path addressing (pkg, not crate) and the build evaluability gate | committed |
| 0023 | Reflective TypeRef: type-as-value in the meta-calculus (RP-003 GAP-1) | committed |
| 0024 | Allen interval algebra as a library (std::allen), not substrate operators | committed |
| 0025 | check discharge: vocabulary-staged compile-time and runtime constraint checking | accepted |
| 0026 | Trait rule members: clause-union dispatch, conformance, and the implements intrinsic | accepted — implementation planned |
| 0027 | The meta-property plane: axis bindings, catalog tiers, value-position resolution, and substrate-neutral modifiers | accepted — implementation planned |
| 0028 | Defeasibility redesign: honest heads, the defeat-directive plane, and strategy as a compilation scheme | accepted — implementation planned |
| 0029 | Derived values and aggregate terms: body-level binding, aggregate sources, rounding | committed |
| 0030 | Package dependencies ([dependencies], path deps v1) | committed |
| 0031 | The relation-constraint plane + meta-property completion | committed |
| 0032 | oxup manages editor-extension installation | committed |
| 0033 | The ad-hoc query and mutation surface | accepted — implemented |
| 0034 | Source text encoding and the Unicode lexical policy (UAX #31 identifiers, NFC, module reachability) | committed |
| 0035 | The composable operator-tree execution pipeline: shared LogicalPlan lowering, tree optimizer, physical mapper, generalized executor, table operators | discussion |
| 0036 | Heterogeneous and specialized data stores: foreign federation, external-valued attributes, persistence swap; the connector SPI, placement, world-assumption tiering | discussion |
| 0037 | The macro atom: a phase-separated, hygienic, declarative-first expander over surface syntax | discussion |
| 0038 | The prelude, ambient scope, and symbol-precise stdlib loading | discussion |
| 0039 | Composable mutations: nested invocation and derived reads | accepted — implemented |
| 0040 | The procedural macro layer: a total, structurally-recursive meta-language over reflected syntax | discussion |
| 0042 | The re-checkable emission boundary: a self-validating .oxbin and sound direct artifact emission | discussion |
| 0043 | Theory packages and the neutrality boundary: where ontologies and higher-order theories live | discussion |
| 0044 | Package registry, workspaces, and distribution: a static content-addressed registry over the CDN | discussion |
| 0045 | The world-assumption write-side: refuse-on-K3-not and the #[world] per-concept opt-in | discussion |
| 0046 | Derivation serving surfaces: query, delta, explain, trace (kill the per-rule derive loop) | discussion |
| 0047 | The temporal value library (TC39-Temporal-modeled std) and the value/ontology boundary | discussion |
| 0048 | The test atom: in-language unit tests, and why a test is substrate | accepted — partially implemented |
| 0049 | Error-tolerant diagnostics: recovery, source-faithful expansion, What/Where/Why/Fix | discussion |
| 0050 | Documentation architecture: three books, correctness by construction, and a verified authoring pipeline | discussion |
| 0051 | oxfmt: a canonical, idempotent source formatter over the shared CST | discussion |
| 0052 | Deployment topologies, the connection abstraction, and host-language parity: embedded vs. standalone, Rust/TS parity, access-vs-durability | discussion |
| 0053 | The standalone concurrent engine: transactor, MVCC reads, and IVM-in-lockstep | discussion |
| 0054 | Set-valued, characterization-relative composition over standpoints | discussion |
| 0055 | The conformance corpus: a standing golden differential-correctness gate | accepted |
| 0056 | Enactment-time-bitemporal rules: the law as it stood | accepted |
| 0057 | Argon as a served platform: the operational host (loopback engine + auth gateway, OTel, lifecycle, config, packaging) | discussion |
| 0058 | The Tableau Model Engine (modal escape hatch) | proposed |
| 0059 | The drift gate as a typed interface manifest | accepted |
| 0060 | The Lean substrate mechanization architecture | accepted |
| 0061 | A 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 |
| 0062 | CI 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 set | accepted |
| 0063 | The bitemporal read-point: the snapshot at now for derived reads (valid-time + transaction-time; clock frozen at the boundary; cache validity-epoch guard) | accepted |
| 0064 | Compiler 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 merges | accepted — implemented |
| 0065 | The 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 |
| 0066 | Agent 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 boundary | discussion |
| 0067 | The 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 obligation | discussion |
| 0068 | Native 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 requires | discussion |
| 0069 | The 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-wins | discussion |
| 0070 | Derive-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 policy | committed |
| 0071 | Agent-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 registration | committed |
| 0072 | Persisted 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 wired | discussion |
| 0076 | Mutability 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 bump | discussion |
| 0079 | Main 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-lands | archived |
| 0081 | Check-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 history | discussion |
| 0082 | Cross-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 work | discussion |
| 0083 | Defeat-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 prerequisite | committed |
| 0084 | Mapped 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 slice | discussion |
| 0085 | Incremental 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 gates | accepted — partially implemented |
| 0086 | Admission-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 theorem | committed |
| 0087 | 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 |
| 0090 | In-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 unmerged | committed |
| 0093 | iof 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 surface | discussion |
| 0094 | Rule-body reads of a completed undefined companion | committed |
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:
| Stage | Identifier shapes |
|---|---|
| Source | qualified_path: String (“demo::Person”) |
| AST | UUIDs minted from Uuid::new_v5(WORKSPACE_NS, facet ++ qualified_path) |
Wire (.oxbin) | UUIDs in 70+ body fields and 7 mandatory event-header fields |
| Storage indexes | UUIDs in BTreeMap keys |
| Reasoner Z-sets | UUIDs encoded as Value::Id(Uuid) in tuple bytes |
| Hot path | UUIDs 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::Idis 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-wideLazyLock<ThreadedRodeo>interner. Per-engineConceptIndexwithTOP=0,BOTTOM=1sentinels —owl:Thing/owl:Nothingbaked in. Global static state. Tightly coupled to OWL semantics. - Nous (
/Users/ivanleon/Code/wt/orca-mvp/main/crates/nous/):define_id!macro generating newtypedu64ids derived from FNV-1a 48-bit hashes of qualified IRIs. Sparse → denseIdBridgerebuilt per schema;DERIVED_ID_BIT = 1<<63overload onIndividualId. 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
| Type | Bytes | Identity source | Scope | Role |
|---|---|---|---|---|
Iri(Arc<str>) | — | Modeler-authored | I/O boundary | Qualified path: "demo::Person". Surface contract; never in hot paths. |
NameRef(NonZeroU32) | 4 | Symbol-table position | Per-workspace | Wire-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) | 4 | Provisioning table | Per-deployment | Tenant partition key. |
ForkId(NonZeroU32) | 4 | Fork table | Per-tenant | Fork partition key (per-tenant scope). |
IndividualId(NonZeroU64) | 8 | System-allocated counter | Per-(tenant, fork) | Dynamic individual identity. Replaces caller-provided UUIDs; external identifiers are data (a hasExternalId property), not identity. |
EventId(NonZeroU64) | 8 | Build-deterministic counter (compile-time); HLC-derived Snowflake (runtime) | Per-(tenant, fork) | Per-event identity. Layout: `[tx_seconds: 32 |
AxiomKey([u8; 16]) | 16 | BLAKE3-128 of canonical body | Per-(tenant, fork) | Logical proposition identity. Same proposition asserted twice has the same AxiomKey. |
InternalId(NonZeroU64) | 8 | Built at Module::load; thrown away at unload | Per-Module-load | Runtime-only hot-path id. Layout: `[kind: 8 |
ContentId([u8; 32]) | 32 | BLAKE3-256 of body | Content-derived | Cryptographic content hash. Replaces SHA-256. |
CompositionSignature([u8; 32]) | 32 | BLAKE3-256 of composition input | Content-derived | Workspace composition signature. Replaces SHA-256. |
Amendment (2026-06-11, issue #270 / PR #285). The
NonZeroU32 → INT4mapping above carries an invariant the original RFD left implicit in the Postgres encoder. Recorded here so call-site comments can cite it: aNameRef’s Postgres wire mapping isINT4with the high bit reserved — the valid band is[1, 2^31-1]. This is enforced by thesqlx::Encodeimpl (oxc-protocol/src/ids.rs), which signed-converts throughi32::try_fromand refuses any value with the high bit set rather than letting it wrap negative on the wire. The0slot stays theNonZero*niche sentinel; the top bit is held in reserve. Two consequences follow. First, any hash-derivedNameRef(theproperty_id_for_field/reflective_sort_name_ref/individual_id_from_namestand-ins, which fold a BLAKE3 prefix into an id pending the symbol-table lift) must mask into the band —& 0x7FFF_FFFF, zero-folded to1— 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 distinctType::fieldpairs 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 derivesNameRefs 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 availableInternalIdfor that lattice’skind+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 TenantId ↔ ForkId 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:
- Allocate a fresh
IndividualId(NonZeroU64)internally. - Emit
iof_assertion(IndividualId, Person). - 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
kindbyte.
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:
-
We control allocation. UUIDs solve the “globally unique without coordination” problem. Argon’s identifiers are either declared (
NameReffrom canonical symbol position), system-allocated (EventId,IndividualId), or content-derived (AxiomKey,ContentId). No coordination problem exists. -
Type safety. All 21 identifier roles collapsing into one
Uuidtype is a regression. Newtypes per role give compile-time discrimination. -
Storage efficiency. Replacing UUIDs with the right-sized type per role saves 60-80% of identifier bytes across the system.
-
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).
-
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_versionmajor (Phase 2 of rollout — see below). - All
_id: uuid::Uuidfields inoxc-protocol::storage::*Bodytypes become typed:NameRef,EventId,AxiomKey, etc. per their role. AxiomEventheader shrinks from 145 bytes of identifiers to 81 bytes (44% reduction)..oxbinfiles produced under the old format are not readable under the new format. We have no externally-deployed.oxbinfiles; this is acceptable.
Code changes
uuidcrate removed from workspace dependencies.blake3crate added (replacessha2).- New
oxc-idscrate (oroxc-protocol::idsmodule) carries the 10 types +define_id!macro. oxc-instantiate::identitybecomes theIri ↔ NameRef↔ symbol-table builder.oxc-runtime::Module::loadbuilds theNameRef ↔ InternalIddictionary.oxc-reasoning::compile::ValuebecomesValue::Internal(InternalId)plus inline variants.oxc-storage-memindexes 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::loadbuilds 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’
.oxbinartifacts need to interoperate, what’s the bridge? Open. Likely: an explicitGlobalReftype at the federation boundary only, derived from(workspace_uuid, NameRef)or from fullIri. Out of scope for this RFD. -
Distributed minting of EventId: the
shard: 12field 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
IndividualIdis system-allocated and the caller’s identifier is data (ahasExternalIdproperty), 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_declare declarative axioms (have stableNameRef-based identity). Should theirAxiomKeybeBLAKE3(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:
- Runtime queries see literal facts, not derivations. Faster, simpler query path.
- The
.oxbinships the closed-world extent. Downstream consumers (other compilation units, runtime, federation peers) don’t re-derive. - Compile-time errors for impossible derivations. A
checkrule that fails at build time is a build error, not a runtime exception. - 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 mode —
Engineinstantiated insideoxc-runtime::Store; rules evaluate against live event state; result is queried viaStore::query_derive. - Compile-time mode — same
Engine, instantiated insideoxc-instantiateduring.oxbinlowering; rules evaluate against the source’s declarative facts (iof_assertionevents frominsertoperations 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 mutateinvocations 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 |
|---|---|
derive | Materialize 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. |
query | Embed 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. |
check | Gate 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.) |
fn | Const-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. |
mutate | Forbidden. 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 amutatedeclaration.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:
- The derivation: every tuple in
p’s extent emitted as aniof_assertionorrelation_tupleaxiom event. - The provenance: each emitted event carries
derivationset to the rule’sAxiomKey(per RFD 0001) so retraction propagates correctly. - A
comptime_lifted: boolfield inRuleDeclBodyindicating “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:
deriveproduces facts → comptime means baking those facts into the artifact.queryproduces a result value → comptime means embedding that value.checkproduces a verdict → comptime means asserting at build time.fnis a function call → comptime means const-evaluation in the Rust/C++ tradition.mutatemutates 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)]— forquerymode: embed the result inline in source as a literal, not just in the .oxbin (useful forlet X: extent = query fooinside afn).#[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-instantiatechange: build-time engine invocation for comptime-eligible rules, comptime fixpoint analysis, OE1307 emission. ~200-300 LOC.oxc-oxbinchange: emit derivation events from comptime rules into the events section. ~50 LOC.RuleDeclBodyschema:comptime_liftedfield. ~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
derivationlineage. If a contributing axiom is retracted at runtime (viadeleteoperation), 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 ont. 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
Engineinterface; 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:
-
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. -
A rule’s body spans multiple tiers (e.g., a recursive rule that joins against an
expressivequalified-cardinality predicate). Today the rule’smain_tieris 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. -
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 viaRelationCatalog; backend internals stay private. -
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)? -
Optional backends (Kora-extension) —
std::owlis a stdlib package that brings in the Kora-EL / Kora-DL backend. How does that get registered? Is it an explicitEngine::with_executorcall, or implicit onimport std::owl?
Decision
The dispatch model: per-stratum, registration-ordered, with explicit OE1305 emission
-
Strata are the unit of dispatch. The physical plan (
PhysicalPlan) carries aVec<Stratum>. Each stratum has a tier (the max of its rules’ tiers). The dispatcher picks one executor per stratum and runs it. -
Registration order = preference order. Executors are pushed onto
Engine::executorsin registration order. The dispatcher picks the first executor whosesupported_tiers()contains the stratum’s tier. This makes the policy explicit and modeler-controllable via build configuration. -
OE1305 emits at build time, not query time. When
oxc-instantiateclassifies 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. -
Backend registration is workspace configuration. Backends are declared in
ox.toml(or the equivalent), not in source code. Default workspaces registerSemiNaiveExecutoronly. Adding SLG, SMT, etc. is an explicit opt-in via:[reasoning] executors = ["semi-naive", "slg-tabled"]Kora-extension is enabled by
import std::owlin source, which addskora-el(andkora-dl) to the executor list automatically. -
Cross-backend exchange is the shared
RelationCatalog. All backends produce and consumeRelationCatalog(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
| Backend | Tiers covered | Algorithm | Status | Estimated effort |
|---|---|---|---|---|
SemiNaiveExecutor | structural, closure, recursive | Naive/semi-naive bottom-up Datalog over Z-sets; stratified NAF | Phase 1 shipped | done |
DBSPExecutor | recursive (preferred over SemiNaive when IVM matters) | True IVM via differential dataflow | Post-Phase-4 of RFD 0001 | ~1 month |
SLGExecutor | expressive | Top-down tabled WFS (chalk-engine-shaped, NOT XSB port) | Post-MVP | ~6-10 person-months |
SMTExecutor | fol | External SMT solver (Z3 or cvc5) via unsafe logic { } blocks only | Post-MVP | ~2 months |
KripkeExecutor | modal | Tableau over Kripke frames | Post-MVP | ~3 months |
KoraExtensionExecutor | DL profiles when std::owl imported | Kora-EL (vendored) | Post-MVP “compiler extension” | ~2 weeks integration |
(Reserved) MetaorderExecutor | metaorder | Bounded order-arithmetic procedure; semi-decidable beyond bounds | Speculative | unknown |
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
recursiveeven 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_costdefault method (deferred-use API). - Add workspace configuration support (
ox.tomlparsing for[reasoning]). - Wire OE1305 emission from
oxc-instantiate::tier_classifywhen 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:
- A new module in
oxc-reasoning::executor::{name}.rsimplementingTierExecutor. - Registration plumbing in workspace config.
- Backend-specific tests showing the keystone-equivalent for its tier (e.g., SLG’s keystone is a recursive-aggregate rule like
countover a stratified-NAF derived predicate). - 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
RelationCatalogexchange 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
recursivebut notrecursivewith 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 asexpressive). 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
modalper §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:
-
#[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. -
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. -
Bootstrap data is awkward. Real ontologies have built-in facts: standard units, well-known individuals, vocabulary mappings (e.g., the OWL stdlib’s
rdf:typepredicate; the temporal stdlib’sSecond,Minuteinstances). Today these would need runtime mutation calls at startup. Source-level facts let them ship as part of the.oxbin. -
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 factdeclarations resolves to the sameIndividualId. - Different builds of the same source produce the same
IndividualId(deterministic). - Different identifiers (
alicevsbob) get distinctIndividualIds.
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 shape | Resulting 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) = value | meta_property { axis_id = NameRef(axis), target_id = NameRef(target), value } (sugar for the meta-property family per §13) |
pub fact A <: B | subsumption_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:
- Directly content-addressed. The fact’s
AxiomKeyisBLAKE3-128(canonical_body), deterministic per RFD 0001. Mutations get freshEventIds at invocation; facts get content-stable identity. - No runtime invocation needed. The
.oxbincarries the events directly; the runtime sees them inStore::seed_from(&Module). - 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:
- Collects facts from
pub factdeclarations → catalog seed. - Runs the comptime rule against the seed.
- Emits the derived tuples (
adult(alice)in this case) as freshiof_assertion/relation_tupleevents with derivation provenance. - 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 FactReferencesUnknownConcept—pub fact Person(alice)wherePersonis not a declared concept.OE0211 FactArgArityMismatch—pub fact hasAge(alice)for a binary relation.OE0212 FactArgTypeMismatch—pub fact hasAge(alice, "twenty-five")where the relation expectsNat.OE0213 FactInsideFn—pub factis 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 topub 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 factdeclaration 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 factdeclarations sorted in the events section? Per the canonical-event-key sort, events sort by(tenant, fork, standpoint, kind, vt_start, tx_from). Facts gettx_from = 0(declared at the workspace’s origin time)? Or do they get a deterministictx_fromderived from source position? The latter preserves source order; defer to implementation. -
Can
pub factreference future-declared symbols?pub fact Person(alice);followed bypub kind Person;— does this work? Symmetric to howderivecan 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 factinsidemodblocks —pub 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 Xis only visible in X (and its supertypes per FDE info-join). Defer to the standpoint substrate work track. -
Negative facts / classical negation —
pub 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:
-
§3.4 name resolution does not walk
<:chains. Resolution order is local → imports → auto-prelude → primordials. Verified inoxc-resolver/src/resolve.rs:207–255—resolve_pathwalks module prefixes only. -
§5.4
impl Typenamespaces members asType::name. Relations declared insideimpl Personare reachable only asPerson::ParentOf, not at module level. Verified inoxc-resolver/src/symbols.rs:57—Item::ImplBlock(_) => return None. Impl-block contents don’t register top-level symbols. -
§5.2 explicitly forbids implicit override.
OE0206 InstantiationFieldShadowsrejects iof-axis shadowing;OE0208 AmbiguousFieldFromMultipleParentsrejects 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 mechanism | What it expresses | Argon’s resolution |
|---|---|---|
{subsets} | R1’s tuples ⊆ R’s tuples; both relations live | <: on rel-decl (this RFD) |
{redefines} | Subclass shadows parent’s same-named property | Not a problem — impl Type namespacing yields distinct paths a priori |
| Implicit specialization | Covariant refinement of inherited property | Substrate-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
-
Arity equality.
R1andRmust have the same parameter count. Mismatch →OE0150 RelationSubsumptionArityMismatch. -
Endpoint covariance. For each position i,
R1.param[i].type <: R.param[i].type. Mismatch →OE0151 RelationSubsumptionEndpointVariance. -
Cardinality refinement. For each slot i,
R1.cardinality[i]must be no looser thanR.cardinality[i]. Formal rule:[c..d]refines[a..b]iffc ≥ aand (d ≤ borb = *). Mismatch →OE0152 RelationSubsumptionCardinalityViolation. -
Metarel compatibility (MVP rule).
meta(R1) == meta(R). Same metarel on both sides. Mismatch →OE0153 RelationSubsumptionMetarelMismatch. Cross-metarel subsumption (e.g.,material <: formalif a vocabulary declares a metarel lattice) is deferred — see Open Questions. -
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 Typenamespacing 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
| Piece | Where | Approximate size |
|---|---|---|
<: clause in rel-decl prose | spec/reference/src/05-constructs.md §5.3 | ~5 line edit |
SupertypeClause on RelDecl in ungrammar | compiler/crates/oxc-syntax/argon.ungrammar | 1 line |
Parser: invoke supertype_clause in rel branch | oxc-parser/src/grammar.rs | ~5 LoC |
| Elaborator covariance + cardinality + metarel + acyclicity checks | oxc-instantiate | ~120 LoC + 5 diagnostic codes |
| Subsumption-closure for relations | oxc-reasoning | ~30 LoC — extend existing concept closure |
| Diagnostic codes OE0150–OE0154 | oxc-syntax/grammar.toml + appendix-c-diagnostic-codes.md | 5 entries |
Lean: extend Argon.Substrate.Construct.Relation with subsumption witness | spec/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
redefinesmechanism — 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::fieldqualification. - 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–578—SubsumptionAxiomBodyoxc-resolver/src/{resolve.rs, symbols.rs}— name-resolution implementationoxc-parser/src/grammar.rs:599–672— concept/rel decl parser andsupertype_clauseoxc-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; howupdate-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 infield-assign.- Worked example (§7.5:334):
update c: Company set { name = new }—nameis 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 iofrejected withOE0211 IofInsertOnRefinedType. - Rigid metatype classification (§7.5:356) —
kind/subkind/categoryindividuals can’t be re-classified;insert iof/delete iofrejected withOE0210 IofInsertOnRigidType. from-derived fields (§5.1:18) — value comes from a relation projection; not directly assignable.- Architectural —
.oxbin, Module, Engine (§19);axiom_eventsonly 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-
mutfield is immutable post-construction. - A
mutfield is mutable post-construction. - A
from-derived field is always derived (not directly assignable);muton afrom-derived field is rejected withOE0822 MutOnDerivedField. - A field with
#[intrinsic]is required at construction; orthogonal tomut.
Lowering
Field mutations lower to append-only event pairs on the underlying axiom. A mutate body with update p set { current_address = "new" } emits:
- A retract event (
Polarity::Retract) on the priorproperty_assertionrow whose body is{ entity_id = p, property_id = NameRef(current_address), value = "old" }. The retract event’sasserts_axiompoints to the prior assert’sEventId. - A new assert event (
Polarity::Assert) on the newproperty_assertionrow 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
UpdateImmutableField—update e set { f = expr }wherefis notmut. Caller is theupdate-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
MutOnDerivedField—mut f: T from rel.range. Thefrom-clause already determines the value;mutis 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:
| Axis | Governs | mut-relation |
|---|---|---|
Metatype rigidity (kind vs role etc.) | Whether x can stop being iof T | Independent. A rigid concept can have mut fields. |
Refinement (concept-level where) | Whether membership in T is derived | Independent. 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 projection | mut is rejected on from-fields (OE0822). |
#[intrinsic] (field-level attribute) | Whether the field must be set at construction | Independent. #[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:
- Parser:
updatestatement parsing (§7.5:306 + 307). - Lowerer:
updatelowers to a newOperation::Updatecore_ir variant which expands to retract+assert event pairs at runtime. - Elaborator: rejects
update p set { non_mut_field = ... }with OE0820. - Runtime:
execute_mutation’s operation interpreter handlesOperation::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:
-
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.
-
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. -
Pairs with the rest of Argon’s modeling discipline.
#[intrinsic]says “must be set”;fromsays “is derived”;<:says “is subsumed by”; refinement says “is constrained by.” Addingmutsays “may change post-construction” — slots into the same pattern. Nomut⇒ 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.mutis value-level. Keyword fits. -
Frequency of use. Field declarations are common;
mutwill 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
mutandupdatetogether as a unit. Documentation lands as one chapter, not two halves separated by a release. - End-to-end testable. A
pub_fact_keystoneanalog withpub mutate move(p: Person, a: Text) { update p set { current_address = a }; }followed by aquery_deriveproves 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 byupdate-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
mutkeyword recognized at the lexer level (added togrammar.toml’s keyword list). field-declparser admits optionalmuttoken between attributes and the ident.update-stmtparser implements §7.5:306 includingwhereclause.
Elaborator
lower_concept_decl/lower_struct_declwalk field declarations; carrymutflag through to wire body (ConceptDeclBody.fields[i].mut_flagor similar — TBD when the field body wire shape solidifies).lower_mutate_bodyrecognizesupdate-stmt, producesOperation::Update.- Elaborator checks (OE0820, OE0822) emitted during
update-stmt lowering.
Runtime
execute_mutation’s operation interpreter gains a Operation::Update arm:
- For each
FieldAssign, evaluate theexpragainst the current state. - Look up the prior property assertion event for
(entity, field_name). - 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
UpdateImmutableField—update e set { f = expr }whereflacksmut. - OE0821
MutOnRefinementDerivedField— reserved; not yet emitted. - OE0822
MutOnDerivedField—mut f: T from rel.rangeis contradictory.
Spec migration
The spec’s worked examples need updating. Specifically:
- §7.5:334
update c: Company set { name = new }— either annotatenameasmutor rename the example to use a field that’s plausiblymut. - §21 walking example: review for any field updates and add
mutannotations.
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
mutadmit+=/-=only on numeric types? §7.5:307field-assignallows=,+=,-=. For text fieldscurrent_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. -
muton collection fields:pub mut friends: List<Person>— doesmutadmit bothupdate p set { friends = [...] }(replace) andinsert p.friends(other)(push)? Probably both, but the semantics need pinning down when collection-field mutation lands. -
muton relation tuple bodies (Form B/C): a relation declared with intrinsic property body (§5.2 Form C) carries its own fields. Do those fields admitmut? 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 asOE0823. -
Interaction with
#[brave]/ stable models (§7.8):mutwrites happen at runtime; brave-derived facts at build time. The two don’t collide today but if a#[brave]rule references amutfield’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 ofmutatebodies 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
mutfield changes, the entity’sIndividualIdstays the same (RFD 0001). TheAxiomKeyfor the property changes (content-addressed). Confirm theMutationReceiptshape adequately captures the lineage for downstream consumers (provenance witnesses, audit logs). -
Should
pub fact(RFD 0004) be able to set amutfield 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), andfield: Truth4Of<T>(epistemic) each mean under CWA and OWA; what happens when the value isn’t asserted; howOption<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.
Noneis on a par withSome(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.
Nonemeans 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 >= 18lift toCanifageisn’t present? And then isnot Adult(p)for a person with no ageIs(true)(so Minor fires erroneously) orCan(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: Can → false. Only Is(true) is designated.”
What the substrate does not specify:
-
How
field: Tunder 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 toCan, not error. -
How
Option<T> comp_op Tevaluates 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 assumedNone >= 18lifts toCan; that assumption isn’t anywhere in the spec. -
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: Natsays 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
discussionstate, and itsOE1014story (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 theinsert 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. Amutatebody that classifiesx into Tand writes fields ofxin the same body is constructingxin-body; completeness is judged at body end (read-your-writes, RFD 0019 RC2) andOE1014refuses atomically if a required field is left unset. A body that only classifies — no in-body field writes tox— 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 (thelegal_norms_can_vote::registerandkeystone add_sat → set_cappatterns), 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’sstate lacks ... CWA → OE1014line) 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
| Surface | Intent | Construction time | Query under OWA (value not asserted) | Query under CWA (value not asserted) |
|---|---|---|---|---|
field: T | Ontologically present. Every instance has this property. | Required; absent → OE0207 | Access lifts to Can | Schema violation; OE1014 |
field: T? | Structurally optional. The property genuinely may not apply. | May be omitted; defaults to None | Returns 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 Can | Returns whatever is asserted | Returns 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
Canat the Truth4 level (the surface returns the field’s declared type via the K3 fail-closed projection:falsefor Bool, omitted for collections); under CWA, emitOE1014 RequiredFieldUnasserted(the schema declared this field present; the KB must record it). At construction time, the existingOE0207 IntrinsicPropertyMissingrule 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
| Code | Severity | Name | Trigger |
|---|---|---|---|
OE0612 | Error | OptionComparisonRequiresHandling | Option<T> comp_op T (or comp_op Option<T>) without explicit handling. Suggests is Some(a) / is_some_and / match. |
OE0613 | Error | Truth4ComparisonRequiresHandling | Truth4Of<T> comp_op T without is Is(a) || is Not || is Can handling. Suggests is outcome-suffix or match. |
OE1014 | Error | RequiredFieldUnasserted | Under 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. |
OW1015 | Warning | OptionalFieldAmbiguousIntent | pub 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)]. |
OE1016 | Error | Truth4OfOnStruct | Truth4Of<T> field type on a struct (no metatype classification). Restricted to ontologically-classified concepts. |
Spec edits
| Section | Edit |
|---|---|
| §5.1 line 42 | Add 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 40 | Footnote: 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 C | Add the five diagnostic codes above. |
Lean mechanization
| Module | Addition |
|---|---|
Argon/Substrate/Construct.lean | Extend FieldDecl carrier with the three intent kinds (Required, StructurallyOptional, EpistemicExplicit). |
Argon/Reasoning/State.lean | Lift accessField to return Truth4 × Value, branching on the field’s declared intent + the governing WA. |
Argon/Reasoning/Fixpoint.lean | The 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 forcesNat?. Option<T> comp_op Trule-body atoms must be rewritten withis Some(a)/is_some_and/match. The Cargo-style ecosystem migration: one-shotcargo ox fixrewrite 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 ofValue. Hot-path impact: one extra tag byte in the tuple-encoding, negligible. relation_tupleaxiom kind unchanged; the lift happens at theaccessFieldlevel, 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 newOE1014covers 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 unknownoutcome-suffix), §12 (Truth4 + Pietz–Rivieccio projection) —spec/reference/src/ Foundation/Truth4.lean,Foundation/Projection.lean,Reasoning/Fixpoint.lean,Reasoning/State.lean— substrate mechanizationoxc-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.leanmechanizes the discrete T3 obstruction equivalence (aft_discharges_T3_obstructionproven). - 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_fpinclusion. - Theorem 3 (equilibrium is minimal global section): from
local_fpbeing 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-theorem | Statement | Status |
|---|---|---|
| T1 | S5 Kripke frame (W,R) over standpoint set with ⊑ induces a canonical Grothendieck topology J on the standpoint category S. | Unproved |
| T2 | Bridge rules form sheaf restriction maps satisfying the gluing axiom (local sections agreeing on overlaps extend uniquely to a global section). | Unproved |
| T3 | Grounded 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:
SheafClassificationinductive:consistentTrue/consistentFalse/undetermined/obstructed.sheafClassify : List Truth4 → SheafClassification— direct semantic overfederate.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 withfederate_eq_both_ifffor 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.
Recommended sequencing
-
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.
-
Arc N+2 (follow-on): Path B. Lifts to frame-theoretic, discharging T1 + T2 + a parameterized T3. The equivalence becomes bidirectional.
-
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. ExtendsFederation.leanwith the canonical theorem statement. -
Arc N+2 (Path B): ~1200 LOC. New file
Argon/Standpoint/FrameEquivalence.lean. Depends on mathlib4Order.Heyting.Basic+Topology.Sheaves.Sheaf. -
Arc N+3 (Path C): ~2000 LOC. New file
Argon/Standpoint/SheafEquivalence.lean(NOT the existingLocality/SheafEquivalence.lean, which proves the discrete Finset form). Depends on mathlib4CategoryTheory.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::mltlibrary that provides Multi-Level Theory (Carvalho-Almeida 2018) as a parallelstd::*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,specializesadmitted via the inheritance lattice +IsCanNotmachinery.categorizes,partitions,subordinatesadmitted viaMLTMetarelKindcarriers.- Order arithmetic semantics live in
Argon/MetaCalculus/Wellformed.leanas parametricProps over an abstractT.
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:
-
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; impliesorder(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. (Spelledsubordinate_toper Higher-order modeling, notsubordinates— 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 ofM. (Carvalho-Almeida §3.5.)
Assertion decorators — desugar to a
checkrule 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 assertshas_order(Self, N); the elaborator emits an implicit check rule that firesOE1905 OrderInconsistencyif 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 fromtier:metaorderto a polynomial tier under the bound. (See Higher-order modeling, Tier ladder.)
-
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/subordinatesinteractions per the §3.4 composition table.
-
Diagnostic codes OE1903–OE1907 wired into
oxc-diagnosticsand emitted fromoxc-instantiate(or a follow-on pass) when CL-1..CL-7 detect violations. -
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;Nonewhen the iof chain is incomplete enough that order isn’t yet determined. (Operationally lowered to a one-shot query over thehas_order/2predicate 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::mltopt 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-parserrecognition for the v0.1 MLT decorator set on type/metatype declarations:- Relational (lower to
metarel_declaxiom events):#[categorizes(T)],#[partitions(T)],#[subordinate_to(M)],#[power_type_of(T)]. - Assertion (lower to implicit
checkrules):#[order(N)]. - Tier (module-level, narrows decidability tier):
#[order_bound(N)].
- Relational (lower to
oxc-instantiatelowers each decorator according to its kind:- Relational → canonical
metarel_declaxiom event viaArgon.MetaCalculus.MLT.declOfKind. - Assertion → synthesized
checkdeclaration; for#[order(N)]on concept X, the check ischeck OrderMatches { has_order(X, N) }keyed toOE1905 OrderInconsistency. - Tier → module-attribute on the elaborated program; the classifier reads it and narrows the tier.
- Relational → canonical
- Diagnostic emission for surface mis-use (decorator on a non-applicable position, bad argument shape, conflicting decorators on the same declaration). Reuses
OE0708 ReservedAttributeNameonly 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-reasoningadmits 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-runtimelifts 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::mltas a workspace package withuse 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 metatypeMdesugars to a canonicalmetarel 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 synthesizedcheckrule 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:
| State | What the check sees | Outcome |
|---|---|---|
Chain complete, has_order(X, N) derivable | Assertion matches derived order | ✓ check passes |
Chain complete, has_order(X, M) derivable for M ≠ N | Assertion conflicts with derived order | ✗ OE1905 OrderInconsistency with message "#[order({N})] asserted on {X}, but iof chain derives order({M})" |
Chain incomplete; has_order(X, _) undefined under the current state | Assertion neither confirmed nor refuted | Vacuous 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:
- 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. - 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.
- 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 assertsN == 0; mostly redundant since individuals always have order 0, but admitted for symmetry.
It is not valid on:
struct/enumdeclarations (no metatype, category error parallel tometa()).- 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_orderpredicate ranges over{0, 1, 2}.- OE1905 emits for
order ≥ 3declarations. - 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:
-
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.
-
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;
mutatenotmutation; noeventatom; standpoints first-class”) is load-bearing for the meta-calculus story; growing it for one foundational ontology breaks the symmetry. -
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.leanalready 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
| Code | Name | Surface | Emit site | Phase |
|---|---|---|---|---|
| OE1903 | CategorizationViolation | #[categorizes(T)] violated at runtime | CL-1 closure | Phase 2 |
| OE1904 | PartitionDisjointnessViolation | #[partitions(T)] overlapping instances | CL-2 closure | Phase 2 |
| OE1905 | OrderInconsistency | order arithmetic violated; also the failure mode of #[order(N)] when assertion disagrees with derived order | CL-3 closure + synthesized check OrderMatches from Phase 1 | Phase 1 (assertion check) + Phase 2 (CL-3 closure) |
| OE1906 | SubordinationViolation | #[subordinate_to(M)] mis-ordered | CL-5 closure | Phase 2 |
| OE1907 | PowertypeIncomplete | #[power_type_of(T)] extent gap | CL-6 closure | Phase 2 |
| OE1908 | OrderAssertionMisplaced | #[order(N)] on a struct/enum/relation declaration | Phase 1 parser/elaborator | Phase 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::E0001–mlt::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::mltandstd::ufo, do theiriof/specializessemantics 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 explicitconcept_orderpredicate; 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 inkind.rs:657but marked for removal. The pattern system + decorator approach in this RFD obsoletes that surface. Removal unblockspub 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
metaordertier. -
Backward compatibility of
iofsemantics: in MLT,iof(x, T)includes order arithmetic. In other foundational ontologies (UFO continuant, BFO universals),iofis order-agnostic. Module-localuse std::mlt::*should NOT retroactively changeiofsemantics 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 (ametarel_decl.is_mlt_primitiveflag 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::E0001–mlt::E0008, with themlt::prefix matching09-higher-order.md:20) are the consistent endpoint. v0.1 keeps OE19xx for continuity with the existing registry; the migration is a coordinated rename acrossoxc-diagnostics, the Lean wellformedness module, andappendix-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)]instd::potency(deep-instantiation level),#[stratum(N)]instd::ml2, etc. Shouldstd::coreexpose 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 afterstd::potencyis sketched. -
Vacuous-pass behavior under incomplete iof chains:
#[order(N)]passes vacuously whenhas_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: thewell_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:
- 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). - 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). - Added
#[order_bound(N)]as the module-level tier decorator (was referenced in §9 / RP-003 but not enumerated in this RFD). - Fixed
@[…]→#[…]throughout. The@[…]form was a typo against Procedural macros and every example in the reference book. - New diagnostic code
OE1908 OrderAssertionMisplacedfor#[order(N)]on inapplicable declarations (struct/enum/relation). - 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. - New open questions: OE19xx →
mlt::E*migration, assertion-decorator pattern as a reusable mechanism forstd::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::Bothoperationally 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’sF, AFT’s(F, F)pair) is never produced from a fact-derived per-source classification. - The bilattice value
Truth4::Both(Belnap’sB, AFT’s inconsistent(T, F)) is consequently never produced byfederate, becauseIs ⊕ Can ⊕ Can ⊕ ... = Isregardless 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 negation —
pub 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:
| Negation | Meaning | Surface | Wire form |
|---|---|---|---|
default / NAF (not) | “not derivable in this scope” — closed-world inference | rule-body atom: not P(x) | rule body, no wire trace |
classical / strong (¬) | “positively asserted to NOT hold” — open-world ground truth | declaration: 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.Truth4has the four-valued carrier with all algebraic laws.Argon.Foundation.Bilattice.infoJoinis proven associative, commutative, idempotent.Argon.Standpoint.Federation.federateis the AFT info-join overList Truth4.federate_eq_classifycharacterizesfederateoutputs structurally.federate_eq_both_iff:federate xs = .both ↔ hasBoth xs ∨ (hasIs xs ∧ hasNot xs)— the precise theorem stating whenBotharises.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: RelationCatalogandnegative: RelationCatalog. IofAssertionevents stamp positive;IofRefutationevents 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-tupleTruth4isBoth, OR some source contributesIsand another contributesNot. - 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 NegativeFactArityMismatch—pub not_fact P(x, y)wherePhas arity 1 (parallels positive-side checks in RFD 0004).OE0641 NegativeFactUndeclaredPredicate—pub not_fact P(x)wherePis 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 afterIofRefutationexists 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:
- 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,PropertyAssertionare all sibling variants), maintaining symmetry. - 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. - Diagnostic and tool surface.
axiom-eventsqueries that want all refutations for a standpoint scan bykind == IofRefutationdirectly. 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
IofRefutationfor every tuple not asserted, making CWA reasoning composable with federation.
Open questions
-
Wire-format symmetry with
Retract. ShouldRetractof anIofAssertionevent subsequently restate the proposition’s status asCan(no positive evidence; no negative evidence) or asNot(positively refuted)? Retract clearly leans towardCan; the semantic gap withIofRefutationis then preserved. The implementation should treat them distinctly. -
What happens when a single standpoint asserts both
pub fact P(a);andpub not_fact P(a);? Per the dispatcher design above, the per-sourceTruth4becomesBothdirectly; federation then surfacesBoth. 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. -
Diagnostic emission scope. Should
OE0640/OE0641ALSO be emitted at federation-query time when sources disagree even under#[federate(strict)]? No — the strict-policy case is already covered byOE1302 FederationDisagreement(other agent’s renumbering).OE0640/OE0641are 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.notconstructorArgon.Standpoint.Federation.federate_eq_both_iff— proven theorem this RFD makes runtime-observableArgon.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 carryCanverdicts; 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:78returnsRuleCompileError::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 inReasoning/Fixpoint.leangive 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:
| Option | Semantics | Problem |
|---|---|---|
| O1 — Silent fail-closed | Can-cells dropped from filter set; aggregate definite | Unsound under OWA. Reproduces SQL-NULL bug. Modeler is never told. |
| O2 — Blanket Can-propagation | Any Can in filter set → whole aggregate is Can | Sound but over-conservative. Loses real conclusions (when sum-over-Is alone already exceeds threshold, verdict is still Can). |
| O3 — Interval bounds | Aggregate evaluates to lower/upper interval; comparison lifts to Truth4 by interval-vs-threshold | Sound 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
| Aggregator | Class | Bound rule |
|---|---|---|
count | Monotone | [|Is|, |Is| + |Can|] |
count distinct | Non-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_collect | Monotone | [set(Is), set(Is ∪ Can)] (containment interval) |
string_join | Non-monotone (order-dependent) | Propagating |
min | Non-monotone | Propagating |
max | Non-monotone | Propagating |
avg | Non-monotone | Propagating |
percentile | Non-monotone | Propagating |
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 boundary | Projection rule |
|---|---|
Bool | Is(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:
| Mode | Level | Code | Rationale |
|---|---|---|---|
query -> T (concrete T) | Info | OW0613 | Ergonomic-first; modeler informed but not interrupted |
query -> Truth4Of<T> | (none) | — | Already sound; nothing to flag |
fn -> T | Info | OW0613 | Same as query |
derive (head over concrete T) | Warning | OW0613 | IDB pollution can propagate; warn more visibly |
derive (head over Truth4Of<T>) | (none) | — | Sound; no need to flag |
check (predicate body) | Error | OE0614 | Compliance — 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
| Mode | Default semantics | Diagnostic | Opt-out | Opt-in to sound |
|---|---|---|---|---|
query -> T | Ergonomic; fail-closed projection at boundary | OW0613 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 projection | OW0613 Warning | #[allow(aggregate_fail_closed)] | Head type Truth4Of<T> |
derive (head Truth4Of<T>) | Sound; IDB carries Can verdict | (none) | (n/a) | (already sound) |
check | Sound; fires alert on any non-Is(true) verdict | OE0614 Error if fail-closed without opt-out | #[check(fail_closed)] | (already sound) |
fn -> T | Ergonomic; fail-closed | OW0613 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
| Piece | Where | Approximate size |
|---|---|---|
| Spec §7.4 — aggregate semantics under OWA section | spec/reference/src/07-rules.md | ~50 lines |
| Spec §12.2 — clarify “set projection” rule is about result-set membership, NOT aggregate input | spec/reference/src/12-truth4.md | ~10 lines |
| Spec §6.6 — note in expression-aggregate grammar | spec/reference/src/06-types.md | ~5 lines |
Diagnostic codes OW0613, OE0614, OW0615 | compiler/crates/oxc-syntax/grammar.toml + Appendix C | ~3 entries |
| Aggregator-kind taxonomy table | oxc-protocol::core_ir::AggregateKind (extend with MonotonicityClass) | ~20 LoC |
Reasoning/Aggregate.lean — interval-bound theorem for monotone aggregators | spec/lean/Argon/Reasoning/ | ~120 lines |
| Runtime aggregate executor with Truth4 evaluation | oxc-reasoning/src/compile/aggregate.rs (new) | ~300 LoC, replacing AggregateNotYet |
| Metadata envelope wire format | oxc-protocol::query_result::AggregationMetadata (new) | ~40 LoC |
CLI ox query aggregate-summary rendering | oxc-driver/src/commands/query.rs | ~30 LoC |
ox query --explain extension | oxc-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 inReasoning/Fixpoint.lean. - No change to
subsumption_axiom,iof_assertion,relation_tuplewire 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 (
Bool→Truth4Of<Bool>). checkrules 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 queryshows aggregate summaries by default when Can-cells were excluded.ox query --explainshows 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:78—AggregateNotYet(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 thedefaultSSO profile (~/.aws/config) — the shared infra account where the ODE/orca-mvp Pulumi stacks, thes3://sharpe-pulumi-statebackend, and the*.sharpe-dev.comwildcard ACM cert already live. It is not theshared-adminprofile (account548277374575, which is empty). Maintainers authenticate withassume default/AWS_PROFILE=default.
Implementation status (2026-06-02)
- Stage 0 deployed to account
285688017134: S3argon-dist-sharpe+ CloudFrontE16VOSAVQFX5Y8(argon.sharpe-dev.com, proxied) +ArgonReleaseRole(OIDC). Edge verified (HTTP 403 = healthy empty bucket). - Stage 4 merged (PR #6):
release.ymlpublish-dist(OIDC → S3 → CloudFront) + nightly cron, wired via repo secretAWS_ROLE_ARGON_RELEASE+ varARGON_CLOUDFRONT_DIST_ID. Graduated to the §5 toolchain (Stages 1–3 having landed): builds all four binaries, assemblesargon-<v>-<plat>.tar.gzviascripts/assemble-toolchain.sh, and publishes the[artifacts.*]channel-<channel>.tomlviascripts/make-channel-manifest.sh— the schemaoxup installconsumes (round-trip tested inoxup/src/fetch.rs; the format is pinned byscripts/make-channel-manifest.sh). Fixed two latent contract bugs (bare-oxtarball;[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-pif76ezmdeniess3:PutBucketPolicy/PutBucketPublicAccessBlockto 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-localpulumi upruns fromnix 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/→oxup→oxfmt→ hostedinstall.sh→ smoke) → tagv0.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-sharpeS3 bucket (us-east-2, private, OAC-only read)- CloudFront distribution (PriceClass_100, TLS 1.2+, two cache policies)
- Reuse of the existing
*.sharpe-dev.comwildcard ACM cert (us-east-1) - Cloudflare CNAME
argon.sharpe-dev.com→ CloudFront (proxied / orange cloud) ArgonReleaseRoleIAM role with GitHub OIDC trust forsharpe-dev/argontag 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) andexecs 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~/.argonlayout, and the local manager commands (which/list/default/uninstall). Its own CI job incheck.yml. - 3b — the network layer:
install/update/self-update+ auto-fetch-on-miss fromargon.sharpe-dev.com, plus therelease.yml§5 tarball (bin/+share/std+manifest.toml) the fetch consumes. 3b also drops the Stage 2 stdlib embed once the tarball shipsshare/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 runnerslinux-x86_64— generic Linux + non-arm CI runners
Intel Mac (macos-x86_64) deferred; Windows deferred.
Build order (eight stages, ~8 focused days)
| Stage | Output |
|---|---|
| 0 | infra/ Pulumi project + apply via assume default (account 285688017134) |
| 1 | Compiler binary split (oxc/ox/ox-lsp/oxfmt bin targets in compiler/) |
| 2 | Stdlib migration (include_str! → share/std/) |
| 3 | oxup crate (argv[0] dispatch + channel resolver + auto-fetch) |
| 4 | Release pipeline (GitHub Actions → S3 → CloudFront invalidate) |
| 5 | oxfmt opinionated formatter (parse → CST walk → normalized emit) |
| 6 | Hosted install.sh at argon.sharpe-dev.com/install.sh |
| 7 | Smoke + 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 runningpulumi up/infra/index.ts— top-level Pulumi wiringontology-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/healthGET /v1/moduleGET /v1/schemaPOST /v1/dispatch/queryPOST /v1/dispatch/mutationPOST /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}/promoteGET /v1/forks/{fork}/diff/{other}POST /v1/forks/{fork}/derivePOST /v1/forks/{fork}/derive/traceGET /v1/snapshotGET /v1/derived/individuals/{id}GET /v1/derived/individuals/{id}/explainGET /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 futureunsafe_logicexecution budgets.
RFD 0015 — mutate body surface: EdgeQL-shaped, set-semantic
- State: committed
- Opened: 2026-06-03
- Decides: the surface and semantics of the imperative
mutatebody that §7.5 promises but the v0.1 implementation never shipped (issue #15) —requireguards,letbindings,insert-as-expression with named-field entity construction,update … set { = / += / -= },insert … into …,foriteration, 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 orcado { } / 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.
- 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. - 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. - Substrate (Lean) —
mutateis deliberately not a substrate rule (lowerRulereturnsnone; 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
mutatewith-> Tmust produce aT. The body’s tail expression (Rust’s rule — no trailing;) is that value; sinceinsertis an expression, ending the body with aninsert T { … }returns the new entity with no keyword. - A value bound with
letis returned by a bare tail (sr) or by explicitreturn sr;. return expr;is first-class, not merely an early-exit affordance — it is what makes returning collections, tuples, and (later)Option/Resultreadable.- 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/Listare ordered (order is the contract).Set/Mapare 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/hashbrownMapis 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.records ≡ update 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:
| Form | In 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
iofedges); 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. insertas expression is not a stylistic preference; it is forced bylet 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 intobecause 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/forare 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 … selectwith noreturnkeyword. Rejected:returnmust stay first-class forOption/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_stmtgrows from four verbs to the full statement set;insertbecomes expression-valued;require/let/for/return/tail-expression parsing; struct-literal-in-tail disambiguation (the Rustif x { S {} }wrinkle). - AST/elaborator: consume the (already-generated)
Stmt/InsertFormnodes; thread an environment forletbindings and insert handles; lower the new forms — the flatVec<Operation>model grows into a statement/expression evaluation with data-flow. - Runtime: entity construction with identity minting (RFD 0001); collection field
+=/-=;forevaluation with canonical ordering; atomic multi-effect commit; tail/returnvalue. - 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/countin guards interacts with RFD 0011 under OWA. - Stdlib:
Set/Map/Listimplementations + 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
- 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. - Are relation/extent tables insert-only-then-read-mostly? (The
#[dense]/elastic precondition — RP-006 Q1.) - Does iteration dominate the reasoning workload? Validate with a real op-trace before finalizing the B-tree-vs-hashbrown default (RP-006 Q2).
- Do DBSP Z-set commutative-monoid deltas discharge the
forevent-order mitigation automatically for IVM-backed mutations? (RP-006 Q3.) - Postgres bridge order — can canonical iteration order be pushed into SQL
ORDER BY? (RP-006 Q4 — the riskiest leak surface.) Resolved by Amendment 1.if/matchin bodies andfor-as-returning-comprehension — surface + value semantics, deferred past the first push.- Error handling —
Option/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,
insertas an expression). A statement-onlyifcontradicts it. - Expressiveness — the split cannot express
let x = if c { let y = f(); g(y) } else { h() }: a bare-Exprbranch admits no statements, and a statement-ifyields 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 chain — match 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
Realis exact (arbitrary-precision rational),Decimal/Moneyare 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
f64is 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.leanmodelsRealasRatdeliberately (“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. avgwants a field.avgover aRealcollection isΣ / n. Over rationals this stays exact (rationals are closed under division); over fixed-precision decimals or floats it rounds. ExactRealis what makesavgmathematically clean.- The reasoner gap.
oxc-reasoning’sValuehasInt(i64)but no exactReal/Decimal; the fold isInt-only. This blocks the overlay’sMetrule (the motivating case) and anyReal/Moneyaggregate.
Decision
Real= exact, arbitrary-precision rational. Numerator/denominator big integers; no precision ceiling; no implicit rounding. This is the canonicalRealfor the data/ontology substrate.DecimalandMoneyare exact.Decimalis arbitrary-precision base-10;Moneyis a currency-taggedDecimal. Both share the exact-rational carrier in the reasoner (aDecimalis 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.- Floats are the explicit opt-in inexact tier.
f32/f64live instd::math::primitive(already the case) and are the only inexact numerics.Realnever silently becomes a float. The implicit-widening chain keepsInt → Real(exact ⊆ exact) but thef32 → f64 → Realstep is removed — a float reachesRealonly via an explicit, lossyto_real-style conversion (a float is not exact, so widening it into the exact tier must be a visible choice). - The reasoner value domain carries exact numerics.
oxc-reasoning’sValuegains an exact-rational variant;Real/Decimal/Moneyliterals materialize to it (replacing the opaque-CBOR/f64 path). MirrorsMutationSemantics.Value.real : Rat. - Aggregate folds are exact.
sum/min/max/avgfold over the exact domain: integer-exact when all operands areInt, else promoted to exact rational;avgstays exact (Σ/nas a rational). This mirrorsMutationSemantics.foldAggregate. - §17.1 is corrected to describe
Realas 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. avgexactness is a concrete, checkable win unavailable under float or fixed decimal.- Implementation. The Rust reasoner uses
num-rational::BigRationalovernum-bigint(MIT/Apache-2.0, on the workspace allow-list) — arbitrary precision, exact division, deterministicOrd/Hashover normalized components. Notrust_decimal(96-bit fixed; would diverge from the LeanRatcarrier and cap precision).
Alternatives considered
Real=f64default (status quo §17.1). Rejected: float drift is a correctness bug for the domain; contradicts the Lean.Real= fixed-precisionDecimal(e.g.rust_decimal, 96-bit). Rejected: caps precision, rounds division (soavgis inexact), and diverges from the Lean’sRatcarrier.- Defer (keep folding
Int-only). Rejected: blocks the motivatingMetrule and everyReal/Moneyaggregate; the deferral was already taken once in §17.1 and this RFD is its resolution.
Follow-ups
- L5 — exact
avg+ aReasoning/Aggregate.leanfold theorem (the interval/fold characterization; closes the Lean item of issue #52). - R4 —
oxc-reasoningexactValuevariant + exactencode_tuple+ exactsum/min/max/avgfold mirroringMutationSemantics.foldAggregate;oxc-runtimeliterals → 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). Confirmsdynremains 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 where→iff 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⊑).Pis a necessary condition. Membership is asserted (by construction orinsert iof).extent(C)is the set of entities assertediof C(oriofa subtype) — identical to an unrefined subtype’s extent.Pis enforced as an invariant at every membership-write point; it never widens the extent.iff { P }— defined (DL≡).Pis a necessary-and-sufficient condition. Membership is derived:extent(C) = { x : iof(x, parentᵢ) ∧ P(x) }overC’s<:-ancestors-including-self — the current runtime behavior, unchanged. Manual assertion is forbidden.
2. insert iof / construction / mutation, per kind
| operation | where (primitive) | iff (defined) |
|---|---|---|
extent(C) query | asserted members (∪ subtypes); no predicate filter | iof(ancestor) ∧ P, derived |
insert iof(x, C) | permitted; P(x) checked → OE0212 if violated | rejected OE0211 IofInsertOnRefinedType |
insert C { fields } | construct + assert iof C; P(fields) checked → OE0212 | construct + set fields; classification derived from P |
update x set { f = v } where some where-C ∋ x constrains f | re-check P → OE0212 if violated | n/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); unknown — information 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 overReal/Decimal/Money/Datewere “unevaluable” to the v0.1 evaluator and therefore silently never enforced. The ratified semantics split the bucket:unknown= missing field only (OWA information-absence; permitswhere, withholdsiffmembership). A predicate that cannot be evaluated — unsupported form, malformed wire data, type-mismatched comparison — is a loud error (RefinementUnevaluableat runtime;OE0660refuses 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 iff — iof(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
<:+wherevs=. (Adult = Person where {…}.) Mirrors DL⊑/≡exactly but a one-character carrier of enormous semantic weight is dangerous, and=already introduces metaxis typed domains. definedmodifier 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-documentationiffgives.- Reuse
dyn(dyn where). Rejected per call (c): collides with trait objects, and “dynamic dispatch” ≠ “derived membership.” - Enforce primitive
wherepurely via a separatecheckrule (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 andcheck-rules coexist; the former is a membership invariant local to the concept, the latter a global constraint.
Consequences
- Migration. All 8 example
wheresites →iff; the corpus tests, CLI pipeline tests, and runtime unit test that assert derived extents update accordingly. Primitive-wherebehavior (no auto-classify, asserted membership,OE0668on insert-iof / construct / update,OE0211oniffinsert-iof, atomic rejection) is covered by five new end-to-end runtime tests. The oxbin round-trip fixture stayswhere-compatible. - Wire format.
ConceptDeclBodygains a classification discriminator; canonical-CBOR field order updated;decode_concept_declback-compat: absent discriminator ⇒iffis not assumed — pre-split artifacts are rebuilt (Argon is pre-1.0, in-process; no stored-artifact compatibility burden). OE0211semantics change. Previously declared-but-unemitted “refined type”; now emitted, and only foriff. Any future code keying on “has refinement predicate ⇒ reject insert iof” must key on “isiff.”- New runtime enforcement path (
OE0668 RefinementInvariantViolated) for primitivewhereat construction /insert iof/ dependentupdate. - Rigidity (
OE0210) remains out of scope here: it gatesinsert iofon anti-rigidity (a UFO meta-property carried asmeta_propertyevents, not in the ontology-neutralConceptDeclBody). Still declared-but-unenforced after this RFD; a separate rigidity-enforcement effort owns it. Noted so the twoinsert iofgates (rigidity, classification) are not conflated.
Open questions
iffrealization soundness (follow-up, RP-007-adjacent #40): mechanizeiof(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 theniffderivation is runtime-correct but not substrate-proven (parity with the pre-RFD status quo).update-time invariant scope. v0.1 re-checks a primitivewherepredicate only against the directly mutated entity/fields. Transitive invalidation (a mutation to entityythat changes an aggregate awhere-predicate onxreads) 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
iffconstruction (insert C { … }) be sugar or an error? Decided: sugar (construct + set fields; classification derived). Revisit if it proves confusing that a constructediff-Cwith predicate-violating fields silently is not inextent(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 LeanevalProgramexecuted 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 (vaultars-substrateR-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_derivecallsmaterialize_predicates(module)(a full scan of allIofAssertion/RelationTuple/IndividualPropertyAssertionevents into a freshRelationCatalog) thenevaluate_to_fixpointfrom 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/unifyiterate every tuple of a relation anddecode_tuple(CBOR) it inside the innermost join loop;Relation<Vec<u8>> = BTreeMap<CBOR-full-tuple, weight>. No join-key index, no arrangement (arrangement_bodyis 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::executeis a stub returning an empty catalog; the RFD-0003TierExecutortrait and the DataFusion-shapedlogical/physical/optimizerlayers are orphaned scaffolding. - Generation counter wired to the wrong cache.
oxc-storage-pghasruntime_generations(bumped atomically with appends, invalidating a projection cache), andoxc-servereads 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-runtimedoesn’t depend onsalsa). So there is no derived-state invalidation today. - Correctness items. Modal
box/diamonderase to the inner atom — unsound for anti-rigid types (the Lean provesbox(anti_rigid) → falseinStaticDischarge.lean). The 1000-iteration cap is a hardErr(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 underList.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) → falseis 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(zerosorry/sorryAx; audited):encode/readoffform a bijection, andneg= stream-swap,⊕= pointwise-OR,⊗= pointwise-AND, plusfederateevaluated in the pair representation reads back to the canonicalTruth4fold (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 whichnegis 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.rsrewrites in-SCCnot Ragainst 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-Bothinvariant (aBothleaking into the reasoner corrupts the fixpoint —spec/lean/Argon/Reasoning/AGENTS.md) is held as a loudI ⊆ Jcheck 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:
+1assert /−1retract, paired byaxiom_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
dbspcrate: 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-scratchEngine::evaluateas the standing IVM equality oracle,maintainer/mod.rs), the LeanevalProgramexecuted oracle (conformance_replay/conformance_fuzzdiff the engine againstlake exe eval-batchdirectly — no Rust port), and the RFD-0055 conformance corpus (hand-pinned goldens through the realoxCLI, 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 derivableconflates FALSE with UNDEFINED, and the Lean oracle emitted onlywfTrue— 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::Rcompanions 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
wherewith 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
closuretier; 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).
OE1309stays 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.rsruns the Van Gelder alternating fixpoint (T = lfp(S_P∘S_P)) as a batch atop the semi-naive evaluator, materializing the definitely-true extent intoRand the undefined extent into the$undefined::Rcompanion. A NAF-cyclic SCC therefore evaluates (the oldOE1309acceptance behavior for that shape is gone); the honest boundary refusals areOE1365(a higher-stratumnot 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) andOE1317(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 viastd::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’sMet-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 publicdbspAPI 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
dbsp0.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 (latestdbspneeds 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 threecargo run --release -- {A,C,B} …lines againstdbsp = "=0.277.0".)
- RESULT — PASSED (2026-06-05). Ran on stock
- Phase 1 — the build-once engine core. Forked
dbspIVM + D1 pair-encoding + D7 arrangements (onInternalId) + D10 executor unification + D5 dual-track provenance carriers + D6 runtime-Salsa/generation wiring (unified with serve’s cache) + the D4StorageBackendseam. 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.dbspis the cleaner thing to own single-node. - Wrap Feldera
dbspas 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 inunsafe/HashMapbehind 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;SemiNaiveExecutorimplemented for real and retained as oracle/fallback; the orphanedlogical/physical/optimizerscaffolding either wired into the real path or removed;oxc-runtimegains asalsadependency and theOxcRuntimeDatabase; aStorageBackendtrait seam for the future persisted read model;tc.rs/provenance/MTL-compile borrowed fromkora-reason-rlunder attribution. - Dependencies. The vendored
dbspfork enters viacompiler/deny.tomlallow-list + an explicit constraint review;unsafeis minimized and fenced (our new code stays#![forbid(unsafe_code)]);HashMapis permitted only in the vendored fork where it cannot affect observable order, justified in this RFD.num-rational/num-bigintalready 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_uniqueis 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 operationalRuleIR→extent evaluator (closes RP-008). - All anchored on porting Bogaerts–Cruz-Filipe 2024 (AFT-in-Coq) to Lean 4.
- 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 (
- Conformance. The ~600 tests +
keystone.rsstay green throughout; new differential + per-feature tests per phase. - Workflow. Branch off
origin/main(this RFD is onrfd/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
DBSPExecutorrow (Felderadbspfork, 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 rule —iof(x, C) :- iof(x, parentᵢ), P(x)— i.e. an incrementally-maintained view (a mutation touchingP(x)updatesC-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 theiffrealization 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 forAS OFqueries (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, TEXTderivation). 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 (themutatebody surface —insertdisambiguation), and connects to RFD 0001 (identity), RFD 0006 (IndividualPropertyAssertion), RFD 0007 (required/missing fields under OWA), and RFD 0016 (exactReal). 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. Soinsert Person(name)lowers to a relation-tuple assertion that mints no individual, persists no fields, and lands underNameRef::DEFAULT(a garbage relation id). Inletposition it is totally silent:lower_let_stmt(mutate_lower.rs:271) only binds anINSERT_STRUCT_OP, solet p = insert Person(name)emits zero ops and binds nothing — and a later unboundpthen collapses to ablake3("individual:{name}")content-hash id (oxc-runtime/src/lib.rs:3874), identical across runs. The brace forminsert Type { f = v }is correct. - RC2 — no read-your-writes; navigation fields invisible. A
Term::Projreader in amutatebody reads committed (pre-mutation) state, not this body’s own buffered writes, socoll = coll + [x]clobbers. Andfrom <rel>.endpointnavigation-view fields — which do resolve in the derive/query read path — are invisible insidemutatebodies. - RC4 — exact
Realmishandled. The rule-body aggregate fold is exactBigRational(RFD 0016), but the mutate/term value path is f64-based:as_f64has noValue::Realarm, so mutate-expression arithmetic/aggregates error on exact-Realoperands. - §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 freshIndividualId, assertsiof(id, C), and persists every supplied field (scalar, list, nested-individual, relation-typed, inherited) as ABox events. Value-producing:let p = insert C { … }bindspto the new individual.insert R(args)— relation-tuple assertion, only whenRresolves to a relation. Emits aRelationTupleunderR’s id.insert C(args)whereCresolves to a concept — hard error (a new diagnostic, tentativelyOE0212, finalized ingrammar.tomlat implementation): “positionalinsertof a concept constructs nothing; use the brace forminsert 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 { … }againstC’s field schema (walking<:for inherited required fields) emits a build diagnostic. The diagnostic-code reconciliation (OE0207vs the existingOE1908/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
InsertFormdistinguishestypedLiteral(construct, mints identity) fromrelation(tuple, yields unit). Brace-only construction maps cleanly ontotypedLiteral; positional-relation ontorelation. 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
mutateexpression 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):
- RC1 — construct vs tuple + identity. Brace-only construction; positional concept-
insert→ hard error (OE0212);let x = insert C { … }binds; delete the unbound-Varhash fallback. Reconcile/extendMutationSemanticsfor 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’sCreate*ops migrate to brace form and persist real data. - RC2 — read-your-writes + navigation fields. Buffered writes feed in-body reads (scalar + collection);
from/multi-hop projections compute in mutate bodies; resolve theupdate target: Typeannotation requirement. Proof:update account set { records = account.records + [r] }accumulates across afor; the overlay’sMaterialize*ops work without the direct-account workaround. - RC4 — exact
Realunification.Value::Realthroughas_f64/eval_binary/mutate aggregates. Proof:require { value == sum(record.value …) }withvalue: Realpasses; differential test vsAggregateExact.lean. - §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. - Loudness sweep + reload fidelity. Remaining silent-wrong paths become diagnostics; round-trip/replay tests assert event-log fidelity for every emitted event kind.
- Overlay integration capstone. The lease
Create*/Materialize*/Recognize*mutations build a faithful instance graph;Met/BreachedAtderives 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
update target: Typeannotation. Todayupdate 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.- Diagnostic-code reconciliation for required fields.
OE0207(spec-planned, nonexistent) vsOE1908(IntrinsicPropertyMissing) vsOE1014(RequiredFieldUnasserted) — which is the build-time code, which the runtime channel? Settled in the §6 phase againstgrammar.toml. property_id_for_fieldinterning. Theblake3("field:{Type}::{field}")field-key hash is a correct-but-stand-in for a real internedNameRef(RFD 0001). Promote to interned ids as part of the loudness/fidelity phase, or defer?- 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 executionpipeline 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:
- 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;
- 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);
- 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-reasoningalready declares the DataFusion-shaped stack:LogicalPlan(logical/mod.rs:Scan/Filter/Map/Join/AntiJoin/Distinct/Recurse/Sink), anOptimizerRuletrait + an empty pipeline (optimizer/mod.rs),PhysicalPlan = Vec<Stratum>(physical/mod.rs), and anEnginedispatchingBox<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)LogicalPlanis “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) goesAtomIR → CompiledRule → evaluate_to_fixpoint(&[CompiledRule], …)directly (6 call sites), bypassingLogicalPlan/optimizer/PhysicalPlan/TierExecutor.SemiNaiveExecutor::executeis an empty stub; the real (semi-naive) evaluator lives as free functions inexecutor/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-checkis pure syntax-driven; the only shared artifact is theTierenum. 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 (thearrangement_bodyslot in.oxbinis reserved + inert).
What the roles demand (and what the references teach)
- Graph-DB role — kuzu is the playbook: a strict
bind → logical-plan → optimizer (visitor passes) → physical-mapper (1:N) → vectorized processorpipeline; factorization (flat vs unflat column groups → compact storage for many-to-many path patterns); multiwayINTERSECT(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 role — kora 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
optdis 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
TierExecutorseam — 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 (withRecurseas the operator), makeSemiNaiveExecutor::executereal, de-magic-number convergence, fix the stale headers. Then: Phase 2 — theLogicalPlan/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 inoxc-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.
- As-built realization: RFD 0021. Phase 1 (dispatch through
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’slogical/optimizer/physical/executormodules become the real pipeline (not scaffolding); the runtime routes throughEngine/dispatch instead of the bare free function; a physicalRelationtrait (BYODS) replaces the fixedBTreeMap<CBOR,weight>; new graph-native + mutation logical nodes; a factorized-batch physical layer; a persisted read-model store behind aStorageBackendseam. - 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-checkstarts issuingLogicalPlangoals (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
Intersectis 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 theforalltriage #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 onInternalId(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
RelationCatalogpublic API (get/get_mut/ensure/insert/iter+ theCatalogEntry.relationfield). 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 evaluates — Predicate (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_stateis 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 itO(M log N), notO(N²);#114alone was memory-optimal but time-O(N²)). Binary stays for acyclic/linear-recursive bodies — the Free-Join hybrid. Gated byatoms ≥ 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 canonicalBTreeMap—CatalogEntry.relationis 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
LogicalPlanis real architecture, but wiring it to round-trip through the provenCompiledRuleexecutor 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
LogicalPlaninterpreter now. Rejected (D1): theLogicalPlan → CompiledRuleround-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.relationa 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), notcrate::/pub(crate)), and (2) thatox buildrefuses 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 (crate→pkg) 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:
- How does a path name a module — and how does a package refer to itself? Rust uses
crate::for self-reference andpub(crate)for package-wide visibility. Argon is not Rust; it has packages, not crates. What is the canonical self-anchor and restricted-visibility spelling? - What does
ox builddo with a rule the runtime cannot evaluate? The lowering admits rule shapes the executor’scompile_rulethen refuses (aforall/existsquantifier → 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::XnamesXin modulea::b. Rename-safe: changing the package’s name inox.tomldoesn’t break internal paths.self— the current module;super(repeatable) — an ancestor module.- A dependency package name (
[dependencies]), and the always-availablestdroot. - 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. cratemeans nothing in Argon. The unit of distribution is a package ([package]inox.toml); borrowing Rust’scratevocabulary 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:crateis 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
OE0001with a clear message is sufficient; a dedicated code can come later if needed.
Consequences
Visibility::pubPackage(LeanSyntax/Decl.lean),Visibility::Package(Rustoxc-db), and the book §3.1/§3.4 are aligned onpub(pkg). There is nooxc-protocolVisibilitymirror, so the rename is maintained by hand, not the drift gate.self::as a path-start segment does not yet parse (selfis a lexer keyword forself.field); resolver support is in place. Acceptingself/super/pkgas 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 frompub) once package boundaries are modeled, rather than the v0Package == Publicsimplification? - Should the unknown-
pub(...)rejection get its own diagnostic code (vs the reusedOE0001)? - Should the build gate ever support a partial/
--allow-unevaluablemode 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
TypeRefwhose values are references to declared types, the bounded formTypeRef<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 inspec/lean/Argon/CoreIR/Term.lean(Term.typeRef) andspec/lean/Argon/MetaCalculus/Reflect.lean(sorts, lattice, theiof/specializes/extentrelation 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,Metatypesealed/self-instantiating); RP-003 §10 (resolved) canonicalized the argument sorts asEntity(value-position) andtype(type-position) and made the intrinsics substrate-scope (nouse). 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 impredicativeType:Type. ATypeRefvalue is a handle into the closed declared catalog (à la OWL-2 punning / JavaClass<?>/ HaskellTypeRep), soiof/extentare 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 legacyEntityRef::Concept/ unified-EntityIdidiom. No newValuevariant 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 currentenumworkaround — i.e. exactly a boundedTypeRef<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 covariant —
TypeRef<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::mlt — TypeRef<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 aTypeRefvalue).- The closed/literal sugar
x : TkeepsAtomIR.typeTest;x :: TkeepsAtomIR.metaEq(fast path). The reasoner evaluates the reserved-head reflection atoms against the catalog’siof/specializesgraph.
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_oflive instd::mlt. The BFO/non-UFO smoke test still passes (a vocabulary usesTypeRef/metawith zero UFO/MLT in scope). - Sound by construction — codes-universe, not impredicative universe (D-132 / s2 §9.4).
TypeRefunsealed keepsMetatypethe 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 —
==overValue::Nameworks immediately; boundedTypeRef<C>is exactly theenum-workaround need, done principledly.
Alternatives considered
- Core sort named
Type(demotestd::mlt::star::Type→OrderlessType). Most ergonomic word, but a four-way name collision and reopens the delexicalization wound; andTypewithMetatype <: Typewould amend “Metatype is the only sealed primitive.” Rejected forTypeRef. Entity+ metatype-as-sort only (no dedicated reflective sort; RP-003 §10 canonicalEntity/type). Most minimal, but: typereads as a keyword and conflates the generic metatype with the universal type-reference sort; a dedicated unambiguousTypeRefis 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.typeRefIR;meta/iof/specializes/extentvalue-polymorphic. Book §4 amended (meta -> TypeRef); RP-003 §10 type-position sort namedTypeRef(lowercasetyperemains the generic metatype). SUMMARY/Appendix updated. - Drift gate:
Term.typeRefis@[language_interface]— the Rustoxc-protocolTermmirror gains a matchingTypeRefvariant (arity 1 over the name/path id). - Lean module inherits
Classical.choiceat the metalevel (D-132 OQ3) — already used inFoundation/Truth4, not new debt. TypeRefmust stay crisply distinct in spelling/scope fromTop,std::mlt::star::Type, and any vocabulary’sType_(D-073) — no respelling that blocks a vocabulary declaring its own.
Open questions
- Codomain scope —
TypeRefreferences concept/construct/relation/metatype; the intrinsics stay category-errors onstruct/enumdata values (§4). (Recommended: include relations.) extent(TypeRef<C>)— enumerate all subtypes of C (symmetric withextent(Metatype)). (Recommended: yes.)- Narrowing — should
t: TypeRefnarrow toTypeRef<K>afteriof(t, K)(occurrence typing)? Deferred.
Implementation status (layered, each checkpoint green)
- ✅ Lean foundation —
Term.typeRef+MetaCalculus/Reflect.lean;lake buildgreen (1034 jobs), 0sorry(commita84d9e3). - Protocol + drift — mirror
Term::TypeRefinoxc-protocol; documentValue::Namecarrier; drift green. - Resolver + checker — resolve
TypeRef/Entity; fold the reflective edges into the<:-closure;TypeRef<C>refinement + covariance; 3-valued OWA membership. - Instantiate — lower the four intrinsics to the value-polymorphic atoms (sugar preserved; fast-path closed types).
- Reasoner — evaluate the reserved-head reflection atoms over the catalog graph.
- Runtime + wire — the
coerce_json_arg_for_typeTypeRefarm (path →NameRef→Value::Name). - SDK (
oxc-gen) — class-path wire shape. - 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.arover the scalarDate/Durationvalue 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, thetier:expressivegrammar in book §7.3.1, andOE0713 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).
AllenOpenumerates 12 relations (before/after/meets/metBy/overlaps/overlappedBy/during/contains/starts/startedBy/finishes/finishedBy— note it omits Allen’s 13th,equals) atspec/lean/Argon/Syntax/Operators.lean:79; the surface atomRuleAtom.allenAtom : FieldPath → AllenOp → FieldPathatSyntax/Expr.lean:222; thefield-path allen-op field-pathgrammar attier:expressivein book §7.3.1 (07-rules.md:127-150); andOE0713 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 onDateboundaries — 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/Datearithmetic, 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
std::allenis a library, pure.ar, over #159. It models intervals and defines the 13 Allen relations (and any convenience dispatch) as ordinaryderiverules overDateboundaries. It requires nothing from the engine beyond the scalar value layer that already shipped.- Remove the reserved Allen operators from the substrate.
AllenOp,RuleAtom.allenAtom, the §7.3.1 grammar, thetier:expressiveAllen classification, andOE0713are 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). - No infix sugar. Library Allen is call/UFCS syntax (
a.before(b)orbefore(a, b)), not infixa 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 nameAllenHolds, nor sniffs{startsOn, endsOn}field names. dateInterval/shiftare library helpers; until thefnreturn-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'sendsOn` is the date itself).
- relativeTime, dateInterval(checkDate))
- The library can be complete (all 13 relations incl.
equals) and may offer anallenHolds(a, rel, b)dispatch over anAllenRelationTypevalue — 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) andRuleAtom.allenAtom(Syntax/Expr.lean); drop the Allen arm from the tier classifier (Decidability/) and anyExpr/@[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; documentstd::allenin §15 (stdlib) instead. - Drift / Rust mirror: drop the
AllenOpmirror inoxc-astand 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 ingrammar.tomlper 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
Intervaltype; an interval is a library concept with twoDatefields. - No infix operators, no fuzzy/INDU variants in scope (those are further libraries if wanted).
DateTime/Timeintervals 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-.arlibrary is cleaner and needs nothing special. - Leave the operators reserved-but-unbuilt. Rejected: a dangling surface that will fork against
std::allenthe moment the library exists; removing it now (unbuilt, no users) is the cheapest it will ever be.
Sequencing
- (Done — prerequisite) Scalar
Date/Durationvalue layer + chronological comparison, #159. - Remove the reserved Allen operators (Lean → reference → drift mirror → grammar), per What is removed.
- Ship
std::allenas a pure-.arpackage 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
checkrule 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 vocabulary | Discharge |
|---|---|
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 individuals | Runtime, 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.leanalready 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:— exactlySeverity::Error | Severity::Warning | Severity::Info(nominal).code:— string literal, namespaced: must contain::(e.g."Lease::E001"), and theOE/OWprefixes are reserved for the compiler (extends #150’s hygiene rule to user space).message:— string literal, orformat!("…{}…", 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:andmessage: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 checkandox build(build does not fail on userErrorchecks over declared EDB? — it does: a firingSeverity::Errorcheck at build is a build failure, same as any other error diagnostic;Warningrenders and passes). - Runtime discharge → mutation results carry rejected-guard diagnostics; dispatch responses
(
oxc-serve) gain adiagnosticssection for observe-channel violations (amended 2026-06-11: the shipped wire key isdiagnostics, not the originally drafted$diagnostics— the$sigil is the reserved internal-relation namespace, the wrong register for a JSON response key);ox queryrenders 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 traitand provided byimpl 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 featuredemand. Design discussion:.local/research/traits/DESIGN-2026-06-10.md. Companion: RFD 0025 (check discharge; merges first — see Sequencing) — traitcheckmembers monomorphize into ordinaryRuleMode::Checkrules 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
| Plane | Member forms | Semantics |
|---|---|---|
| Rule plane (joined) | derive, check, query | Clause 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, mutate | Single 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:
- A bare member atom
Adult(p)requiresp’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. - 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. - 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 construction — meta 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 Foocollision 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 postfixp.Adult()sugar (Ivan, 2026-06-10): it buys spelling only and collides with field-access space (p.Adultis 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:
- Completeness — a trait impl provides every member its trait declares. OE0670.
- 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.)
- 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 Customerwith a declaredpub kind Employee <: Person, Customerwould put everyEmployeeunder two clauses. The worked counter-example for the first arm:impl Adulthood for Person+impl Adulthood for USPersonis 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. - Orphan rule —
impl Trait for Typemust live in the package declaringTraitor the one declaringType(§12.4, previously reserved). OE0672. - Supertraits as requires-constraints —
impl Sub for Tdemandsimpl Super for Texist (Rust-exact; not inheritance). OE0674. Surface token: supertraits are spelled:(pub trait Repaintable: Drawable). Honestly stated: (a) the parser today implements<:(and the sharedsupertype_clausealso admits thespecializeskeyword — 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<:aftertrait Identa parse error with a fix-it (no in-repo source uses it), and corrects STATUS.<:stays the concept-lattice operator exclusively. Selfdiscipline —Selfresolves only inside trait/impl bodies (trait: the obligation’s type parameter; impl: the target type); misuse, a trait member signature with noSelfparameter position, or return-positionSelf(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 toTypeRef(RFD 0023): values are handles into the closed declared trait catalog, carrierValue::Name. Deliberately not folded intoTypeRef— traits stay off the concept lattice.TraitRefdoes not join theMetatype <: TypeRef <: Entitychain; it is a sibling sort underEntity. -
implements(t: TypeRef, tr: TraitRef) -> Bool— the fifth reflection intrinsic, materialized as the RT-closed reserved-head relation$implementsfromImplDeclevents (the$iof/$specializespattern, #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 forPersoncoversUSPerson), coherent with #199. -
implementsis exempt from OE0212 (MetaArgUnbound): unlike the type-position arguments ofmeta/iof/specializes/extent, both positions ofimplementsmay be free —$implementsis a finite catalog-closed relation and enumeration is the intended use (freet: all implementing types; freetr: all traits of a type; instance-level composes withmeta(x), #135). §4.4’s intrinsic invariants are amended accordingly (the “type-position argument is aTypeRefvalue” sentence and the OE0212 rule both carve outimplements; its second argument isTraitRef-sorted). Being catalog-closed also makesnot implements(…)stratification-safe. Animplementors(Trait) -> Set<TypeRef>expression-plane intrinsic rides the same materialization. -
implementsis available in both planes: rule atoms (including NAF) and boolean expressions (if/matchscrutinees 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-sortedbecomes
a check is catalog-level iff every variable in head and body is reflective-sorted (
TypeRef,TraitRef, orMetatype)so that trait-conformance checks — Gustavo’s
PersonImplementsAdulthood— classify catalog-level and discharge atox checkthrough 0025’s harness,#[static]by convention. This RFD builds the vocabulary; 0025 owns discharge. Without the amendment, a freetr: TraitRefvariable would misclassify the check as instance-level.
D7 — Wire, drift, and what does NOT change
- Member rules are emitted as ordinary
RuleDecl/ComputeDeclevents — the runtime and reasoner load them with zero changes. No newAxiomKind. ImplDeclBody.items(alreadyVec<CborValue>, “rule_decl-shaped”) carries the member rules’ identities/provenance for tooling and conformance — not evaluation.TraitDeclBody.methodscarries 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.leanalready modelsTraitDecl.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.leanciting “spec §13”) are corrected in the Lean PR. Drift gate:TraitAtom/TraitDecl/ImplBlockshapes unchanged; the member-signature carrier aligns by the usual@[language_interface]discipline.
Lean obligations (before the Rust slices, per workflow)
- 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. - Coherence instantiation, by construction — a nominal impl target is a
TraitBoundinConditional.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, whencecoherent_of_unique_applicableapplies. 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. - Conformance decidability — completeness/orphan/overlap/coverage/
implementsare finite catalog scans; structural tier (§10 ladder). Substrate/Trait.leanmember-signature model mirroringTraitDeclBody.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), scopedSelf, 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 2 —
TraitRef,$implements,implementsin 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
| Code | Name | Site |
|---|---|---|
| OE0670 | ImplMemberMissing | check/build (elaboration) |
| OE0671 | ImplMemberExtraneous | check/build (elaboration) |
| OE0672 | OrphanImplViolation | check/build (elaboration; §12.4’s reserved code, now real) |
| OE0673 | ImplTargetsOverlap | check/build (elaboration) |
| OE0674 | SupertraitUnsatisfied | check/build (elaboration) |
| OE0675 | SelfMisuse | check/build (parse/elaboration) |
| OE0676 | ImplMemberSeverityDiverges | check/build (conformance; 2026-06-11 amendment) |
| OE1326 | TraitMemberNotYet | parse-time capability gate |
| OE1327 | TraitMemberUncovered | check/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
ImplMemberSeverityDivergesatox 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 isErrorremains 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; theConditional.leantheory is ready for it; return-positionSelfalso 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/
iffconcept; 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/$iofrows (settles #229); aValue::Symbolcarrier shared with enum constants (fixing enum type-erasure); and the substrate-neutral modifiersabstractandfixedthat 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 dropped —
lower_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
validation — pub 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-inRigidity/SortalDependenceenums out of the legacy kernel — “the largest remaining UFO leak in nous” — replacing them with a generic per-targetmeta_properties: BTreeMap<AxisId, Vec<MetaValue>>, withMetaValue::Symbolinterned 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:
- Bound rule variable (appears in a positive body atom or is a head parameter).
- Enum constant —
Status::Active, qualified or imported. - Axis value —
rigidity::anti_rigid, where the first segment resolves to a visiblepub metaxisand the second to a value in its declared domain. - Type reference — single-segment and qualified type names (
TypeRefconstant). - 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 :: IdentmetaEq 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 forA::B(already flagged ambiguous at07-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
MetaxisDeclBodypersists the domain:unordered(values),chain(values)(declaration order is the order; the book’sa < bchains), ortyped(TypeExpr, refinement?). The LeanAxisDomainshape (MetaCalculus/Axis.lean:38-48) already models this; the Lean storage mirror does not and gains it.MetatypeDeclBody.axes/MetarelDeclBody.axesare populated with resolved bindings: axis = qualifiedNameRef(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 typeaxes bind only on metatypes,for relonly on metarels); duplicate axis in one declaration (OE1903, already specified inWellformed.lean). - Per-target assertions ride the existing
MetaPropertyevent ((axis, target, value)— the channel MLT’s#[order(N)]already emits), now actually consumed (D3). - Lean storage mirrors for
MetaxisDeclBody/MetatypeDeclBody/AxisBindingare added; the Rust doc-comment claiming “MirrorsArgon.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
Moduleindexesconcept_id → metatype_idat load (ConceptDeclBody.metatype_idis already resolved on the wire since #213; it currently has zero non-test readers). $metaand$iofgain 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 (iofover 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-targetMetaPropertyassertions, 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 admittedVec<MetaValue>(the vault’smodemetatype binding twonaturevalues); 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-axisconditionclauses 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$metajoin (it already lowers correctly; the rows now exist). -
Sort-directed projection.
base.namewherebaseis reflective: aTypeRef-sorted base resolvesnameas a declared field on the type (the documentedmeta(bar).xwalk-up,05-constructs.md:158-173— unchanged); aMetatype-sorted base resolvesnameas a visible metaxis and lowers to a$axisjoin. 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_rigidtbinds via the catalog$metajoin;.rigidityjoins$axis; the right side is a constant (D1).meta(t).rigidity == rwithrfree bindsr. An unbound axis (the metatype binds nothing for it) yields no row — ordinary Datalog absence. -
Chain comparisons. For
chaindomains,</<=/>/>=compare by declared position (crash-course §1 promises exactly this). Comparing symbols of different owners, or of anunordereddomain, 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 : TypeRefranges over all declared types;m : Metatypeover all declared metatypes (the predicate forms of the promisedextent(TypeRef)/extent(Metatype),04-meta-calculus.md:109). Both bind positively, catalog-closed. This legalizes Tiago’s second attempt rather than refusing it.x : Entitystays 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.ordis populated fromchaindomains at build (None for unordered/enum owners); equality ignores it; ordered comparison requiresSomeon 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_valuerendersowner::name; serve JSON renders a typed envelope with the qualified owner path (the #182 pattern);oxc-genemits 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 consumer | Today | Becomes |
|---|---|---|
| OE1327 trait-coverage instantiability | "sortality"=="sortal" token scrape | abstract — 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; unenforced | fixed — 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 defaults | rigidity (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 — andabstractis 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,
fixedis 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 ascheck. (The keyword isfixed;sealedwas 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 therigidity/sortality/identity_provisionaxes”) is deleted. Anti-rigid admission “through the axis assignment” is rewritten: admission comes from the absence offixed;identity_provisionhas zero substrate consumers and becomes pure user vocabulary like everything else. ArgUFO writespub 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 axisrigidity. StaticDischarge.lean’sRigidityinductive (:38-50) is re-grounded as the Kripke-semantics justification offixed(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:
- Canonical model = the ternary axis relation
(target, axis, value)with per-(target, axis)functionality — the(axis,value)-point instantiation ofState C Acannot express chains or typed domains; the relation reading covers all three domains uniformly. This resolves the deferred wiring note inIsCanNot.lean:48-50(the A-instantiation becomes a derived view of the relation). - Chain-order semantics for symbol comparison; refusal semantics for cross-owner/unordered comparison.
- 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. - Decidability: axis atoms, metatype-tier reflection atoms, and reflective-sort type-tests are D1-tier (catalog joins); the classifier admits them.
- Modifier semantics:
fixed⇒ the mutation-boundary refusal theorem (noiofdelta 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 overfixed. - 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]onanti_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
- S0 — D1 resolution pass + the two filed silent bugs (individual-as-wildcard, qualified predicates). Independent of everything else; converts silent-wrong to loud immediately.
- S1 — D2 wire + validation + Lean storage mirrors.
- S2 — D3 catalog tiers (
$meta/$iofmetatype tier,$axis), closing #229. - S3 — D4 query surface + D5
Value::Symbol(including the enum migration), full rendering chain. - S4 — D6 modifiers + consumer migration (OE1327, §7.5 gate, static discharge, VT defaults)
- the book rewrite (Note deletion, §7.5, §12.4, crash-course).
- 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):
- The defeater writes the head it denies — the polarity flip lives in an attribute, invisible at the head.
- 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 priorityblocks are unimplemented prose. - No surface notion of a default rule —
#[defeasible]is logician-speak; modelers think in defaults and exceptions. - 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:
| Directive | On | Meaning |
|---|---|---|
#[default] | a derive rule | this clause is overridable; it survives unless an applicable attacker blocks it |
#[defeats(target(args))] | a derive rule | when this rule’s body fires, it blocks the targeted conclusion for the bound tuples (D3) |
#[label(name)] | a derive rule | gives 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 labeledadult.head.labelis 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 thepubrules reachable through itsuseimports), 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:
- Rule identity surviving lowering into the catalog.
rule_idandqualified_pathalready survive (post-#217);#[label]extends identity to the clause grain. Identity is catalog-addressable — the same addressing that makes targets resolution-checked. - 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). - 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.
- 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_explainedcomputes each surviving tuple’s+Δ/+∂tag from the compiled strata (the user-visibleox derive --explainsurface, §7.8 tag queries). No proof tag is persisted toAxiomEvent.proof_tag(the comptime lifter writesNone), 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]AxiomEventwire 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], andpub prioritybecome 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 thepub priorityblocks all die; the chapter specifies honest heads + the directive vocabulary as strategy #1 over the four hooks. examples/legal_norms_can_voteis 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:
- 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 (theDefeasibleExtraction.leanshape, lifted from rule grain to tuple grain with argument bindings) agrees with the compiled program’s model. - 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).
- Defeat-graph acyclicity is decidable — decidable checker plus non-vacuity witnesses on a
concrete program (the #221
conformant_iffpattern). - Compatibility with defeat-aware module extraction (§3.5).
defeat_complete_preservesalready 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. - 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. - Wire mirrors under
@[language_interface]for the defeasibility metadata (labels, edges, strategy id); theRuleStrengthmirror 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]orpub 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/defeatsas 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 prioritysuperiority 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:
- 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. - 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). - S3 — provenance. Proof tags through the channel;
proof_tagun-squatted (fork promotion gets its own field); the §7.8 tag-query surface lights up. - S4 — migration. Loud deprecation errors for the old surface;
legal_norms_can_votemigrated; §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 thepub priorityprose 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 @ Typedefeat 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/Dataloglayer.
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 ofexpr, whereexprranges 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 stayscount/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 forDecimal/Money/Realparams. 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 = exprbinds; 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.raterefuses with OE1303 —==is a filter over already-bound operands, never an assignment, soowedis 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 afield-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 … havingrefuses 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). Ifxis 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 (xjoined against the computed value) rather than a binding, collapsing the two readings of=(bind vs. compare) with no diagnostic. It refuses, namingx: use==to compare, or pick a fresh name. This is the path a rebindx = x + 1takes whenxis otherwise bound — the LHS is not fresh. - Range restriction (OE1303
RuleNotRangeRestricted).x = exprbindsxpositively iff every variable inexpris itself positively bound. An unbound RHS variable, or a pure self-reference / cycle where the result variable is bound by nothing else (x = x + 1asx’s only binder;x = y, y = x), leavesxunbound 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]* )
Sourcemay 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_distinctemit a value for the empty group —0. 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/avgdrop the row — they have no value over an empty set (no least/greatest element; the mean is0/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):
| Builtin | Meaning |
|---|---|
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’scoerce_money_argconvention (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, so0.22becomes exactly22/100, not the f64 artifact0.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):
| Code | Name | Meaning |
|---|---|---|
| OE1331 | ValueAggregateBraceForm | A value aggregate (sum/min/max/avg) was written in brace form sum(expr){…}; directs to the comprehension form sum(expr for x in S, …). |
| OE1332 | AggregateOverUndefined | An 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. |
| OE1333 | RoundingBuiltinArity | A rounding builtin (round/round_half_even/trunc, §2.7) was called with the wrong number of arguments. |
| OE1334 | BindingLhsNotSimpleVar | A 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. |
| OE1335 | BindingLhsAlreadyBound | A 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.) |
| OE1336 | BindingInComprehension | A 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 fromBuiltinArityto match the catalog name (only the rounding builtins reach this arity check).
Extended:
- OE1303 (
RuleNotRangeRestricted) — wording extended to name the binding formx = expras 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
| Item | Disposition |
|---|---|
| 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
pubsurface — 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" }, writesuse argufo::kind;, and usesargufo’spub metatype kindas 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.”
- How does a package name and resolve a dependency? What
[dependencies]shape, what resolution semantics, what happens across the package boundary foruse, qualified paths, and — the acceptance test — dependency-provided introducers (pub metatype/pub metarel)? - 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? - What is honest about an
ox.tomlthe 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 anox.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:
- Loads the dependency package (its own
ox.toml+ entry + reachable module closure), exactly as it loads the consumer package. - 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’srootmodule becomes moduleargufo, its submoduleendurantbecomesargufo::endurant, and so on. - Folds the dependency’s source files into the consumer’s
Workspacefile 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 theuseitself, with a did-you-mean suggestion over the names visible in the target namespace (the dependency’spubsurface, a sibling module, an intra-package path). Previously a brokenusewas 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-awaremodule_exports) — it brings in the target’spubsurface. 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.tomlsection 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 withserdesilently dropping it. [lattice].max_tieris wired to the §10 tier classifier: it sets the artifact’s tier ceiling, and a declaration whose classified tier exceeds the ceiling is refused atox check/ox buildwith 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,
useresolution, qualified paths, module extraction, and the.oxbinall 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
.oxcand 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/gitto 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].nameauthoritative (mismatch = OE1241), matching Cargo’s default. - A lockfile for path deps. Rejected (Cargo precedent): a path graph has nothing to lock.
Consequences
ox.tomlnow has a[dependencies]table and recognizes[lattice]/[standpoints]; unknown keys warn (OW1240).[lattice].max_tieris enforced (OE1230, no longer reserved).- No change to the
Workspacesalsa input: dependency roots are detected from the synthetic<dep>/<name>/root.arpath pattern in the file set (mirroring stdlib’s<stdlib>/std/<pkg>/…), soWorkspace::new’s signature is unchanged. - New diagnostics (allocated in
grammar.toml, generated viacargo xtask gen): OE0103UnresolvedUseImport, OE0104GlobImportUnsupported(reserved — glob currently resolves; held for a future feature-named refusal of an unsupported glob shape), OE1240DependencyVersionUnsupported, OE1241DependencyNameMismatch, OE1242DependencyCycle, OE1243DuplicateDependencyName, OE1244DependencyPackageLoadFailed, OW1240UnusedManifestKey. OE1230TierCapExceededis wired (was reserved). - The two-package journey is the living proof:
examples/vocab_pkg_v0(a UFO-shaped vocabulary package withpub metatype kind/category, apub metarel, and apub checkcatalog rule) andexamples/vocab_consumer_v0(a separate package,[dependencies] vocab_pkg_v0 = { path = ".." }, declaringpub kind Personvia the dependency’skind, 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.lockand 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’spub(pkg)items be invisible to a consumer (todaypub(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/$armcatalog atoms so a package-shippedcheckcan 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 onstruct/enumcarriers (§4.4.2 — R-M10); (5) the metaxis typed-axiswhererefinement 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 (reflectiveTypeRef). 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:
-
Metarel endpoints are unverified.
pub metarel mediation(mediator: relator, mediated: kind)declares position metatypes, butpub mediation Bad(a: Vehicle, b: Person)— whereVehicleiskind-sorted, notrelator-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 onMetarelDeclBody). This is the core ask: an ability must inhere via an aspect, not a kind, and nothing enforced it. -
No reflection over relations.
$iof/$meta/$axis/$implementsreflect 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. -
Per-target axis overrides have no surface. RFD 0027 D2 specified per-target
MetaPropertyassertions (“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 declaringmetaxis rigidity for type { … }gave consumers no way to overriderigidityon one specific concept. In-body{ rigidity::semi_rigid }→ OE0001; the attribute form → OE0705. -
Reflection on
struct/enumis silently clean. §4.4.2 promises a category error (“callingmeta()on astruct/enum-declared value is a category error … emits a diagnostic at elaboration”).meta(p) == Pointover a struct checked clean — no diagnostic existed. -
The metaxis
where-refinement does not parse as printed. §4.1’spub metaxis weight for type = Real where _ > 0.0;→ OE0001 (the typed-domaintype_exprgreedily consumed the trailingwhereas a refined-type refinement expecting{ … }). Only the braced= Real where { _ > 0.0 };parsed. -
Relation bracket cardinalities are silently ignored.
pub rel R(...) [1..1] [0..*];parsed into aCARDINALITYtoken-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 relationr(aTypeRefvalue naming the relation),mis its classifying metarel. Catalog-closed: one row per declared relation.$arm(r: TypeRef, pos: Int, t: TypeRef)— for every declared relationr,posis a 0-based arm position andtis 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/updatethat would make an entity participate in more thanhituples of relationRin the constrained position is refused with OE1014 family (the closed-world cardinality gate), feature-named OE1341 (RelationCardinalityExceeded). Max-caps are checkable atox 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-zerolothat 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&&— writea > 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 separatepub fact/construction).
Diagnostics inventory
| Code | Name | Tier |
|---|---|---|
| OE0631 | MetarelEndpointMetatypeMismatch | meta-calculus (06xx) — D1 |
| OE0632 | ReflectionOnUnclassified | meta-calculus (06xx) — D4 |
| OE1341 | RelationCardinalityExceeded | runtime/write gate (13xx) — D6 max-cap |
| OW1342 | RelationMinCardinalityDeferred | build 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 (theoxupmanager + theargon.sharpe-dev.comCDN) and RFD 0013 (theoxupmanager + dist layout). - Implements: a new
oxup extension(aliasext) subcommand; anEditorIntegrationabstraction; a CDN asset path for the.vsix; auto-wiring fromoxup 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.
- What is the command surface? One subcommand, abstracted over editors, so vim/neovim/emacs can slot in without a new top-level verb.
- How does the extension version stay coupled to the toolchain? A user on
stable0.2.1 must get the 0.2.1 extension, not “latest”. - Where does the asset live and how is it verified? Same discipline as the toolchain fetch: immutable versioned CDN path, sha256 sidecar, fail-closed.
- 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: removeargon-lang.argonfrom the detected (or--editor-named) editors.list: show detected editors and, for each, the installedargon-lang.argonversion (or “not installed”).
2. Editor abstraction — EditorIntegration
An enum Editor with a small behavioral surface (oxup/src/extension.rs):
| method | meaning |
|---|---|
name() -> &str | the --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 ininit.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/argononload-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-extensionskips it;--minimalalready 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 updaterefreshes the extension for the channel it updated, but only when the version changed: the installed extension version is recorded insettings.toml([extension] installed_version), andupdatere-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
| Editor | Status |
|---|---|
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
extensionsubcommand with anEditorenum keeps the vim/neovim/emacs work a matter of filling ininstall_argv, not adding CLI surface. The no---editorauto-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
.vsixand 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
.vsixfetch reusesdist_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
oxuplayout is unix-only (RFD 0013). - Marketplace / Open VSX publish. Argon is Sharpe-internal; the
.vsixis 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 mutateforms are a thin named wrapper over it — not the only door. Builds on RFD 0014 (the serving surface), RFD 0015 (themutatebody /OperationIR), RFD 0020 / RFD 0021 (the Engine/CompiledRuleevaluation seam), and RFD 0022 (the build evaluability gate, whose runtime analogue this RFD must define). - Implements (as built): the query-provider
Schemainterface (oxc_types::Schema) with two parity-gated backends —WorkspaceSchema(build-time, over ASTs) andoxc_runtime::ModuleSchema(runtime, over a loaded.oxbin); the checker (oxc-check) fully routed through it; the runtime frontend (oxc-parser/oxc-check/oxc-instantiatenow linked intooxc-runtime);Store::eval_{query,mutation}_source(parse → full type-check → lower → run, ill-typed bodies refused and never run); thePOST /v1/{query,mutation}/adhocHTTP surface +ox query --evalCLI; theAdhocPolicyopt-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:
- What the ad-hoc surface is (wire shape, CLI shape, semantics), for queries and mutations together.
- How a body submitted as source text is compiled at runtime, given that the compiler frontend is not currently linked into the serving binary.
- The resolution context: how names and types in an ad-hoc body resolve against the loaded module rather than a build-time Salsa workspace.
- 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).
- 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:6799–6824) 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:
-
No request field for a body.
DispatchDescriptoris{ qualified_path, args, return_type }(oxc-serve/src/lib.rs:2671); resolution isquery_decls.get(qualified_path)→ 404 if absent. There is nowhere to put a body. (The runtime refusalARGON_RUNTIME_UNSUPPORTED_QUERY_BODYatoxc-serve/src/lib.rs:6749is a narrower executor gap — field projections in bodies are not yet executable — not an ad-hoc policy.) -
The compiler frontend is not linked into the serving binary.
oxc-serveandoxc-runtimedepend onoxc-reasoning,oxc-protocol,oxc-oxbin(+ storage) — and notoxc-parser,oxc-check,oxc-instantiate,oxc-resolver, oroxc-db(verified in bothCargo.tomls). The runtime can compile pre-lowered IR but cannot turn source text into IR. -
Name/type resolution is build-time. The frontend’s full type-checker (
oxc-check) is bound to a SalsaOxcDb/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 closures —
resolve_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
LowerCtxwhose closures are backed by the runtimeModule/.oxbincatalog 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 thequery_body_dispatchpath with the IR sourced from the request instead offind_query_decls.) - Mutations:
Vec<Operation>(+ params) → the existingrun_body_opinterpreter, 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:
- 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/queryHTTP endpoint use. - 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/types’ Importer), 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
Schemainterface — 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-instantiatebody-lowering (already(&SyntaxNode, &LowerCtx), no DB), andoxc-checkall resolve throughSchema. 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 events — not 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_valid → OE1204). 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-hocforgetis 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] mutatestill 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 noForgetgate 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 typedmutate/Operationmechanism with the fullmutateaffordance 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):
Schemastays strictly store-agnostic.Schemaanswers type questions only; it must never know where bytes live. Physical placement is a separate layer — RFD 0020’s BYODS (D6: a physicalRelationis an interface; representations coexist) plus theRuntimeStorageBackendseam, selected per-relation byox.tomlplacement. This is the OBDA shape (data stays in place, queried through the ontology;ox.tomlplacement is the R2RML analogue), and the catalog/connector SPIs (CalciteSchema/Table.getRowType, TrinoConnectorMetadata) 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 sharedLogicalPlan(the IR scaffolded but currently dead inoxc-reasoning/src/logical/). Lowering ad-hoc bodies to that IR — rather than hard-wiring the currentmaterialize_predicatespull-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
Operationinterpreter 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
parseis DB-free andLowerCtxis closure-based, the runtime lowering path is aModule-backed resolver + a dependency edge — not a rewrite of lowering. - One frontend, no drift. Reusing
oxc-instantiatelowering andoxc-checktype-checking against aModule-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
- Declared-only forever (status quo). Rejected: contradicts the stated design intent; “a database you can’t query ad hoc isn’t a database.”
- 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.
- A separate runtime-only frontend fed by an
.oxbincatalog (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. - 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-checkparity is in scope from the start, which is what pulls the checker into the runtime frontend. - 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 isforget, refused for ad-hoc — but that is not a leash on affordance, it is thatforget’s#[allow_forget]capability is conferred at a declaration site a request doesn’t have, so the request can’t forge it (see Security). - A generic untyped entity-write endpoint (
POST /v1/entities). Rejected/kept-absent: ad-hoc writes belong to the typedmutate/Operationmechanism, not an untyped blob surface.
Consequences
- New runtime dependencies:
oxc-serve/oxc-runtimegain the frontend —oxc-parser,oxc-instantiate, and (per decision #1)oxc-check/oxc-resolver/oxc-types, once their environment access is routed throughSchema. This is a substantial change to the runtime’s relationship to the frontend (theruntime/AGENTS.md“the reasoner was not built here” tombstone framing and theoxc-runtime/oxc-serveintent nodes all need updating). IntroducingSchemaas 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-sidemodule_idcollision gap) so two schemas’ symbol ids can’t alias — shared with the storage-identity fix, not additive. - New
Schema-backingModulesurface: name/type/parameter/world-assumption/refinement resolution over the CQRS catalog projection (additive; the facts are already in the.oxbindecl 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:
- The exact
Schemaoperation 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 indexesModulealready 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.ttcpattern) is a future optimization, not needed for v1 sinceModulealready eagerly indexes the (small) schema. - 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.
- Materialization model. Ad-hoc reads today inherit the full in-memory
materialize_predicatesbuild (oxc-reasoning;SemiNaiveExecutor). The intended replacement — a content-addressed, generation-invalidated projection cache (the read-model section is already reserved in.oxbinand invalidation exists inoxc-storage-pgget_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. - Standpoint / fork / bitemporal scoping. Ad-hoc bodies need the same
as_of/ standpoint / fork context as declared dispatch;query_body_dispatchcurrently refusesacross-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: onlymod/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 inoxc-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
- 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// commentwas a hard lexer error. What is the real policy? - 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?
- Module membership. A
.arfile sitting in a package’s source tree but declared by nomodwas 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
.arleft in a package’s source tree no longer breaks the build with misattributed errors; it is ignored and surfaced asOW0710. - 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
LogicalPlanIR 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 currentCompiledRule-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 fromCompiledAtomtoday — 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
CompiledAtomvariant expresses this (the five variants arePredicate,Comparison,Naf,Compute= scalar map,Aggregate= relation→scalar; verifiedcompile/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 →PhysicalOppipeline below — described here as orphaned/unwired — is now on the production query path (thelogical::lowerpipeline 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.
LogicalPlanis orphaned, relational-core only.logical/mod.rs:21—Scan/Filter/Map/Join/AntiJoin/Distinct/Recurse/Sink, eachTier-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-32landmine note).- The front-ends bypass it entirely. All compilation goes
AtomIR → compile::compile_rule → Engine::evaluatedirectly — verified at ~11 sites inoxc-runtime(checks.rs:628,644;lib.rs:1211,6807+ the.evaluate(sites6819/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 aTierExecutor. TierExecutor::executeis whole-program.executor/mod.rs:67-85—execute(&self, rules: &[CompiledRule], catalog: &mut RelationCatalog, policy: ConvergencePolicy), writes derived facts into the catalog in place.SemiNaiveExecutoris the only real impl (coversStructural/Closure/Recursive);SLG/DBSP/SMT/Kripke/Koraare 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 tracksbound: BTreeSet<VariableIdx>(reorder.rs:61) and consumes catalog sizes (reorder.rs:189-195). TheOptimizerPipelineoverLogicalPlan(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(); theStoreholds no materialized catalog (RFD 0021 D7). ACatalogEntrycarries atier+ aRelation<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.rs — integrate/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
CompiledAtomlacks — 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
LogicalPlanthat round-trips into theCompiledRuleexecutor. 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.rsmade live for evaluation). Rejected (D2): regresses the fused semi-naive loop and dissolves the WCOJ/CSR/factorization fusion.runtime/operators.rsstays the IVM-boundary vocabulary (D7), not the query-evaluation path. - Keep the direct
compile_rulepath; 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 monolithnouswarns 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 inoxc-runtimere-point to lower-to-LogicalPlan;reorder.rsis lifted ontoLogicalPlan;TierExecutorgeneralizes to operator-call dispatch; a new table-operator logical node +CompiledAtom/physical contract lands (D4);SemiNaiveExecutorbecomes the fixpoint physical operator under the generalized contract.eval.rsand 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
RelationCatalogpublic-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
CompiledAtomvariant, 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
TierExecutormoves from whole-program to sub-plan/operator dispatch without losing the per-stratum stratification it does internally today. - Checker goals as
LogicalPlan— whetheroxc-checkissues boundedLogicalPlangoals 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 interpreter — not an open question but a stated prerequisite (D5): the opaque-CBOR
LogicalPlan::Filter.predicatemust 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
Tiermetadata; 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
Schemaquery-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
Schemaquery-provider this RFD’s C1 rests on. Merge #401 first; on this branch the0033-*.mdlinks 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
BTreeMapfreeze (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)
AxiomEventalready 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 + aproof_tag(defeasibility) + aderivation(PosBool(M) DNF why-provenance). Federation adds one leaf, not a new token.- Per-concept world assumption is already mechanized —
WorldAssumptionMap,Locality/Cwa.lean, withcwa_owa_transferproven (CWA-true ⇒ OWA-true; the reverse proved unsound), and diagnosticsOE0901/OW0902reserved-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-174clock pin;232-235stable arrangements) — RFD 0035 D3 generalizes it. - No store config exists.
ox.toml(oxc-workspace/src/lib.rs:160-168) parsespackage/project/schema/dependencies/latticeonly. The.oxbin/pg projection cache exists but is inert (Noneeverywhere;oxc-storage-pgget/put_projection_cachetested, never called by the evaluator). - A
RuntimeStorageBackendtrait exists (oxc-runtime/src/lib.rs:2890) — a sync, in-process replay seam;PgStorageis 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
| Pattern | What it is | Driving example | Seam |
|---|---|---|---|
| P2 — Foreign federation (centerpiece) | Foreign data in its native shape, mapped into the ontology, queried through Argon, never copied into the log | the market oracle | connector SPI (D3) + frozen-EDB (RFD 0035 D3) |
| P3 — External-valued attributes | A property’s value is a content-addressed handle; bytes live in a blob store | document on an individual | Ref<Blob> (D8) |
| P1 — Persistence swap | Argon’s own event-log/read-model lives in a chosen durable backend | log in DynamoDB | thin 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.Schemastays store-agnostic (C1, RFD 0033) — the connector sees the IR, neverSchema.binding_patterns()— capability as{b,f}^nadornments (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 samebound: BTreeSet<VariableIdx>reorder.rsalready tracks, lifted toLogicalPlan— RFD 0035 D5).apply_filter(&LogicalPlan) -> (handle, Absorption)with three-valuedAbsorption ∈ {Exact, Inexact, Unsupported}(DataFusionTableProviderFilterPushDown):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. UnderInexactthe 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 thebpositions indemand(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
ExactandInexact, 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
Exactbut 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. AnExactclaim 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 asInexactand 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
Exactverdict 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 “noExactever” 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, itskind(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:
- idempotent content-addressed put —
handle = BLAKE3(bytes), outside any transaction; re-putting identical bytes is a no-op by content hash; - a transactional single-entity reference write — a native
AxiomEventholding the handle (the token already exists); - 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)atas_of” — is runtime state, not config (the NeonIndexPart/ 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).
AxiomEventalready 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
RuntimeStorageBackendto 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
asyncend-to-end, orblock_oninside the sync freeze. Rejected (D11): the first colours the recursive coreasyncfor 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 connectorawaitbelongs 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 inStorestate — D11), the table-operator/analytical-tier executor (RFD 0035 D4/D6), the[store]/[placement]ox.tomlsections + the compiled mapping artifact, the foreign-provenance leaf inderivation, the world-assumption tier-input threading, theRef<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_predicatesgains a plan-demand → await-fetch → freeze → sync-fixpoint split, with theawaithoisted above the existing sync evaluator core (serve) or a single per-commandblock_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 againstCwaOwa.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
- 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
Exactclaim (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. - 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). - Demand-stratify vs monotone bounded re-consultation for recursive foreign demand (D4) — settled with RFD 0035 D5’s magic-sets interface.
- 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).
- Composite multi-source freshness beyond meet-of-leaves; read-your-writes across a mixed native-exact / foreign-barrier boundary (D7).
- The cross-boundary CWA→OWA completeness warrant — the connector contract that discharges
cwa_owa_transferfor a closed foreign predicate, and howOE0901fires when it is absent (D5). - Blob GC over the bitemporal/fork-branched/content-addressed log — conservative mark-and-sweep vs a maintained per-fork refCount projection (D8).
- Whether content-addressed immutable layers unify C10 identity with the C8 fork axis (D9) — a deeper storage simplification to investigate.
- 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_idcollision) is shared with the storage-identity fix and bears on the foreign-leafsource_id. - 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. - 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.
- 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_ofbarrier), 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 (loudOE0705-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 toMACRO_DECLwith 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 noTokenStreamtype.name!(…)bang-invocation does not parse at all — noMACRO_INVOCATIONnode, theBANGtoken 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: itformat!s Argon source —"pub derive {rel}(x, z) :- {rel}(x, y), {rel}(y, z)"(lower.rs:5172) — thenparse_files it, extracts theItem, and lowers it with the livectxso 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 acrosscompiler/is empty). So alpha-variant rules get differentAxiomKeys — contradicting the stated intent “same proposition asserted twice has the sameAxiomKey” (ids.rs:393).#[forall]lowering already mints counter-named$fa_Nvars (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 (localpub metatype→ ambientstd::core→ workspace-unique, elseOE0605/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 aMetaPropertyevent (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:
- 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). - 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::lifecyclesugar, navigation, and even MLT — is declarative (pattern→template ± name resolution). The only workload needing analytic computation (inspect a concept’s fields) is genuinederive-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_introducerlike 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 —.oxbinmust 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_equivinspec/lean/Scratch/MacroHygiene.lean(branchresearch/0037-macro-hygiene; design memospec/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 variablecis 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
.oxbinacross 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 tostd::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 declarativepub 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 synthesizedcheckhas a computed head (__{rel}_{prop}), which the splice-only declarative layer cannot construct (noconcat_idents), and#[functional]-on-relis a cardinality cap, not a surface rule. So astd::rellibrary stub marked#[builtin](a new directive on apub macrodecl — 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 requireuse 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 tostd::mltvia 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 theMetaPropertyevent (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 anOE0705-class error, not a silent shadow (matches Argon’s loud-refusal posture and the existing ambiguous-refuse resolver).use mod::foois 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 — twomacro namedeclarations 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 syntax — not 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
Syntaxtype — 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 genuinederive-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:
- Migration proof (byte-identical):
#[transitive](and the relation-property family), re-homed from the hard-coded synthesis to a librarypub 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. - Grow-a-language proof: at least one genuinely user-defined
pub macroexpands 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
- Expansion is a phase, not an instantiate-time rewrite (D1) — the verified architectural correction.
- Alpha-canonicalization (D4) is a committed correctness fix, prerequisite to the clean hygiene story and valuable independently.
- 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). - The
originbreadcrumb 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. - The
$x:rulefragment’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::lifecycleoperators land native or library in the first cut (D8) — a scoping call, not a design blocker. - The driver/salsa shape of the
EXPANDphase (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:
- Tier 0 — substrate. A fixed, hardcoded, non-opt-outable set: the names that are the language, not library declarations.
- Tier 1 — the prelude. A real, configurable, auto-imported (opt-out)
prelude that carries no ontology by default — no
type/rel. The prelude is a package feature (ox), not a compiler feature: there is no compiler-default prelude. - 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-preludestd::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: nostd::prelude::v1, no#. 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 (hardcodedis_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_stdlibiterates it with no dependency graph, no opt-in, no opt-out. Every artifact carriesstd::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 classifier —pub categorizes AB(A, B)checks clean with nouse(test T2). This directly violates RFD 0009 (“MLT is an explicitly-imported library, nothing privileged”) and §5.2. type/relare hardcoded pins.if classifier == "type" { return std::core::type }(lower.rs:1182;:1220forrel) — special-cased, not resolved through any inclusion mechanism.Top/Bothave two sources of truth. Defined both as primordials (resolve.rs primordial_kind) and aspub types instd/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::relin their own prelude as a convenience”; §4: “afteruse std::core::rel;the modeler can writepub rel …” — both explicit-inclusion. Yet §5.2 / §15: “std::coreis auto-injected …pub type Fooworks with nouse.” 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 explicituse. A bareoxc foo.arwithpub type Person {}and no import is an error (OE0605, unresolved metatypetype) with a hint touse std::core::{type, rel}.ox(cargo): the package tool. It requires anox.toml, and the manifest supplies the prelude ([package].prelude, D4), dependencies, tier ceiling, and world.oxinvokes the sameoxcpasses 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 byox.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
preludethatpub uses the package’s public surface; consumers opt in explicitly withuse pkg::prelude::*, or feed it into their own role-1 config. No special mechanism — just a module. The §3prelude.arconvention 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
useedges 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}⇒MonetaryAmountand its dependency closure, not theCurrency/RoundingModesurface it never references; nouseofstd::fin⇒ zerostd::finaxioms. - 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 existingLocality/mechanization did not cover this (Seminaive.leanproves 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 ofuse-tails, each parsed exactly as if writtenuse <entry>;and auto-prepended to every module of the package. Reusing the realuseparser 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 implicituse). 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
preludemodule, 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, reviewableValidated 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 globuse pkg::prelude::*pulls in the module’s public surface including itspub usere-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 (elseuse pkg::prelude::*is prepended topkg::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 toDIRECTIVE_REGISTRY(module position,ATTR_INNER); expose inner attributes on theSourceFileAST (todayargon.ungrammarhas onlyitems:Item*); thread the flag into prelude injection. A module with the flag gets Tier 0 only — no auto-prelude — and mustuseeverything 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 underoxcwith substrate + explicituses + extraction. - A corpus migration pass: every module that declares
pub type/pub rel, or uses a stdlib classifier without importing it, gains the rightuse(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
oxcwith an explicituse std::core::{type, rel}, or gain a one-lineox.toml(prelude = ["std::core::{type, rel}"]); the test harness (write_temp_source) writes whichever it needs. - The
OE0605/OE0606unresolved-introducer diagnostics gain a hint suggesting the likelyuse(e.g. “did you meanuse std::core::{type, rel}?”). - The stdlib packages themselves declare their own dependencies via
use(e.g.std::mltusesstd::core).
Acceptance tests
- Leak closed:
pub categorizes AB(A, B)with nouse→OE0606(unresolved metarel introducer), notok. (Inverts test T2.) - Baseline opt-in:
pub type Foo {}with nouseand#![no_implicit_prelude]→OE0605; withuse std::core::{type}→ok. - Symbol-precise extraction:
use std::fin::{MonetaryAmount}yields an artifact withMonetaryAmountand its dependency closure but not thestd::findeclarations it never references; nouseofstd::fin⇒ zerostd::finevents. Assert over the event stream. - No regression for committed vocabularies:
use std::ufo::prelude::*; pub kind Person {}checks clean with nostd::corein scope. - Determinism preserved: byte-identical
.oxbinacross builds (S5) holds under extraction (the extracted set is a deterministic function of the use-graph). - 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(theoremagree_on_closure): for a ground positive program, the slice (rules whose head predicate is in the dependency-closure of the used predicate setQ) and the full program agree, at every fixpoint iteration — hence at the lfp — on every atom whose predicate is in the closure ofQ. So loading only the use-graph closure yields the same derived extents over the used predicates. Self-contained (no Mathlib), nosorry, and#print axiomsreports 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
Tis 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 ambienttype/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/relare not reserved ambient names.
Resolved (this RFD)
oxc= rustc,ox= cargo. The prelude is a package feature; there is no compiler-default prelude. Looseoxcfiles use substrate + explicituses.single_file_workspace_with_stdlibis deleted.- One auto-import mechanism (role 1), distinct from a package’s exported
preludemodule (role 2). Nostd::prelude::v1, nostd::core::prelude. [package].prelude= an array ofuse-tails, default empty; per-module opt-out#![no_implicit_prelude]. Bad entry = hard error.type/relopt-in via[package].preludeor explicituse— never privileged ambient.- Loading is symbol-precise (⊥-locality extraction), never whole-package.
Open questions
- 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. #[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 readsOperation::ForEach#571; closed-nesting soundness mechanized on the never-mergedscratch/nested-mutation-soundnessbranch) - Opened: 2026-06-19
- Decides: how a
mutatebody (1) invokes another mutation — including a traitmutatemember — and (2) reads the deductive plane (aderive/queryresult) 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
mutatebody 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
requireaborts 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, andmutation → mutationcomposes 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
Applyexactly 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
selfbug (#550). “Rules that apply rules” is a first-class modeling pattern, not glue.
Consequences
- A new
Operation::InvokeIR variant and a read-goal term; the lowering replaces the silent_ => {}drop. The runtime threadsInvokeinto the currentBodyExecrather 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
mutatebody isevalMutate : MutateDecl → Env → ReadView → Except Abort (List Effect × Value). It threads aMutState := { fresh, effects : List Effect }throughevalStmts; the read-viewrvis a read-only argument — it never changes during a body. Effects accumulate inMutState.effects;commitfoldsapplyEffectover them only on the.okbranch;runMutationno-ops on.error(runMutation_error_noop,require_fail_atomic). - Consequence for D3 (snapshot read), already structural: because
rvis read-only throughevalStmts/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
- A statement constructor
Stmt.invoke (callee : MutateDecl) (args : List Expr)(the Lean image ofOperation::Invoke). - 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’srequireguards + body statements into the sameMutState— appending the callee’s effects tost.effectsand threadingst.fresh. Aborts propagate throughExceptunchanged.
Theorems to prove
- T1 — Closed-nesting atomicity (the headline). For any body containing
invokestatements,runMutationis all-or-nothing: ifevalMutate = .error errthenrunMutation = rv. Strategy: this isrunMutation_error_noopunchanged — it depends only onevalMutatereturning effects solely on.ok, which the shared-buffer/Except-propagationinvokecase 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, aninvokeextendsst.effectsby 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
Siterated byfor 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), thencommitis independent of the iteration order ofS. Strategy: a commutativity lemma onapplyEffectfor footprint-disjoint effects, lifted overfoldl.
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_noopsurvives the shared-bufferStmt.invokeverbatim — 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) Ais false even for footprint-disjoint effects, becauseapplyEffectprepends to the classification/relation lists ((id,c) :: …, as the productionapplyEffectdoes too), so two disjoint asserts land in opposite list order. Confluence is true against anObsinterface — equal classification/relation membership, equal property lookups, equalnextFresh— i.e. up to what reads can observe. T3 is proved up toObs, lifted overList.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
sizeOfwithin 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 branchscratch/nested-mutation-soundness): T1/T2 lift unchanged; T3 confluence holds up to observational equivalence (see Mechanization result above). The minimal shape isrunMutation_error_noopverbatim for atomicity + anObs-quotiented commutativity for confluence.- Dispatch over buffered classification: should
select_member_implat anInvokesite 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 ∘ expandtier-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 ) { … }(anditem.params) insidequote— 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 theconcat_identspaste builtin). So totality needs no decreasing-argument analysis: the body is a closed fragment {let-paste,quote+$(for)+${…}/if,matchon 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 syntax — not 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 arelwith 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:
| Client | What it needs | Why declarative can’t | Status today |
|---|---|---|---|
#[irreflexive]/#[asymmetric] | head __{rel}_{prop} built from the relation name | no concat_idents / paste | P1: 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-emission | builtin 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 surface | builtin-backed stub (RFD 0037 D8) |
derive-class (inspect a concept’s fields, synthesize per-field) | structural recursion over a declaration | no reflection | the 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)→Syntaxof the argument fragment; - attribute / derive
#[name(args)] <item>→ the decoratedDecl(aSyntaxsubtype) plus the attributeargsfragment.
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— buildSyntaxfrom 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
matchoverSyntax— 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.)
- P1 — paste (
concat_idents) + the#[procmacro]surface for non-recursive bodies. Total trivially (no recursion). SHIPPED: retires the#[irreflexive]+#[asymmetric]P0 stub — they become genuinestd::relprocedural macros emitting byte-identical events (their arms insynthesize_relation_property_rulesare deleted;OE0706reservation for#[procmacro]is lifted; the new evaluator isoxc-workspace/src/procmacro.rs).#[functional]does NOT migrate in P1: itsrel-cardinality-cap arm needs to re-emit arelwith a modified cardinality bracket = structural re-emission = P2, and a single attribute name cannot be half-builtin/half-procmacro — sofunctionalstays a#[builtin]stub (OE1362-gated) until P2. This was the committed first implementation step; it is now built. - The axis-assertion surface (D5). Retires the MLT P0 stub — the four
std::mltdecorators become genuine procedural macros. - 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 realderive-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 makesexpanda 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 landed →
std::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 guardingpub checkwhose head isconcat_idents("__", item.name, "_{prop}"). Their arms in the nativesynthesize_relation_property_rulesare deleted, and their import gate shifted fromOE1362toOE0705(a bare unimported#[irreflexive]is now an unknown attribute macro, exactly liketransitive). 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::mltdecorators as#[procmacro]s; deletelower_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
- 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 guardingpub checkwhose head is built byconcat_idents("__", item.name, "_{prop}")— and produce events byte-identical to the former synthesis (relation_property_rehome.rs); their arms insynthesize_relation_property_rulesare deleted.#[functional]stays a#[builtin]stub, not a procmacro: it is one attribute that covers BOTH ametarel-check (paste-able) AND arel-cardinality-cap[0..1]on the target endpoint (lower.rs, the cap arm) — and re-emitting arelwith 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),functionalstays 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. - Totality is enforced, not trusted: a
#[procmacro]whose body recurses non-structurally is refused at declaration (the new totalityOE07xx), with no fuel-cap fallback masking it. - 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. - MLT genuinely re-homed: the four MLT decorators are
#[procmacro]s emitting via the axis-assertion surface;lower_relational_mlt_decoratorsis deleted; events byte-identical.
Lean obligations
- Extend the
@[language_interface]drift coverage to the reflectedSyntaxcarrier (D2), so the RustSyntaxreflection type and the LeanSyntaxinductive stay aligned by the existing gate. - No
MacroExpansion.leanin P1/P2-surface. When P2’sexpandbecomes 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 fnsurface, 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
Syntaxcarrier + 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 aquote { … }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
- Paste × hygiene: the precise hygiene treatment of a
concat_identsresult — 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. - 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. - Reflection accessor API for P2: the precise
Decl/Syntaxaccessor 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. ${}spelling-overlap with runtime string interpolation. P1’squoteantiquotation${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 noText + Textoperator 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.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
MappingArtifactbuild-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 ametafactsurface. - 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:
-
Today’s substrate does NOT re-check emitted events.
classifyruns only onRuleDeclbodies;resolvevalidates names;encodevalidates 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 throughingest_locked/seed_from(an infallible index insert), a separate ingress from the write-path (execute_mutation/apply_operation). So a hand-built or macro-emittedRelationTuplesmuggles past endpoint-existence (OE0232), arity/endpoint-type (OE0221/OE0222) and max-cardinality (OE1341); aMetaPropertysmuggles past axis domain/literal/refinement (OE0622–OE0624); 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 —IofAssertionabstract-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.oxbinalready bypasses these checks today. -
There is a third output kind. A
MappingArtifact(placement) is a.oxbinsection (id 11), deliberately versioned independently of the schema (RFD 0036 D6) and explicitly forbidden from carrying axiom-event semantics (RFD 0036 D1). It is neitherquote!-able surface nor anAxiomEvent. 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. -
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, arityOE0221, endpoint-typeOE0222, max-cardinalityOE1341(incl. the#[functional][0..1]rel-cap, same mechanism). - Meta-properties: axis-value-in-domain
OE0622, axis-literal-type/refinementOE0623, axis-tierOE0624. - Iof / individuals: abstract-target
OE0233(already at load), iof-on-definedOE0211, fixed-reclassificationOE0234(see Fork A — needs an encoding discriminator), property-id injectivityOE0231(already at load). - Construct/property: refinement invariant
OE0668, required-fieldOE0207, value-typeOE0236. - 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 thecheckrules 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,
matchexhaustiveness, defeasibility well-formednessOE0716–OE0721, temporal-qualifier forms, in-body construction completeness, the §3.4 introducer-resolution gate, relation-subsumptionOE0150–OE0154, metarel endpoint-metatypeOE0631) gate surface declarations and stay on the lowering path — macros produce those viaquote!. A handful (OE0631,OE0150–OE0154,OE0222endpoint-category) are load-bearing but not event-derivable; they bound what is emittable as an event (such artifacts stayquote!-surface).
- Relation tuples: endpoint-existence
-
The
.oxbinvalidation framework exists but is unwired and mis-shaped for this.Layer2Invariants(oxc-oxbin/src/validation.rs) is sixalways_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”). AndModule::loadnever calls the framework — its sole caller isoxc-serve, passing the stubs. So this RFD does not “fill stubs”: it adds net-new predicates to the framework, wires the framework intoModule::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 withquote!, 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 itself —
AxiomEvents (the event-stream gate, D3) and build-sections likeMappingArtifact(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_events → Vec<AxiomEvent>), so whole-module visibility is available. This is load-bearing: OE0232 needs the workspace individual set; OE0622–OE0624 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 familyOE0668/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 (
OE1341max-cardinality): lowering only records the[lo..hi]bracket; the actual check is a stateful runtime tuple-count overscan_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] → MappingSections → oxbin_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:
- Expand: the macro emits an unpinned placement payload — a
PlacementDecl(storename +mappingdirectives) keyed by the logical relation name. Everything inplacement_hash()is knowable at expand from the macro’s input;schema_signatureis left unset. - Build: the existing
MappingSections::compile(composition_signature)stamps the pin over the merged placements — identically to the config path. - Load:
OE1213+OE1245run 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!buildsSyntax; splices ($item,$item.name) interpolate reflected values. A singleSyntaxauto-lifts to anExpansion, 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
emitkeyword (taken by the emit/sink feature) and nounsafe— 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
Layer2struct 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
OE1341the 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.leanproves 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 makingexpanda 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 metafactsurface). Sound (it lowers through the validating path) and neutral, but it is the surface-per-metadata-kind accretion: MLT needsmetafact,foreign!needs a placement form, the next vocabulary needs the next form — libraries perpetually gated on compiler surface work. Superseded. unsafedirect emission. Cheap, neutral, no accretion — but a genuine trust-waiver: the review proved a directly-emitted event bypasses real checks, sounsafewould be load-bearing, and a content-addressed.oxbinwould 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 aMappingArtifactplacement through the build-section gate (D4). Unblocked now — it needs only theExpansionsurface (D5), theMappingSectionsunion + collisionOEcode, and the#[foreign]attr-args plumbing; noLayer2work. This is the first client and the proof of the mixed-emission boundary.- MLT (
#[categorizes(T)]…) and#[order(N)]become library#[procmacro]s emittingMetaProperty;lower_relational_mlt_decorators/lower_order_decoratorare deleted; #483 closes genuinely. Added scope the review surfaced: this depends on (a) theMetaPropertyevent checksOE0622–OE0624at load (D3), and (b) building the MLT concept-membership semanticsOE190x/OE1904(partition disjointness, order) which are currentlyreserved/unbuilt even in lowering — so “MLT emitsMetaProperty” 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 emittingcheckrules (self-enforcing, noLayer2);#[functional]on arelstays a#[builtin]stub until P2 (its[0..1]cardinality-cap needs structural re-emission); the metarel-functionalcheck is already re-homed. .oxbinbecomes 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:
- The
Expansion/emission surface (D5) in the procmacro evaluator (procmacro.rs): theExpansionreturn type + auto-lift, the EXPANDVec<EmittedArtifact>buffer, surfaced fromexpand_package_workspace. Includes thequote{}→quote!P1 migration (or a preceding focused PR). foreign!(build-section track) — in parallel, noLayer2dependency: thePlacementconstructor inprocmacro.rs, theMappingSectionsunion + collisionOE, the#[foreign]attr-args plumbing. (Heterostore team owns the connector +MappingSectionsseam; macro side owns the emission buffer.)- The self-validating
.oxbin(D3, substrate-trust track, ships on its own merit): the ~11–12 event predicates, wired intoModule::load; theLayer2framework grown + drift-paired; decision-agreement golden tests. - MLT re-home (#483) — needs (1)+(3) plus building
OE190x/OE1904; delete the builtins. - The Lean predicates + bridge lemmas (D7), including the set-level
OE1341lemma. - (committed, later) the
iofconstruction-site discriminator (Fork A) → rawiofbecomes directly-emittable.
Open questions
a. OE0624 (axis tier). Resolved — event-derivable via the MetaProperty↔MetaxisDecl 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 A — quote!-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
stdto 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:
- the compiler directive registry —
categorizeset al. registered as known directives with hard-coded arg-shapes (oxc-instantiate/src/directives.rs); - the lowering path —
lower_relational_mlt_decorators/lower_order_decoratornatively emitMetaProperty(oxc-instantiate/src/lower.rs); stditself —std/mlt/isinclude_str!-embedded and elaborated in a fixed order afterstd::core(oxc-instantiate/src/lower.rs);- the diagnostic registry — OE1903–OE1907 reserved for MLT’s specific violations (
oxc-diagnostics,oxc-syntax/grammar.toml); - the substrate mechanization —
MetaCalculus/MLTKinds.leannamescategorizes/partitions/subordinatescarriers (itsWellformed.leanpredicates 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):
| Package | Verdict | Why |
|---|---|---|
std::core | reflect | the substrate’s own classification floor (Top/Bot, primordials) |
std::rel | reflect | conventions over the relation mechanism; transitivity has no competing theory |
std::kripke | reflect | the substrate committed to Kripke frames (Decidability/Modal.lean, Standpoint/, Locality/SheafEquivalence.lean); box/diamond are substrate operators |
std::fin | reflect | conventions (currency identity, rounding-mode vocabulary) over the substrate-owned exact Money/Decimal tower; no competing arithmetic |
std::datetime / std::path / std::temporal / std::store | reflect | utility/convention over substrate primitives |
std::mlt | smuggle → OUT | the 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,MetaPropertyevents, 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 emittingMetaPropertythrough the RFD 0042 gate, expresses its semantics asderive/checkrules, 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::Eventboundary + routing an emittedMetaPropertyinto the event stream where the shipped D3 load re-checks (OE0622–OE0624) gate it; delete the privilege; thepackages/mltskeleton (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, andpackages/; notoxc-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 expresspartitions ⇒ disjoint + coverand 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 bypackages/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 samelakebuild (so they are CI-gated and held to the same no-sorry/ no-uncited-axiomdiscipline as the substrate), but namespaced out ofArgon/so theArgon/↔Rust@[language_interface]drift gate and the substrate theorem corpus stay pure.packages/mlt’scategorizes ⇒ order+1/partitions ⇒ disjoint+coversoundness lives atspec/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
lakedependency.
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, emitMetaPropertyevents 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, nostdembed, no reserved diagnostic code. The criterion is parity, not byte-identity: the metarel path moves (std::mlt::categorizes→mlt::categorizes), so the emitted event’saxis_idinterns a different string and cannot be byte-identical to the old builtin output (unlike the relation-property re-home, which stayed atstd::rel). Parity = the semantically-equivalent re-checkable event (correct axis metarel, target, value), load re-check clean,.oxbinround-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
stdis purged of theory vocabulary. MLT moves topackages/mlt; UFO/BFO/potency/ML2 follow the same path.std::kripke/std::fin/std::rel/std::corestay, justified by D1. (std::kripke’s “elaborate afterstd::coreandstd::mlt” wiring simplifies to “afterstd::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 editingoxc-reasoning/ the classifier / the meta-plane. They share one design and one tracker (a converged keystone epic), withpackages/mltPhase 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/addis 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, andstd-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/mltdistribution (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:
Person@1andPerson@2are 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::Personandpkg::mod_b::Personare simply two distinct concepts (like twoErrortypes in different Rust modules), no conflict.- The resolved graph is single-version-per-package-name. A package name is one namespace root
mapping to exactly one package (already enforced:
OE1241name = published[package].name;OE1243a 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.narinfoshape: size,constructsroot, dependency closure, provenance, signature). A Cargo-style sparse-index protocol (config.json+ per-package append-only NDJSON version records, uniform hash-prefix sharding, mandatoryETag/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.oxbinon 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;oxverifies inclusion + consistency proofs and fails loudly. Logging the semanticconstructsroot 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+snapshotfor 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
constructsdrift 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
VersionSetare 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
constructssignature 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
VersionSetfuture-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
OE1240manifest refusal of registry/version dep-forms is replaced by real resolution;version/editionbecome load-bearing;[workspace]lands;ox.locklands (bivalent:content_hash+constructsroot). - 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
oxupCDN/account; the toolchain CDN and the package registry remain distinct surfaces sharing transport. packages/mltbecomes installable, unblocking the MLT vendor-vs-registry decision.- Two substrate prerequisites become correctness floors (Open questions): freezing the
constructscanonicalization, and authoring the breaking-change taxonomy.
Open questions
- The breaking-change / compatibility taxonomy (#697). Cross-version compatibility rides
constructsdrift: additive (newpubdecl, widened bound) = compatible; narrowing a refinement, removing/renaming apubdecl, 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. constructscanonicalization 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.nrecipe). The canonicalization lives inoxc_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 viaoxc_workspace::constructsand recorded in theox.lockconstructscolumn. Vocabulary reconciled: the vault’s D-026 callsconstructs“semantic identity”; functionally it is the drift fingerprint —content_hash= byte fingerprint,constructs= semantic drift/compatibility fingerprint, nominal identity = the qualified path (no separate identity hash).- 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. - Asymmetric publish tokens (Cargo PASETO
v3.public) vs the deferred OIDC path for the internal bootstrap window. - An Argon-native non-SemVer
VersionSetover 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/iffrefinement split — the owned-vs-derived escape), RFD 0027 (the meta-property plane and the$axiscatalog), RFD 0031 (the relation-constraint plane and the D-013disjoint/complete/partitionblock surface) - Blocks: #627 (set-valued metaxis — its membership read needs the world-side semantics settled), #697 (the breaking-change taxonomy’s
closed→openarm), #249 residue,packages/mltPhase 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.WorldAssumption — Closed 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 yieldsisonly if the input was alreadyis. The closed-world collapse turnscanintonot, never intois. It cannot fabricate positive evidence.collapse_conclusion_lacks_positive_evidence(CwaOwa.lean:197) —¬ cwaTrue .can: a conclusion that exists only because the collapse invented anotcarries no positive backing.guard_iff(Reasoning/Checks.lean:288) — the RFD-0025 delta-guard passes iffviolations 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 asnotand 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 isnotunder closed-world → refused (OE0241),canunder open-world → tolerated. - Derived membership (
iff, RFD 0017): a computed membership whose refinement lands K3-undefined isnotunder 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 collapsingcan → isis 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 readscan(unknown), notnot(false). Awhere-asserted membership is still admitted (it is owned — D3); aniff-derived membership that landscanis refused (D1).#[world(open)]on a concept governs the generalization sets declared under it. Apartition/completecover under an open concept softens from refuse to K3-tolerate: an instance not yet placed in a variant iscan, not a violation. Only covering softens. Disjointness is world-assumption-invariant — a disjointness violation (OE0240) is two memberships that are bothis, 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; explicitinsert iofis 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, toleratescan. 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
openis 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 composition — cwaCollapse_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
isfromcan, whichcwaCollapse_is_iffforbids, and the fabricated fact failscwa_owa_transferre-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
-
R1 (this RFD) — record the write-side rule; no code change, the closed behavior already ships.
-
#[world]surface — lift it from reserved (directives.rs:520) to a real per-concept annotation writingWorldAssumptionMap; soften OE0241 (covering) underopen, leaving OE0240 (disjointness) invariant; refuse-on-K3-not on theiffpath. Sequenced after the meta-plane classifier seat clearsoxc-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_worldis 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. Underdefault_world = "open", NAF over a catalog-closed axis relation ($axis, and$setAxisonce #627 lands) wrongly readsnotwhere the open world demandscan. ThreadingWorldAssumptionMapinto theoxc-reasoningexecutor so NAF over these relations is world-gated (v ∉ S⇒notunder closed,canunder open) is a prerequisite this RFD’sopensoftening rests on, and it closes the pre-existing single-valued$axisgap at the same time. The soundness condition is already mechanized (AxisRelation.lean’s “K3-honest only under CWA”; the set analogue inScratch/SetValuedAxis.lean’ssetMembership_k3_honest); the executor is the catch-up. -
#760 Lean-catchup — mechanize covering-completeness soundness, ahead of or alongside the softening.
-
#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.
- #697 —
closed → openis 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:
-
Correctness never touches
/derive. Workflow decisions flow exclusively throughdispatch/queryanddispatch/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/deriveprojection. The author-facingderive()op returns nothing, is absent from the generated SDK, and is called by zero workflows. Removing the per-rule derive plane loses zero correctness. -
Every
/derive+/derive/tracecall 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 derive → engine.<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_factsto 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/mutationgains a derived-delta in its response (D2); new on-demandexplain(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/tracecapture; 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 deadderive()op is dropped. A PR lands againstontology-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 —
stdreflects, 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/Durationare primordials carrying ISO text, lowered inoxc-instantiate(expr_lower.rs); the runtime stores the string and compares it. Nochrono/jiff/temporal_rsdependency exists.std::datetimeis 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::temporalis 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.timepitfalls: 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) andjiff(BurntSushi, Temporal-inspired, stability-first API).temporal_rsis, notably, the crate whose version drift broke a local build during this work. - Doctrine (RFD 0043):
stdmay reflect a commitment the substrate already made, never smuggle one it withholds. A temporal value makes no ontological commitment — so it belongs instd, free ofufo.
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-levelCalendarsystem (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 aufo-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 forceufointostdand make trivial date math a reasoning problem.
Consequences
- The
Date/DateTime/Durationprimordials are enriched with runtime intrinsics backed by the chosen Rust impl; new value types (Instant,ZonedDateTime,TimeZone, …) are added tostd::datetime. std::datetimegrows 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 onstd::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
jiffvstemporal_rsas the intrinsic backing (leanjifffor stability; both expose a TC39-Temporal-shaped API).- v1 surface scope — the full Temporal type set, or
Instant/PlainDate/PlainDateTime/Duration/ZonedDateTimefirst andTimeZone/non-ISO calendars later. - Relation to the bitemporal substrate (
tx_from/tx_to,as_of N) — those extents should bestd::datetimeinstants; confirm the wiring. - Whether
TimeIntervalstays astd::datetimevalue 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 (
mutatebody surface — a test body is a mutate body plusassert), RFD 0025 (checkdischarge — 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-harnessexpectvocabulary (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::TestDeclwith bodyTestDeclBody { name: String, operations: Vec<Operation> }— structurally identical toMutationDeclBody. The body is the sameOperationCore IR amutatelowers to, plus the test-only assertion ops:Operation::Assert { condition, rendered }(value/boolean),Operation::AssertDerivable { predicate, args, negated, rendered }(derivability), andOperation::AssertRejects { ops, expected_code, rendered }(negative enforcement — the block’s own loweredOperationstream, run isolated). The Lean carriestestDeclin theAxiomKindtaxonomy (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, andOperation::Assert’s arity is not in the drift gate (Operationis 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.
-
AxiomKindis 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.AxiomKindis broader: it already carriesruleDecl,queryDecl,mutationDecl,computeDecl, andbridgeDecl— none of which is one of the five atoms.testDeclsits 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.) -
A test body is Core IR, which the Lean scope already covers.
TestDeclBody.operationsisVec<Operation>— the identical elaborated IR amutatelowers 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. -
The carrier/runner split is exactly what “the Lean does not cover tooling” means. The Lean
testDeclis a tag in the carrier enum; it carries no test semantics — there is no mechanized pass/fail relation, noox testmodel, 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. AddingtestDeclto the taxonomy no more “mechanizes tooling” thanmutationDecldoes. -
Trust and reproducibility (RFD 0042). The
.oxbinis 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 thatox check/ox buildvalidated — 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 testwould parsetests/*.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::TestDeclis in the.oxbinevent log and the Lean carrier taxonomy (the variant count is now 26). Tests are inert at query/serve time — onlyox testenumerates them.- A test is not a
check. Acheckis 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, viaassert 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/expectblock forms; refuses loudly until built, never silently accepted). The dedicated derivability formsassert derivable F/assert not derivable Fand the three-valued outcomes (anINCONCLUSIVEresult distinct from FAIL when a derivability assert is evaluated over an open-world relation, where absence iscan, notnot— 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 exitsox testnon-zero. The negative-enforcement formassert 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
Assertop, adjacent toReasoning/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-agnosticIDENT IDENTsync 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, structuredSuggestions 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 source — pub 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:
-
Expansion destroys source fidelity.
oxc-workspace/src/expand.rsexpands one invocation per round byformat!-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 — vsPiece::Lit/Tok— macro-body tokens), but that origin information is discarded the moment the pieces are joined into a flat string. -
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. -
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
Subpiece → the invocation argument’s real source range (the user’s text); - a
Lit/Tokpiece → 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.
- Fix —
Vec<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-derivedexpansion_idalready 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
Diagnostictype 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-timeox 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 explaincorpus 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
.arpackages, 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 withox 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.
D2 — One glossary, one cross-link grammar, no published status surface
- 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):
- 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.
- 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 (iffversuswhere). Examples are neutral; a foundational ontology appears only as one bounded worked example. - Reasoning — the five rule modes; derivation and recursion; the stratified fixpoint and well-founded semantics; the write path (
mutate); queries. - Truth under incompleteness — Truth4; world assumptions (closed by default, the per-concept open-world opt-in); defeasibility; standpoints and federation.
- 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”). - 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 bycargo 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 explainand 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) →
oxcsource (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.mdfeature 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-coveragecheck instead; RFDs leave the Reference reading path; the diagnostic corpus becomes a shared single source feeding bothox explainand 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_equivalentAST-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:
-
Document IR (the one irreversible choice). Each node lowers to a
Docoftext/line/softline/hardline/nest/group/ concatenation; a renderer resolves eachgroupto 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 (agroupcontaining a forced break never probes flat), keeping it linear. -
Greedy selector. Each
groupis 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. -
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 existingox.tomlcascade, not layout. -
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).
oxfmtchooses 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. -
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. -
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, sooxfmtrenders macro definitions andquote {}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. -
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.
-
Location. In
oxc-fmt, over the sharedoxclexer/parser/CST — no second grammar (the Nix cautionary tale).ox fmt [--check] <paths>and theoxfmtbinary share one path;--checkis 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:
- Parse-preservation — the output re-parses cleanly (cheapest gate).
- Token-and-comment equivalence — the safety guard above, enforced on every format.
- Idempotence —
fmt(fmt(s)) = fmt(s)byte-exact, over the whole corpus and under fuzzing. - 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.
- 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
| Topology | Where it runs | Access | Analogue |
|---|---|---|---|
| Embedded | runtime linked into the host process; local storage | native call (Rust crate / TS napi) or SDK over an embedded handle | SQLite, DuckDB |
| Sidecar | Argon process beside the host on one node | wire (/v1) or SDK over a local client | local Postgres |
| Standalone | Argon as its own durable, networked service | wire or SDK over a remote client | managed 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 shapes — Truth4, 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/
protocprecedent: the model is the neutral source (like a.proto),ox genis the generator, and the host’s native build drives codegen (build.rsfor 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.
/v1HTTP is the one wire protocol initially; a binary/streaming protocol can later sit behind the sameRemotehandle 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 rustis a new first-class deliverable, co-equal with the TypeScript target: typed concept types, typedquery/mutate/derivewrappers overConnection, the CBOR codec, and a generated/v1client.- 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-tideplugin shim live Tide-side. oxgrows cargo-style plugin discovery (ox <name>→ox-<name>on PATH), soox tide runis convenience andtide runstandalone always works.- Host integration follows the native build of each language;
ox.tomlremains 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_DIRor a published crate? Leaningbuild.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?
/v1HTTP now; a binary protocol only if the hot path demands it, behind the sameRemotehandle. - 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
oxupdistribute 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_nanoslives in the storage backend behind a write lock; every append assignstx_fromfrom it and ratchets it forward, never backward (oxc-storage-mem).Store::current_tx_nanos()returns the largest assignedtt. The clock is already a strictly monotonic, single-threaded-by-construction allocator. - MVCC-shaped reads exist.
RuntimeReadPoint/RuntimeAsOfcarryNow | AtTt(tt) | AtVt(vt) | At{vt,tt};query_extent_atfilters the append-only log by bitemporal visibility (tx_from ≤ tt < tx_to, valid-time window). Paginated reads already pin a concretettinto the cursor so a multi-page walk sees one fixed snapshot (oxc-serveR-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 theServiceStateRwLock. - 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.
IncrementalMaintainerholds persistent join arrangements and a DRedc retraction structure;maintain_after_commitapplies 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::evaluateis the single evaluation path; WFS handles recursion-through-negation per stratum. - A durable, content-addressed log exists.
oxc-connection::open_durableoverFileKvStore: 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
ForeignRelationSPI 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:
- No durable group commit. Each
append_batchis 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. - 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.
- The visible-
ttadvance is implicit, not a published commit boundary. Readers resolveNowto whatevercurrent_tx_nanos()happens to be; there is no explicit “last durably-committed and IVM-maintainedtt” watermark that a reader pins to get a snapshot guaranteed consistent with maintained derived views. - 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.
- As-of-past reads of derived views are unsolved. Current-
ttderived reads hit the maintained model; a derived read at a pasttthas no answer short of full recompute. #978 names this the one genuinely open data-structure question. - 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:
- 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. - Durable-acknowledge — the event bodies (already content-addressed segments) and the scope manifest reach disk and
fsyncreturns. This is the durability point and the group-commit batching point (D4). - Advance-visible-
tt— publish the new committtinto 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-maintainedtt), and that concretettis what the read uses (and what a paginated cursor pins, extending the existing R-M17 mechanism from “the store’scurrent_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 +
fsyncacknowledges 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
ttorder 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 durablett.
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_placeso 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:
- Open the durable store; read each scope’s manifest (watermark + ordered segment ids).
- 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.
- Replay the manifest-ordered events into a fresh store, preserving each event’s
tt(so pinned historical reads reproduce). - Reconstruct the maintainer. Today: a cold rebuild via
Engine::evaluateover 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’stt. The checkpoint is declined on any key mismatch (schema change, divergent watermark) and recovery falls back to full rebuild — never a stale model. - 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
FileKvStorereference 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-
ttallocator 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
Nowreads resolved against it (not rawcurrent_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 atWnever observes a commit attt > W; a reader’sNowis 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 equalsEngine::evaluateover the committed base facts at thattt(theassert_maintains_against_oracleharness 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
503load-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 atttfrom 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 equalsEngine::evaluateover the base facts visible at thattt; 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).
FullRecomputeMaintaineris the differential oracle: every incremental maintenance is proven equal toEngine::evaluatefrom scratch (assert_maintains_against_oracle). Phase 1 and Phase 5 extend this oracle to the watermark boundary and to pastttrespectively — a maintained/replayed derived read must equal the from-scratch model at thattt. - Snapshot-isolation conformance suite (Phase 0). Property tests over interleaved readers and the writer: monotonic visibility, no torn reads, stable
Nowwithin 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
Connectionand the/v1standalone 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 4per house rules. Each phase merges only with its suite green.
Consequences
oxc-serve/oxc-connectiongain 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 andappend_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
Connectionand/v1surfaces 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-ttarrangement 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-
ttreconstruction must replay the rules as they were at thattt(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 theModuletoday 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 —
forgetunder MVCC. A capability-gated physical erasure conflicts with snapshot retention: a reader pinned before aforgetexpects to see the data, butforgetexists to erase it. Doesforgetwin immediately (erase segments, breaking older snapshots — the data-deletion intent) or lazily (erase only once no live snapshot pins it)? Recommendation:forgetwins 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.
-
Federation does not collapse rival values.
Store::query_derive_federatedkeys the AFT info-join on the full tuple (encode_tuple), so distinct per-characterization values are distinct tuples that each survive asIs. What federation discards is the standpoint index (it folds over(_standpoint, rows)).Truth4::Botharises only for the identical tuple asserted positively in one standpoint and negatively in another (Federation.lean::federate_eq_both_iff;AFTEquivalence.leanT3:both ⟺ no consistent global K3-section). -
Standpoints are the characterization carrier. A standpoint scopes facts and derive rules (§11). Each standpoint is internally K3-consistent under the default
strictconsistency 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 bymaterialize_predicates_for_standpoint(DEFAULT layer ∪ stamped-sfacts; the runtime side ofVisibility.scoped_view_eq_default_union_own). -
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, withboxvia stratified negation or count-equality. The<:ancestor closure is already precomputed (standpoint_lattice.rs::ancestors,global-control.standpoint_ancestor_sets). -
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. -
There is no single “modal engine” — evaluation is a compile-time strategy choice (translation to Datalog vs. tableau-on-demand). The decidability ladder places
modalas its own tier (cost modal+FOL) that dispatches: static discharge, else an external reasoner overstd::kripke(Decidability/Modal.lean). -
Two gaps are live in the implementation. (a)
AtomIR::Modal{op,inner}is frame-blind and is stripped toinnerat compile (compile/rule.rs), while being classifiedTier::Modaland admitted — so abox/diamondprogram builds, runs, and is confidently wrong for anything but the rigid-default case (a live silent-wrong). (b)std::kripkeis not built (no package), and there is no standpoint-frame discharge in the Lean (only the rigidity-framefixeddischarge inStaticDischarge.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 (theBothread).across s in [S₁, S₂](binder) — enumerate: bindsto each standpoint in turn, evaluate the body in standpoints’s view, and projectsas a first-classStandpoint-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.foralllowers 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:
| Atom | Frame / target | Action |
|---|---|---|
box/diamond(x : T), T fixed/rigid | rigidity (classification) | discharge to x : T (the existing sound reduction, now principled) — StaticDischarge.box_fixed_discharge |
box/diamond(P), explicit standpoint frame, decidable | standpoint | translate (D3) |
box(¬(x:T)), dynamic/anti-rigid rigidity frame, empty standpoints, rigid roles, nominals, unstratified modal, modal⊗temporal | needs tableau | refuse — 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
Truth4stays four-valued.Bothis genuine same-proposition contradiction (= no consistent global section), orthogonal to rival values.- Federation default stays
Paraconsistent(no-binderacross). 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) + theStandpoint-typed column + D2 reads as ordinaryforall/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 (
reachrelation + thebox/diamondlowering). Requires D7 below. - Phase C — the escape hatch. D6 tableau engine +
std::kripkepackage.
D7 — Prerequisites surfaced by verification
AtomIR::Modalframe recovery: determine the frame at lowering (wherectx.standpoint_id/ theacrossset / “isinnera classifier atom” are available) and emit translated/discharged ordinaryAtomIR, soAtomIR::Modalsurvives only for the tableau-bound case. This avoids a wire/IR shape change.- Query lowering order: thread the
acrossset into the lowering context before body lowering (today it is extracted after). Derives already carry the enclosing standpoint viactx.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
testatom — the in-language golden vehicle), RFD 0050 (documentation architecture / “every package compiles and runs in CI”), RFD 0018 / RFD 0020 (the reasoner andEngine::evaluatethe 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
FullRecomputeMaintainerIVM 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 ordinaryoxpackage (ox.toml,root.ar, model modules) plus its golden: atests/directory (thetestatom) and/or ademo.tomlscenario withexpectassertions. AREADME.mdcarries provenance (guards:/feature:/method:). Refusal fixtures carry arefuses.txtlisting theOE####code(s) the build must be refused with. - Gate.
compiler/crates/oxc-driver/tests/conformance_corpus_gate.rs— acargo nextesttest that discovers everyox.tomlundercorpus/and shells out toCARGO_BIN_EXE_ox(always source HEAD, never a stale installed toolchain). Positive entries mustox buildthen passox testandox run-scenario; refusal fixtures must be refused byox check --codeswith 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
ProgramFeaturerules, bypassing the surfacefn/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_iterationguards the nearest expressible contract meanwhile). - #1026 — the scenario harness drops the Truth4 verdict from federated rows, so
expectcannot assert it;standpoint_federationpins membership + scoped views meanwhile. - The corpus may later sweep
examples/goldens (theirdemo.toml/testsare 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 Module — compiled_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
The legal-temporal model is settled prior art
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-timet_l, rule time-of-forcet_v(the rule’s valid-time), repository timet_r(transaction-time), and derivation timet_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
AxiomEventcarries aBitemporalExtent { 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_timeis 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,defeatsedges, anddefeat_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 dateDsetsvt_start = D(retroactive enactment isvt_start < tx_from, the existing data-plane “retroactive correction” shape). Prospective repeal / sunset setsvt_endforward.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:
AsOfvt); - rule-VT — which rules were in effect (new);
- viewpoint-TT — which recorded state of the repository (facts and rule transcriptions) we believe (exists today:
AsOftt).
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 atvt, under the law in force atvt— the headline audit query, free, no extra surface;Now⇒ current facts, current law (the conservativity case);- explicit
rule_vtunlocks the counterfactual quadrant — old facts under today’s law (fact-VTpast,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 equalsEngine::evaluateover the flatcompiled_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 therule_vtcoordinate 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)inoxc-runtimereusingevent_visible_at; therule_vtcoordinate 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-scratchEngine::evaluateover 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 atier:folfollow-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 isReadPoint { data: AsOf, rule_vt: Option<DateTime> }withNonetracking the data point’s valid time, and the in-process API isquery_derive_at(.., rule_vt). Specified in the reference manual’s Rule validity over time (temporal substrate) andAsOfsemantics sections. - OQ3 — Annulment follow-on shape. The
tier:folretroactive 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
/v1serving surface), RFD 0036 (heterogeneous stores — the durable backends a deployment configures), RFD 0025 (thecheckdelta-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, andx-fork-idto resolve the(tenant, fork, principal, standpoint)scope of a request. These are scope signals consumed downstream of the network edge. tracingis 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 byRUST_LOG, defaulting toinfo. Per-request structured logging (log_request: method, path,x-tenant-id/x-fork-id, wall-clock duration, outcome) already flows throughtracing.- 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_CHANGEopts 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/AdhocPolicyalready 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:
- 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.
- Observability is logs-only.
tracingis 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. - 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).
- 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. - 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 inoxc-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, andx-standpoint-id(andx-fork-id) from the authenticated identity, overwriting whatever the client sent. These headers are never passed through from clients: a client-suppliedx-tenant-idis 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 inOperabilityLimits, 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 durablett). - 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 atomicModuleswap 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 existingreload_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 unlessARGON_ACCEPT_SCHEMA_CHANGEopts 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 ofServeConfig,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
Dockerfilebuilding 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
.serviceunit 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 (
/metricsscrape) instead of OTLP. Rejected as the primary path: metrics only, no traces, and a second telemetry stack for traces. OTLP via the existingtracingbridge 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-servegains the platform shell: a graceful-shutdown signal handler (with_graceful_shutdownwired to SIGTERM), a layered configuration loader (D4: flags > env > file > defaults, secrets by reference), and an OTel export layer over the existingtracingsubscriber (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 andx-tenant-id/x-principal-id/x-standpoint-idconsumption 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
OperabilityLimitshot-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::kripkeWorlds andaccessiblerelation — 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)anddiamond(P)wherePis a single positive predicate atom whose evaluation is per-world. - Value algebra: K3 —
Truth4constrained to{Is, Not, Can}. - Consistency: strict. A proposition forced to both
IsandNotat 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.leanbox).diamond(w, P) = ∃ w'. R(w, w') ∧ P(w')— possibility is the union (Modal.leandiamond).- 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
IsandNotat 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 ifPisIsat every accessible world; aCan/unknown world does NOT satisfybox(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 thediamond/box-rules overaccessible. - Blocking. A single explicit frame is finite (explicit
accessiblefacts), so slice 1 terminates by the finite frame. Subset/equality blocking is still implemented for the general/cyclic case so a cyclicaccessiblerelation 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 ∧ Notclash 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:
translate_standpoint_modals(instantiate) refuses an unframed modal withOE1104(InstantiateError::ModalNotEvaluated).- The rule compiler (
compile_atom) refuses a survivingAtomIR::ModalwithOE1104(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)
- Differential oracle. The
KripkeExecutor’s result MUST agree with (a) the semi-naive Datalog executor on any program in the decidable overlap, and (b) theModalFramesemantics —box(P)= the intersection (∀ over accessible),diamond(P)= the union (∃). A differential test runs over an explicit frame wherebox ≠ diamond(a proposition true at some accessible worlds, false or unknown at others) so a swap or collapse of the two operators fails it. - Termination. Tested on a cyclic
accessiblerelation — blocking must fire. A non-terminating tableau is a failure. - K3 clash soundness / open-world box.
box(P)holds only ifPisIsat every accessible world; aCan/unknown accessible world must NOT satisfybox. Tested explicitly (the classic open-world modal trap). - 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::kripkeframe (slice 1’sbox/diamondoveraccessible) fibred over a query’sacross [...]standpoint set (or astandpoint {}block). Per-standpoint K3 result, info-joined across standpoints under the query’sFederationPolicy(paraconsistent:Bothsurfaces; 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 value — Is / 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 becomeBoth. (Federation.leanfederate_eq_both_iff: the result is.bothiff some source said.bothor two sources genuinely disagree —hasIs ∧ hasNot. ACancontributes to neitherhasIsnorhasNot, so it cannot induceBoth.)
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:
Isiff every accessible successor hasP = Is(necessity confirmed);Notiff some accessible successor hasP = Not(a determined-false witness defeats necessity);Canotherwise — no successor isNot, but some successor isCan(necessity is open-world undetermined: it could still go either way);- vacuously
Isat 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):
Isiff some accessible successor hasP = Is(a witness);Notiff every accessible successor hasP = Not(every possibility foreclosed);Canotherwise — noIswitness, but some successor isCan(a witness could still emerge);- vacuously
Notat 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):
-
Frame-aware dispatch fix (closes a latent silent-wrong).
translate_standpoint_modals(instantiate) previously checkedis_explicit_kripke_modalonly inside itsroots.is_empty()branch — so a Kripke modal appearing inside anacross [...]query or astandpoint {}block (roots non-empty) was mis-routed into the standpoint-frame Datalog translation (synth_standpoint_modal), silently producing a wrong extent instead of preservingAtomIR::Modalfor the tableau. The fix checksis_explicit_kripke_modalfirst, independent ofroots: a Kripke modal is preserved asAtomIR::Modal(→ tableau) in every standpoint context. A non-Kripke modal with no frame still refusesOE1104; a non-Kripke modal with a standpoint frame still translates as before. -
Per-standpoint query-routing loop. When a federated query carries a non-empty
acrossset and a KripkeCompiledAtom::Modal, the runtime, for each standpointsin the across set, materializess’s per-standpoint catalog (materialize_predicates_for_standpoint(s)— DEFAULT ∪ standpoint-own), runs the slice-1KripkeExecutorover that per-standpoint catalog, and reads the faithful K3 federatable value per head tuple. The contributions are then info-joined by the already-shippedquery_derive_federatedunder the query’sFederationPolicy. The federation for a modal head bypasses the plain(in_pos, in_neg)classify_source_rowspath — that path can only ever produceIs/Canfor a modal head (the executor’s fail-closed projection writesIs-only), so it cannot carry theNotfederation 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)
- 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/diamondevaluated 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 isCan/unknown while another isIs(→Is, notBoth). The Can-vs-Not distinction is the central oracle-covered property. - Mis-routing closed. A Kripke modal in an
across/standpointcontext routes to the tableau, not the standpoint-frame translation (tested). - Cross-nesting refused.
box_standpoint(box_kripke(P))(and converse) are refused (tested). - Soundness factoring. Slice-2 soundness = slice-1 tableau soundness (the
per-standpoint K3 value is the slice-1 frame semantics, distinguishing the
Canit previously projected away) ×Federation.leanfederate_eq_both_iff(federation isBothiff 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 toModalFrame.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.lean↔compiler/crates/oxc-protocol/) - Tracks: issue #1038
- Prior art: the project’s own conformance-vector loop (
EmitVectors.lean→compiler/tests/lean-vectors/*.json→conformance_replay.rs, CI-freshness-gated incheck.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:
- It keys on the unqualified inductive name (the token after
inductive) and resolves duplicates with.or_insert— first-in-filesystem-walk-order wins.read_dirorder is not deterministic across machines. - There is more than one tagged inductive with the same short name.
RuleModeis@[language_interface]-tagged in bothSyntax/Rule.leanandStorage/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. (TermandAtomIRalso have same-named twins inReasoning/EvalProgram.lean; those are saved today only because the twins are untagged.) - 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 theDecl→grammar.tomlcheck 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+ anemit-interfacelean_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 }, … ] }, … ] }arityis the constructor’s field count (parameters excluded) — the same convention the Rust mirror table already uses (metaCalculus : MetaCalculusAtom → Atom⇒ arity 1). Structures carrykind: "structure"and their singlemk(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.rsparses the manifest withserdeinstead of scanning.leanfiles. It then:- Detects ambiguity: if two manifest decls share a
shortNamewith differentctors, hard-error (the divergence hole). Identical twins (today’sRuleMode) are tolerated and bind unambiguously. - 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. - Enforces coverage: every
inductivemanifest decl is either mirrored or in an explicitUNMIRRORED_WAIVERSlist with a one-line reason. A newly-tagged-but-unmirrored type fails the gate instead of passing unseen. - Keeps the
Decl→grammar.tomlvariant check and a sanity floor, now exact against the manifest.
- Detects ambiguity: if two manifest decls share a
-
Freshness. CI re-emits and diffs, exactly as the conformance vectors are gated (
check.yml):lake exe emit-interfacepiped todiffagainst the committed manifest, with a clear regen instruction on mismatch. Thecargotest only consumes the committed file (it does not requirelake), matching the established split.
Non-goals (sequenced separately)
- Namespacing the root-level surface AST types (
_root_.Expr/Pattern/Literal/RuleAtom/RuleMode/…). These shadowLean.*underopen Leanand are a latent footgun for any metaprogram in the tree (it is what broke themechprobes before the tool-side fix). The typed manifest emits qualified names regardless, so it composes with a later move intoArgon.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_inserta 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.lean↔compiler/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:
- 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?
- 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
sorryand zeroaxiomin the build target; the strict proof-discipline gate (mech verify) passes; the development is faithful (mech checkflags are structural false-positives —¬-lemmas, empty/nil base cases). - Crown jewels to preserve verbatim: the
Foundation/bilattices (the K3 “noCompleteLatticeonMetaValue” landmine is mechanized as a theorem,infoJoin_escapes_K3, not merely a comment) and theReasoning/Datalog/AFT proof spine. Mathlib reuse is already disciplined (fine-grained imports, no blanketimport Mathlib). - The dominant defect is namespacing incoherence: 101 of 167
@[language_interface]types are at_root_; of 146 files, 57 usenamespace Argon.*, 48 use none, 41 use a bare-generic namespace (Modal,State,Closure). Core typesAtom,Term,Expr,Literal,Tier,Truth4shadowLean.*/Mathlib — the footgun that broke themechprobe (#1038). - The coupling caused the mess, and RFD 0059 already cut the cause. The original drift gate text-scanned
.leanfiles 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
leanCI 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, nolakeneeded) gates the queue, and the drift key isshortName= the last namespace component, so moving a type from_root_intoArgon.Syntax.*keeps its key stable; the gate survives namespacing. A reorg’s failure mode is therefore post-merge red onmain(a stale committed manifest), not a queue block. - Two content holes, not just structural debt:
CoreIR/Lowering.leanis a placeholder (lowerRuleemits empty heads/bodies,deriveCategoryhardwires.cat1) — the content of the preservation theorem is absent; andARS/Spec.lean’sars_contracts_holdreduces 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.
- Namespace = directory path under
Argon.*. Every declaration lives underArgon.<Layer>[.<Sub>]; no domain type at_root_; no bare-generic namespace (namespace Modal→namespace 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. - 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/Interfacerest only on Mathlib). The resting state is bounded entanglement; only the accidental edges are debt. - 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. - 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). - 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 ofdrift.rshard 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. - Close the two content holes (separately scoped, after the structural work). Implement
CoreIR/Loweringas 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 vacuousars_contracts_holdwith the real contract, or, if Rust wiring genuinely blocks it, an honest tracked open obligation rather than aTrue-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
metaordertier (reflection / higher-order instantiation) is “bounded decidable; otherwise not.” Decidable ⟺ bounded is the ladder’s organizing principle. - Soundness: impredicative
Type:Typeis 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),Storageimports it. (Conflict note:Storage/AxiomBody.leanis modified byfeat/0.3-surface-operators-630-631; sequence after it lands.)WorldAssumption×3 — untagged, but divergent constructor order acrossSchema/and the twoTypeSystem/Soundness/copies (a latent bug). Unify to oneArgon.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 asArgon.Reasoning.Oracle.*so the divergence is named, not accidental. No merge.
Migration plan (proof-safe slices, in order)
- Layering guard (done in this RFD’s PR) —
check_layering.py+ the committed 52-edge baseline, wired as a lake-freelean-layeringCI job (runs on PRs, enforces in the merge queue). Freezes entanglement and foundation purity now; no code moved. - Namespace normalization — move every declaration under
Argon.<Layer>; kill_root_and bare-generic namespaces; ensure noopen Leanin the spine. Proof terms are untouched (rename +importchurn only); the driftshortNamekey survives; regenerate the committed manifest in the same PR. This does not change cross-directory edges, so the layering baseline is unaffected. Sequence theMetaCalculus/*andStorage/AxiomBody.leanfiles aroundfeat/0.3-surface-operators(do them last or after it merges). - 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-neutralTypeSystem → Runtime(MutationWrites, which could move toRuntime). Small and optional; the guard’s stale-edge check locks any win. Do not re-homeMetaCalculus/{IsCanNot,Reflect,AxisRelation}. - Dedup —
RuleMode,WorldAssumption; namespace the oracle twins (Argon.Reasoning.Oracle.*). Coordinate onStorage/AxiomBody.lean. - Re-arm the gate — pull freshness into the queue gate; restore mirror-alignment to hard.
- 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
.vsixcarries 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
VERSIONfile 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-scriptcargo:rustc-envmechanism. This RFD supersedes the git-tag-derived design (git describe, the Go/setuptools-scm/vergenpattern) 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-dependent — git 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:
| build | composed ARGON_VERSION |
|---|---|
| dev (default), git reachable | <base>-dev+g<shortsha> |
| dev (default), no git at all | <base>-dev |
| stable channel signal | bare <base> |
ARGON_VERSION set directly | that 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].versionandoxup/Cargo.toml [package].versionare0.0.0, forever. Cargo requires the field; nothing reads it. (Normal dev never editsVERSIONeither — 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) are0.0.0-devplaceholders. The.vsixis stamped fromARGON_VERSIONat package time (release.ymlbuild-vsix); a plain checkout keeps the placeholder. The editor version-skew nudge (#675) treats a0.0.0/-devstamp as “unstamped” and suppresses the warning. - The VS Code
package.jsonmarketplace version is stamped fromARGON_VERSIONat package time.
What stays deleted
cargo xtask bumpandcargo xtask check-versions(the entirecompiler/xtask/src/version.rs).- The
check-versionsstep incheck.yml’sdriftjob. - 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:
- gate — enforce zero open
P0-blockerissues (RELEASING.md); - tag —
git tag "v$(cat VERSION)"at the greenmaintip and push it (which firesrelease.yml’s stable path; the stable build reports the bareX.Y.Z). An explicitversioninput escalates to a minor/major (must be>=the file line); - atomic advance — bump the patch (
X.Y.Z→X.Y.(Z+1)), write it back toVERSION, and fast-forward push it tomaindirectly as the bypass bot (aprotect-mainbypass actor — no PR, no merge queue, no starvation). The push is rebase-retried in casemainmoved (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 conservativebroadfallback), the on-demand-tag release model (RELEASING.md), RFD 0061 (oneVERSIONfile — no version-lockstep gate to run), RFD 0060 (the Lean layering guard, a pure-text check that gates every tier while the heavyleanjob 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=targetschange-impact, Nxaffected, 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:
- 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_namecomparison buried in eachif:. Reading “which jobs run for a merge_group?” meant tracing every job. A miswired tier was invisible. - 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.
| Tier | Event | What runs | ci-gate |
|---|---|---|---|
| 1 — PR | pull_request | affected 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 — queue | merge_group | Tier 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-merge | push to main | Tier 2 + full lean + full macOS compiler-test + full-workspace compiler-test-release | enforces (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
lean—lake build+ the vector/manifest freshness gates + the batched-Lean conformance differential. Slow, OS-independent, rarely combination-sensitive. (Its pure-text companion, the RFD-0060lean-layeringguard, does gate every tier.) - Full macOS
compiler-test— the macOS test leg. The queue keeps a compile-onlymacos-build-smokeso 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-gateis the sole required check + its self-guard: a step that assertsci-gate.needscovers every job in the workflow, so a job added later but forgotten inneedsfailsci-gateloudly 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.
maincan 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 forgottenci-gatewiring is caught by the self-guard, not by a redmain.
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 therule_vtselector), 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 FORaddress the past; Argon RP-004 (the temporal substrate already in the reference) — defines the bare view as the snapshotVT_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
AxiomEventcarriesBitemporalExtent { vt_start, vt_end, tx_from, tx_to }, andevent_visible_at(event, vt, tt)filters by both.query_extent_at(vt, tt)uses it. But the derived path (query_derive→seed_base_catalog→Engine::evaluate) never threads a read-point: it scansscan_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 embeddableConnection), 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 explicitquery_derive_atAPI; 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 tovt(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 choices — ValidTime { 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
Nowand a derived read atNowmust agree on which facts exist; threading the sameevent_visible_at(vt, tt)into the EDB seed is what makes them agree. - Determinism is preserved by construction. Freezing
vtat 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 intervalvalid_untilis 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 explicitAtVt. 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
nowas its default, not a separate mode. Option<i64>valid-time on the read API instead of a typedReadPoint. Rejected:Noneconflates “deliberately all-valid-times” with “unspecified,” so a boundary that forgets to resolvenowsilently 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_gencache 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_untilinterval-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 embeddableConnection, and theoxdriver resolveNowto a concrete(vt, tt); in-process/test callers inject a fixedvtfor reproducibility. - Rust leads the Lean here (engine/read semantics; AGENTS.md). The bitemporal read-point over
BiStateis aLean catch-upfollow-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/$iofreflection relations are materialized into the catalog; they inherit the snapshot EDB, but whether a reflection read ever wants a distinctrule_vtis deferred until a use appears. - OQ2 — Boundary
vtskew. A served request freezesvtat ingress; a long-running federated read spanning external scans uses that single frozenvtfor 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
mergeattribute / 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::loadare 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 singleEngine::evaluatepath both depend on it. - Generated artifacts conflict even when their inputs do not.
grammar.tomlfeeds a codegen step (cargo xtask gen) that packs discriminants by file-order index intogenerated.rsmirrors 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 monolithicgrammar.toml. Adding a diagnostic or node adds a file; it does not edit a shared list.lower/<kind>—lower.rssplits into one module per declaration kind (concept,rel,fact,rule_ref,mutate,query,standpoint,defeat,meta, …) plus a thinmod.rsdispatch, andatom_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 Storesiblings — the runtime’s non-spine surface splits into cohesive sibling modules each carrying one concern’simpl Store(store_init,store_read,query,rules,refinement,standpoint,federation,read_model,persist,classify,defeasible, …).Store/Modulefields stay put inlib.rs; child-module privacy carries the access the split needs, so no field is widened topubto 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::evaluateinvariants. A uniform ceiling here would optimize a number, not the contention. - Keep
grammar.tomlmonolithic and resolve codegen conflicts by hand. Rejected: the per-entry merge is the whole point; a hand-merge of index-packedgenerated.rscorrupts 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 committedgenerated.rsmirrors 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::evaluatepath; single-sourceddispatch_*_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
/v1wire-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_inneroutput 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), andcheck-driftall 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 waygrammar.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, theCompiledRuleexecuted form, theRelationCatalogpublic-API seam this facade extends), RFD 0035 (the composable operator-tree pipeline — thelogical::lower→ optimizer →PhysicalOppath 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:
-
The runtime is built on the reasoner’s value model. The store, write path, and read model name
oxc_reasoning::compile::Valueand its cluster at 299 sites inoxc-runtimealone (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_TAGconstants — and thecivil/tz/walltemporal helpers,ids::InternalId). This is the shared runtime value vocabulary, but it physically lives inside the reasoner’scompilemodule, so the runtime cannot name a value without depending on the whole engine. -
The store reaches around the
Engineinto its internals. On the store/persist/read-model path the runtime names ~32 engine-internal symbols acrosslogical/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 preserved — Engine::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-protocolis a confirmed leaf. Its only dependencies areblake3,ciborium,semver,serde,serde_json(and optionalsqlx) — zero intra-workspaceoxc-*deps. A new value crate depending onoxc-protocol+ numerics (num-bigint/num-rational/num-traits,ciborium,jiff) introduces no cycle.- The value cluster is mis-located, not engine-coupled.
Valueis a pure-data enum (Bool/Int/Date/Individual/Internal/Name/Null/Symbol/Cbor);Tuple,SymbolCompare, the CBOR/numeric codec, and thecivil/tz/wallcalendar helpers (delegating tojiff) have no dependency on join algorithms, the optimizer, or the fixpoint loop. They sit incompilefor historical reasons — they were authored where they were first needed. Engine::evaluateis genuinely fused and must stay so. It reorders each body (SIP), dispatches to the executor, and is the sole entry behindPhysicalOp::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/optimizerare 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, noimplthat reaches the engine; the data vocabulary, nothing else.Tuple,SymbolCompare, the CBOR/numeric codec (encode_tuple/decode_tuple, theenum_*/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_TAGconstants).- The
civil/tz/walltemporal 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 0021RelationCatalogpublic-API seam). - Becomes private to the engine (no longer nameable by the runtime):
LogicalPlan::ApplyandApplyGate(the store is their sole external consumer →pub(crate)). Note: the materialize unit isphysical::MaterializedRelation(there is noMaterializedLeaftype); 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 fusedfreeze_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) callsoxc_reasoning::logical::lower::{try_evaluate_via_physical, try_evaluate_with_frozen_stratified_naf, try_evaluate_with_frozen_lower_stratum};checks.rscallsevaluate_via_physical;strict_cache.rsand the rule tests callpipeline_compiled_rules.- That path runs
lower_rule/lower_body→OptimizerPipeline::with_default_passes()(logical/lower.rs) →PhysicalOp::{Materialize, Recurse}(physical/mod.rs), whereRecurseis the fused semi-naive fixpoint andMaterializeis 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-valueis 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.QueryEnginenames 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) whileLogicalPlan/ApplyGate/MaterializedLeafbecome 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::evaluateinstead of cutting at thelogical::lowerseam. Rejected: a thin wrapper over the single evaluation entry re-exposes the verylogical/physicalinternals 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 refactorlogical/physical/maintainerwithout breaking the runtime. Both faces must be cut for disjoint ownership.
Consequences
- Disjoint ownership becomes possible. The engine owner evolves
logical/physical/optimizer/executorbehindQueryEngine; the runtime owner evolves the store/persist/read-model path against{oxc-value, QueryEngine}. The fusedEngine::evaluatepath 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-valueis a new leaf crate depending only onoxc-protocol+ numerics; the DAG stays acyclic (no cycle, confirmed against the leaf check).- The narrow dead residue (
runtime/operators.rs, unproducedLogicalPlan::{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 inoxc-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::evaluatestays the single fused evaluation entry. - RFD 0021’s
RelationCatalogpublic-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-valueis a leaf addition, no cycle.
Open questions
- The exact
QueryEnginemethod set. ~7 methods + theProjectionMaintainerassociated 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 anoxc-valuetrait the engine implements.ProjectionMaintaineras 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 — theConnection::Remotefederation path the remote profile rides), RFD 0046 (derivation serving surfaces — the structuredProofTree/explainmodel 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’sRuntimeServiceis the state holderoxc-servewraps; thedispatch_*_coreentry points andexplain_fork_fact_proofare framing-neutral.oxc-serveis a thin axum/v1layer over them (RFD 0014). A coding-agent surface is a third frontend over the same core — sibling to HTTP, not a new runtime. RuntimeServicecarries the coherence machinery the bareConnectiondoes not. It holdsreload_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), theOperabilityLimitsgovernor, per-scope mutation locks, cursor pagination over query rows, and the fork lifecycle. The bare embeddedConnection(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.
OperabilityLimitsenforces 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 theConnection::queryconvenience omits it. Riding the fullDispatchRequestthroughRuntimeServicegets paging for free. - The coherence primitives already exist.
oxc-oxbin::content_hash(SHA-256 over canonical CBOR, §18.6), the loadedmodule_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_factreturns a structuredProofTree; a clean non-derivation is reported asderived:false,root:None. The check-violationwhyis, 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 fromgrammar.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 headline — ox 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, watchingtarget/<pkg>.oxbin) +oxc-workspace— the full authoring and runtime surface, coherent by construction; what a coding agent uses. - Remote = a
/v1HTTP client against a deployedox runtime serve(theConnection::Remoteimpl, 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_buildwrites the.oxbin, then explicitly callsreload_if_changedbefore 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_statusexposes{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 samefull_package_diagnosticsthe 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.oxbinbehind the loud gate, atomically write, reload, and verifyactive_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; bitemporalas_of; cursor-paged; returns the FULL four-valued (Truth4) envelope (per-row truth, hidden K3 counts). Federation is driven by the query’s declaredacross [...]clause — there is no per-callacrossoverride (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; fullTruth4.argon_derive— materialize a derived extent; fullTruth4envelope, optional per-tuple proof tags.argon_scenario_run— materialize the whole derived model once and project every head in the FULLTruth4envelope ({projection, factsDerived, facts:[{head, tuple, truth4, hidden}], hidden}). A scenario materializes only definite-Isfacts (foreign connectors hard-refused;across/ enumerate heads held separately; defeat planes 2-valued), so each fact carriestruth4:"Is"andhiddenis 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:truerenders the bare{head, tuple}. (As-built rename of the RFD’sargon_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 corederived_snapshot_value— the/v1/snapshotflat shape is unchanged.)argon_explain_fact— the full structural AND/ORProofTreefor one fact (live read-point only);maxDepthfolds the tree. A not-derived fact returnsderived:falseplus awhyNotnegative-provenance tree — an honest membership answer (D12), not a synthesized why-not.argon_expand_proof— re-reconstruct a fact’s proof at a largermaxDepthto drill past afoldedmarker (D8). (As-built: folding is by depth, not by per-node id —maxDepthis the stable handle; same reconstruction asexplain_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 inoxc-runtime, prose + structured trace — is NOT what shipped here.argon_whyreturns the structured firing set the runtime already produces; the deeper D11 build item inoxc-runtimeis 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, whenincludeDerivedDeltais 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-basedbatchStep. RidesRuntimeService::run_batch(lifted by #1205), with the step projection single-sourced inoxc_connection::render_batchso the/v1/batchand MCP surfaces cannot drift.
Compute (pull-plane evaluation):
argon_compute— evaluate a declared compute fn over the scope’s committed state (optional bitemporalas_of); returns{value, emissions, moduleHash}. A top-level payloadless enum constant renders{tag}(RFD 0027 D5). RidesRuntimeService::run_compute(lifted by #1205), with the value projection single-sourced inoxc_connection::render_compute(the same/v1compute 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 separateargon_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,constructfilters 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 wholeOE####catalog (every code, severity, description, long-form explanation, reserved flag); the agent’s authority on what a code MEANS, sourced from the sameOxcDiagnosticCodethe 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’sargon://schema/{path}per-path template — per-construct lookup is theargon_schema_lookuptool’s job; the resource is the index.)argon://standpoints— the standpoint lattice (artifactstandpoint-latticesection, §D.7).argon://tiers— the decidability tier table (artifacttier-tablesection, §D.8).argon://module— the active artifact’s load status + module hash. (As-built rename of the RFD’sargon://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? Drivesargon_status→argon_explain_fact→argon_expand_proof, reading every row’struth4.argon_diagnose_absence— the counter-abduction loop: a query is empty / a fact is missing; inspect thehiddenCan/Bothcounts first, then derive, explain a representative missing tuple, and propose the smallest base-fact change as a type-checkedargon_ad_hoc_mutatefor a human to confirm.argon_audit_mutation— what would this write change (the D2 derived delta) and does it cross any check? Drivesargon_mutatewithincludeDerivedDeltaandargon_whyon 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 (
/v1client) profile + streamable-HTTP transport. The backend is theArgonRuntimeHandletrait precisely so aRemoteimpl 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 embeddedstdioprofile 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-neutralRuntimeServicesurface the #1205 dispatch lift exposed — fork orchestration lives on the embeddedoxc_connection::Connectionand as serve-private/v1handlers (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-hocargon_mutatewrite path. - Structured check-violation
whyinoxc-runtime(D11).argon_whyships glass-boxing the runtime’s existing firing set; the deeper D11 build item — one unified structured trace shape (prose + structured) shared byexplain_factand the check-violation why — is a realoxc-runtimeitem, still to land. - Claude Code plugin bundle (D13).
ox mcpships with the toolchain; the reference plugin manifest registeringox 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 rideRuntimeServicerather than the bareConnectionis 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:falseprecisely 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
ProofTreemodel, 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-implementreload_if_changed(duplicate logic, the drift risk above) or serve a stale module.RuntimeServicealready 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:falseplus a guided decomposition. - Naive full
ProofTreeserialization. Rejected: a recursive derivation branches exponentially and would blow the agent’s context. Bounded projection + lazyexpand_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-mcpcouples toRuntimeService+dispatch_*_coreexactly asoxc-servedoes; 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 explainand LSP, not only MCP. ox mcpships with the toolchain viaoxup, 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 embeddedstdioprofile (increments 1–5, the read/write/introspect surface above) is built —oxc-mcp/ox mcpship 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-violationwhy, 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)
oxc-mcpskeleton +Backend(Local =RuntimeService+ workspace) + actor +McpError+argon_status/ coherence stamping.- Authoring tools + catalog fix-class enrichment + inlined remediation + server instructions/prompts.
- Runtime read tools with paging + governor config + cancellation.
explain_fact+ProofTreefolding +expand_proof; structured check-violation why inoxc-runtime.- Discovery tools + schema/catalog resource templates.
- Fork model (ephemeral + capability-gated promote with diff-summary).
- Remote backend profile +
ox mcpsubcommand + Claude Code plugin bundle.
Open questions
- The exact
rmcpAPI. 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 sametarget/<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/ZonedDateTimeliteral forms and the inner-shape kind rule; drops the mandatory-Zdatetime 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) and0063-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_nanossilently 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 andZ=“UTC known, local offset unknown”); TC39 Temporal and Rustjiff(per-type parse — “you pick the type, the suffix only validates”; theInstant/Plain*/Zonedsplit); XSD 1.1 Part 2 (xsd:dateTimetimezone-optional partial order with the ±14h incomparable window;xsd:dateTimeStamptimezone-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;pandasdatetime64[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:
| Carrier | Representation | Kind |
|---|---|---|
Date | i64 days since epoch | civil, day |
PlainTime (surface Time) | i64 nanosecond-of-day | civil wall-clock, no date/zone |
PlainDateTime (surface DateTime) | canonical jiff civil string | civil wall-clock date+time, no zone |
Instant | i128 nanoseconds since epoch | absolute UTC point, zone-free |
ZonedDateTime | canonical jiff Zoned string (…-04:00[America/New_York]) | Instant + IANA zone; the offset is baked into the string |
TimeZone | validated IANA id | reference datum |
Duration | i64 whole days | exact, 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 arei64in the timeline’s granularity unit (“days, matchingValue::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;
txandrule_vtare singlei64snapshot 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:614—atrequires 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)
- 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 aDatefield) passox checkandox buildin 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. - Out-of-range valid-time silently saturates.
epoch_day_to_nanosisepoch_days.saturating_mul(NANOS_PER_DAY)(oxc-value/src/civil.rs:241-243). Any valid-time beyond thei64-nanosecond window (1677-09-21 … 2262-04-11) clamps to the boundary. Round-trip-proven:at #5000-01-01#andat #9999-01-01#collapse to the same instant (as_of #5000#andas_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:
| Literal | Carrier |
|---|---|
#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_nanossaturation 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 atox 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 (thepandas 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/Instantcarriers 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_ofaxis 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 → Datefunctions (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 byfield: T?(StructurallyOptional→ a positive⟨.is, none⟩, Lean-proven inTypeSystem/Soundness/FieldAccess.lean),[0..0]cardinality, or conditional impls.Canstays reserved for existing-but-unknown. Truth4 stays four values (drift-gate-locked).- Cross-time identity:
=stays rigid, two-valued, congruent over system-allocatedIndividualId(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-neutralsuccessorOf/mergedIntosuccession relation carrying valid-time, joined explicitly by the modeler, never the engine=. - Replay across
oxup update: stored values read identically by construction —Instantis an integer;ZonedDateTimeis 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 — stampjiff_tzdb::VERSIONinto 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] resolutionmanifest setting (resolution = "week", or"<N><unit>"like2weeks) — 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 mirrorsdefault_world(a package-global evaluation cut, stamped into the artifact’sGlobalControl, threaded to the runtime). It lives under a[temporal]umbrella, not a bare top-level[metric]:metricalone is overloaded (it reads as a measurement/units/observability knob, especially beside theReal/Decimal/Moneyvalue 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 modalbox/diamondfamily; 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 flatmetric_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 wheresince[3]= 3 ticks — was rejected as it silently reinterprets existingbox_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] resolutionvalue 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 ani128axis 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 bareZcontextually 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 forcedInstantinto 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.rsfixed 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/Instantto 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 fifthNAvalue, a standaloneOffsetcarrier. Rejected respectively: breaks the join engine; is a schema-layer applicability concern; an offset-only string is losslessly absorbed byInstantandZonedDateTimecorrectly 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-instantiatefact_parse: accept anInstantinat/as_of; refuse out-of-range valid-time (replacesaturating_mul).- Extend the static loud gate to insert-body constants.
- Drift fixes: the tree-sitter grammar (
grammar.jsstill requiresZ), thelit.tomlDATETIME desc, the book (lexical-structure.md,stdlib.mddrop the mandatoryZ), and the DST-gap-invalid canonical example invalue.rs. - Directed diagnostics (e.g. a typed code for a malformed instant literal that names the carrier).
Builds (scheduled):
- Surface the
Instant/ZonedDateTime/TimeZonetype names (std::datetime, per RFD 0047) — with the read-point tzdb-version pin, not as a hollow surface. - The
successorOfsuccession 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
DateTimevs an absoluteInstant, an offset-unknown value, or two values across the XSD ±14h window are genuinely incomparable, which is aCanverdict 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-explosiveBothattacks 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 nativeCan/Bothtemporal 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-explosiveBothagainst 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 isBoth— 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 toi128-nanos (matching theInstantvalue 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] resolutionpackage-global setting; oneMetricResolutionfeeds fact projection + bound scaling; DAY default). -
Explicit
as_of/as_recordedaxis 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 —
CanvsNA), 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 designatedBothinherits 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 overApprox. This is pillar (b) reformed for paraconsistency: well-definedness withBothpresent, not ⊥-absence.both_feeds_metric— aBothverdict at one time point propagates through a nonzero metric shift (the non-vacuity witness; see §3).- The reduction chain
canonicalModel_ultimatelyPeriodic_of_twoValued→entails_periodiccarries 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_metricand thebothAtZerowitnesses 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 remainingsorrys are named import points, not scattered gaps. - The Belnap-pair constraint is not a style choice.
both_feeds_metricshows 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 procedureentails_decidable.) - Re-prove the 2-valued ruler bound in-repo. Rejected: it is an established, multi-page published
result; importing it (a cited
axiomper 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 namedsorrys). - Remaining to close before claiming decidable: import
TwoValuedRulerBound(the published 2-valued periodicity — vendor or cite-axiom), and dischargeentails_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; underReasoning/Datalog/orDecidability/), where it becomes drift-gated substrate.
- Scratch preserved at
- Binds the implementation: four-valued metric operators in
oxc-reasoningmust propagate the full Belnap pair (§3). - Then ships: native
Can/Bothtemporal 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 isBoth— 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
Truth4reasoning layer. (D1.) - One merge operator (a single CRDT-or-resolver). Rejected: Result 1 — the coexistence/resolution split (D2).
- Flat-
mainlast-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
.oxbinstandpoint-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 carriesBoththrough 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,Truth4is a group-free bilattice → a semiring/semimodule IVM, unbuilt); a coexistence-preserving bitemporal merge; the tertiary designation layer (new surface); a source/standpoint-indexedBothhuman 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-
Bothmerge + 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
Bothhuman/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)
- Mechanize the two-layer composition soundness in Lean (scratch, never-merged until it holds); stand up the differential oracle. Gates 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.
- Standpoint-scoped write + monotone append-
Bothmerge (D1/D3), replacingpromote()-LWW (D8). - The read-time defeasible resolver + the tertiary designation layer (D2/D5).
- The
Truth4-native engine at scale + group-free four-valued IVM (the through-line). - The
Bothagent/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
- State: committed (ratified 2026-07-08)
- Provenance: the semantic core (§4–§6) was discharged in a kernel-checked scratch mechanization over the shipped
Argon.Reasoning.Datalogsubstrate before ratification (statement ledger in Appendix B; the scratch file is deliberately never merged), and the full document passed an adversarial evidence audit prior to ratification — every load-bearing citation verified against the repo, the mechanization, and the external literature. - Decides: (a) that a shape-defined concept is an ordinary IDB atom of the reasoner’s WFS/AFT fixpoint, with a stated + mechanized realization biconditional; (b) the guardrail floor; (c) the construct count (§7); (d) the
Can-membership dispatch policy (§8). Relates to RFD 0017 — Refinement classification:where(primitive) vsiff(defined) (does NOT widen the inlineifffragment), RFD 0045 — The world-assumption write-side: refuse-on-K3-not and the#[world]opt-in (refuse-on-K3-not), RFD 0060 — The Lean substrate mechanization architecture (delimited reflection), RFD 0069 — The shared-live-KB write model: concurrent authoring, coexistence, and resolution (federation two-layer composition), the persisted-derived-read-model proposal (#1493), RP-008 (the open derive-backed realization home; NOT RP-007 — see §10).
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 metatype | instantiation condition | Argon plane |
|---|---|---|
| phase (anti-rigid, sortal) | intrinsic — over the individual’s own qualities | inline iff { … } (local, decidable, outside the fixpoint) |
| role (anti-rigid, sortal) | relational — over other entities / their derived membership | shape 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
ifflocal 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 (
countoverselfnav-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)
- 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 syntacticCompiled.CRule.Safe/Safe.head_determined). - 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 4arefuse_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. - Negation-through-classifier →
Can+ loud cycle diagnostic. Recursion through negation across classifiers is allowed (WFS gives it a model); membership surfacesCan. 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 becauseCanis native to the envelope. Acceptance case 2breach_fulfillment(asymmetric chain → definite; symmetric self-guard →Can) and case 5cwa_owa_pair(Eligible/Blocked standoff →Can; ground breaker → definite). - 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}, evaluatingPupover the materializedwfTrue(Plow)derivesras definitely-true, while the whole-program WFM leavesrundefined (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
wfFixis 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 onlywfTrueand letting a later read treat absence asNotre-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-assertedIsmeeting a law-derivedNotinfo-joins toBoth, escaping K3 and corrupting the fixpoint. Two planes with separate sources (assertedwhere; derivediff/shape; no manualinsert iofon 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.leanprove 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 (whereinvariants 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
Can→Is. A surface requiringIsmust not treatCan/BothasIs. 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
CanLOUDLY rather than silently fail closed. The same mechanized chain that proves never-promote also models the silent K3 fail-closed reading:CwaOwa.member/memberOfreturnBool, collapsingCan/Both→falsewith no signal — and that is mainstream practice (OWL open-world entailment, SHACL validation, SQLNULL3-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 droppedCanis never invisible. The never-promote half is theorem; the loud-vs-silent half was the policy call, now decided.
| Surface | Envelope | Decision on Can |
|---|---|---|
query / derive | labeled-Can (+ hidden) | read — no decision; projectK3:true gives the fail-closed view; whyNot needs Can ≠ Not |
serve (/v1) / MCP | labeled-Can (+ hidden) | read — no decision |
| dispatch (impl/method selection) | consumes labeled envelope | definite-only (theorem: never-promote); loud-refuse (ratified policy) |
| closed-world write gate (OE0258) / narrowing | — | definite-only (theorem: never admit/narrow on Can); loud (ratified policy) |
| checks (Cat3 observers) | report Can | observe, 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
iffrealization 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 → Propopaque over the classificationState). 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 localiffrealization 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: “noRuleIRinterpreter;Reasoning.Stateis meta-property only”). This RFD’s mechanization discharges it at theDataloglayer (Program Atom/wfFix), sidestepping RP-008’s missing-interpreter blocker for the classification-atom case. Net: RP-007 §4.5 = local value-dependentiff(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):
legally_capable— relational phase via shape;extent = {alice}(§1 flagship).breach_fulfillment— mutual-classifier recursion-through-negation; asymmetric→Is, symmetricloop→Can.usc26_singletons—pub factstatutory 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).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.
| # | Statement | Status |
|---|---|---|
| L0 | wfFix P = (wfTrue P, wfPossible P) — realization is over the AFT joint operator on the product bilattice, not a hand-rolled alternating fixpoint | PROVED-substrate (AFT.wfFix_eq) |
| L1/L2 | WFS true-/possible-support lemmas | PROVED-scratch |
| L3/L4 | single-source realization, is-/Can-region | PROVED-scratch |
| L5 | parent/body split = surface promise iof(x,C) ↔ iof(x,parent) ∧ shapeBody | PROVED-scratch |
| L6/S-K3 | K3 form wfModel C = KleeneAnd (wfModel parent) (wfModel shape) | PROVED-scratch |
| L7 | single-store consistency wfTrue ⊆ wfPossible (no Both) | PROVED-substrate (wfTrue_subset_wfPossible) |
| L8/S-165A | #165 disagreement witness (compositional over-asserts) | PROVED-scratch |
| L9/S-165B | agreement when boundary two-valued | ASSUME-cite (Vennekens-Gilis-Denecker TOCL; Ross JACM 1994) |
| L10/S-NV | Can/Is/Not witnesses (theorems non-vacuous) | PROVED-scratch |
| L11/S-G1 | no existence invention | PROVED-scratch (+ Safe.head_determined cite) |
| — | single-store realization “with Both” | REFUSE — vacuous; Both is federation-only (RFD 0069), deferred |
| L12 | vacuity-refutation attestation clean on all PROVED theorems | attestation (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 access — oxup 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.
3. Skills — one symlink covers three hosts
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
mainratified 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
.oxbinsection id 6 (projection-cache, reserved since the format’s first cut, inert ever since) with the post-fixpoint derived read-model (the serializedRelationCatalog) at build time, keyed on a derivation-input commitment (the already-shippedartifact_hashwidened 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 staleFORMAT.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 specifiedOE1205refusal 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 10instruction counts ±0.00%,perf record -gcall 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-1442 → Store::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:17snapshots the host date per evaluation andeval.rs:1699feeds 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_temporalpredicate 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 buildrecords 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), andRelationCatalog::to_bytesis 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_genequality alone is an invalidator within one store history, not a content identity:load_eventsaccepts 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. TheStoretherefore gains an unforgeable pristine-seed marker: set exclusively byseed_from(module)(recording the seeding module’sartifact_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’sartifact_hash∧storage_gen== the payload’sseed_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_v1assertions; 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_versionequals 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/*.oxbinwith 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-scenariobuilding before reading; a build immediately consumed) — self-built by construction; - prior invocation:
ox buildappends the BLAKE3 hash of every artifact file it writes to a per-user trusted-artifacts ledger outside every workspace ($ARGON_TRUSTED_ARTIFACTSexplicit 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 poisonedtarget/*.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 itstrust-derived-cachegrant specifically, every other key of the file still applying. Containment is checked on the RESOLVED path (canonicalized, against the outermostox.tomlroots enclosing the invocation’s input and the working directory) regardless of which variable produced it, so redirectingXDG_STATE_HOMEorHOMEinto the repository is the same refusal. Two honest limits: a repository carrying noox.tomlanywhere 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-cachetrusts the named artifact’s cache for this invocation; - standing:
trust-derived-cache = trueunder[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 workspaceox.tomlmust 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-cacheforce-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 viagrammar.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;
- same invocation: the artifact was built by this process (
- the section’s directory
content_hashverifies 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-llheader walk — today only a transitive dependency viaciborium; the implementation declares it explicitly inoxc-oxbin’s Cargo.toml): validate declared dictionary / relation / per-relation row counts and cumulative declared byte lengths against fixed caps before anyserdematerialization allocates; only thenRelationCatalog::from_bytes(catalog/mod.rs:460today decodes generically — the preflight wraps it, it does not modify it); - every D5 validity gate passes.
Then the arm seeds a
ModelCachemirroring the durable warm path’s architecture precisely — stated precisely because thepersist.rs:236-243precedent 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-25is 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 deferredfrom_modelupgrade). First ineligible write drops the cache and rebuilds exactly as today. (Thefrom_modelincremental upgrade is deferred out of v1 — see D1 and Open Questions; v1 is frozen-seed only.) Consumers, scoped honestly: every read that flows throughensure_strict_cache—ox 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 whyis 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 viagrammar.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 pgsurface — 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”):
- 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 realAUDIT_CORPUSexamples (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 intoensure_strict_cacheAND the fragment/provenance/pristine-seed gates) as a deliberately narrow#[doc(hidden)] pubaudit API —pub(crate)alone is unreachable fromtests/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 existingaudited/fellbackdiscipline), 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 ofstrict_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 throughensure_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. - Conformance-corpus entries (RFD 0055 gate, auto-discovered — and exercised for
real because D4 makes
ox test/ox run-scenariocache 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. - Fuzz:
check_oxbinalready fuzzesread_oxbinend-to-end; seed corpus gains cache-bearing artifacts, and the payload decoder gets structure-aware proptest cases inoxc-fuzz(the MAGIC_BYTES-prefix pattern) with the D4 size bounds asserted (over-bounds inputs must refuse before allocation). - CI placement (RFD 0062 tiers, stated accurately):
compiler-testand the corpus gates do not run on PRs — the PR tier is build/clippy/fmt/deny/drift only; they run in the merge queue (whereci-gateenforces) 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. - 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_cache
— ox 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 (
JoinCachekeys are positions into a specific optimizer output) and a dictionary-consistency treatment;from_modelmay 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 buildruns 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_EPOCHbecomes 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, thecompiler/AGENTS.mdreview 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
OE1205refusal 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-cacheas the on-demand audit for caches trust alone cannot vouch for. ox whykeeps re-deriving in v1 (explanation fidelity); its latency is unchanged.
Open questions
- 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.) - The
from_modelupgrade (deferred out of v1). Two obligations before it lands: (a) the EDB/dictionary reconstruction contract —from_modeltakes 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 hazardpersist.rs:328names, 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 (DredcStaterebuild + arrangement warm-up) cheap enough to beat frozen-first? Measure on the bench baseline shapes. - Dated modules. v1 declines them (D3, the RFD 0063 epoch invariant). The upgrade
is persisting the model’s validity epoch (
valid_untilfrontier) and gating consumption on the read clock falling inside it — worth doing only with evidence dated modules are a hot CLI shape. ox buildself-reuse. Build’s own internal load repetitions (check/discharge/encode) could consume the model they just derived — measure whether that’s worth the plumbing.- Mechanical epoch derivation. Deriving
DERIVATION_SEMANTIC_EPOCHfrom 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. - Serve-side consumption.
oxc-serve/RuntimeServicehydration 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. - Housekeeping surfaced during recon, tracked separately: RFD 0063 is double-allocated
(
0063-bitemporal-read-point.mdindexed;0063-datalogmtl-temporal-engine.mdorphaned from README/SUMMARY); FORMAT.md carries further drift beyond §D.10 (missing §D.4artifact_hashkey, stale appendix cross-references);read_oxbinverifies 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 onInheresIn’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 immutablebrainend, the person must be retracted first; the parthood assertion then disappears by cascade. -
Immutable perdurants.
metarels.arrepeatedly 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:
| Declaration | Default | Opt-in |
|---|---|---|
| Field | field: T is immutable | mut field: T admits update |
| Relation end | end: T is lifetime-bound | mut 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 ofmutis sufficient. - Does not introduce relation-end
updateor stable relation-binding identity. - Specifies the required individual-retraction cascade. The source spelling
for requesting a
RetractIndividualsset is decided:retract x;for a single individual andretract {x, y};for a set, lowering to the kernelRetractIndividualseffect. 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
iismut, that set may grow or shrink while the dependent context survives, provided the transition satisfies every other end’s lifetime gate; - if endpoint
iis 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
spousetuples required bybindsSpouse(...)[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
worksForaremut, so the old tuple may be retracted directly before the new tuple is inserted; spouseinbindsSpouseis immutable, soMarriageis the dependent individual. Retract the marriage first; itsbindsSpousetuples are then retracted automatically. Themut marriageend permits a surviving person to participate in another marriage later;braininhasBrainis immutable, soPersonis 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:
- 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 allocatedImmutableRelationEndRetractiondiagnostic. Because deleting one tuple removes one value from every endpoint fiber, direct tuple retraction is admitted only when every end ismut. - 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 anImmutableRelationEndInsertiondiagnostic. - 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
braincannot leave its dependentpersonalive, and retractingspousecannot leave its dependentmarriagealive. When the condition holds, incident relation facts retract by cascade. - 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
deletenames 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-planedeletematch under “Compatibility and migration” is a plane-locality rule for locating the named assertion, not a mutability exemption.) Purepub deriveor 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;
RetractIndividualsretracts 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 correctedinsert, 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
| Option | Reads like the domain | Un-confusable with delete | Greppable | Parser fit | Composite atomicity evident |
|---|---|---|---|---|---|
amend R(a); + amend R(a) => R(a'); (chosen) | yes — “amended return” is the tax/legal register | yes — separate verb | yes — one keyword | one reserved word; statement-head, like delete/forget | yes — => fuses old→new |
rescind R(a); primitive + amend R(a) => R(a'); composite | yes | yes | yes | two reserved words for one channel | yes |
single #[correction] delete R(a); | no | no — it is a delete with a modifier | attribute, easy to miss | no new keyword | no — replacement is a separate insert |
keyword bikeshed (recant/rectify/correct) | weaker than amend | yes | yes | one word | n/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
amendrefuses to build (OE1405 AmendWithoutCapability) unless the enclosingmutatedeclaration grants#[allow_amend], exactly asforgetrequires#[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 argued | Resolution |
|---|---|
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.
The legal exit — the cascade
// 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(effectivemut= the relation AND every transitive superrelation declaremutat the position — the runtimeEndMutabilityvector’s per-position value), witheffMutAt_le_declared(effective is a lower bound on the declared bit),effMutAt_antitone(adding a parent can only tighten, never loosen), andimmutable_parent_forces_immutable(any immutable transitive parent forces the end immutable). OE0268asoe0268Accepts, withaccepted_declared_is_effective: in any accepted schema a child’s declaredmutequals its effectivemutat every position — a weakening declaration is refused, never folded into an effective loosening.OE0267asoe0267Accepts, withoe0267_context_individual_bearing(an accepted immutable end has a nonempty, identity-bearing dependent context) andoe0267_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.
Choosing mut end by end — gallery and decision procedure
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 formadeForandissuedTo— 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 requiresdelete, which needs every endmut— so any transferable association is all-mutby 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:
- Will a tuple ever be deleted while both individuals still exist? → every end
mut(it is an association;ownedBy,worksFor). - 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). - Is the only honest exit the disappearance of a participant? → that is the
cascade doing its job, and the declaration needs no
muton 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-mutvector 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-mutprogram admits every effect.
The two write gates (refusals are theorems, not test cases).
retract_matched_immutable_refuses— adelete 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 fieldupdate. - The relation declaration chapter of the reference manual
(
spec/reference/src/declarations/constructs/relations.md) — the existingrel-paramgrammar 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), andarg_ufo/metarel_constraints.ar(the existing relation-arm audits and the intended home of mutability checks). OE0234— thefixed-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:
- Body-scoped syntactic rules. Rules that walk a body (a
fn,mutate,check,derive,query, ortestbody). 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. - Item-level gates. Wired per item kind by hand in the
check_file_withdispatch loop (compiler/crates/oxc-check/src/infer.rs): a chain ofif let Item::… = &itemarms, each calling the checks that item kind needs. - Inference-interleaved rules. Rules that fire inside expression inference
(
infer_exprand theinfer_exprsfamily) 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
BodyPlanemask) 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.
- a
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 (BodyPlanefor body rules; anItemKindmask for item rules).fn— the rule function.wired—driver(the axis driver enumerates and invokes it) orinline(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:
| Rule | Code(s) | Current site | Item kinds |
|---|---|---|---|
| Nested-declaration refusal in standpoint body | OE0733 | check_standpoint_body_with | StandpointDecl |
Impl-member mutate tuple-target undeclared program-wide | OE0266 | flag_mutate_tuple_target_undeclared_everywhere | ImplBlock |
| Duplicate-import binding | OE0105 | check_use_imports_with | use (file-scoped) |
| Glob-import ambiguity | OE0106 | report_glob_ambiguity via use walk | use (file-scoped) |
Module-level mutate target gate | OE0223 | check_mutate_with (scoped) | MutateDecl |
| Refinement-predicate well-formedness | OE0101, OE0106 | check_refinement_predicates + type_check_refinement_predicates | items carrying refinements |
| Signature checks (procmacro) | OE0727, OE0729, OE0734 | infer_signature via check_fn_with | FnDecl |
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:
| Rule | Code(s) | Why inline |
|---|---|---|
| Emit-value typing | OE0201 | Needs 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 typing | OE0201 | The 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) | OE1326 | Enforced 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) | OE0266 | Deliberately 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
driverrule 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
driverrules, the row’s mask fully determines the run set; forinlinerows, 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_littype-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+ItemKindmask); 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 awired: inlineinventory row:report_glob_ambiguityfires 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 areinline. - 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
userules warrant a distinct axis (FILE) rather than a file-scoped mask on the ITEM axis. - Whether
codeson 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.
D16 — Consent to be overridden: #[default] is the token, within the package
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
pubin its module, so the attacker cannot name it (D12). The message states that defeat visibility equals name visibility and that the target must be madepubby 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:
- 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.
- 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.
- 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 (auseleaf, glob, or alias) and the bare spelling resolves exactly as a local one does; import only its MODULE and the qualified-path spellingmodule::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-pubtarget refuses (fix: its author makes itpub). 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:
- a per-head memo (
DefeasibleSurvivorsMemo) keyed by relation short name, mutated as heads resolve; - a reentrancy guard (
visiting: BTreeSet<String>,let reentrant = !visiting.insert(...), thevisiting-stack guard indefeasible.rs); and - a fail-open fallback — on re-entry the resolver serves
catalogs.support.clone(), the raw pre-defeat extent (the re-entry arm ofdefeasible_survivors), and writes that under-computed result into the shared memo.
Every defect is a facet of these three:
| Issue / PR | Symptom | Shape class | Mechanism |
|---|---|---|---|
| #1788 / PR #1789 | Defeat filtering lost beyond stacked #[default] readers; answers change with head-resolution order | Order-dependent memo poisoning | Over-broad dependency seeding trips the reentrancy fallback; the raw extent is memoized and poisons later readers. Bound is stack order, not chain length. |
| #1790 | A reinstated (pardoned defeated-defeater) tuple fails to propagate to a second stacked reader | Propagation under-suppression | The same surviving-extent seeding bookkeeping, failing in the opposite direction — the mirror image of #1788. |
| #1791 | A recursive #[default] head (a default reading itself transitively) is served its raw pre-defeat extent | Fail-open termination fallback / declaration-plane gap | Elaboration 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. |
| #1792 | Two same-short-named defeasible heads in different modules merge | Namespace-collision keying | The 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
Hlowers 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_contributionmade static — the head is never cleared). - Blocking atoms — for each
#[defeats]edge intoH, 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
His 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:
- 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.
- 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. - 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-reasoningcompile/stratify.rs) refuses a cross-stratumnot RwhereRis 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 compiledH :- 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-stratumnotover 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
.oxbinsetsis_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
RuleDeclbodies, 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/Termshape changes — only more ordinary rules. Adding rules is not an IR-shape change. .oxbinformat minor bump for the additiveDefeatCompilationProvenancesection 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 tooxc_oxbin::projection_cache::derivation_commitment, so a cache built against one artifact can never be served against the other — the caches separate with noderivation_commitmentchange. 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 noderivation_commitmentslot for the switch value. (The compiler/AGENTS.md contract — “any semantic change under materialize/prepare/evaluate MUST bumpDERIVATION_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.rsdeleted 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
=1produces 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.
- (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
-
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 viaOXC_DEFEAT_PLANE_SEED=<n>; run underbinary(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 byTransform.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
.oxbinbytes, whichEmitVectorsdoes not carry); (4) evaluate the produced program throughEngine::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 — reuseReasoning/WellFounded.wfModeland the compiled-rule adequacy ofReasoning/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 — theExtension.wfModel_union_oldconservativity 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 existingpardoned_target_survivesfrom 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 asnegated_block_same_sccinCompiledFixpoint.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
RuleStrengthmirror 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, thevisitingreentrancy guard, the fail-opencatalogs.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;DefeasibleSurvivorsMemoand the short-name keying throughout;strength_stratified_snapshotand the RFD-0026-era fusedstrict ∪ (defeasible \ defeater)fast path — superseded (theRuleStrengthtriple 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 evaluation —
oxc-runtimechecks.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 gate —
projection_consume.rs:268, branching onhas_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 path —
derive_read.rs:1214/1223(incl.strength_stratified_snapshot). Scenario snapshots read the compiled public-head extent through ordinary provenance; theRuleStrength-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_atcloned 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_irversion 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 areundefinedand 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
.oxbinformat minor bump is owed (the additiveDefeatCompilationProvenancecarrier); noDERIVATION_SEMANTIC_EPOCHbump 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.rsis 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
DefeatCompilationProvenanceshape and whether it is a new.oxbinsection 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-migraterewrite at its next toolchain upgrade. - Affects: relation declaration grammar; the elaborator subsumption checks
(
Argon.Substrate.RelationSubsumption—arityEqual, 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 reference —
fⱼ = eᵢ, contributing child endeᵢ’s value at parent endfⱼ(a rename, stated by both names); - a cast child-end reference —
fⱼ = eᵢ as S, whereSis the parent’s declared sort atfⱼandchild’s sort ateᵢis a subsort ofS(childSort(eᵢ) <: S); the value is the child end’s, admitted at the wider parent sort; - a constant pin —
fⱼ = κ, a literal valueκof the parent’s declared sort atfⱼ, 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 name — parentEnd = 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.
| # | Rule | Refusal (notional) |
|---|---|---|
| a | Every filler’s left name resolves to a declared parent end; a name matching no parent end is refused. | MappedSubsumptionUnknownParentEnd |
| b | Every 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 |
| c | Every child end is referenced by at least one filler (no dropped child ends — see rationale). | MappedSubsumptionUnmappedChildEnd |
| d | A 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) |
| e | A constant pin’s literal has its named parent end’s sort. | MappedSubsumptionPinSort |
| f | Cardinality refinement and metarel compatibility (OE0152/OE0153) are checked through the mapping — see D2. | OE0152 / OE0153 (reused) |
| g | The subsumption graph stays acyclic (OE0154 reused). | OE0154 |
| h | The 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 generalizesarityEqualtoarityMapsParent(filler count = parent arity) and endpoint covariance to covariance-through-the-filler (covariantFillers, over the per-position helperentryCovariant). The existing (now-removed-from-surface) positional lemmas are recovered as the identity-mapping specialization — stated as amappedChecks_identity_eq_bareCheckscorollary. 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, carryingstructural_containmentanddispatch_selected_iff) gains the obligationmapped_containment: a mappedchild <: parentedge entailsφ(ext(child)) ⊆ ext(parent)in a mapped-closed program (MappedClosed, the generalization ofstructural_containment’sStructurallyClosedfrom identity inclusion toφ-image inclusion). Itsdispatch_selected_iffobligation is restated asmapped_dispatch_selected_iffso 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 by | Mutability 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)removesIncomeItem(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 ontoIncomeItem(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”). Adelete/amendon 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 ofamend— the RFD 0076OE1406(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 plusvia k— is the union plus “which one”: the same tuples, each additionally bound tok, the member relation the tuple was declared in.specializes(k(memberArgs), Parent(parentArgs))— the generalized atom — isvia’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 kis the canonical user spelling of a family query. The generalizedspecializes(k(memberArgs), Parent(parentArgs))form is the meaning ofvia— 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). Thereforeviaplus 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 writevia; 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
ktoParent’s family —kranges over the family’s members (reflexively includingParentitself); - applies
kin its own frame —memberArgsisk’s declared argument list (the member’s own shape); - constrains the image in the parent frame —
parentArgsisParent’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:
kis inParent’s family — reflexive:kmay beParent;k(u…)holds ink’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);φ_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:
- Over-returns — the reflexivity × closure duplicate.
$specializesis reflexive (compiler/crates/oxc-reasoning/src/compile/rule.rs, the reflexive-transitive<:closure;compiler/crates/oxc-runtime/src/standpoint.rsdocuments$specializesas reflexive-transitive), sokranges 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 intoIncomeItem’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. - Buries the frame convention.
k(p, a, c)applies everykat the parent’s three-place frame, butWagesUSAis two-place; which frame a bare application speaks is left implicit.viawrites only the parent frame and letsφbridge, so no frame is left implicit (the φ-not-position rule).
| Query | Spelling | Rows 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:
| Candidate | Verdict | Collision |
|---|---|---|
Parent(args) via k | chosen | via 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 k | rejected | from 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 construction — via
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, andkis a body variable like any other. - Aggregates. The witness is a first-class grouping key:
count{ a } group by koverIncomeItem(p, a, c) via kcounts 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 != k2finds 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:
-
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 namefⱼresolved to its parent position, and aMappingEntrystored 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. -
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. ThemappedChecks_identity_eq_bareCheckscorollary (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). -
Exhaustive, wildcard-free matches over
MappingEntry. The entry enum isChildEnd { 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. -
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.
-
Single-authority translation obligation. The dispatch translation (D5) and the cascade translation (D4) are the same
φapplied per delta sign, recorded as thedispatch_cascade_same_mapobligation (D2). In slice 1 this obligation is definitional (holds byrflbecause both directions are the oneimagefunction) — 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 therflbecome an anti-drift proof rather than a statement of intent (see D2, “What this obligation is, honestly”). -
Per-plane drift-impossibility. Each plane interaction names what makes drift impossible rather than unlikely:
Plane What makes drift impossible End-mutability (D3) Inheritance reads the resolved mapping’s entries; a pinned position has no mutfield to compare, so theOE0268check 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_iffits 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 theConstantentry, 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:
| p | a | c | via |
|---|---|---|---|
| alice | 500 | USD | WagesUSA (mapped, currency pinned) |
| bob | 300 | EUR | ForeignIncome (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.
4 — Refusal gallery (one per new gate, notional codes)
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
| Option | What it is | Why not |
|---|---|---|
(a) Status quo: bare-only <: + hand-written derive rules | Keep 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 feature | A 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 form | One 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 list | The 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 removed | The 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 <: parentbecomeschild <: parent(f₁ = e₁, …, fₙ = eₙ)wheref₁ … fₙare the parent’s declared end names ande₁ … 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 aritynon both sides (fⱼ = eⱼforj = 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). Thespecializes-keyword spelling of a bare clause (child specializes parent) migrates the same way, tochild 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-migraterewrite 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
-
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 amutposition 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. -
Constant pins under bitemporal amendment of the constant’s meaning. A pin
USDis a compile-time constant reference. If the referent ofUSD(say aCurrencyindividual) 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 whateverUSDdenotes; but whether a pin may reference a mutable individual at all, versus only aconst, is open. -
Do the bare positional checks (
OE0150–OE0154/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, socheckSubsumption/arityEqual/endpointsCovariantare not retained as a live elaborator code path. Every edge is elaborated to a resolved mapping and run throughcheckMappedSubsumption, and the meanings ofOE0150–OE0154are 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 ofmappedChecks_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. -
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 (dropnaming the child end that maps to no parent end), defining the collapse semantics (image is the projection; two child tuples differing only atccoincide in the parent). Deferred — it needs its own coverage/cascade story because the collapse loses the child identity the cascade direction (D4) relies on. -
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. -
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 writtenp = p, never a barep. 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 tokenpas sugar forp = 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 itsparentEnd =key. -
Arity overloading would be blocked by bare relation references — a forward constraint, mostly settled by D6. If relation-name overloading (
Loc(a, b)andLoc(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 framek(memberArgs)and the parent frameParent(parentArgs). The barespecializes(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 trailingvia(Parent(args) via k), whose parent atom carries its arity, with the generalizedspecializes(k(memberArgs), Parent(parentArgs))(both argument lists required) as its meaning — and the bare two-argumentspecializes(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-carryingspecializesform” — is therefore no longer provisional forspecializes: 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. -
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 toA(x = alice, y = bob)viaLeftand toA(x = bob, y = alice)viaRight: it would occupy two different rows inA, and a family query overAfollows 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:
- 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.
- Lean mechanization —
Argon.Substrate.RelationSubsumptiongains the mapping data,arityMapsParent,covariantFillers, and themappedChecks_identity_eq_bareCheckscorollary (the migration-soundness witness);Argon.Reasoning.Datalog.RelationApplicationgainsmapped_containmentand the restatedmapped_dispatch_selected_iff. - 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).
- 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.
- 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-migraterewrite is prepared here and applied at that corpus’s next toolchain upgrade (Migration). No deprecation lint — the refusal is direct.
- 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
- 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
specializeschildren. - Family-query surface slice (callable-port slice D) — the generalized
specializes(k(memberArgs), Parent(parentArgs))grammar (both argument lists required), the canonicalviasurface (Parent(args) via k ≡ specializes(k(_fresh…), Parent(args))), the parent’s own-tuples view for the reflexivek = Parentarm, and the deprecate-then-refuse schedule for the bare two-argumentspecializes(k, Parent)(D6). This slice splits by mapping kind:- IDENTITY families now — the generalized atom and
viaover 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 canonicalviaspelling 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).
- IDENTITY families now — the generalized atom and
References
- RFD 0005, Relation subsumption — the bare
positional form this RFD generalizes; its elaborator checks (
OE0150–OE0154) 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.RelationApplicationand itsstructural_containment/dispatch_selected_iffobligations. - #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 QΔ 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:
- DBSP: Automatic Incremental View Maintenance for Rich Query Languages
- DBSP: Incremental Computation on Streams and Its Applications to Databases
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
RelationIdis 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.NodeIdis 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.CircuitPlanIdcommits to the circuit schema version, the sorted multiset of exact compiled-rule commitments, and the complete canonicalCircuitPlanencoding. The plan retains 32 bytes per source rule, not another rule copy.CircuitInstanceIdis(tenant, fork, module commitment).- A
Frontieris the totally ordered event-log position for one tenant/fork. A batch names(prior_frontier, next_frontier, batch_commitment), wherenext_frontieris 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 edges | Component rebuild p50 | Established full evaluation p50 | Ratio |
|---|---|---|---|
| 100 | 337.67 us | 286.63 us | 1.18x |
| 1,000 | 3.767 ms | 3.160 ms | 1.19x |
| 10,000 | 42.470 ms | 35.736 ms | 1.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 operator | Delta behavior | Retained state |
|---|---|---|
| singleton | emit one empty binding at the initial logical time | none unless consumed by a stateful operator |
| scan | forward input batch | relation trace |
| filter/map/compute | apply directly to batch | none |
| binding projection | retain selected variables as a joinable binding | none |
| join | bilateral delta join | both indexed inputs |
| multiway join | ordered multilinear delta join | one trie trace per logical atom |
| distinct | emit support zero-crossings | tuple weights |
| antijoin | emit right-support zero-crossings | left trace and right key counts |
| semijoin | emit positive right-support zero-crossings | same left trace and right key counts as antijoin |
| aggregate | update changed groups | group multiset and accumulator |
| recursion | nested delta fixpoint | recursive traces by iteration |
| sink | consolidate rule support | output 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:
singletonis 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.scanaccepts only the batch for itsRelationId, validates arity, and stages the exact base trace update. It does not decode or allocate relation identity.filterretains a row iff the existing expression evaluator returns true. False and undefined drop it, matching full evaluation.- Scalar filters carry one canonical predicate (
Comparisonor expressionDefined) plus an explicit selection (SatisfiesorDoesNotSatisfy). This representsnot <comparison>andnot <compute>as stateless selection, including nested negation.DoesNotSatisfyretains 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. mapandcomputepreserve 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 projectionis 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 placedistinctexplicitly where projection collapses multiple supports. Terminalprojectremains the binding-to-head-tuple boundary.unionadds all producer weights. It does not impose set semantics.sinkconsolidates 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.
countsums multiplicities with checked integer arithmetic.sumuses the exact numeric domain and errors of the full value evaluator.averageretains exact sum and count and performs the same exact division as full evaluation.count-distinctretains per-value support and counts zero-to-positive values.minimumandmaximumretain 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.
- Exact Z-sets, typed refusals, immutable plans and per-frontier differential equality with full evaluation.
- Positive acyclic scan, filter, map, compute, union, join, distinct and sink.
- 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.
- Qualified schema-carrying
RelationId; structural collision-checkedNodeId; committedCircuitPlanIdand scopedCircuitInstanceId. - Explicit
(prior frontier, next frontier, batch commitment)application with duplicate idempotence and conflict, gap and stale-plan refusal. - Stratified antijoin with lower-stratum settlement, followed by count, sum, average, count-distinct, minimum and maximum.
- 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.
ProjectionMaintainerimplementation exposed only throughQueryEngine. 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^6facts 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^4–10^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:
| Workload | Circuit (ms) | Established (ms) | Ratio |
|---|---|---|---|
| shallow insertion | 0.588 | 2.646 | 4.50x faster |
| wide recursive insertion | 0.672 | 5.306 | 7.89x faster |
| well-founded exit assertion | 6.229 | 47.083 | 7.56x faster |
| deep-chain recursive insertion | 21.046 | 9.846 | 2.14x slower |
| recursive multiway retraction | 5.036 | 73.008 | 14.50x faster |
| upper recursive retraction | 0.610 | 10.610 | 17.39x faster |
| lower negation assertion | 7.756 | 45.971 | 5.93x faster |
| lower count assertion | 2.148 | 401.311 | 186.83x faster |
| lower count pipeline assertion | 4.304 | 613.258 | 142.49x faster |
| factorized count retraction | 1.202 | 1,195.429 | 994.53x faster |
| low-overlap retraction | 0.586 | 18.302 | 31.24x faster |
| high-overlap retraction | 0.342 | 4.939 | 14.44x faster |
| projected sum retraction | 1.484 | 266.124 | 179.33x faster |
| dense mutual-support retraction | 115.963 | 397.955 | 3.43x faster |
| mixed recorded trace | 3.488 | 20.537 | 5.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.
| Workload | Circuit p50/p95 | Established p50/p95 | p50 speedup | Probes/rounds |
|---|---|---|---|---|
| shallow insertion | 0.392/0.648 ms | 28.806/30.718 ms | 73.5× | 100/0 |
| wide recursive insertion | 0.430/0.523 ms | 58.066/60.978 ms | 134.9× | 300/1 |
| low-overlap retraction | 0.385/0.537 ms | 206.494/210.381 ms | 536.6× | 100/0 |
| high-overlap retraction | 0.263/0.327 ms | 61.569/64.291 ms | 233.7× | 0/0 |
| Workload | Retained C/E (ratio) | Construction peak C/E (ratio) | Mutation peak C/E (ratio) |
|---|---|---|---|
| shallow insertion | 32.911/54.972 MB (0.599×) | 199.574/121.647 MB (1.641×) | 135.411/131.869 MB (1.027×) |
| wide recursive insertion | 26.775/24.601 MB (1.088×) | 88.494/114.787 MB (0.771×) | 95.086/119.249 MB (0.797×) |
| low-overlap retraction | 32.900/54.955 MB (0.599×) | 199.494/121.623 MB (1.640×) | 135.365/128.747 MB (1.051×) |
| high-overlap retraction | 8.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:
| Workload | Circuit p50 | Established p50 | Circuit retained | Established retained |
|---|---|---|---|---|
| shallow insertion | 0.363 ms | 320.874 ms | 311,902,826 B | 561,408,256 B |
| wide recursive insertion | 0.423 ms | 645.292 ms | 250,855,327 B | 253,069,789 B |
| low-overlap retraction | 0.364 ms | 2,090.161 ms | 311,890,546 B | 561,391,896 B |
| high-overlap retraction | 0.248 ms | 633.624 ms | 84,866,742 B | 152,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:
| Workload | Circuit p50 | Established p50 | Retained ratio |
|---|---|---|---|
| well-founded exit | 52.624 ms | 524.349 ms | 1.41 |
| deep recursive insertion | 11.470 ms | 59.028 ms | 1.09 |
| text-key multiway retraction | 48.512 ms | 1,086.944 ms | 1.98 |
| upper-stratum retraction | 0.314 ms | 114.613 ms | 0.84 |
| lower-stratum negation assertion | 4.173 ms | 496.313 ms | 1.52 |
| recursive count assertion | 1.818 ms | 3,816.232 ms | 0.55 |
| recursive count pipeline assertion | 2.201 ms | 3,946.810 ms | 0.85 |
| projected sum retraction | 1.248 ms | 2,911.354 ms | 0.71 |
| projected maximum retraction | 1.767 ms | 2,903.440 ms | 1.97 |
| mixed recorded trace | 1.351 ms | 171.176 ms | 0.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:
| workload | scale | causal circuit | full evaluation | ratio |
|---|---|---|---|---|
| deletion cascade | 16 | 0.177 ms | 0.080 ms | 2.2x slower |
| deletion cascade | 32 | 0.433 ms | 0.294 ms | 1.5x slower |
| deletion cascade | 64 | 1.214 ms | 1.173 ms | 1.0x slower |
| deletion cascade | 128 | 4.229 ms | 4.892 ms | 1.2x faster |
| alternative support | 16 | 0.030 ms | 0.088 ms | 2.9x faster |
| alternative support | 32 | 0.053 ms | 0.313 ms | 5.9x faster |
| alternative support | 64 | 0.140 ms | 1.211 ms | 8.6x faster |
| alternative support | 128 | 0.543 ms | 4.927 ms | 9.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:
| workload | scale | causal circuit interval | prior full-evaluation midpoint | ratio at circuit midpoint |
|---|---|---|---|---|
| deletion cascade | 16 | 0.135–0.136 ms | 0.080 ms | 1.7x slower |
| deletion cascade | 32 | 0.281–0.284 ms | 0.294 ms | 1.0x faster |
| deletion cascade | 64 | 0.603–0.616 ms | 1.173 ms | 1.9x faster |
| deletion cascade | 128 | 1.352–1.367 ms | 4.892 ms | 3.6x faster |
| alternative support | 16 | 0.023–0.025 ms | 0.088 ms | 3.7x faster |
| alternative support | 32 | 0.031–0.033 ms | 0.313 ms | 9.9x faster |
| alternative support | 64 | 0.053–0.054 ms | 1.211 ms | 22.5x faster |
| alternative support | 128 | 0.172–0.229 ms | 4.927 ms | 25.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.
| Workload | Product/circuit p50 | Established p50 | Speedup | Product retained / established | Product mutation peak / established |
|---|---|---|---|---|---|
| WFS independent self-loops | 0.068 ms | 3.749 ms | 54.8x | 1.04x | 0.66x |
| WFS independent two-cycles | 0.099 ms | 3.937 ms | 40.0x | 1.04x | 0.66x |
| Projected Sum | 0.391 ms | 2.794 ms | 7.15x | 1.00x | 0.62x |
| Projected CountDistinct | 0.394 ms | 2.774 ms | 7.05x | 1.00x | 0.76x |
| Projected Min | 0.434 ms | 2.807 ms | 6.47x | 1.00x | 0.84x |
| Projected Max | 0.443 ms | 2.787 ms | 6.29x | 1.00x | 0.84x |
| Factorized projected Sum | 0.406 ms | 2.755 ms | 6.79x | 0.99x | 0.62x |
| Factorized projected CountDistinct | 0.400 ms | 2.776 ms | 6.94x | 1.00x | 0.76x |
| Factorized projected Min | 0.553 ms | 3.496 ms | 6.32x | 1.00x | 0.85x |
| Factorized projected Max | 0.477 ms | 3.439 ms | 7.21x | 1.00x | 0.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 ofC_Eand 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
checkwhose violation body reads membership in a computed concept (aniff/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-valuedCan— 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
isonly”), 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.
| Moment | Today | Under this record |
|---|---|---|
ox check | passes | unchanged — passes |
ox build | writes the artifact, warning about undefined heads | unchanged — 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 membership | commits, degrading the violation to an information-grade note | refused (OE1430); nothing commits |
A write whose check is Warning / Info / #[observe] | commits, reports | unchanged — commits, reports |
A read (query, derive) | unaffected | unchanged — 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 say | Closest on the default branch | What 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:
| Verdict | Condition | Meaning |
|---|---|---|
| Definite violation | body holds under wfTrue (is) | fires exactly as a classic check (RFD 0025 D4) |
| Definite non-violation | body definitely false at the point | does not fire |
| Inconclusive | body’s firing verdict hinges on a membership atom that is Can (undefined) at the point | first-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 shape | What is judged |
|---|---|
| write inside a check’s firing closure | that check discharges at the write’s admission point |
| write outside every firing closure | no membership-gated discharge runs (zero admission work) |
operation-qualified write (at t) | judged at point t; no all-times view is materialized |
| unqualified write | judged 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 / marker | Default disposition of a new inconclusive tuple |
|---|---|
Severity::Error (blocking) | block — refuse the write, OE1430 |
Severity::Warning / Severity::Info | report at undefined grade; never blocks |
#[observe] on any severity | report 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:
- committed evidence on a cone relation/concept/field carrying a bounded (non-all-time) valid-time window;
- a rule-validity window on a cone rule clause (RFD 0056);
- 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 aninlineINVENTORY 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:
-
Definite member, over cap. Residency and support evidence definitely establish
DependentFiler(f); the deduction ismoney(1_400). The body fires underis; the write is refused as a classic violation (Tax::E1099). Unchanged from today. -
Definite non-member. The filer is definitely independent (independent residency established). The body does not fire; the write commits. Unchanged.
-
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)isCan, and the deduction ismoney(1_400). Today (Context) this commits silently with an[undefined]note. Under this RFD the check isSeverity::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
Severitya 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-registrationOE1332 → OE1430(D5) is likewise unimplemented. - Behavior change for existing modules. A membership-gated
Severity::Errorcheck 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 toWarning/Infoor 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.leanon the honesty-gate branch (pull request #1826), not yet on the default branch: the possible reading (bodyHoldsPossible,wfViolationsPossible),inconclusiveas its difference against the definite setwfViolationsK3, 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 isOE0676). Does the undetermined disposition track the inherited pin, or is such a check’s undetermined disposition alwaysreportuntil 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_violationsask 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::Errorchecks 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 → OE1430re-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.leanon that branch adds the possible reading of a check body (bodyHoldsPossible— positive atoms againstwfPossible, negated subjects merely∉ wfTrue), its firing setwfViolationsPossible, andinconclusive := wfViolationsPossible ∖ wfViolationsK3— the violation-tuple-level set of D1, with the membership-atom case (wfPossible ∖ wfTruelifted 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_possibleandundefinedObservations_subset_inconclusive(the observe residue is part of the gate’s subject, never the whole), andhonesty_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 onlywfViolationsoverwfTrueand the negation-drivenundefinedObservationsresidue; 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
OE1430on 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 typeT— 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 fromT’s ancestry and the presence of awherefilter. 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::loadinoxc-runtime/src/module_load.rs, plusoxc-instantiate’slower/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_EPOCHand 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 —OE0660rejects any other form — soPis 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 say | Closest today | Now |
|---|---|---|
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-Iscollapse. - 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-Notcollapse. - 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) plusfrom_filter: Option<CborValue>(the optionalwhereterm).oxc-astcarries the surface;Argon.TypeSystem.Soundness.FieldViewis 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_seedresolves 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 byblake3(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_valueapplies theiof Tselection (only a strict narrowingT <: E,T != Eis a real filter) and thewhereterm.concept_admits_individualat this site admitsiff-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 runningoxbuilt fromorigin/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 toOk(false)and silently excluded — the view drops the row and a count over it is silently short (index.rs: “a legitimateOk(false)— the predicate is definitely false, or a missing-field open-world-unknown — still excludes”). A bracediffbody cannot call a rule (OE0660), so it can never itself stand off; the dishonesty here is an open-world-unknown collapsed tofalse, not a standoff coerced tofalse. - Shape-derived / rule element type (genuine standoff). Membership is
fixpoint-dependent.
concept_admits_individualreturnsTruth4::Canfor such a concept — “the faithful value isCan, never a fabricatedNot” (classify.rs) — and the store-local read path surfaces thatCanas 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.
- Field-refinement
- 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 noraw + gatesplit; the walk is recomputed at each read-model rebuild keyed on the storage write-generation pair (oxc-runtimeAGENTS.mdmodel_cachecontract).
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 build →
ox 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 dave —
dependentCount 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:
| Level | Condition | Outcome |
|---|---|---|
| L1 — head-declared | rule head parameter declares the receiver’s concept, and that concept’s file-local chain declares the field | Keyed(head). An unanswerable chain (cross-module, or chain not fully local) trusts the static annotation → Keyed(head) |
| L2 — single body pin | exactly one pin in scope | Keyed(pin); the runtime resolves the declaring ancestor on its side, so a cross-module single pin stays answerable |
| L3 — body-pin evidence | collect provable declarers; a pin the file-local oracle cannot answer falls back to the workspace <:-ancestry walk | unique 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 relation — R 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 x | View extent of self.f | Surfacing |
|---|---|---|
definitely-true (∈ I) | x is in the extent | ordinary row on $field::<C>::<f> |
| definitely-false | x is not in the extent | absent — 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.f— this 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 siblinghiddenobject counting the undetermined (can), over-determined (both) and determined-false (not) rows. A head’s WFS-undefined companion tuples ride that envelope asCanrows by default (derive_classical_envelope/derive_tagged_envelope), andhidden.canis 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 theCanrows drop from the row list whilehiddenis 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::AggregateOverUndefinedinoxc-reasoning/src/error.rs, raised fromoxc-reasoning/src/executor/aggregate.rs; the diagnostic isOE1332, 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). Acheckwhose 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
derivebinding the aggregate (OE1332, the runtime refusal raised whileox buildmaintains the read model). A valuederivesuch aspub derive dependentCount(h, n) :- Household(h), n = count(d for d in h.dependents)binds the count into a scalarn. 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 foldingundefinedtofalse. 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 acheck(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.fdirectly (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-
derivecase 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 bareexistsconjunct 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 norfalse. -
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”; “unknownmeans 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 ofOW0740) — 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)contributesIncomeItem(p, amount, USD); a view overIncomeItem.cseesUSD). - 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 thearity >= 3holder resolution (verified incatalog_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. Thewheremachinery (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.
| # | Decision | Status | Rationale |
|---|---|---|---|
| D1 | One pin-resolution ladder keys every concept-pinned field read; winner kind (plain / defined / incomparable) selects read shape | settled | same-named fields on distinct concepts are distinct cells; a single layered ladder is auditable and short-circuits file-local |
| D2 | Raw (extent-blind EDB) + gate (fixpoint rule) split for a fixpoint-decided extent | settled | keeps a no-seed-extent view live and delta-maintainable; membership decided inside the fixpoint, not at seed time |
| D3 | View 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 standoff | read-surface arm settled; aggregate arm OPEN | the 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 |
| D4 | The ladder consumes per-scope resolution; it does not rewrite the pass — one scope model in rule_ref.rs | settled | two 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 |
| D5 | A view over a subsumed-parent relation walks φ-images (RFD 0084); no re-lowering of subsumption | settled | mapped-tuple-inclusion is the merged semantics; D1-no-dropped-ends keeps the parent frame information-complete |
| D6 | Raw = delta-maintainable EDB, gate = ordinary fixpoint rule — compatible with the incremental strict model, no new recompute obligation | settled | states the contract without deep-integrating the maintenance circuits |
| D7 | Filtered views over a STANDOFF-CAPABLE element type refuse at build (fail-closed); lift is filter-free only; a braced refinement is neither lifted nor refused | settled | the where machinery is not fixpoint-hosted; never admit check-green / runtime-dead |
| D8 | Shared with admission RFD: one principle (undetermined never silently coerces), independent policies, no shared dispatch structure | settled | a 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 |
| D9 | Lift is a derivation-semantic change: bump DERIVATION_SEMANTIC_EPOCH, enter the change in the RFD 0072 projection-cache derivation commitment | settled | a 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.
| Code | Lane | Condition |
|---|---|---|
| OE0683 | filtered-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 |
| OE0685 | check-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 |
| OE0686 | check-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
OE068xrange is emitted or reserved anywhere on the tree. This record claims exactlyOE0683–OE0686as tabulated above;OE0680,OE0681andOE0682are 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.rsreturnsCan; the refusal isRuntimeError::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 toOk(false)(verified inindex.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
checkdeclaration, 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, theraw + gatecells 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-cachederivation_commitmentof 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 thederivation_commitmentstate 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) andOE0685/OE0686(pin incomparability) land with the check-plane slice;OE0684stays 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_wflemma and anavView_no_silent_coercecorollary againstArgon.Reasoning.Datalog.WellFounded, homed either in a newArgon/Reasoning/Datalog/NavView.leanor as an extension ofFieldView.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
- 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 >= 1when 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. - 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 asCanrows and already counts them inhidden.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 thehidden.cancount and thetruth-4/--explainCanblock 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 viewHousehold::dependents, so a future change to the companion breaks a test rather than a client.
- (a) Canonical surface — SETTLED. The
- 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
OE1430for 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). - Nested views (
self.f.gwherefandgare 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. - 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.
- Epoch composition with the projection cache (D9). Confirm the lift’s epoch
bump composes cleanly with the RFD 0072
derivation_commitmentstate on currentmainbefore the runtime slice lands. - Friendlier teaching render for aggregate-over-a-view’s-undefined-region
(cheap follow-up, not allocated). A scalar
derivecounting 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 “mycountoverself.dependents.” A dedicated, view-aware teaching render (possibly its own code later) that names the view and points at thecheck/ direct-read alternatives would be a cheap ergonomic win. Open — do not allocate a code now. - 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 propagationOE1367currently 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.
- Lean (substrate first). Extend
Argon.TypeSystem.Soundness.FieldView(already the reader of thefrom_relationclause, RFD 0005 §5.6) with the standoff-capable element-type case, and state the view-in-WFS semantics againstArgon.Reasoning.Datalog.WellFounded: a lifted view’s extent is thewfTrueslice of$field::<C>::<f>and its undefined region is thewfPossible ∖ wfTrueslice (the K3 boundary of D3). New statement plan: anavView_extent_wflemma (definite/undefined regions coincide with the fixpoint’sI/J ∖ Iat the view head) and anavView_no_silent_coercecorollary (an undefined element is in neither the definite-true nor the definite-false region). Home: a newArgon/Reasoning/Datalog/NavView.leanimportingWellFounded+FieldView, or an extension ofFieldView.lean— decide at slice time. - Check plane. The pin ladder (D1) as the single cell-resolution discipline
in
lower/rule_ref.rsconsuming 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). - Runtime slice A — lowering.
Module::loadsynthesizes the$field::<C>::<f>fixpoint rule, the raw+gate gate clause (D2), and the classifier re-homing clause for a liftedexists; records the raw-gated-cell load-time fact. - Runtime slice B — seeding + K3 boundary.
catalog_seedemits 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-timenav_catalog_keeps_valuetwo-valued drop for lifted views (store-local materialization stays for unlifted views). - Runtime slice C — epoch + cache. Bump
DERIVATION_SEMANTIC_EPOCH; enter the change in the RFD 0072 projection-cachederivation_commitment; add the cache-seeded ≡ recomputed differential arm for a lifted-view fixture (D9). - 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.
- 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 dualdetermined(A)) that reads the converged model and holds iff the membership atomA’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 readingCan-ness in a check body is sound (D2); the same operator in aderive/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’sCan-ness routed to no clause — the check’s firing verdict can still beCanon 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
isonly”), 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):
- The three verdicts, never collapsed (RFD 0086 D1). For each candidate
violation tuple, discharge distinguishes definite violation (body holds under
wfTrue— fire onis), definite non-violation, and inconclusive (the firing verdict hinges on a membership atom that isCan). Inconclusive is exactlywfPossible ∖ wfTruelifted to the check head — mechanized asbodyHoldsPossible ∖ bodyHoldsK3inspec/lean/Argon/Reasoning/Checks.leanon the slice-1 branch. - The default disposition, severity-derived (RFD 0086 D3 defaults column). An
unhandled inconclusive tuple blocks on
Severity::Error(OE1430), reports at undefined grade onWarning/Infoor#[observe], and is not emitted for an=> emitcheck. This RFD reuses that default exactly — it is what an unhandled path in a body falls through to (D3). - 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 atom | Holds iff | Reads |
|---|---|---|
undetermined(A) | A’s verdict is not definite at the point | the concept’s undetermined region (below) |
determined(A) | A’s verdict is definitely true or definitely false | the 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 A | Closed world (#[world(closed)], the default) | Open world (#[world(open)]) |
|---|---|---|
| derived / asserted | determined (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, unrefuted | undetermined | undetermined |
| absent | determined (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):
undetermined(A)/determined(A)— an intrinsic-predicate atom, sibling toiof/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.A is undetermined/A is determined— a postfix modal spelling on the reservediskeyword (reserved today, no active grammar rule). Reads as prose but introduces a secondis-form and a state-word lexicon (undetermined,determined) in operator position; rejected to avoid a bespoke postfix grammar for one operator.A?postfix /possible(A)— rejected:?collides with optionality (T?,Truth4Of<T>), andpossiblenameswfPossible(true-or-undefined), not theCan-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:
- 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
bridgebody, whose rules derive into a target standpoint and so contribute to the fixpoint. - 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 spellsdeterminedas a negation, so the writtennotmakes it doubly negated — semantically identical to theundetermined(A)one spelling away, and equally refused. - 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:
- Arming is per-body. The coverage analysis runs on a check declaration iff
some clause of that check contains
undetermined(...)ordetermined(...). A check body that uses no determinedness operator is not armed and never warns — itsCandisposition 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). - Once armed, every membership atom is in scope. For an armed check, the
analysis considers every membership atom
Mthe 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 holeOW0744catches. - The uncovered-path test. For each membership atom
Mof an armed check, warn (once, namingM) if the conservative analysis finds — or cannot disprove — a feasible body path where (a)MisCan, (b) that uncertainty leaves the check’s overall firing verdictCan, 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 routingundetermined(M)to the check’s own default disposition — e.g.undetermined(M) => blockon anErrorcheck — which silences the warning by making the fallthrough intentional and visible). Mere occurrence ofundetermined(M)/determined(M)does not establish coverage; the guard must cover the relevantCanpath.
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.arcorpus, of 18 non-fuzzcheckdeclarations only ~3 consume genuinely computed or defeasible membership whose verdict can beCan(and a dumb syntactic pass that cannot prove a base-marker typeCan-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 —Errorblocks 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
OW0744nearly 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 changeOW0744’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 atomAover 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 ak-column predicate would be ak-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
Canby a mutual-defeat cycle (RFD 0028;OW0740) is exactly the caseundetermined(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
OE1430per 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 acheckbody (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 existinginlineINVENTORY 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; evaluatingundetermined(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
| Dimension | Declaration-level disposition attribute (#[undetermined(...)]) | This RFD — in-body determinedness test |
|---|---|---|
| Expressiveness | One 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 cost | Near-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 readability | Trivial 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 / governance | Maximally 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. |
| Migration | None — additive over slice 1. | None — additive over slice 1. Both are pure additions on the shared honesty-gate base. |
| Soundness boundary | No new operator, so no plane-boundary risk. | Requires the check-plane-only invariant (D2) as a standing rule; refusal (OE1431) enforces it. |
| Implementation status | Drafted — 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 armed ⇒ no 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
| Form | Kind | Where legal | Meaning |
|---|---|---|---|
undetermined(A) | body atom | check body only | A’s membership verdict is Can (wfPossible ∖ wfTrue) at the point |
determined(A) | body atom | check body only | A’s verdict is definite (wfTrue or ∉ wfPossible); ≡ not undetermined(A) |
OE1431 | refusal | — | a determinedness test outside a check body, or over a non-membership argument |
OW0744 | warning | — | an 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 say | Under the declaration-level disposition attribute | Under 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.leangains the denotation ofundetermined(A)asA ∈ wfPossible P.ground ∖ wfTrue P.groundover the converged ground model, and a lemma that this denotation is well-defined only overP.ground(the mechanized statement of the D2 check-plane boundary — no denotation inside the fixpoint iteration). Extends the slice-1bodyHoldsPossible/bodyHoldsK3partition.
Verification (per slice)
- A module with no determinedness test is byte-identical in check behavior (negative control).
undetermined(...)/determined(...)in aderive/query body refusesOE1431;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’sCanpath unrouted warnsOW0744and 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
Canemits noOW0744and 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
Aand leaves a sibling membershipB’sCanpath unrouted warns namingB(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 aniffbody), 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’sCan? 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
oxsubcommand, 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 inC, whole — asserted classification,<:closure, predicate-refinement-derived membership, and rule-derived membership alike — and thatextentandmetafollow it.C(x),x : C, andiof(x, C)become three exact spellings of one question — for a bare or module-qualified concept path; theself::-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) == Cstays narrower thaniofunder upward<:closure (the metatype yields<:-minimal classifiers, by design);x :: Candself::-qualifiediofstay refused; theself::-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
$iofrule-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 wayiof,extent, andmetaanswer 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 chapterspec/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 : Voterfinds the voter.iof(p, Voter)finds nobody, silently, with no diagnostic.meta(p) == Voterfinds nobody, silently. Worse,meta(p)reports that person’s most-specific type asPerson— 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
| Spelling | Path | Behaviour over a shape-defined C |
|---|---|---|
C(x) | concept atom, rule plane | correct |
x : C, C a bare name | lowered to the concept atom (atom_to_atom_ir in atom_lower/mod.rs) | correct |
x : pkg::C, qualified | refused — OE0223 | fail-closed, not silent |
iof(x, C) | reserved head over $iof, seed plane | silently empty |
not iof(x, C) | same | silently matches everybody |
meta(x) == C | reserved head over $meta, seed plane | silently empty |
x :: C | refused — OE0223 in a declared rule; OE0204 ad-hoc | fail-closed |
not (meta(x) == C) | refused — unsupported term shape | fail-closed |
extent(C) in a rule | desugars to iof | silently empty |
ox query --extent C | store-local classifier | fail-closed |
x is C | not a membership spelling at all — see below | n/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 formx : T… is syntactic sugar foriof(x, T).”:123— “extenthas the signatureextent(t: TypeRef) -> Set<Entity>… Returns the set of entitiesxsuch thatiof(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 frompub fact T(x)assertions and the catalog-tier type×type rows from each: Tinstantiation 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:89introduces the intrinsic as the general membership predicate —iof(x: Entity, t: TypeRef) -> Bool // sugar: x : T— withextent(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:36defines derived membership in terms ofiof: “Membership is derived:extent(C) = { x : iof(x, parentᵢ) ∧ P(x) }”. Ifiofmeant asserted-only, the equation would be ill-formed.spec/rfd/0070-derive-backed-classification.md:48settles the shape case: “A shape-defined conceptCis an ordinary atomiof(x, C)of the reasoner’s well-founded / AFT fixpoint — uniform with every other ground atom. There is no separate classifier fixpoint”. Line:143records the biconditional as a proved surface promise: “L5 | parent/body split = surface promiseiof(x,C) ↔ iof(x,parent) ∧ shapeBody| PROVED-scratch”.spec/lean/Argon/TypeSystem/Realization.lean:13-15states the realization biconditional for the refinement case, andrealize_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 (assertediofUNION derivediff,<:-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:
| spelling | asserted | braced iff {…} | shape clause | upward closure | imported shape clause |
|---|---|---|---|---|---|
C(x) | member | member | member | member | member |
x : C | member | member | member | member | member |
x : mod::C | member | member | member | member | member |
x : self::C | empty | empty | empty | empty | empty |
iof(x, C) | member | member | empty | member | empty |
iof(x, self::C) | refused | refused | refused | refused | refused |
iof(x, mod::C) | member | member | empty | member | empty |
iof(x, t) bound | member | member | empty | member | empty |
meta(x) == C | member | member | empty | empty by spec | empty |
x :: C | refused | refused | refused | refused | refused |
x in extent(C) | member | member | empty | member | empty |
x in extent(t) bound | member | member | empty | member | empty |
P(x), not C(x) | non-member | non-member | non-member | non-member | non-member |
P(x), not x : C | non-member | non-member | non-member | non-member | non-member |
P(x), not iof(x, C) | non-member | non-member | EVERYONE | non-member | EVERYONE |
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 specis not a defect.meta(x) == Cunder 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:
$specializesis reflexive (standpoint.rs:286-290: “for each declared conceptt,ancestor_concept_ids_including_self(t)yieldstitself 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, andmetawithout 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
OE0211refuses precisely where the two planes would diverge, so the place where the distinction has operational meaning is already governed. (OE0211is narrower than a general plane-divergence gate: it refuses allinsert/delete iofon 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
iofdoes not.iof,meta, andspecializesare lexed as keywords (compiler/crates/oxc-syntax/src/generated.rs, thefrom_keywordtable), so they can never collide with a user identifier.extentis 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). Anasserted_iofwould 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
iofmeaning one thing. Under a refusal,iofmeans asserted-plane membership andx : Tmeans full membership, while the manual documents them as synonyms — a permanent two-meaning split every reader must learn, and a:115-versus-:143contradiction 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
iofread 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:
| Group | Cells | Closed by this record? |
|---|---|---|
The shape-clause shape: iof (bare, qualified, bound type variable), meta ==, extent (constant, bound), and not iof | 7 | Yes — D2 |
| The same seven, reached through an import | 7 | Yes — D2; the module boundary is not the cause |
p : self::C, silently empty in all five shapes | 5 | No — 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.
meta(x) == Cunder 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 ofC, its minimal classifier is that subtype, soCis absent — the individual is a member ofCand is not most-specifically classified by it. The sweep records this cell as differing by specification, not as a defect.metaasks a narrower question thaniofby 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.- The two refusals stay refusals.
x :: Cis refused, andiofwith aself::-qualified path is refused (OE0227). Neither becomes an answer under this design, in any shape.x :: Cis the metatype-equality sugar and is refused before evaluation; widening it is a separate decision with its own surface consequences. Theself::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; makingiofsilently 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), andmeta(x) == Creturn the same answer for the same concept, for a constant or a variable type position — with the two non-goals above, and withself::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 --extentover 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. -
$iofmoves 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$iofmoves 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_entityaccumulation 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.
-
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. -
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. -
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_commitmentincompiler/crates/oxc-oxbin/src/projection_cache.rs). -
Derived
$iof. The bridge rules, the deleted individual-tier scan, the stratification wiring. First user-visible change; epoch bump — 12 or later.extentfollows for free (D3), and the--extentrefusal is retired here. D7’s warning ships one release before this slice.Preconditions, both blocking. (a) Issue #1902 — the
is unknowntruth 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. -
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. -
Optimization. Bounds on the free type position, incremental-maintenance work for the widened
$iofimpact 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
- Does
x is Tbelong to this surface at all? It does not appear to share the:lowering path:isintroduces the optional-payload test and the reasoning-outcome suffix (rule_atomandatom_suffixincompiler/crates/oxc-parser/src/rule_atom.rs), andp is Voteris refused as a relation-valued application (OE1386). No claim in this record covers it. Settled by: an audit of theissuffix grammar against the membership surface, deciding whetherx is Tshould be a membership spelling, a refusal with a message pointing atx : T, or left as it is. Required before any statement about “every membership spelling” is complete. - 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_nameincompiler/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 theis unknownsuffix so the undefined case is readable at all (atom_suffixincompiler/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. - What bounds a free type position?
iof(x, t)withtunbound 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$specializesor equality constraint ont, by a cost-based warning, or not at all. This is slice 6 and must not become a soundness argument. - 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.
- 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. - Should the mechanized type-test atom gain an evaluation semantics?
typeTestandmetaEqare 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. - 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 whetherself::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. - 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 asNotor asCan, 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) vsiff(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/$metametatype 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 #1902 —
is unknownreturns 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 andextentOfspec/lean/Argon/TypeSystem/Realization.lean— the realization biconditionalspec/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.