Argon
Argon is a knowledge-representation language and graph database. You declare an ontology — concepts, the relations between them, and the rules that derive new facts — and Argon compiles it to a content-addressed artifact you can query, reason over, mutate, and serve.
This book is the surface specification: the syntax and meaning of an Argon program — the precise reference, chapter by chapter, in the technical specification and its appendices.
For a guided, motivated tour of the whole language, read The Argon Book, the teaching narrative (spec/book/). To watch real packages compile and run, see Argon by Example (examples/).
Two companion sources of truth sit beside this book:
- The Lean 4 mechanization (
spec/lean/) is canonical for the substrate — the type system, reasoning semantics, and decidability results. Where the Lean and this book disagree on something the Lean covers, the Lean wins; the book is the bug. - RFDs (Part II) record design decisions and their rationale. They are history, not normative spec.
Each chapter leads with a grammar fragment and a worked example before the prose. Cross-references are named links; each names the chapter or section it points to.
Toolchain: installation, channels, and versioning
How Argon is installed, versioned, and switched. oxup is the toolchain
manager (rustup-style); ox/oxc/ox-lsp/oxfmt are the tools it dispatches.
Install
curl -fsSL https://install.argon-lang.org/install.sh | sh
Installs oxup into ~/.argon/bin, links the tool shims, and installs the
stable toolchain. Ensure ~/.argon/bin is on PATH.
Platforms: macos-arm64, linux-x86_64, linux-aarch64.
Channels
| Channel | Built from | Cadence |
|---|---|---|
stable | a vX.Y.Z tag on main | per release |
nightly | main tip | daily, 04:00 UTC |
dev | main tip | every merge to main (coalesced); also on-demand per branch |
Version strings
X.Y.Z is the in-development line; dev/nightly are prereleases of it (they
sort ahead of the last stable). oxup and the toolchain share one version.
| Channel | Format | Example |
|---|---|---|
| stable | X.Y.Z | 0.2.2 |
| nightly | X.Y.Z-nightly.<UTC-date> | 0.2.2-nightly.2026-06-16 |
| dev (main) | X.Y.Z-dev.<UTC-timestamp> | 0.2.2-dev.20260616T034155Z |
| dev (side branch) | X.Y.Z-dev-<branch>.<short-sha> | 0.2.2-dev-feat-x.c6b2b3fa6 |
Distribution layout
Served from https://install.argon-lang.org (S3 origin behind CloudFront):
/install.sh installer
/toolchains/<version>/<platform>/argon-<version>-<platform>.tar.gz(.sha256)
/dist/channel-<channel>.toml current version per channel
/dist/index-<channel>.json all published versions
/oxup/latest/<platform>/oxup(.sha256) oxup self-update (stable)
/oxup/<channel>/latest/<platform>/oxup(.sha256) oxup self-update <channel>
/editors/vscode/<version>/argon-<version>.vsix(.sha256) editor extension
/toolchains/<version>/is immutable. dev/nightly tarballs are pruned 30 days after publish; stable is kept indefinitely.index-<channel>.jsonis newest-first: an array of{ version, released, platforms[], sha256, available }. A pruned build stays listed with"available": false.
Toolchain spec
A spec identifies a toolchain to install/use/+<spec>:
| Spec | Resolves to |
|---|---|
stable / nightly / dev | the channel’s current version |
<channel>:latest | same, explicit |
<channel>:<date> | the channel build for that day (nightly:2026-06-16) |
<full-version> | exact (0.2.2-nightly.2026-06-16, 0.2.1) |
latest | alias for stable |
oxup commands
oxup use <spec> install if needed, then set as default
oxup default <spec> alias for `use`
oxup install <spec> install without changing the default
oxup uninstall <spec> remove an installed toolchain
oxup update [channel] re-fetch a channel's latest
oxup list installed toolchains (default marked)
oxup list --available [--channel C] [--limit N] published versions
oxup search <pattern> search published versions across channels
oxup show active toolchain, default, installed
oxup which <tool> [--toolchain <spec>] resolved binary path
oxup self-update [channel] update oxup itself (stable; or dev/nightly)
oxup extension install|uninstall|list editor extension (matches the active toolchain)
--json is available on list, search, and show (for tooling).
Selecting a toolchain (per invocation)
First match wins:
OXUP_TOOLCHAIN=<spec>+<spec>as the first argument — e.g.oxc +nightly buildox-toolchain.tomlin the working directory or a parent —[toolchain] channel = "<spec>"~/.argon/settings.toml—[default] channel- the built-in
stable
A missing toolchain is fetched automatically on first use.
~/.argon/
~/.argon/
bin/
oxup the manager
ox oxc ox-lsp oxfmt symlinks → oxup
toolchains/<spec>/
bin/ ox oxc ox-lsp oxfmt
share/std/ bundled stdlib packages
manifest.toml version, platform, components
provenance.toml source (cdn|local), origin, sha256, install time
settings.toml [default] channel, [extension] state
ox/oxc/ox-lsp/oxfmt on PATH are symlinks to oxup; oxup reads
argv[0], resolves the active toolchain, and execs
~/.argon/toolchains/<spec>/bin/<tool>.
Tools
| Binary | Role |
|---|---|
ox | CLI driver — build, query, serve |
oxc | compiler |
ox-lsp | language server |
oxfmt | formatter |
Formatter notation policy
Argon’s dual notation (Unicode notation) lets source mix Unicode glyphs (∀ ⊑ → ⊞) with their ASCII spellings (forall <: -> box_plus); both lex to the same token. The formatter can normalize a file to one form, governed by [fmt] notation in ox.toml:
[fmt]
notation = "preserve" # | "unicode" | "ascii"
preserve(the default) never rewrites between the two forms — a file is formatted as typed, so mixed notation is left untouched.unicoderewrites every ASCII spelling to its glyph (forall→∀);asciidoes the reverse. The rewrite is lossless and round-trips, because both forms produce the identical token.
The policy resolves through a nearest-wins cascade (rustfmt/prettier-style): a package’s own [fmt] overrides a [workspace.fmt] at the workspace root, which overrides the global user config. The notation table itself is the single [[notation]] source shared with the lexer aliases and editor input methods. Notation is policy (a per-project taste call); spacing and layout are the Argon convention and are not configurable.
Editor support
Everything an editor knows about an Argon program comes from one place: the language server, ox lsp. The reason is the meta-calculus. Argon’s surface vocabulary is user-extensible — a package declares its own metatypes, concepts, relations, and constructs, so the set of meaningful words in a file is not fixed by the language. A static grammar can color and brace-match the punctuation, but it cannot know that Obligation is a concept or that owes is a relation; only the resolver that elaborated the package knows that. So all the language-aware behavior — diagnostics, hover, jump-to-definition, the right token colors — lives in the server, and editors are thin clients of it. One server, written once, drives every editor; an editor plugin is just an LSP client plus a syntax floor.
What ox lsp provides
ox lsp speaks LSP over stdio and advertises:
| Capability | What it gives the editor |
|---|---|
| Diagnostics | The full ox check diagnostic set, live as you type (debounced) |
| Hover | The type / declaration of the symbol under the cursor |
| Go-to-definition | Jump to where a name is declared |
| Semantic tokens | Vocabulary-aware coloring the static grammar cannot produce |
| Completion | Member completion after ::, and identifiers |
| Document symbols | The file outline / breadcrumbs |
| References | Find-usages across the workspace |
Inlay hints and code lenses are not built yet. Because the diagnostics are the server’s, they always match the toolchain that produced them — there is no second, drifting implementation of the checks.
Installing it
oxup installs and version-locks the editor integration, so the editor support matches the active toolchain:
oxup extension install # auto-detect every installed VS Code-family editor
oxup extension install --editor neovim # or name one explicitly
oxup extension uninstall --editor emacs
oxup extension list # detected editors + the installed version
The two editor families install differently because they expose different contracts:
- VS Code family (VS Code, Cursor, VSCodium, VS Code Insiders, and the browser-hosted code-server) have an
--install-extensionCLI.oxupfetches the.vsixstamped with the active toolchain version from an immutable CDN path, sha256-verifies it (the same fail-closed discipline as the toolchain fetch), and installs it. A user onstable0.2.2 gets the 0.2.2 extension, not “latest”. - Neovim, Vim, and Emacs have no extension-CLI contract, so
oxupinstalls them by file placement: it writes the embedded plugin files into the editor’s package directory and adds an idempotent, oxup-managed block to the init file (between>>> argon (oxup-managed) >>>sentinels) that points the client at the active toolchain’sox lsp. Re-running updates the block in place and leaves everything outside the sentinels untouched;uninstallremoves both.
oxup init and oxup update auto-install and refresh the integration after the toolchain is placed (oxup init --no-extension opts out). A failed editor install is a soft, one-line warning — the toolchain is what matters — never a hard error.
The VS Code extension ships the Argon “A” glyph as the .ar file icon. The Neovim and Emacs plugins cannot reuse that SVG (their file explorers draw a Nerd Font glyph, not an image); their READMEs give a suggested glyph-and-color registration keyed on the .ar extension.
Environment
| Variable | Effect |
|---|---|
OXUP_TOOLCHAIN | override the active toolchain |
OXUP_DIST_URL | override the distribution base URL (mirror, offline, testing) |
ARGON_CONFIG | path to the global ox user config (formatter notation cascade) |
Scope
Argon is a programming language for declaring and reasoning over a richly-typed knowledge graph at the data-system layer. The canonical specification of the language core lives in Lean 4 under spec/lean/; this document specifies the surface in prose. Where they disagree, the Lean wins.
The language is built from five atoms — meta-calculus, constructs, rule, trait, macro — and nothing else. Every surface form lowers to one of them. Argon has no surface for IO, threads, network, or filesystem; those live in the host runtime. Tenancy and multi-tenant orchestration are application concerns, not language concerns.
Lexical structure
Source
UTF-8. Source files have extension .ar. Non-ASCII text is admitted in identifiers
(Identifiers, UAX #31), comments (Comments), and string/char
literals (Literals). Operators and keywords have a canonical ASCII spelling and an equivalent
Unicode form — Lean-style dual notation (Operators): ∀ is forall, ⊑ is <:, → is ->,
⊞ is box_plus. The two are interchangeable surface spellings of the same token, so a
program may be written in either (or a mix), and editors enter the Unicode forms through a
\abbrev input method (\forall → ∀). Delimiters and the remaining punctuation are ASCII.
Comments
//— line comment, not extracted byox doc.///— outer doc comment, attaches to the next item.//!— inner doc comment, attaches to the enclosing module./* … */— block comment, may be nested. Not extracted byox doc. Comments out arbitrary tokens including//-prefixed lines, useful when temporarily disabling sub-expressions.
Identifiers
Identifiers follow UAX #31: an identifier starts with a
XID_Start character or _, and continues with XID_Continue characters. The ASCII subset
[A-Za-z_][A-Za-z0-9_]* is the common case and the recommended style. An identifier’s text is its
exact source bytes. NFC canonical equivalence (so that a precomposed é U+00E9 and the
decomposed e + combining acute U+0065 U+0301 would denote the same name) and a mixed-script
confusable warning (UAX #39, Latin A U+0041 vs
Cyrillic А U+0410) are the RFD 0034 name-resolution design (Out of scope). Conventions: snake_case for value bindings, fields, and rules (fn, derive, query, mutate, check); CamelCase for types, metatype-introduced concepts, and metarel-introduced relations; lowercase (typically snake_case) for the metatype- and metarel-introducing keywords themselves (kind, role, rel, mediation, …); SCREAMING_SNAKE_CASE for stdlib constants. Conventions are not enforced.
Operators
| Token | Meaning |
|---|---|
: | typing / instantiation (x: T, Dog : AnimalSpecies); metatype-body axis binding (rigidity: rigid) |
<: | specialization, trait inheritance, trait bound (⊑ alias) |
:: | qualification — path or axis::value |
:- | Datalog rule-body separator |
=> | rule head, match arm, check-rule diagnostic emission |
= | value assignment, single-expression body, kind-level value |
? | optional sugar (T? ≡ Option<T>); error propagation on Result |
.., ..= | range, cardinality |
-> | rule return type |
+, * | arithmetic; transitive / reflexive-transitive closure on role paths |
-, / | arithmetic |
==, !=, <, <=, >, >= | comparison |
&&, ||, ! | boolean (expression context) |
, | conjunction in Datalog body; tuple/list element separator |
; | statement terminator |
| | variant separator |
#[…] | attribute (compiler-known or procedural macro), outer form |
#![…] | inner attribute, applies to enclosing item |
name!(…) | declarative or procedural macro at expression position |
.. (prefix) | struct-update spread |
_ | wildcard / anonymous binding |
Top / ⊤ and Bot / ⊥ are type-level constants.
Unicode notation
Each operator/keyword below has an equivalent Unicode glyph; the glyph and its ASCII
spelling lex to the same token, so they are fully interchangeable and nothing past the lexer
distinguishes them. Glyphs and the editor \abbrevs follow the Lean lean4-input conventions;
the metric-modal glyphs (⊞/⊟) follow the DatalogMTL literature. The authoritative table is
[[notation]] in compiler/crates/oxc-syntax/grammar.toml, the single source from which the
lexer’s aliases and the editor input methods are generated.
| Glyph | ASCII | \abbrev | Glyph | ASCII | \abbrev | |
|---|---|---|---|---|---|---|
∀ | forall | \forall | ≤ | <= | \le | |
∃ | exists | \exists | ≥ | >= | \ge | |
↔ | iff | \iff | ≠ | != | \ne | |
¬ | not | \not | → | -> | \to | |
∧ | && | \and | ← | :- | \l | |
∨ | || | \or | ∈ | in | \in | |
⊑ | <: | \sqsubseteq | ∪ | union | \cup | |
⊤ | Top | \top | ∩ | intersect | \cap | |
⊥ | Bot | \bot | ∖ | except | \setminus | |
□ | box | \box | ⊞ | box_plus | \boxplus | |
◇ | diamond | \diamond | ⊟ | box_minus | \boxminus |
=> is deliberately not given a Unicode form: it is match-arm / head syntax, not implication,
so ⇒ would mislead. diamond_plus / diamond_minus have no single conventional glyph (the
notation is ◇ with a directional marker) and stay ASCII-only for now.
Boolean operator placement. Argon distinguishes two syntactic positions:
- Rule-body position — after
:-inderive/check, insideunsafe logicblocks, thefrom/whereclauses ofquerybodies, subquery bodies (collect/set/one/count/exists), refinementwhere { … }/iff { … }clauses, andrequire { … }precondition blocks. Conjunction is,; NAF isnot atom. Disjunction is not inline — write multiple rules with the same head, or combine subquery variants viaunion/intersect/except. - Expression position — inside
fnbodies,if/matchconditions,letinitializers, the RHS of comparison atoms inside rule bodies, and atom arguments. Boolean operators are&&,||,!.
The two are non-overlapping: && is not accepted between rule-body atoms; , is not a Boolean operator in expressions.
Literals
INT ::= [0-9]+ ( '_' [0-9]+ )*
REAL ::= INT '.' INT
STRING ::= '"' chars '"'
TEMPORAL ::= '#' ISO '#'
BOOL ::= 'true' | 'false'
LIST ::= '[' (expr (',' expr)*)? ','? ']'
RECORD ::= '{' (Ident ':' expr (',' …)*)? ','? '}'
A temporal literal is one #…#-delimited form whose inner ISO-8601 / RFC-9557 shape totally
determines the carrier (RFD 0067) — there is no per-type prefix and no trailing
suffix that silently flips the kind. The shape is decided statically at parse; a malformed or
ambiguous inner form is loud-refused, never coerced:
FRAC ::= '.' [0-9]+
OFFSET ::= ('+' | '-') HH ':' MM
DATE ::= '#' YYYY '-' MM '-' DD '#' → Date (civil, day)
TIME ::= '#' HH ':' MM ':' SS FRAC? '#' → Time (civil wall-clock)
DATETIME ::= '#' YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS FRAC? '#'
→ DateTime (civil, no zone)
INSTANT ::= '#' … 'T' … (SS FRAC?) ('Z' | OFFSET) '#' → Instant (absolute UTC point)
ZONED ::= '#' … 'T' … (SS FRAC?) OFFSET '[' IANA/Zone ']' '#'
→ ZonedDateTime
The trailing Z denotes an Instant (UTC, absolute), following ISO/RFC-9557 (Z = UTC), not
a civil DateTime; a bare #…T…# with no zone marker is the zoneless civil DateTime. There is no
mandatory Z. Only Date and civil DateTime are compiler-executable today; the Time,
Instant, and ZonedDateTime literal forms are recognized by the surface grammar ahead of the
value-layer landing them (issue #1285).
Set/map literals are constructor calls (Set::new(a, b, c), Map::new((k, v), …)); there is no {1, 2, 3} form (Out of scope).
Modules and packages
Structure
Modules take three forms, all equivalent at the symbol table:
- A
.arfile is implicitly a module named for its file stem. - A directory containing
mod.aris a module; sibling.arfiles (and subdirectories containingmod.ar) are submodules of it. - An inline
mod Name { … }declaration introduces a nested module in the current file.
A mod Name; declaration with no body has a single meaning — Rust’s child-module declaration: it asserts that a sibling Name.ar (or Name/mod.ar directory module) exists and attaches its contents under the current namespace as Name. A file does not rename itself: a content-carrying file whose mod Name; resolves to no sibling is refused with OE0715 (audit dc-06 retired the legacy “module-header” reading, where the first mod Name; of a file silently relabeled that file’s own declarations as module Name). To set a module’s name, use the file/directory layout — the project entry is module root, and any other .ar file is the module named for its stem; to declare a child, create the sibling Name.ar; to keep declarations where they are, omit the mod line. Explicit mod declarations are required; the compiler emits OW0710 when a sibling .ar file exists but no mod declaration references it.
Both project and package entry default to the root module file — preferring the Cargo-style src/root.ar, falling back to a flat root.ar at the package root. A prelude.ar is not an entry default: a package’s prelude is the manifest [package].prelude import model (the auto-imported use-tails of RFD 0038), and a prelude.ar file is an ordinary re-export module reached through the root — conventionally pub used at the root and imported by dependents via use pkg::prelude::*. The entry’s directory is the source root under which the package’s .ar files are indexed.
visibility ::= 'pub' | 'pub' '(' 'pkg' ')'
mod-decl ::= 'mod' Ident ';' // file-include
| 'mod' Ident '{' mod-item* '}' // inline
| 'mod' Ident '=' path '(' expr-list ')' // functor alias (Functor modules)
Default visibility is module-private, following Rust: a default-visibility item is visible to the module that declares it and every descendant module. A private item declared at the package root is therefore visible package-wide (every module descends from the root); a private item in mod M is visible to M and M’s submodules, but not to the root or to sibling modules. This governs the privacy check only — name resolution is unchanged: a descendant still names an ancestor’s item through pkg::/super::/use (there is no bare-name inheritance from a parent module). pub exposes to dependents; pub(pkg) limits to the package.
Imports
use ::= 'use' use-tree ';'
use-tree ::= path ('as' Ident)?
| path '::' '*'
| path '::' '{' use-tree (',' use-tree)* ','? '}'
path ::= Ident ('::' Ident)*
Rust-style: single-symbol (use std::math::Nat;), brace-list (use std::math::{Nat, Int};), glob (use ufo_foundational::prelude::*; — a third-party vocabulary package), and aliased (use foo::bar::Sym as Other;) forms.
A plain use is a private import — visible only inside the importing module. A pub use re-exports: the imported items join this module’s public surface and are visible transitively to dependents, which is how a package prelude works (pub use pkg::prelude::*).
Re-exports resolve, including renamed. A single-item use M::X where X is re-exported by M (rather than declared there) resolves, and so does a renamed re-export pub use M::X as Y. A renamed re-export is reachable only under the alias Y — the original name X is not brought into scope through it (the same rule a renamed use … as follows). Import-resolution limit. One form parses but does not resolve: a capitalized Self:: type path (lowercase self:: works).
Glob-import shadowing follows the Rust convention. A glob import (use m::*) is the lowest-priority binding: an explicit import (use m::X) and a same-module declaration both silently shadow a glob-imported name of the same spelling — no diagnostic, and the choice does not depend on import order. Two glob imports that bring the same name from different items are ambiguous — using that name is refused (an unused ambiguous name is not an error); import the intended item explicitly, or qualify the reference, to disambiguate. Two globs that resolve to the same item (for example one module re-exporting the other’s) are not a conflict.
The prelude is a package feature, not a compiler default (RFD 0038). A package’s [package].prelude (an array of use-tails in ox.toml) is auto-prepended to every module of that package; it is empty by default and carries no vocabulary. type/rel are not in it unless the package opts them in (prelude = ["std::core::{type, rel}"], or prelude = ["pkg::prelude::*"] pointing at a prelude.ar of pub use re-exports). A module opts out with #![no_implicit_prelude]. There is no auto-imported std::prelude; what is always in scope is the language substrate (step 3 below), which is not a library prelude.
Manifest
ox.toml, Cargo-flavored:
[package]
name = "lease-story"
version = "0.1.0"
edition = 0
# Optional package entry override. Without this, the default search prefers the
# Cargo-style `src/root.ar` and falls back to the flat package root `root.ar`.
entry = "root.ar"
[dependencies]
# PATH dependencies (RFD 0030). The key is the path root the
# consumer uses (`use ufo::endurant::Object;`) and must equal the dependency
# package's `[package].name`.
ufo = { path = "../ufo" }
[lattice]
# Decidability-tier ceiling (Standpoints and modal operators). A declaration whose classified tier exceeds
# this ladder name is refused at `ox check` / `ox build` (OE1230, RFD 0030 D6).
max_tier = "recursive"
[standpoints]
# optional package-boundary declarations
[schema]
# Optional source root for generated or nested package layouts. Entry paths are
# resolved relative to this root when the nested entry exists.
root = "src"
Tooling resolves a package directory entry in this order: [project].entry, [project].main, [package].main, [package].entry, then the conventional defaults. With no explicit entry, the default search prefers the Cargo-style src/ layout and falls back to the flat package root (so existing packages keep resolving): both [project] and [package] try src/root.ar then root.ar. The first that exists wins, and its directory becomes the source root — a package laid out as src/root.ar roots all its .ar sources under src/ with no config (ox new/ox init scaffold exactly this). An explicit [schema].root overrides the source root. When no entry is found, the error names every path it searched.
Dependencies (RFD 0030). Only dep = { path = "…" } resolves: each path dependency’s package is loaded and its pub surface folded into the consumer’s workspace under the dependency name as a path root (see Name resolution). The registry/VCS surface — a bare version requirement (ufo = "1.0") or the version/git/branch/tag/rev/registry keys — is recognized and refused with OE1240 (Out of scope), never silently ignored. The dependency name must equal the dependency’s published [package].name (OE1241 otherwise); a dependency cycle is OE1242; the same name resolving to two different directories is OE1243. There is no ox.lock for a path-only graph (path deps are unlocked, Cargo precedent); ox.lock is reserved for the registry/git graph. Manifest honesty (RFD 0030 D6): an unrecognized section or key surfaces a Cargo-style OW1240 “unused manifest key” warning rather than being silently dropped, and [lattice].max_tier is enforced against the Standpoints and modal operators classifier (OE1230).
Name resolution
Resolution of a bare identifier proceeds in order:
- Local scope. Definitions in the current module, including the lexical chain of enclosing
modbodies andimplblocks. - Imports + package prelude. Items brought into scope by
usein the current module, and — unless#![no_implicit_prelude]is set — the package’s prelude ([package].prelude, auto-prependeduses; RFD 0038). The prelude is empty by default and carries no vocabulary. - Substrate. The fixed, always-in-scope language primitives — not a library, and not opt-outable: the primordial types (
Nat,Int,Real,Decimal,Money,Date,Time,DateTime,Duration,Bool,String,Top,Bot) and the builtin type forms (Option,Result,Ordering,List,Set,Map,Range,Truth4,Truth4Of,Diagnostic,Severity, …).
A qualified identifier (containing ::) is resolved by walking the path. The leading segment selects a root:
pkg— the current package’s root. This is the sole self-reference anchor: a package never refers to itself by its own name, so internal paths survive a package rename.pkg::a::b::Xnames itemXin modulea::bof this package. (Argon has packages, not crates — there is nocratekeyword.)self— the current module;super— the parent module (repeatable:super::super::X).- A dependency package name declared in
[dependencies]— e.g.ufo::endurant::Object(a path-dependency vocabulary package, RFD 0030). The dependency’spubsurface is folded into this workspace under its name as a path root;ufo::Xanduse ufo::X;resolve into it. The stdlib rootstdis always available (std::math::Nat). - Otherwise the leading segment resolves against the scope chain — a submodule of the current module, or a name brought in by
use.
Subsequent segments resolve within the preceding module’s namespace. An item therefore has a single absolute name viewed from outside the package (mypkg::a::b::X); viewed from inside, the equivalent absolute form is pkg::a::b::X.
Contextual introducer identifiers (type, or vocabulary names like kind, role, phase, …) are ordinary identifiers — recognized by an IDENT IDENT shape at declaration position and resolved via the algorithm above against the pub metatype (concepts) / pub metarel (relations) declarations visible in scope: declared in this package, or imported from a vocabulary package (across a [dependencies] path-dependency boundary, RFD 0030). Nothing is ambient here — not even type/rel: a package brings the no-commitment baseline into scope explicitly, via use std::core::{type, rel} or its [package].prelude (RFD 0038), exactly as it brings any vocabulary (appendix A). The compiler emits OE0101 UnresolvedName for general failures, and OE0605 UnknownMetatype / OE0606 UnknownMetarel specifically when a leading identifier at declaration position fails to resolve — at ox check and ox build; no artifact is emitted. A use statement that does not resolve is itself refused with OE0103 UnresolvedUseImport (with a did-you-mean over the visible namespace, RFD 0030 D5) — a broken import is never silently accepted.
Module extraction (tree-shaking)
When a module imports another, the elaborator extracts a ⊥-locality module (Cuenca-Grau–Horrocks–Kazakov–Sattler 2008): the smallest subset of the imported module’s axioms that stays non-trivial once every concept outside the used signature is reinterpreted as ⊥. Extraction is a fixpoint, linear-time per iteration, terminating in at most |B| iterations.
The guarantee is Σ-scoped conservativity: every entailment about the used signature survives extraction. Argon strengthens the classical result for its own semantics — closed-world conclusions are preserved (CWA-conservativity), ghost individuals reachable only through non-Σ concepts are pulled in (domain-conservative extraction), defeasible rules drag in their defeaters (defeat-aware extraction — a classical extractor is unsound under non-monotonic semantics, Bonatti-Faella-Petrova-Sauro 2022), and extraction composes safely across an import chain. All are mechanized at spec/lean/Argon/Locality/.
Extraction is how the substrate composes decidability tiers across heterogeneous modules (Tier ladder): each extracted module is classified independently, and conservativity guarantees no entailment is lost. Failures emit OE0710 (conservativity cannot be proven under the importer’s world assumption) or OE0711 (defeat-aware extraction cannot close the defeat graph).
Meta-calculus atom
Three declarations: metaxis, metatype, metarel. They make the language ontology-neutral by letting packages declare the concept-introducing keywords.
metaxis
metaxis-decl ::= 'metaxis' Ident 'for' target metaxis-body? ';'
target ::= 'metatype' | 'metarel' | 'individual' | 'macro' | '[' target (',' …)* ']'
metaxis-body ::= '{' enum-body '}' | '=' TypeExpr ('where' refinement)?
enum-body ::= Ident (',' Ident)* // unordered
| Ident ('<' Ident)+ // totally ordered (chain)
pub metaxis rigidity for metatype { anti_rigid < semi_rigid < rigid };
pub metaxis sortality for metatype { sortal, non_sortal };
pub metaxis identity_provision for metatype { provides, inherits };
pub metaxis dependency_kind for metarel { existential, contingent, none };
pub metaxis ordinality for metatype = Nat;
pub metaxis weight for metatype = Real where _ > 0.0;
A metaxis characterizes the meta level: its values are assigned to metatypes (for metatype) and metarels (for metarel), never to a concrete type directly. The targets name the meta-sort the axis applies to — metatype for the concept tier, metarel for the relation tier — not the type / rel concept introducers. (A future for <DeclaredType> target — a type-specific refinement axis bound to one declared type — is left open by the model but not yet specified.)
Cross-axis constraints are check rules at module level, never embedded in the body.
metatype
metatype-decl ::= 'abstract'? 'fixed'? 'metatype' Ident '=' '{' axis-assign (',' …)* ','? '}' ';'
axis-assign ::= Ident ':' value
value ::= Ident | literal
A metatype body is a record of axis values, so each binding reads like a struct field — a single : separates the axis from its value.
pub abstract fixed metatype category = { rigidity: rigid, sortality: non_sortal };
pub fixed metatype kind = { rigidity: rigid, sortality: sortal, identity_provision: provides };
pub fixed metatype subkind = { rigidity: rigid, sortality: sortal, identity_provision: inherits };
pub metatype role = { rigidity: anti_rigid, sortality: sortal };
pub metatype phase = { rigidity: anti_rigid, sortality: sortal };
A metatype name in scope is contextually a concept-introducing keyword: pub kind Person { … }.
Axes are pure user vocabulary. The axis bindings in a metatype body are the declaring package’s own ontology — validated against the declared metaxis domains (RFD 0027 D2), persisted on the wire, and queryable — but the compiler never reads an axis name. No axis assignment changes elaboration, coverage, or mutation semantics; importing a vocabulary cannot alter substrate behavior because its author happened to name an axis rigidity. The metatypes shown above (kind, subkind, role, phase, category) are example declarations of the kind a third-party UFO vocabulary package would ship — they are not language atoms, and no UFO vocabulary ships with Argon or its stdlib. Other vocabularies (BFO, GFO, custom) define their own metatypes with their own axis assignments; the language treats all of them uniformly.
Substrate behavior comes from two ontology-neutral modifiers (RFD 0027 D6) — the operational shadows of the classic meta-property distinctions, with PL precedents rather than ontological commitments:
abstract— no direct instances. Every type the metatype introduces refuses construction (insert T { … }), direct classification (insert iof(x, T)), and seeded assertion (positivepub fact T(x)) withOE0233; subtypes are unaffected, andpub not_fact T(x)(refuting membership) stays legal. Abstract types are exempt from trait impl-coverage obligations (Resolution) — they structure the hierarchy, and every individual is classified through some non-abstract subtype.abstractmay also be declared per-type on a concept declaration (pub abstract type Vehicle { … }, the UML/PL convention); a type introduced by anabstractmetatype is abstract with no override semantics.fixed— classification decided at construction.insert iof/delete iofagainst a type introduced by afixedmetatype refuses withOE0234(mutate); construction itself is unaffected. Dynamic classification is the default — the restriction is the opt-in, the same posture ascheck. Fixed classification is what makes static modal discharge sound (Modal operators); the Kripke-semantics justification (rigid designation = fixed classification) is mechanized inArgon.Reasoning.StaticDischargeand the mutation-gate theorem inArgon.Runtime.ModifierGates.
A vocabulary author writes both layers together — pub abstract fixed metatype category = { sortality: non_sortal, rigidity: rigid }; — the modifiers carry the behavior, the bindings carry the package’s ontology. Misplaced modifiers (fixed on a concept declaration, either modifier on a struct/enum/relation, duplicates) are refused at parse with OE0008.
metarel
metarel-decl ::= 'metarel' Ident '(' endpoint-list ')' metarel-body? ';'
endpoint-list ::= endpoint (',' endpoint)*
endpoint ::= (Ident ':')? Ident // optional name, then metatype name
metarel-body ::= '{' axis-assign (',' …)* ','? '}'
pub metarel mediation(mediator: relator, mediated: kind) {
dependency_kind: existential,
};
pub metarel material(kind, kind) { dependency_kind: contingent };
pub metarel three_way_dependence(kind, kind, kind);
The metarel name is itself a contextual relation-introducing keyword (Relations): pub mediation Involves(m: Marriage, p: Person) declares a relation classified by mediation. The elaborator verifies the relation’s endpoint metatypes match the metarel’s positions. (An alternative decorator-attachment spelling — #[mediation] on a generic relation declaration — was sketched in earlier drafts but is not built: user-declared metarel names are not compiler directives, so the attribute form refuses at the directive registry, OE0705. The introducer form above is the supported surface.)
The stdlib ships a generic metarel for ontology-uncommitted use:
// in std::core — the generic metarel (opt-in via `use std::core::rel`,
// RFD 0038), shown as it is declared inside the stdlib (not a user file).
pub metarel rel<E1: metatype, E2: metatype>(E1, E2);
rel accepts any endpoint metatypes (inferred from the relation’s parameter types). After use std::core::rel;, the modeler can write pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*] for relations without committing to a specific ontological vocabulary. Vocabulary packages may re-export std::core::rel in their prelude as a convenience.
Reflection
Reflective sorts: Entity, TypeRef, Metatype
The substrate exposes three reflective sorts and a fixed lattice over them:
Metatype <: TypeRef <: Entity
Entityis the universal sort of all entities — individuals and type-references alike. A type is an entity (so it can itself be classified by a higher-order type; see Higher-order modeling).Entity <: Top.TypeRefis the sort whose values are references to declared types (concept / construct / relation / metatype) — a handle into the closed, nominal catalog of declarations, in the manner of OWL-2 punning, Java’sClass<?>, or Haskell’sTypeRep.TypeRefis unsealed, soMetatyperemains the only sealed primitive. BecauseTypeRefdenotes a closed catalog of already-declared types rather than an impredicative universe, there is noType : Type: the reflective layer is a predicative, stratified universe of codes, not a self-membership hierarchy.Metatypeis the sealed primitive that classifies all metatypes (kind,role,phase, …, andMetatypeitself). SinceMetatype <: TypeRef, every metatype name in scope is also aTypeRefvalue.
Type-as-value. A declared type’s name in value position is a TypeRef value — Person, kind, and Metatype are all TypeRef values, distinguished from the type Person by syntactic position. This is the substrate’s type-as-value facility: a field or parameter may be typed TypeRef (or the bounded TypeRef<C>; see Built-in type forms) and carry a reference to a declared type. The runtime carrier is the declared-symbol reference Value::Name; on the wire, a TypeRef/Entity argument is a qualified class-path ("pkg::mod::Type", or {"$typeRef": "pkg::mod::Type"}) resolved to the declared type’s identity — entity refs (#i…) are not accepted for these parameters. See RFD 0023.
Reflection intrinsics
The substrate provides five reflection intrinsics — meta, iof, specializes, extent, and implements (RFD 0026; signature implements(t: TypeRef, tr: TraitRef) -> Bool, materialized as the catalog-closed $implements relation with supertrait closure and <: upward target-coverage, Reflective conformance: TraitRef and implements) — admitted in any rule body without a use declaration. The first three have syntactic-sugar atom forms in Rule-atom grammar; extent and implements are called directly. For meta/iof/specializes/extent, each type-position argument is a TypeRef value — a literal type-reference or a bound variable of sort TypeRef — which is precisely what makes the intrinsics first-class, value-polymorphic predicates: library code can pass, count over, or quantify across type-values (see Higher-order modeling and the unprivileged mlt package, realizing the spec/research/RP-003-mlt-as-library.md GAP-1). implements differs in two deliberate ways: its second argument is TraitRef-sorted (traits are not types and stay off the <: lattice), and because $implements is a finite catalog-closed relation, both of its positions may be free — enumeration is the intended use (Reflective conformance: TraitRef and implements).
meta has the signature meta(x: Entity) -> TypeRef: it returns the immediate classifier of x — its <:-minimal declared type — as a TypeRef value. Under multiple classification (an entity instantiating incomparable types, e.g. a UFO individual that is both a Person and a Customer with neither <: the other), meta(x) is multi-valued: it yields one TypeRef per <:-minimal classifier. (It is a Metatype value only when x is itself a type — e.g. meta(kind) == Metatype — since Metatype <: TypeRef.) x :: T is syntactic sugar for meta(x) == T.
meta() applies only to ontologically-classified concepts (those declared under a vocabulary or stdlib metatype). Calling meta() on a struct/enum-declared value is a category error — language-level data carries no metatype — and emits a diagnostic at elaboration.
Worked examples (each right-hand side is a TypeRef value): meta(Person) == type when Person is pub type Person { … } under std::core::type; meta(Person) == kind when Person is pub kind Person { … } under an imported (or locally-declared) UFO vocabulary; meta(kind) == Metatype; meta(Metatype) == Metatype (sealed self-instantiation). Metatype is the sealed primitive that classifies all metatypes; it is special-cased by the compiler. The classifier kind and the primitive Metatype are TypeRef values because Metatype <: TypeRef.
iof has the signature iof(x: Entity, t: TypeRef) -> Bool. The rule-atom form x : T (Rule-atom grammar type test) is syntactic sugar for iof(x, T). Because the type-position argument t is a TypeRef value, iof is a first-class predicate: it can be passed as a relation argument, counted over, or quantified across — for example, library code building higher-order theories (the unprivileged mlt package, Higher-order modeling, Stdlib (selected)) treats iof as a first-class predicate.
iof is read at two tiers. The individual/ABox tier is membership of an order-0 entity in a type, asserted by a pub fact T(x) ground atom. The catalog/type tier is a declared type×type membership — a type X declared an instance of a higher-order type T via the : T instantiation clause (Vocabulary concepts and the generic type metatype, pub kind USD : Currency). The subject x is itself a TypeRef value (a type used as a higher-order individual), so the same iof relation carries both; which tier a read sits in is decided by whether the subject is an individual or a type, not by a separate relation. The catalog tier is declaration-derived (never an ABox event), so a #[static] check (Rule atom — fn, derive, query, mutate, check) may read it and extent projects it at the type level.
specializes has the signature specializes(t1: TypeRef, t2: TypeRef) -> Bool. The rule-atom form t1 <: t2 (Rule-atom grammar specialization) is syntactic sugar for specializes(t1, t2). Same first-class, value-polymorphic motivation as iof — both arguments are TypeRef values.
iof and specializes apply only to ontologically-classified concepts. Calling them on struct/enum-declared values is a category error and emits a diagnostic at elaboration, parallel to the meta() discipline.
extent has the signature extent(t: TypeRef) -> Set<Entity> (t is a TypeRef value naming the type whose extent is enumerated). Returns the set of entities x such that iof(x, t) holds at the current state. Both function-style (extent(EmperorPenguin)) and UFCS method-style (EmperorPenguin.extent()) are admitted; the method form lowers to the function form.
Worked examples: extent(Person) returns all order-0 entities classified as Person; extent(BirdSpecies) returns all order-1 kinds that are iof-instances of BirdSpecies (e.g., {EmperorPenguin, GoldenEagle, Grouse, Pheasant}); extent(Metatype) returns the set of all metatypes in scope. extent() on a struct/enum-declared value is a category error, parallel to the other reflection intrinsics.
Relation-signature reflection (rel / arm). Two further reflection predicates reflect over declared relations (the relation tier of the reflective catalog, RFD 0031 D2), so a vocabulary package can write relation-level compile-time constraints:
rel(r: TypeRef, m: TypeRef) -> Bool—ris a declared relation andmits classifying metarel. One catalog row per declared relation.arm(r: TypeRef, pos: Int, t: TypeRef) -> Bool— for relationr,posis a 0-based arm position andtthe endpoint type declared there. One row per (relation, position).
Both are catalog-closed (one row per declared relation / arm, never the ABox), so — like $implements — every position may be free and enumeration is the intended use. Since meta is polymorphic over the relation tier, meta(r) == <metarel> is an admitted equivalent spelling of the rel(r, <metarel>) membership test — the two bind against mirrored catalog rows; rel/arm remain the surface for arm-level (endpoint) reflection. rel/arm are abstract reflection primitives, never ontological vocabulary; the metarel/type names a check matches on are user vocabulary, compared by catalog identity. A package-shipped check quantifies over the catalog to enforce an endpoint discipline:
// "an endpoint of a `characterization` relation must be aspect-sorted"
pub check BadCharacterization(r: TypeRef, t: TypeRef) :-
rel(r, characterization), arm(r, 1, t), meta(t) == kind
=> Diagnostic { severity: Severity::Warning, code: "UFO::W010", message: "..." };
(The metarel introducer itself also verifies its endpoint metatypes at elaboration — metarel, OE0631; $rel/$arm let a package extend or audit that discipline declaratively.)
Lowering. A declared type in value position lowers to the IR primitive Term::TypeRef. This lowering is position-uniform across the surface: rule bodies and imperative bodies (mutate / test / fn) coerce identically, so a relation with a TypeRef-typed endpoint is writable in-language (insert Regards(k, Person)), over the wire ({ "$typeRef": "pkg::mod::Person" }), and readable back through equality dispatch (t == Person) — all three carry the declared symbol reference Value::Name. Binders shadow the coercion (a parameter / let named like a declared type is the local value); the landing is sorted — OE0262 refuses a type-as-value pinned into a variable-expecting rule slot, OE1384 the mutate-body analogue (an entity-declared endpoint or field). iof(x,t) (and x : t) lowers to the Core-IR typeTest atom and meta(x) == T (and x :: T) to the metaEq atom (spec/lean/Argon/CoreIR/RuleIR.lean); specializes(t1,t2)/t1 <: t2 has no dedicated atom and lowers to a reserved-head predicate, and extent(t) to an application of the extent builtin. Beneath Core IR, the reasoner evaluates these against catalog relations it materializes from the declarations — $iof (entity × type, closed under supertypes; this carries both tiers — the individual-tier rows from pub fact T(x) assertions and the catalog-tier type×type rows from each : T instantiation clause, the latter closed upward over the target type’s <: ancestors), $specializes (the reflexive-transitive specialization graph plus the built-in Metatype <: TypeRef <: Entity edges and the sibling TraitRef <: Entity edge; declared relations are first-class nodes of this graph, so $specializes carries the relation tier too — relation × relation rows from each pub rel R(...) <: S edge, closed reflexive-transitively, so specializes(R, S) binds R over the S family and specializes(X, X) holds for every declared relation X — the mechanism the relation-value application selector rests on), $meta (entity × its <:-minimal classifiers, PLUS the relation tier: relation × its declared metarel — meta is polymorphic over relations exactly as over types, so meta(relation) == <metarel> binds and fires against these rows), $implements (type × trait, supertrait closure and <: upward target-coverage folded in — Reflective conformance: TraitRef and implements), and the relation-tier $rel (relation × metarel) / $arm (relation × position × endpoint type) — RFD 0031 D2. OE0212 MetaArgUnbound is the reserved refusal for an unbound argument of the four type-position intrinsics; implements is exempt by design — both of its positions enumerate over the catalog-closed $implements relation, which is the intended use.
Typical use: deriving cross-level properties of higher-order types from their instances.
#[categorizes(Bird)]
pub type BirdSpecies {
numberOfLivingInstances: Int = extent(self).count(),
averageHeight: Decimal = extent(self).map(|b| b.height).avg(),
}
The = expr field-default form shown above refuses with OE0237 FieldDefaultUnsupported (see struct and enum — language built-ins (data declarations) for the full disposition). Express cross-level derivations such as EmperorPenguin.numberOfLivingInstances with the from-navigation field form ([RFD 0005, struct and enum field navigation](constructs/struct-and-enum.md)) or an explicit derive/query rule (Rule atom — fn, derive, query, mutate, check).
Constructs atom
Concepts and relations are co-equal first-class entities. The construct atom is layered:
- Language built-ins (lexer-level, always available):
structandenum— pure data declarations with no ontological commitment, no metatype classification, structural equality. - Stdlib ontological constructors (opt-in, not ambient — brought into scope via
use std::core::{type, rel}or a[package].preludeentry, RFD 0038): the generic metatypetypeintroduces ontologically-classified node-concepts (pub type Foo { … }); the generic metarelrelintroduces ontologically-classified relations (pub rel R(...)). They are the no-commitment baseline, imported like any vocabulary — nothing is ambient. - Vocabulary classifiers (require a
pub metatype/pub metareldeclaration in scope — declared in this package, or imported from an external vocabulary package): metatypes like UFO’skind,role,phase,categoryand metarels likemediation,materialfor richer ontological commitment. No vocabulary ships with the language or the stdlib — a vocabulary is an ordinary external package (e.g. a UFO package authored by the UFO authors), and an introducer that resolves to no visible declaration is refused withOE0605/OE0606.
All four layers compile to one substrate IR. The distinction lives in the metatype/metarel calculus (Meta-calculus atom); the language itself ships only the calculus, the data-declaration shorthands, and the meta-declaration keywords.
struct and enum — language built-ins (data declarations)
struct-decl ::= attribute* 'pub'? 'struct' Ident generic-params? where-clause? '{' field-list '}'
enum-decl ::= attribute* 'pub'? 'enum' Ident generic-params? where-clause?
'{' variant (',' …)* ','? '}'
field-list ::= field-decl (',' …)* ','?
field-decl ::= attribute* 'mut'? Ident ':' TypeExpr ('=' expr)? ('from' relation-ref)?
variant ::= Ident ( '(' TypeExpr (',' …)* ')' | '{' field-list '}' )?
pub struct Diagnostic {
severity: Severity,
code: String,
message: String,
range: Option<Range>,
}
pub enum Severity { Error, Warning, Info, Hint }
pub enum Option<T> { Some(T), None }
pub enum Result<T, E> { Ok(T), Err(E) }
pub enum Shape { Circle, Square, Triangle }
One enum spelling.
enumhas a single declaration form — the brace list above — matching Rust and the only form that carries payloads. An earlierenum Name = A | B | Cpipe spelling is removed (OE0009). Conceptcoverdeclarations (Vocabulary concepts and the generictypemetatype) take the matching brace formpub kind Name { A, B }(OE0012on the old= A | Bpipe), so no=-pipe surface survives.
struct/enum declarations carry no metatype classification. They are language-level data shapes — structural equality, no fact-store identity beyond storage, no participation in the metatype calculus. They cover stdlib data records (Diagnostic, Severity, Option, Result, Path, Range, Truth4) and user plumbing types. meta() reflection (Reflection) does not apply.
Struct and enum values
A struct value is constructed by naming the type and giving every field: Row { a: 1, b: 2 }. This is an ordinary expression — it can be a let binding, a field value, a function argument, a return value, a list element, or either side of a comparison. The full construction and projection grammar lives in the expression grammar (Expression grammar); this section states the data semantics.
pub struct Point { x: Int, y: Int }
pub fn origin() -> Point = Point { x: 0, y: 0 };
pub fn project_x(p: Point) -> Int = p.x;
Structural equality. Two struct values are equal when their fields are equal, field by field — there is no identity to compare. Field order in the literal does not matter: Row { a: 1, b: 2 } and Row { b: 2, a: 1 } are the same value. Equality is recursive: a struct whose field is itself a struct compares that field structurally too. This is what “pure data” means concretely — a struct is its fields, nothing more.
pub struct Row { a: Int, b: Int }
test "structural equality is field-wise and order-independent" {
assert Row { a: 1, b: 2 } == Row { b: 2, a: 1 };
assert Row { a: 1, b: 2 } != Row { a: 1, b: 3 };
}
Immutability. A struct value is immutable: its fields are fixed at construction and never change. There is no in-place update. To produce a struct that differs from an existing one in a few fields, use functional update — the ..base spread — which yields a new value and leaves the base untouched (Expression grammar):
pub struct Row { a: Int, b: Int }
test "spread is a functional update yielding a new value" {
let base = Row { a: 1, b: 2 };
assert Row { ..base, a: 9 } == Row { a: 9, b: 2 }; // a overridden, b carried
assert base == Row { a: 1, b: 2 }; // base is unchanged
}
Because a struct value is immutable, mut on a struct field is meaningless and is refused — mut field: Type on a struct raises OE0253 MutOnStructField. The mut modifier marks a concept field updatable by an update … set { … } statement inside a mutate body (mutate); that notion belongs to identity-bearing concept individuals, not to value-semantic data.
pub struct Point { mut x: Int, y: Int }
No identity — insert is refused. insert T { … } mints an identity and records an iof classification; it is how concept individuals are born. A struct has no metatype and no identity, so insert on a struct type is refused — OE0252 InsertOfStruct. Construct the struct as a plain value instead (drop the insert). The inverse direction is refused too: a brace literal T { … } whose head names a concept is OE0264 StructLitConceptHead (construct the individual with insert T { … } in a mutate/test body, or declare T a struct for plain data), and an insert written inside a pure fn body is OE0265 FnBodyInsert (construct in a mutate/test body and pass the value in).
pub struct Point { x: Int, y: Int }
pub mutate make() {
let p = insert Point { x: 1, y: 2 };
}
A struct literal must state every field exactly. A missing field is OE0249 StructFieldMissing, an undeclared field is OE0250 StructFieldExtra, and a field given the wrong type is OE0251 StructFieldTypeMismatch. These are build-time refusals, named to the field, not deferred to runtime.
Enum values. A payloadless variant is written as a path: Severity::Error, Shape::Circle. A payload variant carries one value and is constructed by applying the variant to that value: Msg::Text("hi"). Enum values compare structurally, the same as structs — Msg::Text("hi") == Msg::Text("hi"), and a payload variant never equals a different variant. A payload variant takes exactly one value; any other count is OE0256 EnumPayloadArity.
pub enum Msg { Text(String), Empty }
pub struct Mail { body: Msg }
pub fn greeting() -> Mail = Mail { body: Msg::Text("hi") };
pub fn blank() -> Mail = Mail { body: Msg::Empty };
User-concept generics are decorative. A user-declared generic
enum Opt<T>(orstruct) parses, but its type parameters reach no storage slot, so applying it —Opt<Int>— is refused (OE0235). The optional and result shapes used in practice are the library genericsOption<T>/Result<T, E>and the optional-field formT?, whose argument shapes the elaborator does interpret. Payload binding for the optional form is the rule-bodyis Some(a)/is Nonetest (next paragraph) or a value-positionmatchexpression (let v = match opt { Some(x) => x, None => d };, below); a payload-binding arm in statement position refuses withOE1319.
Reading an enum payload — is Some(a). Inside a rule body, an optional field’s payload binds with the membership test path is Some(binder), and absence tests with path is None. A present field materializes one row binding the payload; an absent field materializes none, so the surrounding conjunction fails (RFD 0007 Rule-atom grammar):
pub type Person { mut age: Int? }
pub derive Adult(p, a) :- p: Person, p.age is Some(a), a >= 18;
pub derive HasNoAge(p) :- p: Person, p.age is None;
Reading an enum payload — value-position match. A match expression in value position — a let right-hand side, a comparison operand, a fn body, a return/require value — binds an enum payload and evaluates. let r = match opt { Some(x) => x, None => 0 }; reads the payload into x and yields the selected arm’s value; this is the idiomatic way to read or consume an enum value. The arm binds exactly one variable positionally; a zero-, multi-, or record-binder arm is OE0257 EnumPayloadPattern (Expression grammar).
pub enum Opt { Some(Int), None }
test "value-position match binds the payload and evaluates" {
let some = Opt::Some(7);
let none = Opt::None;
assert (match some { Opt::Some(x) => x, Opt::None => 0 }) == 7;
assert (match none { Opt::Some(x) => x, Opt::None => 99 }) == 99;
}
A payload-binding arm in statement position — an effectful arm body inside a mutate statement-match, e.g. match opt { Some(v) => { let _ = insert T { n: v }; }, None => {} } — refuses with OE1319 rather than silently mis-evaluating. Bind the payload in value position (let v = match opt { … };) first, then run the effect. The rule-body is Some(a) form above is the other supported reader; the constant-pattern subset of match (payloadless variants, literals, _) executes in both positions (Pattern matching).
Field defaults. The field-default form field: Type = expr refuses with OE0237 FieldDefaultUnsupported: the parser recognizes the = expr clause and rejects it loudly rather than silently dropping the default. Supply the value at the construction or kind-level binding site, or use the from-navigation form (next paragraph) for a derived value.
Field navigation views — from. Unaffected by the above: a field may carry a from Rel.endpoint navigation-view clause supplying a derived value (RFD 0005, relation subsumption). This form is parsed and elaborated normally; it is the supported way to compute a field’s value from related entities.
Field mutability — mut. A field declaration may carry the mut modifier between any leading attributes and the field name: mut field: Type. Without mut, the field is immutable post-construction — its value is fixed at the construction site (or by #[intrinsic] kind-level binding, or by from-clause derivation) and may not subsequently be re-assigned. With mut, the field admits update-stmt writes inside mutate bodies (mutate).
pub type Person {
name: String, // set at construction; not updatable
dob: Date, // set at construction; not updatable
#[intrinsic] ssn: String, // must be set at kind binding; not updatable
mut current_address: String, // updatable via `update`-stmt
mut current_employer: Company?,
#[intrinsic] mut current_role: Role, // required at construction AND updatable later
}
mut is orthogonal to the other field-decl modifiers: #[intrinsic] governs construction-time required-ness, not updatability (both can apply); a mut on a from-derived field is contradictory and rejected with OE0822 MutOnDerivedField; a default = expr supplies a fallback at construction; the metatype’s fixed modifier (metatype, mutate) governs whether iof classification can change, not whether fields can, so a fixed kind admits mut fields. The modifier reads identically on struct, enum-variant, and vocabulary-concept fields. Mutations lower to append-only retract/assert event pairs on the axiom log (Storage layer); the proposition’s (entity_id, property_id) identity is stable across edits, so bitemporal queries reconstruct prior values (Temporal substrate).
Vocabulary concepts and the generic type metatype
When a vocabulary metatype is in scope, its name introduces a concept:
// A declaration is terminated by exactly ONE of: its `{ }` body, its `{ }`
// cover, or — when it has NEITHER — a REQUIRED ';' (the same item-termination
// rule as Rust: `struct Foo;` vs `struct Foo { … }`). A missing terminator on
// the bodyless form is `OE0011 MissingTerminator`.
concept-decl ::= attribute* 'pub'? <metatype-name> Ident generic-params?
supertype? refinement-clause? ( cover | body | ';' )
supertype ::= specialization? instantiation?
specialization ::= '<:' TypeExpr (',' TypeExpr)*
instantiation ::= ':' TypeExpr (',' TypeExpr)*
cover ::= '{' variant (',' variant)* ','? '}' // bare-ident members; self-terminating
body ::= '{' (field-decl | instance-value) (',' …)* ','? '}' // self-terminating — no ';'
instance-value ::= Ident '=' expr // see body modes below
A cover and a field/instance body share the { … } surface; the member shape distinguishes them. A cover’s members are bare concept names (a brace whose first entry is an Ident followed by , or }), matching enum’s { Circle, Square } and the complete Parent { A, B } group axiom; a body’s first entry carries a : (a field name: Type), :: (a per-target axis override), or = (an instance-value).
The leading identifier (type, or a vocabulary name like kind, subkind, role, phase, category, …) is contextually a concept-introducing keyword — resolved per Name resolution against the pub metatype declarations visible in scope: declared in this package, or imported from a vocabulary package. type is not ambient — it is the std::core baseline, brought into scope by use std::core::{type} or a [package].prelude entry (RFD 0038). An introducer that resolves to no visible pub metatype is refused with OE0605 UnknownMetatype at ox check and ox build.
use std::core::{type}; // the no-commitment baseline, brought into scope (RFD 0038)
pub type Document { title: String, body: String }
// Vocabulary-classified concepts: the vocabulary must be IN SCOPE.
// Here it is declared in-package (the external-vocabulary-package
// pattern — a real UFO package would ship these declarations and be
// imported instead; nothing UFO-related ships with Argon):
pub metatype kind = { };
pub metatype subkind = { };
pub metatype category = { };
pub kind Person { name: String, age: Nat }
pub subkind Adult <: Person iff { self.age >= 18 }; // defined class: auto-classified (Refinement)
pub category Animal;
pub kind Vehicle;
pub subkind Car <: Vehicle { license_plate: String }
pub subkind Truck <: Vehicle { license_plate: String, cargo_kg: Nat }
pub subkind Motorcycle <: Vehicle { license_plate: String, weight: Real }
A concept cover body (pub kind Vehicle { Car, Truck, Motorcycle }) is parsed
but its subkind-threading semantics are not yet implemented — an unconsumed
cover refuses loudly rather than silently asserting nothing. Declare the
subkinds explicitly (as above), adding a partition/complete group axiom for
the covering constraint; the cover form is tracked to do this in one stroke.
pub metatype kind = { };
pub kind Car; pub kind Truck; pub kind Motorcycle;
pub kind Vehicle { Car, Truck, Motorcycle }
Supertype clauses. <: T declares specialization (intra-classification subtyping): Adult <: Person says every adult is a person. : T declares iof-instantiation: pub kind USD : Currency says USD is an instance of the higher-order type Currency. Both clauses are general substrate features — neither commits to any specific higher-order theory. Higher-order theories like MLT (the unprivileged mlt package, Higher-order modeling, Stdlib (selected)) interpret the order arithmetic over :; the substrate itself only enforces that iof is well-founded and acyclic.
Multiple parents on either axis. Both clauses admit a comma-separated list, and both can co-occur on a single declaration:
pub subkind Penguin
<: FlyingAnimal, AquaticAnimal // specializes two kinds at order n
: Vertebrate_Species, Endangered_Species // instance of two categories at order n+1
{
name: String,
body_plan = "tetrapod",
iucn_status = IucnStatus::Vulnerable,
}
When both clauses are present the canonical order is <: then :, mirroring the order arithmetic (specialization parents at the concept’s own order, instantiation parents one above). The order is a style convention, not a rule: both clauses are independent (parents are an unordered set on each axis), so either source order parses and means the same thing — writing : before <: is accepted with the OW0010 style warning (never an error), and oxfmt normalizes it to the canonical order. All parents in a single <: clause must be at the same order; all parents in a single : clause must be at one order above. Unprivileged theory packages (e.g. mlt) enforce additional cross-parent constraints over the iof DAG.
Diamond resolution. When multiple parents (on either axis) declare a field with the same name, the elaborator rejects the declaration with OE0208 AmbiguousFieldFromMultipleParents. The modeler resolves the ambiguity by qualifying the field with its parent:
pub category Vertebrate_Species { habitat: String }
pub category Endangered_Species { habitat: HabitatProtectionZone }
pub subkind Penguin : Vertebrate_Species, Endangered_Species {
Vertebrate_Species::habitat = "Antarctic coast",
Endangered_Species::habitat = HabitatProtectionZone::IUCN_II,
}
Field access from instances follows the same qualification when ambiguous: bar.habitat is rejected with OE0208; the modeler writes bar.Vertebrate_Species::habitat to disambiguate. The substrate offers no automatic merge, override-by-position, or last-wins behavior — every collision is resolved explicitly. (This is consistent with how Argon’s trait system handles method-name collisions; see Trait atom.)
Body modes when : T is present. When a concept is declared as an iof-instance of another (pub kind USD : Currency { … }), the body admits two statement kinds:
field = value— sets the value of a field defined on the targetT. This is the kind-as-instance facet:USDis filling in fields thatCurrencydeclared on its instances.field: Type— declares an instance-level field that the kind itself will carry forward to its own instances. This is the kind-as-kind facet: everyUSDbill will have its ownserial_number.
pub category Currency {
#[intrinsic] iso_code: String,
#[intrinsic] decimal_places: Nat,
symbol: String,
}
pub kind USD : Currency {
iso_code = "USD", // value for the Currency-defined field
decimal_places = 2,
symbol = "$",
serial_number: String, // instance-level field on bills
denomination: Nat,
}
let my_twenty = USD { serial_number: "L12345678A", denomination: 20 };
#[intrinsic] on a field declares it must be set at kind level by every iof-instance. Cross-level binding diagnostics: OE1901 InstantiationFieldNotInTarget (a = binding names a field absent from the target metatype), OE1902 InstantiationFieldShadows (a : field shadows an inherited one), OE1908 IntrinsicPropertyMissing (a required #[intrinsic] field is left unbound), and OE0208 AmbiguousFieldFromMultipleParents (a field declared by multiple parents, unqualified). Theory-specific cross-level constraints (e.g., MLT’s categorization / partition / subordination / powertype rules) are not compiler diagnostics: a higher-order theory ships as an unprivileged package (e.g. mlt, Higher-order modeling) and authors those constraints as its own rules/checks over the neutral substrate; see appendix C.
Field access through the iof chain. For an instance bar: SomeKind where SomeKind : SomeCat and SomeCat declares field x, the access bar.x is resolved by walking up the iof chain:
- Instance fields first. If
barhasxdeclared as an instance-level field (viax: TypeinSomeKind’s body), returnbar’s own value. - Kind-level values next. Otherwise, look at
meta(bar) = SomeKind. IfSomeKindsetx = valueat its kind level, return that value. - Walk further up. If
SomeKinditself does not bindx, continue:meta(meta(bar)), etc., until eitherxis found or the chain bottoms out at an entity with no further:parent. - Not found. Error:
OE0101 UnresolvedName(or a more specific field-not-found variant).
Three equivalent surface forms reach the same value when x lives at the kind level:
| Form | Meaning |
|---|---|
bar.x | auto-walk; returns the first x found going up the iof chain |
meta(bar).x | explicit step to bar’s kind; field access on the kind |
SomeKind.x | direct field access on the type entity itself |
Shadowing across levels. If x is bound at both an instance level and a higher kind level — different types, or the same type with different values — the elaborator rejects the declaration with OE1902 InstantiationFieldShadows. Modelers either rename, or accept the shadow explicitly by writing meta(bar).x / SomeKind.x when they need the kind-level value. There is no implicit override.
Relations
Like concepts, relations are introduced by a vocabulary-defined metarel — there is no built-in rel keyword at lexer level. Stdlib ships a generic metarel std::core::rel<E1, E2>(E1, E2) for ontology-uncommitted relations; vocabulary packages can ship richer metarels (mediation, material, etc.) or re-export the generic.
// Same terminator rule as a concept (Vocabulary concepts and the generic `type` metatype): the `{ }` rel-body self-
// terminates; the bodyless form requires a ';' (`OE0011` if absent).
rel-decl ::= attribute* 'pub'? <metarel-name> Ident '(' rel-param-list ')'
cardinality-list? rel-supertype? ( rel-body | ';' )
rel-supertype ::= ('<:' | 'specializes') TypeExpr (',' TypeExpr)*
rel-param ::= 'mut'? Ident ':' TypeExpr // RFD 0076 — `mut` only in the named form
cardinality-list ::= cardinality+ // one slot per endpoint
cardinality ::= '[' range ']'
range ::= Nat '..' Nat | Nat '..' '*' | Nat '..=' Nat | Nat
// In cardinality position, `..` is INCLUSIVE on both ends
// (UML convention): `[1..1]` means exactly one, `[0..*]`
// means zero or more. Distinct from Rust range semantics.
rel-body ::= '{' field-list '}'
The leading identifier (after pub) is contextually a metarel-introducing keyword, resolved per Name resolution against the pub metarel declarations visible in scope — declared in this package or imported from a vocabulary package; rel is not ambient — the std::core baseline is brought in by use std::core::{rel} or a [package].prelude entry (RFD 0038). An unresolved introducer is refused with OE0606 UnknownMetarel. The parser disambiguates concept-decl (no (...) after the new name) from relation-decl (has (...)).
Three surface forms (using the std::core baseline, brought into scope via use std::core::{type, rel};):
use std::core::{type, rel};
// Form A — anonymous binary field on a concept
pub type Person { children: [Person] }
// Form B — named relation with optional navigation views
pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*];
pub type Person {
parents: [Person] from ParentOf.parent,
children: [Person] from ParentOf.child,
}
// Form C — n-ary relation with intrinsic property body
pub rel Marriage(spouse_a: Person, spouse_b: Person) [0..1] [0..1] {
since: Date,
status: MarriageStatus,
}
pub rel Transaction(seller: Person, buyer: Person, item: Asset) {
amount: Money,
at: DateTime,
}
Cardinality (positional, UML association-end convention). For a relation with endpoints e₁, e₂, …, eₙ, the i-th cardinality slot bounds the number of distinct eᵢ for each fixed combination of all the other endpoints — the UML association-end multiplicity. (Equivalently: hold every endpoint but the i-th fixed, and the slot bounds how many distinct eᵢ may complete the tuple.) Omitted slots default to 0..*. For binary relations the slots read left-to-right; pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*] reads “each child has 1..1 parents (slot 1 bounds the parents per fixed child); each parent has 0..* children (slot 2 bounds the children per fixed parent).” For n-ary relations the same per-position rule holds against the fixed combination of the others; pub rel Transaction(seller, buyer, item) [0..*] [0..*] [0..1] reads “for each (buyer, item) pair, 0..* sellers; for each (seller, item) pair, 0..* buyers; and for each (seller, buyer) pair, 0..1 items.”
Relation subsumption (<: / specializes). A relation declaration may name one or more superrelations after its endpoints, in either glyph — <: or the keyword synonym specializes (they are exact synonyms; a declaration reads identically under either). A superrelation clause makes the child’s extent flow into the parent’s: every tuple of the child is, by that edge, a tuple of the parent, so the parent’s closed extent (and every navigation view and rule over it) includes the child’s. The clause takes an optional parenthesized filler list stating, for each parent end by name, what fills it — parentEnd = value:
use std::core::{type, rel};
pub type Person;
pub type City;
pub rel Loc(p: Person, c: City);
// Bare form — pair the child's ends onto the parent by position.
pub rel Home(p: Person, c: City) <: Loc;
// Named filler list — each parent end paired, by name, with the child end
// at the same position. This means EXACTLY what the bare form means: both
// record the same identity mapping, and the bare form desugars to this one
// list at a single point, so there is one downstream representation, not two.
pub rel Residence(p: Person, c: City) <: Loc(p = p, c = c);
// `specializes` is the exact keyword synonym of `<:`.
pub rel Domicile(p: Person, c: City) specializes Loc(p = p, c = c);
Each filler is one of three shapes, all keyed by a parent end: parentEnd = childEnd (pair the parent end with a child end); parentEnd = <literal> (a constant pin, fixing that parent end to a value); or parentEnd = childEnd as ParentSort (a cast, widening a child end to a parent supersort). A mapping whose every parent end is paired with the child end at the same position is the identity mapping; anything else — a rename that permutes ends, a constant pin, or a widening — is a non-identity mapping. Both are evaluated: a child tuple contributes its mapped image to the parent’s extent, threading child ends into the parent’s positions by name, filling pinned positions with their constant, and admitting cast ends at the parent sort. The parent’s closed extent — and every navigation, rule, and query over it — sees the mapped image, so the child is a live member of its parent’s family.
A constant pin fixes a parent end the child has no end for. This is the worked example for the whole mechanism. WagesUSA records only a person and an amount; its images join IncomeItem with the currency pinned to USD. ForeignIncome is an identity member alongside it, so the parent family carries both a pinned image and a pass-through tuple:
use std::core::{type, rel};
pub type Person;
pub type Currency;
pub const USD: Currency;
pub const EUR: Currency;
pub rel IncomeItem(mut p: Person, mut amount: Int, mut c: Currency);
// Constant pin: the parent's `c` end is fixed to a literal rather than
// paired from a child end.
pub rel WagesUSA(mut p: Person, mut amount: Int) <: IncomeItem(p = p, amount = amount, c = USD);
// Identity member: every end paired to the child end at the same name.
pub rel ForeignIncome(mut p: Person, mut amount: Int, mut c: Currency) <: IncomeItem(p = p, amount = amount, c = c);
pub fact Person(alice);
pub fact Person(bob);
pub fact Currency(USD);
pub fact Currency(EUR);
pub fact WagesUSA(alice, 500);
pub fact ForeignIncome(bob, 300, EUR);
// The parent-union query over the whole family.
pub derive anyIncome(p, a, c) :- IncomeItem(p, a, c);
// A query that pins the currency: only USD-imaged rows answer it.
pub derive usdIncome(p, a) :- IncomeItem(p, a, USD);
WagesUSA(alice, 500) is an IncomeItem with its currency filled by the pin; ForeignIncome(bob, 300, EUR) passes through unchanged. anyIncome reads the parent family and sees both:
anyIncome(alice, 500, USD)
anyIncome(bob, 300, EUR)
The pinned query IncomeItem(p, a, USD) fixes the currency, so it matches the pinned image but prunes the EUR tuple — usdIncome yields only alice:
usdIncome(alice, 500)
pub const declares a named individual constant. pub const USD: Currency; declares the individual USD with concept sort Currency: when the ascribed type resolves to a declared concept, the ascription lowers to the same classification assertion pub fact Currency(USD); emits, so the constant resolves as a classified individual wherever an individual is written — a constant pin (c = USD), a rule-body value position (s == USD), a fact argument. Individual identity is the global bare-name hash, so writing the explicit pub fact Currency(USD); alongside the const (as the examples here do) re-asserts the same one row — a relation extent is a set, and re-assertion is harmless. A const whose ascribed type is not a declared concept (a primordial like Int, a struct/enum) declares the bare name only; no classification is asserted. The initializer form const X: T = …; does not execute yet and refuses (OE1355).
Retraction runs the mapping in reverse of assertion: delete WagesUSA(alice, 500) withdraws the child, and its USD image disappears from IncomeItem (a two-support image survives until its last contributing child is retracted).
That last clause is worth showing, because a family image is a derived conclusion held by set-union support, not a stored tuple. When two different members pin the same parent tuple, the image carries two independent supports and is one row that survives until the last support is retracted:
use std::core::{type, rel};
pub type Person;
pub type Currency;
pub const USD: Currency;
pub rel IncomeItem(mut p: Person, mut amount: Int, mut c: Currency);
// Two members that both pin the currency to USD — different relations, same image.
pub rel WagesUSA(mut p: Person, mut amount: Int) <: IncomeItem(p = p, amount = amount, c = USD);
pub rel SalaryUSA(mut p: Person, mut amount: Int) <: IncomeItem(p = p, amount = amount, c = USD);
pub fact Person(alice);
pub fact Currency(USD);
pub fact WagesUSA(alice, 500);
pub fact SalaryUSA(alice, 500);
pub mutate dropWages() { delete WagesUSA(alice, 500) }
pub mutate dropSalary() { delete SalaryUSA(alice, 500) }
pub derive anyIncome(p, a, c) :- IncomeItem(p, a, c);
pub derive totalIncome(p, t) :- p: Person, t = sum(a for p2 in Person, IncomeItem(p2, a, _), p2 == p);
WagesUSA(alice, 500) and SalaryUSA(alice, 500) both image onto IncomeItem(alice, 500, USD) — one row, two supports:
anyIncome(alice, 500, USD)
Run dropWages: one support is gone, but SalaryUSA still contributes, so the image survives unchanged —
anyIncome(alice, 500, USD)
Then run dropSalary: the last contributing child is retracted, and only now does the image withdraw —
(no rows)
Propositions, not events
A relation extent is a set of propositions, not a log of events. Two child facts whose translations produce the identical parent tuple are the same proposition asserted twice; the family holds it as one row carrying two supports. This is correct set semantics and the single most common source of aggregate surprise, so it is worth pinning precisely with three runs of the IncomeItem model above.
Identical fact twice — one row. WagesUSA(alice, 500) and SalaryUSA(alice, 500) both translate to IncomeItem(alice, 500, USD). An aggregate over the family folds that one row once. totalIncome sums the amount over IncomeItem per person:
totalIncome(alice, 500)
— 500, not 1000, even though two child facts were asserted. That is the right answer for the proposition “alice has USD income of 500”: the two children merge into one row carrying two supports, and (per the retraction above) the row survives until the last supporter is withdrawn.
Distinct amounts — two rows. Change one fact to SalaryUSA(alice, 501). The translations now differ (500 vs 501), so they are two propositions, two parent rows, and the aggregate sums both:
totalIncome(alice, 1001)
Event identity — two rows. If the two children stand for distinct events — two separate payments that happen to share amount and currency — the identical-tuple merge silently undercounts every multiplicity-sensitive aggregate over the family (sum, count, avg; min/max are unaffected because they ignore duplicates). The fix is in the model, not the query: give the parent a distinguishing end (a source or category end, an event id) so the two events map to two different parent tuples. Add an event-id end e to IncomeItem and each member, assert WagesUSA(alice, 500, 1) and SalaryUSA(alice, 500, 2), and the two events stay apart:
totalIncome(alice, 1000)
Pins-merge corollary. Pinning an end away is the usual way a modeler creates a collision: two members that both pin c = USD and expose the same remaining ends have byte-identical translations, so their rows always merge. Two distinct members in exactly this shape — the WagesUSA / SalaryUSA pair above — are flagged by the collision-capable-siblings warning (OW0741): it names both members and the parent and states the one-row merge consequence, so the aggregate surprise is caught at the declaration rather than discovered in a query. It is a warning, not a refusal (the merge is correct set semantics) and is suppressible where the merge is intended; the fix, when it is not, is to keep or add a distinguishing end so the two members stop pinning the distinction away.
The family-query provenance surface — the generalized specializes(k(…), Parent(…)) query and its Parent(…) via k sugar — carries the mapping through φ: a via family query reaches a non-identity mapped member, reads that member’s own extent, and presents the parent-framed rows with the pinned/renamed/cast ends filled and the answering member bound to k. The generalized specializes form with an explicit member frame still refuses (OE1416) for a non-identity member, because that form ties the member frame positionally to the member’s own ends — sound only for an equal-arity identity member; it points the modeler at via. The parent-union query above reads the closed extent and sees the mapped image directly.
A reorder routes the child’s ends to swapped parent ends by name. BackEdge(u, v, w) reverses an Edge’s endpoints:
use std::core::{type, rel};
pub type Node;
pub rel Edge(mut src: Node, mut dst: Node, mut w: Int);
// Reorder: parent end `src` is fed by child end `v`, `dst` by child end `u`.
pub rel BackEdge(mut u: Node, mut v: Node, mut w: Int) <: Edge(src = v, dst = u, w = w);
pub fact Node(n1);
pub fact Node(n2);
pub fact BackEdge(n1, n2, 7);
pub derive anyEdge(s, t, w) :- Edge(s, t, w);
BackEdge(n1, n2, 7) contributes the reversed Edge:
anyEdge(n2, n1, 7)
A cast widens a child end to a parent supersort; it is identity on the value (the value carries no sort tag), admitting the narrower child end at the parent’s declared sort:
use std::core::{type, rel};
pub type Agent;
pub type Person <: Agent;
pub type City;
pub rel Loc(a: Agent, c: City);
// Cast: the child end filling parent end `a` is widened to the supersort.
pub rel Home(p: Person, c: City) <: Loc(a = p as Agent, c = c);
pub fact Person(bob);
pub fact City(paris);
pub fact Home(bob, paris);
pub derive anyLoc(a, c) :- Loc(a, c);
anyLoc(bob, paris)
A filler that names a parent end the parent does not declare refuses with OE1411; a mapping that fills a parent end twice, or leaves one uncovered, refuses with OE1412. A mapping this build cannot yet resolve to parent positions — a filler list against a cross-module parent whose declared ends are not visible in the child’s file — refuses with OE1410 MappedSubsumptionNotYetEvaluable rather than being recorded under-resolved. (The negative-polarity dual — refuting a parent family when a non-identity edge would have to flow the refutation down to the child, which needs the mapping’s ill-defined inverse — is refused separately at the read path with OE1413.)
Why the filler list is keyed by parent end. Prefer the named filler list over the bare form on any relation whose parent ends share a sort — and note that its key names the parent end, not a position. Bare subsumption pairs the child’s ends onto the parent by position, recording that pairing nowhere in the child’s source. When two parent ends have the same sort, reordering them is a change the sort-covariance gate cannot catch — every position still type-checks — and the child declaration is byte-identical before and after, so a bare pairing flips with no diff at the site where the child is declared. Consider a transfer whose ends are both Account:
use std::core::{type, rel};
pub type Account;
// Parent ends both `Account`. Bare pairing sends the child's first end to
// `payer`, its second to `payee` — but that pairing lives only in the
// positions, not in the child's text.
pub rel Transfer(payer: Account, payee: Account);
pub rel WireTransfer(src: Account, dst: Account) <: Transfer;
Reorder the parent’s ends to Transfer(payee, payer) and this same child still compiles: src now lands on payee and dst on payer, silently reversing every transfer’s direction with no change visible at the WireTransfer declaration. The named filler list states the pairing at the child site, keyed to the parent’s end names:
use std::core::{type, rel};
pub type Account;
pub rel Transfer(payer: Account, payee: Account);
pub rel WireTransfer(src: Account, dst: Account) <: Transfer(payer = src, payee = dst);
Because each filler names the parent end it fills, the correspondence is anchored to names, not positions: it re-anchors under a parent-end reorder instead of silently re-pairing. Reorder the parent to Transfer(payee, payer) and payer = src still routes src to payer — the mapping’s meaning is unchanged; only the parent position it resolves to moves. That re-anchored mapping is now a permutation of the parent’s positions, and this stage evaluates it, so the reordered edge compiles clean and its images join the family correctly:
use std::core::{type, rel};
pub type Account;
// Parent ends reordered vs. the block above; the child is byte-identical.
pub rel Transfer(payee: Account, payer: Account);
pub rel WireTransfer(src: Account, dst: Account) <: Transfer(payer = src, payee = dst);
pub fact Account(acc_a);
pub fact Account(acc_b);
pub fact WireTransfer(acc_a, acc_b);
pub derive allTransfers(payer, payee) :- Transfer(payer, payee);
The mechanical guarantee is completed by a meaning guarantee. In the upright parent (Transfer(payer, payee)) WireTransfer(acc_a, acc_b) records acc_a as the payer; reorder the parent to Transfer(payee, payer) with the child byte-identical, and acc_a is still the payer — the filler payer = src re-anchors to the parent’s payer end wherever it now sits, and the evaluator applies the resolved permutation. The stored tuple’s positional layout follows the parent’s new order (so the raw row reads Transfer(acc_b, acc_a) — payee first now), but the account each parent end names is invariant:
# parent declared Transfer(payer, payee): allTransfers(acc_a, acc_b) → payer = acc_a
# parent declared Transfer(payee, payer): allTransfers(acc_b, acc_a) → payer = acc_a
A same-sort parent-end reorder therefore can never change a mapping’s meaning — not merely “not silently”, but not at all: the mapping is re-anchored by parent end name and evaluated through that re-anchoring, so who the child records as the payer is the same before and after the reorder. That end-to-end invariance is the acceptance criterion for keying fillers by parent end name.
When two paths disagree — the divergent-map diamond. A relation can reach the same ancestor through more than one subsumption path. When every path composes to the same mapping onto that ancestor, the diamond is harmless — a fact frames to one ancestor row whichever path is read — and it compiles. When two paths compose to different mappings, there is no single answering frame, and the declaration is refused. Here D reaches A straight through Left but end-swapped through Right:
use std::core::{type, rel};
pub type Person;
pub rel A(x: Person, y: Person);
pub rel Left(a: Person, b: Person) <: A(x = a, y = b);
pub rel Right(a: Person, b: Person) <: A(x = b, y = a);
pub rel D(p: Person, q: Person) <: Left(a = p, b = q), Right(a = p, b = q);
A D(alice, bob) fact would land at A(x = alice, y = bob) through Left and at A(x = bob, y = alice) through Right — two different ancestor rows — and a family query over A follows only the first path, silently dropping the other. The refusal names the child, the ancestor, and both composed framings in parent-name-keyed filler form:
OE1424: subsumption of `D` reaches ancestor `A` through paths whose composed mappings DIFFER — via D → Left → A: (x = p, y = q); via D → Right → A: (x = q, y = p). A `D` fact would occupy two different rows in `A`, and a family query (`via D`) would silently report only one framing (the closure follows the first path). Fix: reconcile the fillers so both paths compose to the SAME mapping onto `A`, or remove one subsumption edge. If both images are genuinely wanted, that is a feature request — the mapped-relation-subsumption design record keeps both-images open as future work.
Fix it by making the two compositions agree, or by removing one edge; if the child genuinely belongs at both ancestor rows, that is a feature request the design record keeps open as future work.
Relation-end mutability (mut, RFD 0076). Every relation end is immutable unless declared mut — the same rule, and the same modifier placement, as concept fields (RFD 0006): mut is written immediately before the end name, only in the named-end form of a concrete relation declaration. An anonymous-end mut or a mut inside a metarel signature is refused with OE0013 (mutability is declared per concrete relation end; there is no metarel-level mut). The analogy with fields stops at the declaration posture — a relation assertion is an immutable proposition and there is no relation-end update; mut governs which assertion history is legal:
use std::core::{type, rel};
pub type Person;
pub type Organization;
pub type Marriage;
// A material relation: both ends vary over time, so both opt in.
pub rel worksFor(mut employee: Person, mut employer: Organization) [0..*] [0..1];
// A mediation-shaped relation: a particular marriage cannot involve
// different spouses (`spouse` keeps the immutable default), while a
// surviving person may participate in another marriage later. The
// exact `[2]` bracket makes the initialization minimum atomic: because
// an immutable fiber freezes at its first asserting transaction, BOTH
// spouse tuples must be asserted together (`OE1398` refuses an
// under-filled initialization — the frozen set could never grow to
// complete it).
pub rel bindsSpouse(mut marriage: Marriage, spouse: Person) [0..1] [2];
The write path enforces the declaration per endpoint fiber (hold every other endpoint fixed — the same fixed-complement reading as cardinality): a matched delete R(args) on a relation with any effectively-immutable end refuses with OE1402 (deleting one tuple removes one value from every fiber, so direct retraction needs every end mut; an unmatched delete stays an idempotent no-op); for an immutable end, the first transaction asserting a fiber freezes its complete value set, and a later transaction inserting a new value there refuses with OE1403 (re-asserting an active tuple stays idempotent; a fresh dependent context initializes its own fiber). The legal removal of an immutable binding is retracting its dependent context — the individuals at the other positions — whose kernel RetractIndividuals effect cascades the incident relation retractions, refusing with OE1404 when the set would strand an immutable end’s surviving context; the capability-gated forget erasure runs the same dependent-context gate over its target’s incident tuples (on every standpoint plane the erasure sweeps), so physical erasure cannot stand in for a refused cascade. Correcting a mis-recorded binding — one that was never true — is a separate belief-time channel, the capability-gated amend verb (the record-exit, described below); it is distinct from ending a binding whose world changed (the world-exit cascade). Two declaration gates complete the picture: effective mutability is conjunctive over relation subsumption — a child may tighten a parent mut end by omitting mut, but declaring mut where any transitive superrelation keeps the position immutable refuses with OE0268 — and an end may stay immutable only when every member of its dependent context is an identity-bearing individual (a primordial-, value-, or reference-sorted context member has no lifetime whose retraction could release the binding; refused with OE0267, mark that end mut) — and the same OE0267 refuses the sole end of a unary relation, whose dependent context is empty outright (no other position exists, so no retraction could ever release the binding and the fact would be permanently frozen). The reflection plane exposes the effective bit as the total, catalog-closed armMutability(relation, index, is_mut) atom, one row per declared end.
The gates read the closed extent, not just the tuples a statement names. An insert into a subrelation contributes a row to every transitive superrelation, so an immutable parent fiber initialized through one route (a direct parent insert or a child’s) refuses growth from every other route — direct, sibling child, or child under a parent-initialized fiber — with the same OE1403 (a child re-asserting the parent’s already-active value still initializes its own fiber and is admitted). The closure spans planes too: the freeze witness reads the relation’s retained history as a single global extent — folded on the fixed complement with no standpoint axis — so an insert whose value is new for a fiber any plane initialized refuses with the same OE1403, whichever plane the initializing fact lived on. This global reading is strictly stricter than a per-plane witness (it over-refuses a value new only to a different plane, and never admits an illegal one); per-plane scoping of both the freeze witness and the cardinality gate — so a standpoint’s composed view freezes and counts only against its own retained history — is designed but not yet shipped. And a pub derive whose head names a declared relation feeds that relation’s extent, so a premise mutation that would make an immutable derived binding disappear while its dependent context survives, or grow an already-initialized immutable derived fiber, refuses the whole transaction with OE1397 — the transaction’s net rule-derived extent delta is checked atomically before anything commits; declared relation heads do not bypass mutability merely because no source delete/insert names them directly (pure derive/query heads with no relation declaration carry no end-mutability metadata and are unaffected). The initialization set itself remains governed by the declared cardinality: because an immutable fiber freezes at its first asserting transaction, a declared minimum of two or more is decidable exactly there — an initialization establishing fewer distinct values refuses with OE1398 (the frozen set could never grow to complete it) on every route: a direct insert, a subrelation insert initializing the parent fiber, a rule-derived initialization, and a static fact set (refused at ox check / ox build). A mutable end’s minimum keeps the deferred OW1342 recorded-not-enforced posture. And because the ArgUFO audit story rests on armMutability being total, the name is reserved: a user declaration named armMutability refuses with OE0704 — a same-module derive of that name would otherwise mask the catalog rows and let the very module under a forbidden-opt-out audit discharge it vacuously.
A gallery, end by end. mut is read per fiber — fix the other end and ask whether that value set may still change after its first assertion.
pub type Kid; pub type Toy; pub type Person; pub type Passport;
// Transferable ownership — a pure association, like `worksFor`. Giving a
// toy away is a `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; the kid's side still grows as they receive more toys.
pub rel madeFor(mut toy: Toy, recipient: Kid) [0..*] [0..1];
// Birth: a child's birth-mother never changes; a mother's set of
// children grows with each birth.
pub rel bornTo(mut child: Person, mother: Person) [0..*] [1];
// Issuance: a passport is issued to one person forever; a person
// accumulates passports over a lifetime.
pub rel issuedTo(mut passport: Passport, holder: Person) [0..*] [1];
Fix a child and their birth-mother is one frozen value (immutable mother); fix a mother and her children accumulate (mut child) — the same shape as madeFor and issuedTo, one end constitutive of the tuple and the other accumulating history. A transferable association like ownedBy is all-mut by necessity: rebinding requires delete, and a matched delete removes a value from every fiber. Choosing mut reduces to three questions per relation: (1) will a tuple ever be deleted while both individuals still exist? — then every end is mut (an association: ownedBy, worksFor); (2) otherwise, which end is fixed the moment the tuple exists and which keeps accumulating? — the constitutive end stays immutable, the accumulating end gets mut (bornTo, madeFor, issuedTo, bindsSpouse); (3) is the only honest exit the disappearance of a participant? — then the cascade does the work and the end needs no mut. On these relations the gates read: insert bornTo with a second birth-mother for a recorded child refuses OE1403 (a second birth-mother contradicts frozen information rather than adding to it); delete issuedTo while holder and passport both survive refuses OE1402; retracting the passport cascades the frozen binding and the holder survives; retracting the holder alone refuses OE1404 while the passport’s context survives.
The choice between bindsSpouse and worksFor(mut, mut) is a modeling posture, not two kinds of fact. bindsSpouse reifies the relator — the marriage is an entity whose relata are constitutive, so its ends are immutable and its lifecycle runs through retract-and-cascade — while 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 an EmploymentContract and gives its relation immutable ends, making a job change the retraction of one contract and the creation of another — the marriage’s shape. What this design adds is that the schema now states which posture each relation takes; before, every relation silently behaved like the coarse view.
Correcting the record — amend (RFD 0076). Immutability protects two different things the write path used to conflate: the world cannot rebind an immutable end, and the record cannot be silently rewritten. Ending a binding because a participant ceases is a world-exit — the cascade (retract the dependent context, the incident tuples fall away, history retains the binding, and the vacated value can never be reused). Discovering that a binding was never true — a mis-recorded assertion, false when it was made — is a record-exit, and it is what amend performs: the assertion is withdrawn as false ab initio, releasing the freeze contribution it should never have made, while the bitemporal history retains what-was-believed-when (the correction is auditable, not an erasure). amend R(args); is the primitive (withdraw when the true value is unknown; positive evidence is withdrawn, so the proposition returns to unknown, not refuted); amend R(args) => R(args'); is the atomic composite (withdraw and assert the corrected proposition against one transaction-time view — all-or-nothing). It is a distinct verb from delete on purpose: delete is the valid-time ender (the binding stops being true now), amend is the belief-time corrector (the binding was never true). amend is capability-gated at the source level exactly as forget is: a mutate body using it refuses to build (OE1405 AmendWithoutCapability) unless the enclosing mutate declaration grants #[allow_amend]; correction authority is further authorized per invoking principal, target-scoped, at the serving layer.
pub type Person;
// Birth: a child's birth-mother never changes (immutable `mother`), but a
// mis-recorded mother is corrected at belief time.
pub rel bornTo(mut child: Person, mother: Person) [0..*] [1];
#[allow_amend]
pub mutate correct_birth_mother(child: Person, wrong: Person, right: Person) {
// The record was wrong from the start: `child` was never born to `wrong`.
// Withdrawing `bornTo(child, wrong)` releases the frozen `mother` fiber,
// so the corrected `bornTo(child, right)` is admitted where a plain
// `insert` would refuse `OE1403`.
amend bornTo(child, wrong) => bornTo(child, right);
}
#[allow_amend]
pub mutate rescind_birth_mother(child: Person, wrong: Person) {
// Truth not yet known: withdraw the false assertion alone. The `mother`
// fiber re-initializes as if it had never been frozen at `wrong`.
amend bornTo(child, wrong);
}
The amendment gates read one net corrected view of the whole transaction — the retained assert-polarity history minus the tuples this transaction amends, with corrected assertions and any lifecycle effects overlaid — so a correction cannot launder a companion cascade (the corrected tuple is live in the view the dependent-context gate reads). Two amendment-specific refusals complete the surface: amending one of several tuples in a fiber whose declared minimum exceeds one, when the surviving corrected set would drop below that minimum, refuses with OE1407 AmendmentBelowMinimum (distinct from OE1398, which refuses an under-filled initialization — OE1407 refuses an under-filling correction of an already-established fiber); and 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 (you amend premises, not conclusions; dependency maintenance recomputes the consequence). On an all-mut relation amend is admitted (its freeze-release is vacuous there, but the false-ab-initio marking still differs from delete’s valid-time close). A tuple with neither an event nor a live row is the idempotent no-op, as an unmatched delete is.
The declaration-plane gates (OE0267/OE0268) are enforced by oxc-check in
its end_mutability classifier, run over each relation declaration in the
check pass; the write-path gates (OE1402–OE1404) are enforced by
oxc-runtime in its end_mutability module.
Anonymous-field cardinality default. A field declared with the inline anonymous-relation form field: [T] (Form A) synthesizes a binary relation with default cardinality 0..* and Set semantics. The bounded spellings — field: [T; n..m], ordered-list opt-in field: [T; n..m, ordered] (lowering to List<T>), and the [T; <=1] singleton hint (OW2402, suggesting T? / Option<T>) — are reserved surface: no enforcement is wired yet, so a cardinality bound on a list type refuses by name (OE0014) rather than being silently discarded. Use a plain [T] until write-path enforcement lands.
Anonymous fields are structural shorthand: they do not engage the metarel calculus. No metarel name is attached; the synthesized relation is accessed only via dot-traversal. This is symmetric with struct/enum, which are likewise language-level structural and carry no metatype. Named relations (Forms B and C) require a metarel-introducing keyword in scope.
[T] on struct vs concept. On a struct or enum field, [T] is plain List<T> — no synthesized relation, no closure traversal, no metarel. On an ontologically-classified concept (declared under a metatype), [T] is Form A — a synthesized binary relation supporting closure traversal (alice.children+(b)). The difference is whether the enclosing declaration is data (struct/enum) or ontology (declared under a metatype — type or a vocabulary introducer).
When to upgrade Form A to Form B or C. Promote an anonymous field to a named relation when any of: (a) the relation needs to be classified under a metarel (mediation, material, …) so the substrate can reason over its properties; (b) the relation carries intrinsic data of its own (since: Date, amount: Money); or (c) the relation must be referenced as a first-class predicate from rule bodies (derive ancestor(d, a) :- ParentOf(d, a), …). When none of those apply, Form A is the right choice.
Generic relation-property characteristics. A named relation (rel or metarel) may carry the standard relation-algebra property characteristics (the OWL object-property characteristics) — PL/DB-neutral, describing the shape of the relation’s extent with enforcement, not an ontological commitment. All four are shipped by the std::rel standard-library package and must be brought into scope — use std::rel::{transitive, irreflexive, asymmetric, functional} (nothing is ambient — Name resolution / RFD 0038). #[transitive] is a genuine declarative macro (it expands to the closure derive). #[irreflexive] and #[asymmetric] are genuine procedural macros (RFD 0040): they re-emit the relation and paste the guarding check’s internal __{rel}_{prop} head via concat_idents. They read the relation’s name, so they apply to a rel; on a metarel they decline (the invocation is refused, OE0723). #[functional] is likewise a genuine procedural macro (RFD 0040 P2): BOTH its rel and metarel arms re-emit the decorated declaration and paste a __{rel}_functional check that fires when a source maps to two distinct targets (OE1361) — the same check-based enforcement as its siblings. A rel whose explicit target cardinality permits more than one target contradicts #[functional] and is refused (OE1378). A characteristic applied without its import is refused — OE0705 for every macro (#[transitive]/#[irreflexive]/#[asymmetric]/#[functional]; OE1362 is retired). Multiple characteristics compose on one declaration (re-emitting $item verbatim, a stacked sibling and the user’s brackets/generics/body all survive).
| Characteristic | Meaning | Enforcement |
|---|---|---|
#[transitive] (std::rel macro) | the relation is transitively closed | expands to a closure rule R(x, z) :- R(x, y), R(y, z) over the relation’s own extent, so the relation’s queryable extent includes its transitive closure |
#[irreflexive] | no element relates to itself | a check fires on any self-loop R(x, x) (OE1359) |
#[asymmetric] | R(x, y) forbids R(y, x) | a check fires on any mutual pair (OE1360); since a self-loop is its own converse, #[asymmetric] also forbids R(x, x) |
#[functional] | each source maps to at most one target | a check fires when a source maps to two distinct targets (OE1361) — the same check-based mechanism as #[irreflexive]/#[asymmetric]; an explicit target cardinality whose upper bound exceeds one contradicts the constraint and is refused (OE1378) |
use std::rel::{transitive, irreflexive, asymmetric, functional};
#[transitive]
#[irreflexive]
#[asymmetric]
pub rel is_proper_part_of(part: Top, whole: Top);
#[functional]
pub rel inheres_in(burden: Aspect, bearer: ConcreteIndividual); // each aspect inheres in one bearer
Enforcement applies to the declared relation’s own extent. Propagation of a property borne by a metarel to the relations it classifies is out of scope (Out of scope). A characteristic applied without importing it from std::rel is refused — OE0705 for every macro (#[transitive]/#[irreflexive]/#[asymmetric]/#[functional]; all genuine macros, not directives). #[functional] applies to both a binary rel and a binary metarel (each arm pastes the source-maps-two-targets check); #[irreflexive]/#[asymmetric] apply only to a rel and decline on a metarel (the invocation is refused, OE0723). On a non-relation declaration (a concept, a rule) the invocation likewise fails to match and is refused (OE0723). (The ontology-specific relation properties — MLT’s #[partitions], #[categorizes], etc. — are vocabulary decorators and live in their packages, distinct from this generic family.)
impl Type { … } — grouping
impl-block ::= 'impl' generic-params? TypeExpr '{' impl-item* '}' // grouping
| 'impl' generic-params? TypeExpr 'for' TypeExpr '{' trait-impl-item* '}' // trait impl
The grouping form admits any declaration kind (impl-item); the trait-impl form is
restricted to the contract member forms (trait-impl-item: fn / mutate / derive / check /
query) and is governed by the conformance obligations of
the trait surface and conformance (RFD 0026).
impl Person {
pub fn greeting(self) -> String = "Hello, " + self.name; // instance method
pub fn adult(p: Person) -> Bool = p.age >= 18; // associated function
pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*]; // lexically grouped
pub query oldest() -> Option<Person> {
select p from p: Person order by p.age desc limit 1
}
}
An impl Type block accepts any declaration kind: methods, associated functions, relations, queries, mutations, checks, associated type / const. All declarations inside the block are namespaced under Type::. Methods (items with self first parameter) are additionally accessible via UFCS dot-notation (alice.greeting()); associated functions and relations are reachable only as Type::name(args).
impl Person {
/// Instance method (has `self`): both `alice.greeting()` and `Person::greeting(alice)` work.
pub fn greeting(self) -> String = "Hello, " + self.name;
/// Associated function (no `self`): reached as `Person::adult(p)`.
pub fn adult(p: Person) -> Bool = p.age >= 18;
/// Type-associated relation: reached as `Person::ParentOf(a, b)`.
pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*];
/// Method-shaped query: `alice.oldest()` and `Person::oldest(alice)` both work.
pub query oldest() -> Option<Person> {
select p from p: Person order by p.age desc limit 1
}
}
A relation declared inside impl Type is not exposed at module level. Choose the placement (module level, inside a topic mod, or inside impl Type) according to the semantic association the modeler intends — see Connecting concepts and relations.
Connecting concepts and relations
A relation is first-class, but where it is declared expresses its semantic association with the participating concepts. Three placements coexist; the modeler picks by intent:
- Module level — a free predicate owned by no single endpoint:
pub rel ConnectedTo(a: Node, b: Node) [0..*] [0..*];, accessed asConnectedTo(x, y)anywhere in the module. - Topic
mod— grouped with related concepts; accessed asfamily::ParentOf(a, b)afteruse family::*;. impl Type(impl Type { … }— grouping) — conceptually “about” the type, alongside its methods; accessed asPerson::ParentOf(a, b)only, not at module level.
Navigation from a concept value to its participations. Three mechanisms link an instance to the relations it participates in.
1. Field-form navigation view (cheapest, declarative):
pub type Person {
parents: [Person] from ParentOf.parent, // alice.parents
children: [Person] from ParentOf.child, // alice.children
}
A field may be scalar when the relation is binary and the projected endpoint is functional — its declared cardinality caps it at one ([0..1] / [1..1]). For a binary relation the holder is the sole other endpoint, so a functional value endpoint is a holder→value dependency: one value per holder. The scalar view then yields a single value, not a collection:
pub type Person {
manager: Person from HasManager.manager, // a single Person, not [Person]
}
A scalar view over a non-functional endpoint is refused: the endpoint may back several tuples for one holder, so a single value is ill-defined. Declare the field as a collection ([T]) or constrain the endpoint:
pub type Person {
parent: Person from ParentOf.parent, // scalar over a [0..*] endpoint
}
pub rel ParentOf(parent: Person, child: Person) [0..*] [0..*];
A scalar view over an n-ary relation is refused even when the value endpoint is [0..1]: the bracket bounds the value per fixed combination of the other endpoints, not per holder, so it is not a holder→value functional dependency. Here [0..1] bounds salary per (person, company) pair, but one person may hold two employments and so two salaries — which a scalar projection would silently collapse. Project the whole set as a collection, or model the dependency with a binary relation:
pub type Company;
pub rel Employment(person: Person, company: Company, salary: Int) [0..*] [0..*] [0..1];
pub type Person {
pay: Int from Employment.salary, // scalar over a ternary relation
}
A collection view over an n-ary relation is admitted — it projects the whole endpoint set — provided the holder endpoint is unambiguous. The holder is the endpoint the declaring concept occupies, resolved by type: exactly one non-value endpoint whose type is comparable with the declaring concept. If no non-value endpoint is comparable the holder can never occupy the relation, so the view is statically empty; if two or more are, from Rel.endpoint cannot say which is the holder. Both are refused (OE1376). A binary relation is never ambiguous — its holder is the sole other endpoint — so this only constrains arity ≥ 3:
pub type Item;
pub rel Pairing(buyer: Company, seller: Company, item: Item);
pub type Company {
deals: [Item] from Pairing.item, // holder ambiguous: buyer or seller?
}
Each field is a derived view over the relation. The compiler verifies the field type matches the role’s endpoint type and that cardinality is consistent with the relation’s per-endpoint cardinality (a scalar field over a non-functional endpoint, or over any n-ary relation, is refused — OE1370; a collection field over an n-ary relation whose holder is not uniquely type-resolvable is refused — OE1376). The view materializes against the relation’s closed extent: because relation subsumption (Relations) makes a subsumed relation’s tuples tuples of its supersuming relation, a view over Rel.endpoint includes the endpoints contributed by every <:-subsumed relation — so a subtype that refines an inherited collection by projecting from a subsumed relation (SatisfactionAccount.records from satisfactionAccountRecords <: recordAccountRecords) sees exactly its narrower elements.
Filtering the view — where. A navigation view keeps only the projected elements that are iof the field’s element type; a where clause narrows it further. The clause is one expression, not a braced body, and it binds exactly one variable: the projected endpoint, named by the endpoint selector the from clause spells. Module-scope declarations are visible as usual, so a type test, a rule call and a field predicate all resolve:
pub type Drawing {
circles: [Circle] from Contains.shape where shape : Circle, // type test
bigCircles: [Circle] from Contains.shape where shape.radius > 10, // field predicate
}
self is not bound here. A refinement body binds self to the individual whose membership is being decided (Refinement), but a navigation-view filter runs over the projected elements, so a filter written against self names nothing and is refused (OE0107) with the endpoint variable it should have named. Nor are the relation’s other endpoints bound — including the holder, the individual whose field is being computed — so a condition comparing a projected element against the holder’s own state cannot be written in the clause; express it as a rule and call the rule with the endpoint variable.
pub type Shape { radius: Int }
pub type Circle <: Shape;
pub rel Contains(drawing: Drawing, shape: Shape);
pub type Drawing {
circles: [Circle] from Contains.shape where self.radius > 10, // `self` is unbound
}
2. UFCS method via impl (richer navigation):
impl Person {
pub query siblings(self) -> [Person] {
select s from ParentOf(p, self), ParentOf(p, s) where s != self
}
}
Methods are appropriate when navigation requires filtering, joining, ordering, or aggregation beyond a simple projection. Accessed as alice.siblings().
3. Direct rule-body invocation (always available):
pub derive grandparent(g: Person, c: Person) :- ParentOf(g, p), ParentOf(p, c);
Inside any rule body, the relation is a predicate addressable by name (or by qualified path: family::ParentOf(...), Person::ParentOf(...)).
Inverse declaration (when both sides are field-form):
pub type Country { citizens: [Citizen] }
pub type Citizen {
#[inverse(Country.citizens)]
countries: [Country],
}
The compiler verifies cardinality consistency and exposes both navigation directions over a single synthesized relation.
Naming convention. Relation names are CamelCase (per Identifiers) — ParentOf, Marriage, IsCitizenOf — symmetric with the CamelCase of metatype-introduced concepts. The name plus argument order encodes semantic direction; ParentOf(p, c) reads “p is the parent of c.” The convention is the modeler’s responsibility — the language does not enforce it. Use field-form views or methods to expose semantically-named navigation in the opposite direction.
Fact heads resolve in the declaring file’s scope; individuals travel by bare name. A pub fact P(...) head must resolve to a concept or relation in the file’s fact-head scope: the concepts and relations the file itself declares, plus imported ones its use/prelude scope reaches by module-qualified path (use vocab::Executable; and a pub use re-export hub both work). A head that resolves to nothing refuses with OE0220 FactReferencesUnknownPredicate; when the name is declared elsewhere in the workspace, the refusal names the declaring module and the working pattern. That pattern rests on identity: an individual’s identity is the global bare-name hash — alice is the same individual from every module — so the canonical cross-module shape is to mint where the concept is declared (pub fact Person(alice); in the concept’s own module) and reference anywhere by bare name (in rule bodies, mutate bodies, and as relation-fact arguments in other modules).
Facts assert propositions, not events. A relation’s extent is a set: asserting the same pub fact twice — same relation, same arguments — adds nothing, because the two identical assertions are one tuple. A bare proposition is true once, however many times it is written. Because a literal in-file repeat is therefore almost always a copy-paste slip, the compiler warns on a same-file literal duplicate pub fact (duplicate-fact-in-file, a suppressible warning). The remedy is to delete the duplicate; or, if the repeats are meant to record distinct events (a marriage that happened twice, a payment repeated), give each fact its own identity — add an event-id endpoint so the arguments differ and both tuples survive. Duplicates are matched on the canonical encoding of the fact, not its source spelling — whitespace and formatting differences do not matter, and a relation reached through a renamed import (use pkg::Married as Wed;) is the same relation as the original name, so Married(a, b) and Wed(a, b) are one fact and warn. The check is deliberately narrow: it fires only within one file. The same fact asserted in two different files, or re-inserted by a mutate, is legitimate idempotent re-assertion and is never flagged.
Three import postures
A modeler picks an ontological posture per module by choosing what to use. Three cover almost all code:
// Posture 1 — pure data, no ontology. `struct`/`enum` are lexer built-ins.
use std::math::Nat;
pub struct Receipt { lease_id: Nat, paid: Money, at: Date }
pub enum Severity { Error, Warning, Info, Hint }
// Posture 2 — generic ontological commitment via the std::core
// baseline, brought into scope explicitly (RFD 0038).
use std::core::{type, rel};
pub type Document { title: String, body: String }
pub rel Cites(citer: Document, cited: Document) [0..*] [0..*];
// Posture 3 — vocabulary-classified. The vocabulary is an ordinary
// EXTERNAL package — here a hypothetical third-party UFO package;
// nothing UFO-related ships with Argon or its stdlib — or the module
// declares its own `pub metatype` / `pub metarel` vocabulary locally.
use ufo_foundational::prelude::*; // a third-party vocabulary package
pub kind Person { name: String, age: Nat }
pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*];
pub mediation Involves(m: Marriage, p: Person);
Postures compose and are per-module, not per-package: a module may use the imported std::core baseline (use std::core::{type, rel}, or via the package prelude) alongside another imported (or locally-declared) vocabulary, and a package may ship modules in different postures behind one prelude. The metatype calculus (Meta-calculus atom) distinguishes the declarations by the keyword in declaration position.
Group axioms — disjoint, complete, partition
A group axiom constrains a set of sibling concepts collectively: that they never overlap, that they jointly exhaust a parent, or both. The three forms share one declaration shape — a contextual keyword, an optional parent, and a brace list of member concept paths:
disjoint { Cat, Dog, Fish } // pairwise non-overlapping
complete Animal { Cat, Dog, Fish } // jointly exhaustive over Animal
partition Animal { Cat, Dog, Fish } // disjoint AND complete
The keywords (disjoint / complete / partition) are contextual — they lex as ordinary identifiers and bind as group-axiom keywords only in declaration position followed by the group-axiom shape, so they remain available as concept and field names elsewhere. A group axiom introduces no name of its own; it names existing concepts.
disjoint { A, B, C } asserts the members are pairwise non-overlapping: no individual is an instance of two of them at once. Disjointness is instance-level. A write that would classify one individual into two declared-disjoint members is refused (OE0240), so the contradictory state never enters the store. disjoint takes no parent — the members need not share a supertype.
complete Parent { A, B, C } asserts the members jointly cover Parent: every Parent instance is an instance of at least one member. Under the closed-world default (Type system), a Parent instance derivable into none of the members is an uncovered instance, and the write that creates it is refused (OE0241). Each member must be a subtype (<:, transitively) of Parent, checked at elaboration (OE0243 otherwise). Completeness has a second, static half, but it fires only when uncoveredness is provable. The build is refused (OE0242) when the schema declares an instantiable (non-abstract) concept S <: Parent that is provably disjoint from every cover member — so no individual can be both an S and a member (for instance because a disjoint/partition declaration separates S from each member, or because the cover is empty). A subtype that merely fails to be a static subtype of any member is not refused: under multiple classification an S individual can still be classified into a member at write time, so its covering is left to the runtime OE0241 guard. (This is why orthogonal generalization sets over one class — two partitions of Parent along independent dimensions — are admitted: a member of one is not disjoint from the members of the other.) An abstract subtype, having no direct instances, is exempt. This static gate ranges only over the concepts declared in the same file as the group axiom; a subtype introduced in another module is not seen at this point, and — like an in-file subtype that is not provably uncovered — its covering is left to the runtime OE0241 guard rather than refused statically.
partition Parent { A, B, C } is the conjunction: the members are both disjoint and complete over Parent. It imposes the OE0240 overlap guard, the OE0241 covering guard, and the OE0242 / OE0243 static gates together.
A declared disjointness also constrains typed-slot writes, because it makes a slot’s demand unsatisfiable rather than merely unmet. Writing into a relation endpoint, or into a concept-typed field, an individual already classified under a concept declared disjoint from the slot’s declared concept is refused: classifying that individual under the slot’s concept would itself violate the group, so the write can never be satisfied in this state or any later one. The refusal fires at elaboration where the value’s concept is statically known (OE1426 endpoints, OE1434 fields) and at write time against the individual’s real classification (OE1428 endpoints, OE1435 fields). It does not depend on the slot concept’s world assumption — an open world permits membership that is unknown, not membership a declaration forbids. Two concepts with no declared disjointness are unaffected: multiple classification is legal, an individual under one may later also be classified under the other, and the write stands.
Disjointness and covering are enforced as ordinary substrate checks (Rule atom — fn, derive, query, mutate, check) the compiler synthesizes from the declaration — one overlap check per unordered member pair, one negation-as-failure covering check per cover — so a violating write is refused by the same delta-guard that enforces a hand-written check, with no separate axiom machinery. The covering check is closed-world: it fires when no member membership is derivable. Its interaction with a concept’s #[world(open)] assumption is specified in World assumptions (CWA / OWA) (RFD 0045).
A degenerate group axiom synthesizes no runtime check, because it has nothing to guard. An empty disjoint { } and a one-member disjoint { A } have no unordered pair to overlap, so no overlap check is produced and the declaration is accepted in silence. An empty complete Parent { } likewise produces no covering check — but it still meets the static cover gate above, so any instantiable subtype of Parent (including Parent itself, if non-abstract) is then provably uncovered and the build is refused (OE0242).
Types and refinement
Generics
Angle brackets, Rust-aligned:
generic-params ::= '<' generic-param (',' …)* '>'
generic-param ::= Ident ('<:' TypeExpr ('+' TypeExpr)*)?
Type arguments at call site use turbofish for disambiguation: sort::<Money>(prices).
Subtyping
A <: B declares A is a subtype of B. Two universal bounds frame the lattice: A <: Top and Bot <: A hold for every A.
Beyond the declared <: graph, the substrate has three built-in subtyping sources (oxc-check/src/infer.rs::types_compatible):
- Numeric tower:
Nat <: Int <: RealandNat <: Int <: Decimal.Moneyis not in the tower — there is no implicitInt → Money;Moneyonly arises through the result-side widening of monetary arithmetic (Numeric tower and coercion),Money ▷ Decimal ▷ Real ▷ Int. - Covariant collections:
List<A> <: List<B>andOption<A> <: Option<B>iffA <: B(written[A]/A?). - Invariant elsewhere: tuples,
fn(…) -> _, and generic applications subtype only by equality.
Real, Decimal, and Money are exact arbitrary-precision rationals (BigRational); source numeric literals parse to exact rationals with no f64 round-trip, so 0.1 + 0.2 == 0.3 (RFD 0016, oxc-reasoning/src/compile/value.rs).
Refinement
refinement-clause ::= ('where' | 'iff') '{' refinement-pred (',' …)* '}'
refinement-pred ::= D1Pred | D2Pred
A refinement attaches a predicate to a concept. The keyword chooses how the predicate relates to membership — the description-logic split between a primitive class (⊑, necessary conditions only) and a defined class (≡, necessary and sufficient conditions). See RFD 0017.
where { P }— primitive.Pis a necessary condition: every member satisfies it, but a supertype instance that merely satisfiesPis not thereby a member. Membership is asserted (by construction orinsert iof,mutate);Pis enforced as an invariant at each membership write and never widens the extent. This is Rust’swherebound lifted to a concept’s members — a constraint, not a definition.iff { P }— defined.Pis necessary and sufficient: an instance of the supertype is a member if and only if it satisfiesP. Membership is derived — the substrate auto-classifies and rejects manualinsert iof(mutate,OE0211). Reads as the membership biconditional: “anAdultis aPersoniffage >= 18.”
pub type Adult <: Person iff { self.age >= 18 && self.has_id }; // defined: any qualifying Person is automatically an Adult
pub type ExtinctSpecies <: Species iff { self.living_count == 0 }; // defined: classification derived from state
pub type VettedVendor <: Vendor where { self.risk_score <= 30 }; // primitive: vetting is conferred; the score is a necessary invariant on members
D1Pred (substrate-locked): metaEq, isDet, hasAnc, hasDesc, countGeq, forallC, existsC, plus Booleans. D2Pred: QF-LIA, GNFO, enum equality. The elaborator stages D1 ahead of D2 residual.
Mechanically: D1Pred / D2Pred / MixedPred are inductives at spec/lean/Argon/Decidability/Fragment.lean; the staging theorem stage_correct lives at CrossDomain/Staging.lean; the polynomial bound on D1 evaluation (d1EvalCost n φ ≤ d1Size φ · (n+1)^(d1QuantifierDepth φ)) is proven in Complexity/Bounds.lean. The bound grounds the polytime claim Tier ladder makes for tier:structural and tier:closure.
Three-valued membership under OWA. When the governing world assumption is open (World assumptions (CWA / OWA)), 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 — matching SHACL’s sh:Violation discipline and SQL’s three-valued NULL semantics. Under CWA, unknown collapses to false. The mechanization at TypeSystem/Soundness/CwaOwa.lean proves that CWA-true refinement results survive transition to OWA (monotonic knowledge transfer). The symmetric rule for a primitive where invariant: a membership write is rejected only on positive evidence of violation (P evaluates to a definite false); unknown permits the write (mutate, OE0668). unknown means information absence — a referenced field with no recorded value. A predicate that cannot be evaluated at all (an unsupported form, malformed stored data, or a type-mismatched comparison such as ordering a Date against an Int) is not unknown: unsupported forms are refused at build time (OE0660) — including a predicate whose expression shape is supported but whose field locus is not: an iff projecting a field declared only on the refining concept’s own body (structurally absent from every supertype candidate), or a where projecting a relation-backed navigation field (not evaluable against stored fields at a membership write) — and anything that slips past the gate fails the operation loudly at runtime — never a silent permit (where) or a silently-empty derived extent (iff). Refinement predicates evaluate on the same exact value tower as every other value position (RFD 0016): exact rationals for Real/Decimal/Money, chronological Date comparison, with no value-dependent enforcement.
Built-in type forms
| Form | Meaning |
|---|---|
T? | Option<T> |
[T] | List<T> |
[T; n..m] | refined list with cardinality — reserved; refuses OE0014 until enforcement lands |
Set<T>, Map<K, V>, Range<T> | stdlib generics |
Result<T, E> | error-carrying result |
fn(T1, T2) -> U | function-type expression |
Path<NodeT, EdgeT> | first-class alternating node-edge sequence (The Path type) |
TypeRef, TypeRef<C>, Entity | reflective sorts (Reflection) — values are type-references |
First-class function values include references to declared fns and closures (Expression grammar); both share the fn(T1, T2) -> U type. Higher-kinded types and runtime trait objects (dyn Trait) are out of scope (Out of scope).
Reflective sorts. Entity, TypeRef, and Metatype are the reflective sorts of the meta-calculus (Reflection), ordered Metatype <: TypeRef <: Entity (and Entity <: Top). These are distinct from Top (Subtyping): Top is the <:-greatest type, whereas TypeRef is the sort whose values are references to declared types. The bounded form TypeRef<C> is sugar for the Refinement refinement { t: TypeRef where specializes(t, C) } (RFD 0017, where = primitive/asserted), with TypeRef == TypeRef<Top>. It is covariant — TypeRef<A> <: TypeRef<B> iff A <: B — which follows directly from the refinement ({t | t <: A} ⊆ {t | t <: B}), adding no new structural subtyping rule. Membership is three-valued under OWA (World assumptions (CWA / OWA), RFD 0007): specializes(t, C) unknown ⇒ not a member. Powertype / order / categorization semantics stay in the unprivileged mlt package (Higher-order modeling); TypeRef<C> carries none of them. See RFD 0023.
? propagation
expr? on a Result<T, E>-typed expression unwraps to T on Ok or returns the Err from the enclosing rule. Admitted in fn and mutate bodies, where the enclosing rule’s return type is Result<U, E> for some U. Not admitted in derive (which has no return type — predicates do not fail) or query (whose return type is the projection type, not a Result); check rules emit diagnostics rather than returning Results. Calling ? outside an admitted context is a parse-time error.
Expression grammar
expr ::= primary-postfix
((arith-op | comp-op) primary-postfix)*
(('not')? 'in' primary-postfix)?
('&&' expr | '||' expr)*
primary-postfix ::= unary-op* primary postfix*
unary-op ::= '!' | '-'
postfix ::= '.' Ident '(' expr-list ')' // n-arg method call
| '.' Ident // field access OR zero-arg method call
| '[' expr ']' // index
| '[' expr? '..' expr? ']' // slice
| '?' // try (Result propagation)
primary ::= literal
| '_' // wildcard / anonymous binding (rule-atom arg positions only)
| '(' expr ')'
| list-or-comprehension
| if-expr
| match-expr
| block-expr
| meta-call
| aggregate
| subquery // `query` (count/set/one/collect/exists braces)
| call-expr
| path
| struct-literal
| closure
literal ::= INT | REAL | STRING | DATE | DATETIME | BOOL
list-or-comprehension
::= '[' ']'
| '[' expr (',' expr)* ','? ']'
| '[' expr 'for' Ident 'in' expr ('where' expr)? ']'
if-expr ::= 'if' expr block ('else' (block | if-expr))?
match-expr ::= 'match' expr '{' arm (',' arm)* ','? '}'
block-expr ::= '{' stmt* expr? '}'
block ::= '{' stmt* expr? '}'
call-expr ::= path '(' expr-list ')'
meta-call ::= 'meta' '(' expr ')'
aggregate ::= ('sum'|'count'|'min'|'max'|'avg')
'(' expr ('for' Ident 'in' expr
('where' expr)? (',' rule-atom)*)? ')' // RFD 0029: trailing atoms refine the fold
struct-literal ::= path '{' (struct-init-list)? '}'
struct-init-list ::= field-init (',' field-init)* ','?
| '..' expr (',' field-init)* ','? // struct-update spread
field-init ::= Ident (':' expr)? // `name` shorthand or `name: expr`
closure ::= '|' closure-params? '|' ('->' TypeExpr)? closure-body
closure-params ::= closure-param (',' closure-param)* ','?
closure-param ::= Ident (':' TypeExpr)?
closure-body ::= expr | block
path ::= Ident ('::' Ident)*
expr-list ::= (expr (',' expr)*)?
arith-op ::= '+' | '-' | '*' | '/' | '%'
comp-op ::= '==' | '!=' | '<' | '<=' | '>' | '>=' // non-chainable
Precedence (tightest to loosest):
- Postfix (
.f(),[i],[a..b],.field,?) - Unary prefix (
!,-) - Multiplicative (
*,/,%) — left-associative - Additive (
+,-) — left-associative - Comparison (
==,!=,<,<=,>,>=) — non-associative (a < b < cis rejected; usea < b && b < c) - Membership (
in,not in) — non-associative - Logical AND (
&&) — left-associative, short-circuit - Logical OR (
||) — left-associative, short-circuit
Turbofish disambiguates generic call sites at expression position: sort::<Money>(prices).
Rounding builtins (RFD 0029). A small family of rounding functions is available in expression position — both in rule-body bindings and in the fn/compute plane — operating exactly over the numeric tower (Decimal stays Decimal, never via f64):
| Builtin | Meaning |
|---|---|
round(x) | nearest integer, ties away from zero |
round(x, n) | nearest multiple of 10⁻ⁿ, ties away from zero |
round_half_even(x, n) | nearest multiple of 10⁻ⁿ, ties to even (banker’s rounding) |
trunc(x, n) | toward zero at 10⁻ⁿ |
round_half_even is the money default: financial rounding rounds half to even to avoid the systematic upward bias of half-away-from-zero. All four return an exact value (integral results collapse to Int). A wrong argument count is refused at compile with OE1333 RoundingBuiltinArity.
The decimal-place count n has a supported scale domain of |n| ≤ 1000. The scale is 10⁻ⁿ, formed as an exact rational, so the bound caps the magnitude of 10^|n| for resource safety; 1000 places is far beyond any real-world money / decimal / scientific quantity (SQL DECIMAL caps scale at 38; IEEE 754 decimal128 carries 34 significant digits). A count outside the domain is refused at evaluation with OE1374 RoundingScaleOutOfRange — a typed client diagnostic, never a silent truncation of the count to a wrong scale.
Closures capture environment by value (Argon is value-semantic; no references). A closure’s type is fn(T1, T2) -> U — interchangeable with declared fn references at any function-typed argument or binding.
Property-style accessors. The paren-less postfix .Ident resolves to a stored field if the receiver type declares one with that name; otherwise to a call of a zero-argument method with that name. This unifies the .field and .zero_arg_method() forms — both read obj.name. The 30.days, 5.minutes, 60.seconds shorthand in Numeric tower and coercion is exactly this affordance over zero-arg methods on numeric types. Methods with one or more arguments always require explicit parens.
Struct-literal evaluation. A struct-literal T { … } evaluates each field initializer and assembles a struct value of type T (the data semantics — structural equality, immutability, no identity — are stated in struct and enum — language built-ins (data declarations)). The literal must state every field: a missing field is OE0249, an undeclared one is OE0250, and a field given the wrong type is OE0251. The field shorthand in field-init (a bare Ident with no : expr) takes the field’s value from a same-named binding in scope.
Functional update — the ..base spread. The ..expr form in a struct-init-list constructs a new struct value from an existing one: it evaluates base, copies every field, then overrides the fields the literal restates. Row { ..base, a: 9 } is base with a replaced by 9 and every other field carried over. The result is a fresh value; base is never modified (struct values are immutable, struct and enum — language built-ins (data declarations)). The spread supplies the fields the literal omits, so the every-field rule is satisfied by the base — a spread literal need only name the fields it changes.
pub struct Row { a: Int, b: Int }
test "spread overrides one field and carries the rest" {
let base = Row { a: 1, b: 2 };
assert Row { ..base, a: 9 } == Row { a: 9, b: 2 };
assert base == Row { a: 1, b: 2 }; // base unchanged
}
Field projection. The postfix .f reads field f off a struct value, by structure. Projection nests — outer.inner.v reads v off the inner struct off outer. A field the struct’s type does not declare is OE0254 StructFieldAccessUnknown.
pub struct Inner { v: Int }
pub struct Outer { inner: Inner, tag: String }
test "projection reads through nested struct values" {
let o = Outer { inner: Inner { v: 7 }, tag: "x" };
assert o.inner.v == 7;
assert o.inner == Inner { v: 7 };
}
Enum payload binding in match. The constant-pattern subset of match — payloadless enum constants, literals, or-patterns of those, and _ — executes (Pattern matching). A payload-binding arm (match opt { Some(x) => …x… }) destructures one payload value into one fresh variable, bound over the arm body. In value position — a let right-hand side, a comparison operand, a fn body, a return/require value — this binds and evaluates: let r = match opt { Some(x) => x, None => 0 }; is the idiomatic way to read an enum payload. The arm binds exactly one variable positionally; a zero-, multi-, or record-binder arm is OE0257 EnumPayloadPattern. A payload-binding arm in statement position (an effectful arm inside a mutate statement-match) refuses with OE1319 rather than mis-evaluating; bind the payload in value position first. The rule-body membership test path is Some(binder) / path is None is the other supported reader (struct and enum — language built-ins (data declarations), RFD 0007 Rule-atom grammar).
Functor modules
A module declaration may take type or value parameters, producing a functor module — a module-valued function:
functor-mod ::= 'mod' Ident generic-params '{' mod-item* '}'
// generic functor
| 'mod' Ident '(' param-list ')' '{' mod-item* '}'
// value-parameter functor
mod-alias ::= 'mod' Ident '=' path '(' expr-list ')' ';' // instantiation
Functor modules are instantiated via the mod Name = path(args); alias form introduced in Structure; the body is re-elaborated per instantiation site with the parameters bound. The substrate guarantees monomorphization at elaboration — there is no runtime module dispatch.
mod TaxJurisdiction<J: Jurisdiction> {
pub type TaxableIncome <: Money;
pub derive owes_tax(p: Person) :- p.income: TaxableIncome, p.income > 0;
}
mod us_federal = TaxJurisdiction<USFederal>;
mod california = TaxJurisdiction<California>;
Parametric modules, not parametric concepts. Argon admits parameterization only at the module level. There is no parametric pub type Container<T> { … } form — a metatype-classified concept is always a closed, monomorphic classifier. When a modeler wants Container<Apple> vs Container<Orange>, the answer is either (a) a specialization hierarchy (pub type AppleBox <: Container) when the variation is ontological, or (b) a functor module whose body declares the per-instantiation concepts when the variation is parametric. This restriction keeps the metatype calculus first-order and the inference tier classifier decidable; lifting it would require admitting kind-polymorphism into refinement and standpoint federation, both of which are unsound at present without further machinery.
A functor body admits any module-level item: concepts, relations, rules, traits, macros, sub-mods, and other functor modules. Each instantiation produces a fully elaborated namespace; symbols from one instantiation do not unify with symbols from another (us_federal::TaxableIncome and california::TaxableIncome are distinct types, both <: Money).
Functor modules are the canonical mechanism for vocabulary-parameterized libraries (per D-007).
The Path type
Path<NodeT, EdgeT> is the first-class type produced by path-projecting query forms (query). It appears in the built-in type forms (Built-in type forms) because the compiler synthesizes path values from query results; the value-level API lives in the stdlib (std::path).
A path is an ordered, alternating sequence n₀, e₁, n₁, e₂, n₂, …, eₘ, nₘ where m = length(path) is the number of edges. A path of length zero is a single node. NodeT and EdgeT are the least common supertypes of the node and edge values respectively.
Edge values by relation form. Edge values vary with which relation form (Relations) the path traverses:
- Forms B and C — named relations. Each edge value is the relation tuple-entity itself, carrying any intrinsic properties:
path.nth_edge(0)?.distanceretrieves thedistancefield of the first edge when the underlying relation declares it. - Form A — anonymous fields. Each edge value is the structural placeholder
Edge<From, To> { from: From, to: To }. Form A carries no metarel classification and no intrinsic data, so the placeholder exposes only endpoint identities.
Heterogeneous chains. When a path traverses relations with differing endpoint types (e.g., user.knows(friend).works_at(company) where knows: User → Person and works_at: Person → Company), NodeT is the join (least common supertype) of User, Person, and Company, and EdgeT the join of the involved relation types. When no common supertype is in scope, both fall back to Top. Modelers needing precise heterogeneous typing decompose into single-relation queries and join at the modeler level.
Hyperedges. Paths are over binary relations only. n-ary relations (pub rel Transaction(seller, buyer, item)) are queryable but not path-traversable; hypergraph traversal semantics is out of scope (Out of scope).
API. The shape/projection/fold operators (length, nodes, edges, nth_edge, reverse, concat, map_*, …) are stdlib API on impl<N, E> Path<N, E> in std::path — see Stdlib (selected). The type-system-relevant facts: cost(self) -> Real lives in a conditional impl gated on E <: Weighted (the Rust-aligned conditional-impl pattern), so a uniform Path<N, E> admits cost() only when the edge type implements the std::path trait Weighted { fn weight(self) -> Real; }; without it, shortest-path queries rank by edge count. The Form-A edge value is the placeholder pub struct Edge<A, B> { from: A, to: B } — no metarel classification, just a first-class edge for uniform iteration.
Equality. Path<N, E> has structural equality: two paths are equal iff their node and edge sequences match position-by-position. Paths are value-semantic and immutable; all transformations produce new path values.
Temporal substrate
Why bitemporal
The substrate before this section operates on a single state mapping concepts to extents. The mutation grammar (mutate) advances the state from snapshot to snapshot; prior snapshots are not addressable. This section introduces the bitemporal substrate: every fact carries a valid time and a transaction time. Bare queries default to the current snapshot; explicit temporal operators address the past, the future, and the historical record of the system’s beliefs.
The formalism is DatalogMTL (Wałęga et al., IJCAI 2019) with stratified negation (Tena Cucala et al., AAAI 2021) over the integer timeline; the reference reasoner is MeTeoR (Wałęga et al., AAAI 2022).
Valid time and transaction time
Two interval annotations on every fact (and every relation tuple):
- Valid time (VT) — when the fact holds in the world.
[VT_start, VT_end], with open-ended[VT_start, ∞]for ongoing. - Transaction time (TT) — when the system recorded the fact.
[TT_start, TT_end], with open-ended[TT_start, ∞]for current belief.
The snapshot view is the projection ${,\mathsf{iof}(x, T),\mid,VT_{\text{start}} \le \mathit{now} \le VT_{\text{end}} ;\wedge; TT_{\text{start}} \le \mathit{now} \le TT_{\text{end}},}$. Bare rule atoms and bare queries default to this view.
The bitemporal extension applies uniformly to all relations, not just iof. A pub rel Enrolment(s: Person, u: University) tuple carries VT and TT just like an iof fact.
Mechanized at spec/lean/Argon/Reasoning/Temporal.lean (BiState as the bitemporal State; TTHistory and TTMonotone for the transaction-time invariant). The bitemporal extension is additive — the termination machinery of Argon.Reasoning.State lifts pointwise without modification.
Retroactive correction and audit
Mutations that close a VT interval retroactively (delete iof(x, T) at #PAST-DATE#) do not erase the prior record. They close its TT interval at now and append a new record with the corrected VT. Audit queries as of TT = #PAST-DATE# reconstruct what the system believed at any past point. The substrate is append-only by default; explicit erasure (forget), crypto-shredding (#[shred_on_forget]), and #[retention(…)] bounds are storage-layer concerns specified in mutate and Storage layer.
Rule validity over time
A rule carries valid time and transaction time just as a fact does. Its valid time is the interval over which the rule is in effect; its transaction time is when the rule was recorded or its text corrected. This is the bitemporal model of any fact — when it holds versus when the system recorded it — applied to rules, with transaction time serving as the shared viewpoint across facts and rules.
A rule’s start-of-validity is written with the same at #DATE# qualifier a fact uses, placed on the head:
pub derive eligible(x) at #2018-01-01# :- enrolled(x), in_good_standing(x);
Without the qualifier a rule has unbounded validity — in effect at every point — so a program that dates none of its rules behaves exactly as one with no validity windows at all. A rule’s record time, when it entered the system, is the separate transaction-time axis and is never folded into its validity: an undated rule is in effect at past points too, not merely from the moment it was written.
Evaluating derived facts at a chosen rule valid-time point selects exactly the rules in effect at that point, by the same valid-time visibility the data plane applies to facts (VT_start ≤ t ∧ t < VT_end). Reading at the present selects every currently-in-effect rule, so present-time evaluation is unchanged by the feature. A reconstruction of what was derived at a past point replays under the rules in effect then, not the rules in effect now.
A prospective retirement closes a rule’s VT_end going forward; it is forward-only: derived facts that held while the rule was in effect stand, and only points at or after the close lose that rule’s support. Retroactive invalidation — removing a rule as if it had never been in effect — is a distinct, transaction-time operation outside this section.
A rule’s validity window is a property of the rule — which rules apply — and is independent of any metric-temporal operator (since, until) appearing inside a rule body, which constrains when within the world timeline a body atom holds.
Three-valued evaluation under OWA
Under open-world assumption, temporal atoms evaluate to Is | Not | Can. The since and until operators (see Temporal rule atoms) lift to three-valued via the strong-Kleene meet/join tables that truth values defines, composed pointwise across the interval. Unknown temporal extent projects to Can, never to Both — there are no conflicting witnesses, just single-source uncertainty.
Temporal sub-tier
Temporal expressiveness is an orthogonal sub-tier on top of the main tier ladder. Every program has a tier pair (main, temporal) — see Tier ladder.
Metric-timeline resolution
The metric temporal operators (since, until, and the box/diamond bounded forms) reason over a discrete integer timeline whose tick is a package-declared resolution, set in ox.toml:
[temporal.metric]
resolution = "week" # or "day" (default), "hour", "nanosecond", "2weeks", …
The tick is a package-global choice (one metric axis is shared by every rule and fact), mirroring default_world. It lives under [temporal] — the umbrella for temporal-reasoning config — rather than a bare [metric], so the precise word metric stays scoped to the metric-operator family (distinct from the modal box/diamond family) and leaves room for future siblings ([temporal.modal], …). It is a cost/coarsening lever: a metric operator’s work scales with the number of ticks between interval endpoints, so a program reasoning over long spans can declare a coarser tick. Because decidability rests on discreteness — not on the tick equalling a day — any positive-integer tick keeps the metric fragment decidable (see Decidability).
- Snapshot reads are unaffected. “The fact valid at instant
t” (seeAsOfsemantics) stays a full-nanosecond integer comparison; it never consults the tick. The tick governs only the metric operators. - Bounds are authored in days (
.days/.weeks, or a bare integer) and coarsened onto the tick. A bound that is not an integer multiple of the tick loud-refuses rather than rounding —since[3 days]under a week tick refuses (3⁄7 is not a whole number of weeks);since[14 days]is two ticks. - The default is
day, so a package that declares no[temporal.metric]table behaves exactly as before. - An unrecognized
resolutionvalue is a non-fatal manifest warning (it falls back to the day default), per the manifest-honesty convention.
The design record is RFD 0067 §6.
Rule atom — fn, derive, query, mutate, check
Argon distinguishes the definition of derived predicates (derive) from inference tasks over the resulting theory (query). derive populates IDB predicates that other rule modes can join against, contributing to the inductive closure. query consumes them and returns a typed value with no IDB side-effect. The split mirrors IDP’s { … } inductive-definition block vs. propagate/expand inference tasks. fn is pure value computation over its arguments; mutate is the only side-effecting verb; check emits diagnostics observer-only (lowers to Cat3 — never populates the IDB).
Purity ladder
| Mode | Callable from | Calls | Lowers to |
|---|---|---|---|
fn | any | fn, query | pure compute |
query | query, derive, mutate, check | fn, query | Cat1 + projection |
derive | derive, mutate (head population), check | fn, query | Cat1 (monotone) or Cat2 (NAF) |
check | observers only | fn, query | Cat3 (observer) |
mutate | host, other mutate | all five | side-effecting |
fn and query are cacheable on (args, state_version). derive is stratified fixpoint. mutate is transactional.
fn
fn-decl ::= attribute* 'pub'? 'fn' Ident generic-params? '(' param-list ')' '->' TypeExpr fn-body
fn-body ::= '=' expr ';' // single-expression
| '{' stmt* expr? '}' // block, implicit return
param ::= 'self' | Ident ':' TypeExpr
self and Self. Argon is value-semantic; there is no reference or borrow form. self is always owned-by-value (no &self / &mut self) and is admitted only as the first parameter of a method inside an impl block or a trait declaration; its type is the enclosing impl’s target. Self resolves to the implementing type in trait declarations, to Type inside impl Trait for Type, and to Type inside bare impl Type { … }.
pub fn rent_per_day(l: Lease) -> Money = l.monthly_rent / 30;
pub fn classify(area: Decimal) -> Size {
if area < 50.0 { Size::Small }
else if area < 200.0 { Size::Medium }
else { Size::Large }
}
pub fn safe_div(a: Money, b: Money) -> Result<Money, Diagnostic> {
if b == 0 {
Err(Diagnostic { severity: Severity::Error, code: "Math::DivZero", message: "..." })
} else {
Ok(a / b)
}
}
derive
derive-decl ::= attribute* 'pub'? 'derive' Ident '(' param-list ')'
( ':-' atom-list )? ';'
atom-list ::= atom (',' atom)*
atom ::= predicate-call | comparison | type-test | 'not' atom | path-pattern
pub derive ancestor(d: Person, a: Person) :- ParentOf(d, a);
pub derive ancestor(d: Person, a: Person) :- ParentOf(d, p), ancestor(p, a);
pub derive senior(p: Person) :- p: Person, p.age >= 65;
// Ground-fact form (no body, concrete args): the head holds for the named terms.
pub derive iof(Type, Type); // MLT* OL3
pub derive Within(usc26, usc); // seed a derived predicate
Multiple derives with matching head name and arity compose as Datalog union (modulo the stratification check below). not atom is NAF, evaluated under well-founded semantics. A derive with no :- clause is bodiless, and its head arguments decide its reading:
- Concrete arguments → a ground fact. When every head argument is a concrete term — a declared individual (
Within(usc26, usc)), an enum constant, an axis value, or a type reference (iof(Type, Type)) — the bodilessderivedeclares that tuple as a ground fact, equivalent to:- true. This is the sanctioned way to seed ground tuples directly on a derived predicate: the seed tuples union with the head’s otherderiveclauses and rules over the same relation node (same name, same arity), so a recursive rule reads them and computes their closure (seeds ∪ derived-closure). A non-concrete (free-variable) argument is the error — a bodilessderivehas no body to range-restrict it — and is refused withOE1342, the bodiless analogue ofOE1303(below), naming the offending argument and directing you to declare the individual or add a:- …body. - Type-annotated parameters → an empty predicate. A bodiless head whose arguments are typed parameters (
pub derive adult(p: Person);) introduces the predicate with that signature and an initially empty extent — the introduce-empty idiom (Rule-atom grammar). Its rows arrive from later same-headderiveclauses.
Every body-carrying rule must be range-restricted (safe): each variable in the head, in a negated (NAF) atom, or in a comparison/compute operand is bound by some positive body atom; an unsafe rule is refused at compile (OE1303), since an unbound variable would silently project Null or mis-evaluate.
Evaluation model. A derive program denotes the tuples its rules derive, under the semantics fixed in Reasoning: the least fixpoint of the positive program, stratified negation layered along the axis dependency graph, and — for a negation cycle no stratification can layer — the well-founded model computed by the alternating fixpoint. The surface consequences a rule author relies on:
- Recursion through negation is accepted. A negation cycle (
p :- not q,q :- not p) that strict stratification cannot layer is evaluated under well-founded semantics rather than refused. Cross-stratum, acyclic NAF is ordinary stratified negation. (OE1309no longer fires from stratification; it survives only as the dispatch seam.) - Four-valued query surface. WFS is three-valued (true / false / undefined). The engine materializes the definitely-true extent as the head’s rows — a paradoxical atom resolves to
undefinedand does not fire downstream rules — and the head’s undefined tuples surface on the query/derive read asCanrows, by default (drop-but-count under the opt-in K3 projection; see the envelope contract under well-founded semantics). A rule whose conclusion is genuinely undecided neither fires nor is denied — and the read says so (Can) instead of omitting it. #[brave]/ stable-model semantics is out of scope (Out of scope).
Parametric (axis-generic) rules. A derive rule may be generic over a set of axes; the elaborator monomorphizes it to a finite set of concrete rules at elaboration, with no change to fixpoint semantics.
Stratified aggregates. Aggregate atoms (count, sum, max, min, avg, set_collect, …; see query) are admitted inside derive bodies when the aggregated predicate sits at a strictly lower stratum than the rule head, where the stratification dimension is a well-founded relation (typically the iof DAG, the specialization lattice, or an explicit user-declared ordering). The classic library use case is computing a derived level function over iof:
pub derive has_order(t: Entity, n: Nat) :-
n == 1 + max { select m from t': Entity, m: Nat
where iof(t', t), has_order(t', m) };
The stratifier accepts this because the inner max aggregates has_order over iof-predecessors of t, and iof is well-founded. Stratified aggregates that would loop through an aggregation step on the same stratum are rejected with OE0510 NonStratifiedAggregate.
Universals over recursive predicates. A common composite pattern — a conjunction is fulfilled iff all of its children are — is a universal over the very predicate being defined:
pub derive Fulfilled(p: Conjunction, t: Instant) :-
Instant(t), forall c: PC where childOf(p, c), Fulfilled(c, t);
This form is refused (OE1317 RecursionThroughAggregation): the forall lowers to the count-equality aggregate (Rule-atom grammar), and stratified-aggregate semantics (Faber–Pfeifer–Leone) require the aggregated predicate in a strictly-lower stratum — here Fulfilled aggregates over itself, so its SCC crosses an aggregate boundary and has no stratification. No intermediate derive helps: recursion depth follows the data (the part-whole tree), so any helper joins the same cycle. The supported phrasing is negation-as-failure double negation — ∀c.F(c) rewritten as ¬∃c.¬F(c):
pub derive HasUnfulfilledChild(p: Conjunction, t: Instant) :-
childOf(p, c), Instant(t), not Fulfilled(c, t);
pub derive Fulfilled(p: Conjunction, t: Instant) :-
Conjunction(p), Instant(t), not HasUnfulfilledChild(p, t);
This converts recursion-through-aggregation into recursion-through-negation, which the well-founded executor evaluates. On two-valued-total data — every child’s Fulfilled status resolves to true or false, as it does over a finite acyclic part-whole tree of leaves with definite status — the double negation derives exactly the universal’s extent, bottom-up through the tree. The honest caveat: where a child’s status is genuinely undefined under WFS, the double-negation form propagates undefined to the parent rather than guessing either way — and the surface reports the parent as a Can row rather than omitting it, per the four-valued projection rule under well-founded semantics. Admitting the forall form directly (structural stratification over a provably well-founded relation) is tracked in issue #185.
Worked example — double-entry accounting (RFD 0029). Derived values and aggregate-as-term together make quantitative domains authorable. The double-entry invariant — within every journal entry, total debits equal total credits — is a check comparing two aggregates; the per-account balance is a derived value, grouped per account (the outer bound variable). The running package is examples/double_entry_v0.
pub type Account { mut name: String, }
pub type Entry { mut memo: String, }
pub type Posting { mut amount: Decimal, mut side: String, } // side = "D" | "C"
pub rel postedTo(posting: Posting, account: Account);
pub rel inEntry(posting: Posting, entry: Entry);
// Per-account balance = Σ debits − Σ credits, grouped per `acct`.
pub derive accountBalance(acct, bal) :- acct: Account,
debits = sum(p.amount for p in Posting, postedTo(p, acct), p.side == "D"),
credits = sum(p.amount for p in Posting, postedTo(p, acct), p.side == "C"),
bal = debits - credits;
// The double-entry invariant — two aggregates compared, per entry.
pub check EntryNotBalanced(e: Entry) :- e: Entry,
debits = sum(p.amount for p in Posting, inEntry(p, e), p.side == "D"),
credits = sum(p.amount for p in Posting, inEntry(p, e), p.side == "C"),
debits != credits
=> Diagnostic {
severity: Severity::Error,
code: "Ledger::E001",
message: "journal entry is not balanced — total debits must equal total credits",
};
// A sales-tax line, rounded to cents with banker's rounding (the money default).
pub derive accountTax(acct, tax) :- acct: Account,
debits = sum(p.amount for p in Posting, postedTo(p, acct), p.side == "D"),
tax = round_half_even(debits * 0.075, 2);
Every amount is an exact Decimal: sum folds rationals (no rounding), the balance subtraction is exact, and round_half_even rounds to cents exactly — 150.75 * 0.075 = 11.30625 rounds to 11.31, never an f64 artifact. The EntryNotBalanced check is an instance-level Error, so at runtime it is a delta guard: a mutation that would leave an entry unbalanced is rejected atomically (double-entry is enforced, not merely reported).
Rule-atom grammar
Bodies of derive, query from clauses, check, and unsafe logic blocks are conjunctive sequences of rule atoms. The atom shapes:
rule-body ::= rule-atom (',' rule-atom)*
rule-atom ::=
// Negation as failure
'not' rule-atom
| 'not' '(' rule-atom (',' rule-atom)* ')'
// Modal (tier:modal)
| 'box' '(' rule-atom ')'
| 'diamond' '(' rule-atom ')'
// Restriction quantifiers — DL ∀R.C / ∃R.C (tier:expressive)
| 'forall' '(' field-path ',' type-expr ')'
| 'exists' '(' field-path ',' type-expr ')'
// FOL binding quantifiers — admitted at tier:fol (inside unsafe logic)
| 'forall' Ident ':' type-expr 'where' rule-body
| 'exists' Ident ':' type-expr 'where' rule-body
// Aggregate as atom, optionally compared (paren form, Expression grammar)
| aggregate (comp-op expr)?
// Subquery as atom, optionally compared (brace form, `query`)
| subquery (comp-op expr)?
// Reflection
| meta-call (comp-op expr)?
| path '::' Ident // sugar for meta(path) == Ident
// Predicate call with optional outcome / comparison suffix
| path '(' rule-arg (',' rule-arg)* ')'
('is' outcome-suffix | comp-op expr)?
// Role invocation with closure (+ = transitive, * = reflexive-transitive)
| field-path ('+'|'*') '(' Ident (':' type-expr)? ')'
| field-path '(' Ident (':' type-expr)? ')'
// Type test and `is` sugar
| field-path ':' type-expr
| field-path 'is' ('not'? 'unknown' | type-expr | Ident)
// Specialization
| field-path '<:' field-path
// Binding — single `=`, distinct from the comparison `==` (RFD 0029)
| Ident '=' expr
// Comparison — RHS is a full expression (Expression grammar)
| field-path comp-op expr
// Membership — RHS is a full expression (set, list, or range)
| field-path 'not'? 'in' expr
// Bare-path Boolean test
| field-path
field-path ::= Ident ('.' name)*
rule-arg ::= expr | expr comp-op expr
outcome-suffix ::= 'not'? ('ambiguous'|'unknown'|'timeout') ('(' Ident ')')?
The truth qualifier R(arguments) is unknown selects R’s well-founded-undefined tuples. These are tuples left neither definitely true nor definitely false by a recursion-through-negation component (Truth values). The component must finish first: the stratifier places the consumer in a higher stratum and then reads R’s undefined companion. A consumer that feeds back into R’s component is refused with OE1441, because it would inspect an incomplete result. Move that consumer into a separate pub derive that does not feed back into R. is not unknown, payload-binding is unknown(x), and path-headed x is unknown also refuse with OE1441; only a predicate call identifies the companion, and a complement requires an explicit finite domain. The multi-valued suffixes is both(a, b) / is ambiguous(a) / is timeout(a) remain the separate OE1349 refusal. This qualifier does not classify ordinary open-world absence; it reads only the WFS companion materialized by evaluation.
Binding atoms (x = expr, RFD 0029). A rule body may bind a fresh variable to the value of an expression: x = expr (single =) is assignment, distinct from the comparison x == e (double =, a filter). expr ranges over bound variables, literals, field projections (t.income), exact arithmetic over the numeric tower, the rounding builtins (Expression grammar), and aggregate expressions. This is the established Datalog/Soufflé assignment concept and is what lets a rule head carry a derived value:
pub derive Tax(t, owed) :- appliesTo(b, t), owed = t.income * b.rate;
The binding introduces a fresh variable, and is range-restricted. Two distinct refusal paths, never a silent Null:
- Freshness (
OE1335 BindingLhsAlreadyBound). The left-hand sidexmust be a new name. Ifxis already bound — by a prior positive predicate atom, a head parameter bound elsewhere, a projection, an aggregate result, or an earlier binding — the=would silently degrade into an equality filter (xjoined against the computed value) rather than a binding. It is refused, namingx, with the directed hint: use==to compare, or pick a fresh name. This is the path a rebindx = x + 1takes whenxis otherwise bound (e.g. by a body predicate) — the LHS is not fresh. - Range restriction (
OE1303 RuleNotRangeRestricted).x = exprbindsxpositively iff every variable inexpris itself positively bound. An unbound right-hand-side variable, or a pure self-reference / cycle among binding atoms where the result variable is bound by nothing else (x = x + 1as the only binder ofx;x = y, y = x), leavesxunbound under the binding fixpoint and is refused as unsafe.
So x = x + 1 refuses either way — via OE1335 when x is already bound elsewhere (freshness), or via OE1303 when x has no other binder (range restriction) — and the two codes name the two genuinely different errors.
Aggregates as bindable terms (RFD 0029). Because an aggregate is a bindable expression, comparing two aggregates — the double-entry sum(debits) == sum(credits) invariant — is simply binding each and comparing the bound variables:
pub derive balanced(e) :- e: Entry,
debits = sum(p.amount for p in Posting, inEntry(p, e), p.side == "D"),
credits = sum(p.amount for p in Posting, inEntry(p, e), p.side == "C"),
debits == credits;
The aggregate source extends the comprehension form: after for x in Source, a comma-separated list of additional body atoms (relation atoms, comparisons, type tests) refines the fold’s domain, and variables from the outer rule body are visible inside. That visibility is the grouping — the group key is the set of outer bound variables free in the aggregate, the standard Datalog reading. pub derive balance(acct, s) :- acct: Account, s = sum(p.amount for p in Posting, postedTo(p, acct)); groups per acct. A binding is not a comprehension trailing-atom form — sum(w for u in T, …, w = u.v) is refused with OE1336 BindingInComprehension, which directs you to project the value into the fold directly (sum(u.v for u in T, …)) or to bind in the outer rule body and aggregate the bound variable. The brace form (sum { … }) stays count/exists-only; a value aggregate in brace form is refused with OE1331 ValueAggregateBraceForm, which directs to the comprehension form. The SQL-ish group by … having is not built; it refuses with OE0007 carrying the binding-form rewrite. (Aggregates evaluate over the definitely-true extent: an aggregate folding over a relation with well-founded-undefined atoms is refused at runtime with OE1332 AggregateOverUndefined rather than silently treating undefined as false. The refusal is whole-relation, not per-group; three-valued aggregate intervals are tracked under issue #250.)
The value aggregates are numeric folds (RFD 0016): sum / avg / min / max lift every projected value into the exact-rational domain — min / max included, so no orderable-but-non-numeric projection (a Date, a String) is admissible either. A projection whose declared type is statically non-numeric — a temporal value, a String, a Bool, an entity or structured value — refuses at ox check / ox build with OE1433 AggregateProjectionNotNumeric, naming the projection expression, its type, and the fold’s numeric domain; a non-numeric value that reaches the fold only dynamically raises the same OE1433 loudly at runtime. count folds cardinality and admits any projection.
Empty-group semantics — a deliberate split. When a group is empty (the fold sees no rows), the aggregators divide by role:
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 value.min,max,avgdrop the row — they have no value over an empty set (there is no least/greatest element, and the mean is0/0), so no grouped row is produced for an empty group rather than fabricating one.
This split is load-bearing for the double-entry invariant. sum(debits) == sum(credits) must catch an entry that has 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 examples/double_entry_v0 package relies on exactly this behavior.
Conjunction between atoms uses ,. NAF uses the not keyword. Disjunction is not inline — write separate derive rules with the same head and arity; the union composes structurally. The same-head idiom is derive-only: a check head carries payload identity (its => Diagnostic { severity, code, message } report, its guard classification, its violation relation), so two checks sharing one head would cross-talk and are refused at elaboration (OE1328 DuplicateCheckHead) — rename one check, or express the disjunction through a shared derive predicate whose same-head clauses union, read by a single check. (Trait check members are unaffected: each impl’s monomorphized member rule is qualified by its impl target and keeps a distinct head, Trait atom.) The RHS of comparison atoms is a full expression (Expression grammar); inside that expression context, && / || / ! apply normally.
Payloadless enum constants. In value position, a path of the form EnumName::VariantName denotes the canonical enum value when the variant has no payload. This is distinct from the standalone rule atom sugar path :: Ident above, which means meta(path) == Ident. Payload-carrying variants require constructor semantics and are not constants by themselves.
Predicate resolution. The head of every predicate-call atom (path '(' … ')') must resolve to a declared predicate in scope — a rel, a concept used as a classification predicate, a derive / query head, a trait rule member (qualified Trait::member(...), or bare when exactly one provider is in lexical scope; subject to the coverage gate, Resolution), a pub fact / pub not_fact predicate, a fn, or a reflection intrinsic (iof, specializes, meta, extent, implements). check heads are not consumable: checks are observers only (Purity ladder) — their violation sets never populate the IDB — so a body atom resolving to a check head (module-level or trait check member) is refused with OE1329 CheckHeadConsumed; derive from the underlying body predicates instead (factor the violation pattern into a pub derive head read by both the check and the consuming rule). This applies to the predicate atoms of derive and check bodies and to both the body and head atoms of bridge rules (Bridge rules). An unresolved head is OE0223 RuleReferencesUnknownPredicate, the rule-body analogue of the pub fact obligation OE0220 (RFD 0004). A resolved atom is further checked for arity (OE0225) and — where both the argument’s type and the declared parameter type are concretely known — argument type (OE0226, sound under multiple classification: it fires only on provable disjointness). Entity-typed positions additionally carry a declared-sort discipline (OE1432 RuleAtomArgSortMismatch): a logic variable whose declared sort — its head-parameter annotation, a body sort guard x : T, or a unary concept-membership atom T(x) — is <:-incomparable with the position’s declared parameter concept is refused. This is deliberately not a disjointness proof (multiple classification can still make the join non-empty at runtime); it is the signature-level discipline: an atom whose declared sorts are incomparable can only succeed through a multi-classification the program never states, and in practice compiles to a silently-empty join. Both <: directions pass (a subtype in a supertype slot is subsumption; a supertype in a subtype slot narrows, exactly as an x : T occurrence-typing test narrows), and asserting the second sort in the body (x : T or T(x)) licenses the atom. A predicate declared in several clauses — one per admissible sort — licenses an argument comparable with any clause’s parameter concept at that position, and a clause leaving the position unannotated admits anything there. Sorts a variable merely picks up by occupying other predicates’ argument slots are classification evidence, not declarations, and never trigger the refusal. This is independent of the world assumption: the world assumption governs the truth value of instances of a declared predicate (under the CWA default an unasserted instance is false; under an #[world(open)] concept it is unknown/Can, World assumptions (CWA / OWA)) — it never admits an undeclared predicate name. To introduce a predicate that is intentionally empty until populated, declare it (pub rel P(...), or a bodiless pub derive P(...); head). ox check enforces this, and ox build refuses to emit an artifact for a program containing an unresolved predicate.
Relation-value application. A rule-body variable whose value is proven to range over a finite, exhaustive set of declared relations may itself stand in predicate position — r(p, c) where r is such a variable. The proof comes from the same body: a specializes(r, Parent) atom pins r to the Parent specialization family (relations are first-class citizens of the <: graph, so specializes ranges over declared relations exactly as it ranges over concepts); an equality with a relation literal (r == Home) pins a singleton; rel/arm reflection atoms pin by metarel or endpoint signature. The application lowers to a static dispatch over the proven candidate set — reading the dispatch at selector r equals applying the concrete relation r names.
If you only want the union of a relation family, do not use this — query the parent relation directly. Every subrelation’s tuples flow into each ancestor relation’s extent by declared subsumption (the RFD 0005 closure, materialized at seeding). A rule body naming
Localready ranges overHome,Work, and any member added later, with no dispatch and no domain proof. Relation-value application earns its cost only when the parent query would erase information you need — see the cases below.
pub type Person;
pub type City;
pub rel Loc(p: Person, c: City);
pub rel Home(p: Person, c: City) <: Loc;
pub rel Work(p: Person, c: City) <: Loc;
// UNION — no relation-value application. `Home` and `Work` tuples flow
// into `Loc` by subsumption, so the parent query already unions them.
pub derive anyLoc(p: Person, c: City) :- Loc(p, c);
The motivating case is provenance: you need to know which member of the family a tuple came from. The parent query collapses the family to one relation and erases that; a relation-valued selector keeps it, because specializes(kind, IncomeItem) binds kind to each member and kind(p, a) reads the tuple through it — so kind stands in the output as which relation each row was declared in, kept as a value.
pub type Person;
pub rel IncomeItem(mut p: Person, amount: Int);
pub rel Wages(mut p: Person, amount: Int) <: IncomeItem;
pub rel Interest(mut p: Person, amount: Int) <: IncomeItem;
pub fact Person(alice);
pub fact Person(bob);
pub fact Wages(alice, 50000);
pub fact Interest(alice, 1200);
pub fact Wages(bob, 30000);
// PROVENANCE — `kind` is a relation-valued output column.
pub derive report(p: Person, kind, a: Int) :-
specializes(kind, IncomeItem), kind(p, a);
// UNION — the plain parent query; same rows, no `kind` column.
pub derive total(p: Person, a: Int) :- IncomeItem(p, a);
Read what the selector is, row by row. Built as a standalone file and queried with ox derive, report produces (verbatim):
derive(report): 6 tuple(s)
(#i7582462787275748164, <anonymous>::IncomeItem, 1200)
(#i7582462787275748164, <anonymous>::IncomeItem, 50000)
(#i7582462787275748164, <anonymous>::Interest, 1200)
(#i7582462787275748164, <anonymous>::Wages, 50000)
(#i8648907253816210642, <anonymous>::IncomeItem, 30000)
(#i8648907253816210642, <anonymous>::Wages, 30000)
The first column is the Person identity, printed as its stable content-addressed handle — #i7582… is alice, #i8648… is bob. The middle column is the relation each row was declared in — Wages, Interest, IncomeItem are relation values, not strings (a standalone file elaborates its relations under <anonymous>, so a member prints <anonymous>::Wages; inside a multi-module package it is the module path, e.g. model::Wages). Every concrete row also appears under IncomeItem itself: specializes is reflexive (a relation is a kind of itself) and the RFD 0005 closure copies each child tuple up into the parent’s extent, so the family selector legitimately witnesses both the leaf and the ancestor. The plain parent query total produces the same underlying rows without the category column — (#i7582…, 1200), (#i7582…, 50000), (#i8648…, 30000) — and nothing downstream can recover which member each came from once the union has merged them. That recovery is the entire reason to apply a relation-valued selector instead of naming the parent.
For one row per concrete member — dropping the reflexive parent witnesses — exclude the parent in the body (kind != IncomeItem narrows the proven domain; the specializes atom still supplies it, so the finite-domain proof holds):
pub type Person;
pub rel IncomeItem(mut p: Person, amount: Int);
pub rel Wages(mut p: Person, amount: Int) <: IncomeItem;
pub rel Interest(mut p: Person, amount: Int) <: IncomeItem;
pub fact Person(alice);
pub fact Wages(alice, 50000);
pub fact Interest(alice, 1200);
// Leaf-only provenance: exclude the reflexive parent witness.
pub derive reportLeaf(p: Person, kind, a: Int) :-
specializes(kind, IncomeItem), kind != IncomeItem, kind(p, a);
reportLeaf drops the IncomeItem rows, leaving one (p, kind, a) per concrete member — (#i7582…, <anonymous>::Interest, 1200), (#i7582…, <anonymous>::Wages, 50000) (again #i7582… is alice).
The generalized family query — each row once, by construction
The kind != IncomeItem idiom above is a workaround for a specific over-return: the bare specializes(kind, IncomeItem), kind(p, a) reads every candidate’s closed extent, and the parent’s closed extent already holds a copy of every member’s tuples (the RFD 0005 closure), so each concrete row surfaces twice — once under its declaring member, once under the reflexive parent. The canonical family query — the via sugar — names the family in one atom and reads each member’s own extent, so every row is returned once, under its declaring relation, by construction — no exclusion idiom, no dedup.
via is the spelling you write; it desugars to a generalized specializes atom that is the same query written out:
Parent( args ) via k ≡ specializes( k( _… ) , Parent( args ) )
Each example below is shown as a pair — the via form and the equivalent generalized specializes form — with one shared meaning and one shared output; the two spellings produce byte-identical rows (a differential test pins this end to end). Every block parses on this branch; every output is ox build + ox derive, verbatim. The shared fixture:
pub type Person;
pub rel IncomeItem(mut p: Person, amount: Int);
pub rel Wages(mut p: Person, amount: Int) <: IncomeItem;
pub rel Interest(mut p: Person, amount: Int) <: IncomeItem;
pub fact Person(alice);
pub fact Person(bob);
pub fact Wages(alice, 50000);
pub fact Interest(alice, 1200);
pub fact Wages(bob, 30000);
// A tuple asserted DIRECTLY on the parent relation — it arrives under
// `kind = IncomeItem`, not under any leaf member.
pub fact IncomeItem(alice, 500);
// A second family: engagements between an audit firm and its clients.
pub type Firm;
pub type Company;
pub rel Engagement(firm: Firm, client: Company);
pub rel AuditEngagement(firm: Firm, client: Company) <: Engagement(firm = firm, client = client);
pub rel ConsultingEngagement(firm: Firm, client: Company) <: Engagement(firm = firm, client = client);
pub fact Firm(deloitte);
pub fact Firm(kpmg);
pub fact Company(acme);
pub fact Company(beta);
// deloitte both audits AND consults for acme; kpmg only audits beta.
pub fact AuditEngagement(deloitte, acme);
pub fact ConsultingEngagement(deloitte, acme);
pub fact AuditEngagement(kpmg, beta);
// ── Provenance: for each income row, also give me the relation it was
// declared in. `via` (canonical) and its generalized form are one query.
pub derive report_via(p: Person, kind, a: Int) :-
IncomeItem(p, a) via kind;
pub derive report_gen(p: Person, kind, a: Int) :-
specializes(kind(p, a), IncomeItem(p, a));
// ── Guard as meaning: the same rows, but exclude tuples asserted directly
// on the parent relation itself.
pub derive leaf_via(p: Person, kind, a: Int) :-
IncomeItem(p, a) via kind, kind != IncomeItem;
pub derive leaf_gen(p: Person, kind, a: Int) :-
specializes(kind(p, a), IncomeItem(p, a)), kind != IncomeItem;
// ── Multi-witness, witnesses DIFFER: the same firm serves the same client
// under two different engagement categories — a firm may not both audit
// and consult for one client (auditor independence).
pub derive independence_via(firm: Firm, client: Company) :-
Engagement(firm, client) via k1, Engagement(firm, client) via k2, k1 != k2;
pub derive independence_gen(firm: Firm, client: Company) :-
specializes(k1(firm, client), Engagement(firm, client)),
specializes(k2(firm, client), Engagement(firm, client)), k1 != k2;
// ── Multi-witness, witness JOINS: two different people with income of the
// SAME category — the witness `kind` is an ordinary join variable, shared
// across both atoms rather than compared unequal.
pub derive sameCategory_via(p1: Person, p2: Person, kind) :-
IncomeItem(p1, a1) via kind, IncomeItem(p2, a2) via kind, p1 != p2;
pub derive sameCategory_gen(p1: Person, p2: Person, kind) :-
specializes(kind(p1, a1), IncomeItem(p1, a1)),
specializes(kind(p2, a2), IncomeItem(p2, a2)), p1 != p2;
Provenance — for each income row, also give me the relation it was declared in. report_via and report_gen return the same rows: each family tuple exactly once — the three leaf rows and the directly-asserted parent row, under IncomeItem:
derive(report_via): 4 tuple(s) # report_gen is byte-identical
(#i7582462787275748164, <anonymous>::IncomeItem, 500)
(#i7582462787275748164, <anonymous>::Interest, 1200)
(#i7582462787275748164, <anonymous>::Wages, 50000)
(#i8648907253816210642, <anonymous>::Wages, 30000)
Guard as meaning — the same, but only rows declared on a leaf member. leaf_via and leaf_gen add kind != IncomeItem and both drop the directly-asserted parent row:
derive(leaf_via): 3 tuple(s) # leaf_gen is byte-identical
(#i7582462787275748164, <anonymous>::Interest, 1200)
(#i7582462787275748164, <anonymous>::Wages, 50000)
(#i8648907253816210642, <anonymous>::Wages, 30000)
Note what the guard now means. Under the old bare closed-extent read, kind != IncomeItem corrected an artifact — it discarded the reflexive duplicate rows the closure copied up into the parent. Under the own-extent read those duplicates never exist, so the same guard now states a choice: exclude facts asserted directly on the parent relation (they arrive with kind = IncomeItem) and keep only the leaf members. Under the old read the guard corrected an artifact; under the family query it states a meaning — report includes the IncomeItem, 500 row, leaf excludes it.
Multi-witness, witnesses differ — the same firm serves the same client under two DIFFERENT engagement categories — a firm may not both audit and consult for one client. independence_via and independence_gen bind two witnesses over the same firm/client pair and require them distinct; only deloitte/acme qualifies (both audit and consult), kpmg/beta has one engagement kind:
derive(independence_via): 1 tuple(s) # independence_gen is byte-identical
(#i7895632145248339177, #i1312706375208229289)
The two handles are deloitte and acme. The parent query cannot express this. Engagement(f, c), Engagement(f, c) is the same union self-joined on itself — it holds whenever the firm has any engagement with the client, and two audit engagements alone would satisfy it. Only the witness distinguishes the categories: k1 != k2 demands two rows from different declaring relations, which is exactly “audit and also consult”.
The independence rule needs a proper-member guard when the parent carries direct facts. via is reflexive: a tuple asserted directly on Engagement arrives in the parent’s own extent with witness = Engagement. The fixture above has no direct-parent engagements, so its output is exact — but on a schema where firms are also recorded through a bare Engagement fact, a firm/client with one real member engagement plus a direct parent fact carries two distinct witnesses (Engagement and the member), and the rule above spuriously flags it though only one real engagement category exists. Add the same “exclude direct parent assertions” guard the reportLeaf example uses — k1 != Engagement, k2 != Engagement — so only genuine member categories count:
pub derive independence_unguarded(firm: Firm, client: Company) :-
Engagement(firm, client) via k1, Engagement(firm, client) via k2, k1 != k2;
pub derive independence_guarded(firm: Firm, client: Company) :-
Engagement(firm, client) via k1, Engagement(firm, client) via k2,
k1 != k2, k1 != Engagement, k2 != Engagement;
With AuditEngagement(kpmg, beta) and a direct Engagement(kpmg, beta), the unguarded rule returns two rows — the genuine deloitte/acme and the spurious kpmg/beta — while the guarded rule returns only the genuine pair:
derive(independence_unguarded): 2 tuple(s)
derive(independence_guarded): 1 tuple(s)
Multi-witness, witness joins — two different people with income of the SAME category. Here the witness is an ordinary join variable — kind is shared across both atoms, not compared unequal. sameCategory_via and sameCategory_gen return the person pairs sharing a category (alice and bob both have Wages; each ordering appears once):
derive(sameCategory_via): 2 tuple(s) # sameCategory_gen is byte-identical
(#i7582462787275748164, #i8648907253816210642, <anonymous>::Wages)
(#i8648907253816210642, #i7582462787275748164, <anonymous>::Wages)
The identity handles: #i7582… is alice, #i8648… is bob; the witness column is the relation each row was declared in, kept as a value (a standalone file elaborates under <anonymous>).
The legacy bare form, for contrast. The pre-existing two-argument specializes(kind, IncomeItem), kind(p, a) reads every candidate’s closed extent — so the parent’s closure copies surface alongside the members, returning each leaf row twice and the direct parent tuples once, seven rows for this fixture:
derive(report_bare): 7 tuple(s)
(#i7582462787275748164, <anonymous>::IncomeItem, 500)
(#i7582462787275748164, <anonymous>::IncomeItem, 1200)
(#i7582462787275748164, <anonymous>::IncomeItem, 50000)
(#i7582462787275748164, <anonymous>::Interest, 1200)
(#i7582462787275748164, <anonymous>::Wages, 50000)
(#i8648907253816210642, <anonymous>::IncomeItem, 30000)
(#i8648907253816210642, <anonymous>::Wages, 30000)
This is the behavior the via / generalized own-extent form replaces: the three tiers are the bare two-argument form (legacy, closed extents, duplicates), the via sugar (canonical, own extents, each row once), and the generalized specializes form (via’s meaning, written out — identical rows).
Why the member frame is not a user spelling. The generalized specializes(k(memberArgs), Parent(parentArgs)) atom bounds k to the family, applies k in its own argument frame, and constrains the tuple in the parent’s frame; for an identity family the two frames have the same width, shared variables thread a column through both, and the correspondence is the declared subsumption mapping, never visual column position. The parent frame is information-complete — the mapping rules forbid dropped child ends, so every value a member carries is available in the parent frame. A member frame therefore binds nothing the parent frame does not already bind, which is why via plus the parent atom expresses every practical family query and the generalized form appears only as via’s desugar. The member frame becomes independently expressive only if subsumption mappings that drop a child’s ends are ever admitted — an open design question — at which point the general form is already specified and via remains its all-wildcard special case.
Rule of thumb. The parent atom alone is the union; + via k is the union plus which one; the generalized specializes(k(...), Parent(...)) form is the meaning of via, written out.
The via sugar reaches mapped members too — a member joined by a non-identity (argument-mapped) edge, whether a rename, a constant pin, or a subsort cast. A via family query reads such a member’s own extent and presents the rows in the parent’s frame, with the pinned / renamed / cast ends filled by the mapping and the answering member bound to the witness. The generalized specializes form with an explicit member frame still refuses (OE1416) for a non-identity member: that form ties the member frame positionally to the member’s own ends, which is sound only for an equal-arity identity member — it points the modeler at via, which is frame-agnostic. The witness member position must be a plain variable (OE1415 otherwise); the member frame’s width must match the parent’s arity (OE1414) and the parent frame’s width must match the parent relation’s declared arity (OE1418). A via k clause on an explicit specializes(...) atom is a misuse — via is itself the sugar for a specializes query — and refuses with OE1419; write one spelling or the other, not both.
A mapped family reads cleanly through via. With WagesUSA(p, a) <: IncomeItem(p, a, c = USD) (a constant pin), an incomeSource view over the parent family binds each row’s currency through the pin and its witness through the edge — no member-frame spelling, no dispatch:
use std::core::{type, rel};
pub type Person;
pub type Currency;
pub const USD: Currency;
pub rel IncomeItem(mut p: Person, mut amount: Int, mut c: Currency);
pub rel WagesUSA(mut p: Person, mut amount: Int) <: IncomeItem(p = p, amount = amount, c = USD);
pub fact Person(alice);
pub fact Currency(USD);
pub fact WagesUSA(alice, 500);
// Reads the mapped member through φ: `c` is filled by the pin, `kind` by the edge.
pub derive incomeSource(p: Person, a: Int, c: Currency, kind) :- IncomeItem(p, a, c) via kind;
incomeSource(alice, 500, USD, WagesUSA)
The USD came from the pin (the member never stores it), and kind is WagesUSA — the member that answered.
Don’t-care ends in a family query — the anonymous wildcard _. A parent frame does not have to bind every end. When you only want which member answered — not the amount, not the currency — write _ for each end you don’t care about. This is the natural query for provenance: “who has income, and from which kind of source?” ignores the figures entirely. The wildcard is a genuine don’t-care in the family-query frame, one anonymous slot per _, so an all-_-but-one frame is not mistaken for a lower-arity query.
The fixture carries a pinned member (WagesUSA, currency pinned to USD) and an identity member (ForeignIncome, currency passed through):
use std::core::{type, rel};
pub type Person;
pub type Currency;
pub const USD: Currency;
pub const EUR: Currency;
pub rel IncomeItem(mut p: Person, mut amount: Int, mut c: Currency);
pub rel WagesUSA(mut p: Person, mut amount: Int) <: IncomeItem(p = p, amount = amount, c = USD);
pub rel ForeignIncome(mut p: Person, mut amount: Int, mut c: Currency) <: IncomeItem(p = p, amount = amount, c = c);
pub fact Person(alice);
pub fact Person(bob);
pub fact Currency(USD);
pub fact Currency(EUR);
pub fact WagesUSA(alice, 500);
pub fact ForeignIncome(bob, 300, EUR);
// Who has income, and from which kind? Amount and currency are don't-cares.
pub derive whoHasIncome(p, kind) :- IncomeItem(p, _, _) via kind;
// Keep the amount, still ignore the (pinned or passed-through) currency.
pub derive amountAndKind(p, a, kind) :- IncomeItem(p, a, _) via kind;
whoHasIncome drops both figure ends and returns one row per person with the answering member — alice earns WagesUSA, bob earns ForeignIncome:
derive(whoHasIncome): 2 tuple(s)
(#i7582462787275748164, <anonymous>::WagesUSA)
(#i8648907253816210642, <anonymous>::ForeignIncome)
amountAndKind keeps the amount and still ignores the currency — useful precisely because the currency is pinned on WagesUSA (the member never stores it) and passed through on ForeignIncome; a single _ reads across both shapes without naming a column that one member fills by a pin and the other by an end:
derive(amountAndKind): 2 tuple(s)
(#i7582462787275748164, 500, <anonymous>::WagesUSA)
(#i8648907253816210642, 300, <anonymous>::ForeignIncome)
#i7582… is alice, #i8648… is bob. The wildcard behaves the same in a via family query as in a plain parent atom: it matches any value and binds nothing.
A subsumption edge may not drop a child end — use a derive view instead. A mapping must reference every declared child end (a rename or a cast source; a child end may feed several parent ends — a diagonal — but never none). Dropping one is refused at the declaration with OE1421:
use std::core::{type, rel};
pub type Person;
pub type Currency;
pub type Text;
pub const USD: Currency;
pub rel IncomeItem(mut p: Person, mut amount: Int, mut c: Currency);
pub rel Bad(mut p: Person, mut amount: Int, mut note: Text) <: IncomeItem(p = p, amount = amount, c = USD);
// ^ OE1421 — child end `note` is referenced by no filler
OE1421: subsumption of `Bad` onto `IncomeItem` drops child end `note` — dropping an end inside a subsumption edge would merge distinct `Bad` facts (differing only at `note`) into one `IncomeItem` row, so it is refused. Fix, in order: (1) if `Bad` genuinely belongs to the `IncomeItem` family, reference every child end in the filler list (a rename or a cast source; a diagonal may reference one child end at several parent ends); (2) if a lossy projection is intended, write a derive rule instead — a projection VIEW is not family membership, e.g. `pub derive view(p, a) :- Bad(p, a, _);`
Why every child end must be referenced (φ-injectivity). The mapping φ translates each child tuple to a parent image. A dropped end makes φ non-injective: two child tuples that differ only at the dropped end collapse to the same parent image with no declared meaning for the collapse, and — because a relation’s extent is a set — merge into one parent row that any aggregate over the family then counts once. Worse, the §D4 withdrawal cascade relies on the child identity the dropped end carried — a distinction the collapse erases at the parent image, so the cascade can no longer tell the two child tuples apart. Requiring every child end (each fed to at least one parent end) keeps φ injective on the distinct-child-tuple part of the domain, which is exactly what family membership must preserve. A diagonal is still injective: reusing one child end at several parent ends duplicates a value into the image but discards nothing.
The derive view is the honest spelling of a lossy projection. The OE1421 remedy — pub derive view(p, a) :- Bad(p, a, _); — is a rule, not a family edge, so it is explicit that it forgets note and is not Bad’s membership in a family. It also makes the merge visible where it happens: two Bad facts that agree on (p, a) and differ only in note project to one view row.
use std::core::{type, rel};
pub type Person;
pub type Text;
pub rel Bad(mut p: Person, mut amount: Int, mut note: Text);
pub fact Person(alice);
pub fact Text(memoA);
pub fact Text(memoB);
pub fact Bad(alice, 500, memoA);
pub fact Bad(alice, 500, memoB);
pub derive view(p: Person, a: Int) :- Bad(p, a, _);
view(alice, 500)
One row, not two — the two notes are projected away by the derive, exactly the merge a dropped-end edge would have performed silently. A dedicated projection marker on a subsumption edge (a surface way to declare “this edge intentionally forgets an end”) is a reserved design under consideration in the mapped-relation-subsumption design record and is not yet a surface form; until it lands, the derive view is the spelling.
Multiplicity is a schema choice, and aggregates read it. The merge above is the same set-membership rule an aggregate obeys: whether a family sum double-counts two payments is decided by the schema, not the query. A relation Payment(mut p: Person, mut amount: Int) with two facts Payment(alice, 500) twice holds one proposition and sums to 500; add a distinguishing source end — Payment(mut p: Person, mut amount: Int, mut src: Source) with s1/s2 — and the two payments are distinct rows summing to 1000. This is the same trap the relations chapter draws out for family images; the fix is identical (keep a distinguishing end, do not pin it away). Grouping such an aggregate by the family witness is not yet expressible: a via clause is not admitted inside an aggregate comprehension, and threading the witness through an intermediate derive trips the finite-domain proof on the selector column — so per-witness totals are computed today by aggregating each leaf member directly, not by grouping a parent-family sum on kind.
Two clarifications on the selector. kind is an ordinary rule variable, not a keyword — name it anything (r, src, which). And it takes no type annotation: like every rule variable its type is inferred from where it stands — appearing as the family member in specializes(kind, IncomeItem) constrains it to that family, the compiler establishes the complete closed set of member relations at build time (the finite-domain proof; a selector it cannot pin to a finite exhaustive set is refused with OE1386, below), and refuses the rule if it cannot.
Two further cases need the selector in body position, not the head. Per-member differentiated logic — the same selector applied twice, or joined against a per-member table, so the rule branches on which relation matched (specializes(kind, Loc), kind(p, c), priority(kind, n)). Families not modeled by subsumption — members collected by a rel/arm/meta reflection atom (a shared metarel or endpoint signature) rather than a <: edge, where no single parent relation exists to query. A pinned singleton (r == Home) reads exactly one relation’s tuples and is the degenerate case of the same dispatch:
pub type Person;
pub type City;
pub rel Home(p: Person, c: City);
// Singleton: equality with the relation literal `Home` pins `r`, so
// the application reads exactly `Home`'s tuples.
pub derive homed(p: Person, c: City) :- r == Home, r(p, c);
The candidate set must be statically proven — Argon refuses rather than guess a domain or evaluate an open dispatch. A selector bound only by a constraint that proves no finite exhaustive set (an inequality binds r as a value variable but enumerates nothing) is refused with OE1386:
pub type Person;
pub type City;
pub rel Loc(p: Person, c: City);
// `r != Loc` binds `r` but proves no finite exhaustive relation domain.
pub derive anywhere(p: Person, c: City) :- r != Loc, r(p, c);
A value proven not to denote a relation (pinned to an Int, a String, a struct value, …) applied in predicate position is refused with OE1387:
pub type Person;
pub type City;
pub rel Loc(p: Person, c: City);
// `r` is pinned to the integer 3 — not a relation value.
pub derive anywhere(p: Person, c: City) :- r == 3, r(p, c);
A negated relation-value application must be safe: the selector and every tuple argument must be positively range-restricted outside the negation. An argument (or the selector) bound only inside the not is refused with OE1391:
pub type Person;
pub type City;
pub rel Loc(p: Person, c: City);
pub rel Home(p: Person, c: City) <: Loc;
// `c` appears ONLY inside the negation — the negated dispatch is unsafe.
pub derive unhoused(p: Person) :- p : Person, specializes(r, Loc), not r(p, c);
The remaining refusals in the family are structural, and each guards against a silent misderivation the dispatch would otherwise produce:
- An application whose argument count matches no candidate’s arity is
OE1388. With the wrong tuple width there is no coherent pairing of arguments to a candidate’s columns, so a dispatch built anyway would read misaligned columns (or an always-empty relation) — a wrong answer, not an error. - An argument category-incompatible with a candidate’s declared endpoint is
OE1389(e.g. a text literal against anIntcolumn). Such a candidate can never match the tuple; dispatching over it would either drop rows the modeler expected or force a type-incoherent join, again silently. - A family containing a relation not lexically accessible from the applying module is
OE1390. A dispatch that silently dropped the inaccessible candidate would change the rule’s meaning based on visibility — the family it reads would depend on who is looking, so the whole application is refused rather than narrowed. - A candidate whose signature metadata (arity, endpoints, visibility) is unavailable to the build — a relation carried by an artifact too old to record it — is
OE1393. The dispatch cannot be proven sound without that metadata, so Argon refuses rather than guess it (App. C).
pub fact targets a base predicate, never a derived one. A derive / query head is a valid rule-body atom (it is in the resolution set above), but it is not a valid pub fact target. pub fact asserts into an extensional relation (a concept-as-classification or a pub rel); a derived predicate’s extent is intensional — computed by rule derivation. A pub fact P(...) whose P is a pub derive / pub query head is refused with OE0239 FactReferencesDerivedPredicate: the asserted seed tuple would key a different relation node than the rule head reads, so it would silently drop out of the rule’s fixpoint — the derive would evaluate to a result that omits the asserted tuples (no error, wrong answer). To give a derived predicate ground tuples that participate in its derivation, use one of two sanctioned forms, and OE0239’s help names both:
- A bodiless
pub derive P(args);clause over concrete arguments (derive). The seed tuple is itself aderiveclause onP, so it sharesP’s relation node and participates directly inP’s fixpoint — the simplest way to seedPitself. - A base relation the rule reads — declare
pub rel Base(...), seed it withpub fact Base(...), and add a clausepub derive P(...) :- Base(...). Use this when the seed tuples are themselves a reusable extensional predicate.
Either keeps P a single-origin intensional head (the same rule Build pipeline and .oxbin enforces for a foreign-placed-vs-derived relation, OE1246). (A pub fact over a check head is the ordinary OE0220 — a check head is observer-only, not a predicate at all.)
Build loud-gate. More broadly, ox build refuses (writing no .oxbin) any derive / query rule the engine does not evaluate — an unsupported quantifier shape, a nested or non-count-class aggregate, not <aggregate>, an unsupported type-test or meta-eq (OE1311–OE1315). Argon refuses rather than emit an artifact that would silently mis-derive; see Build pipeline and .oxbin.
Modal atoms (box(...), diamond(...)) carry Kripke-frame semantics over the standpoint and classification frames described in Modal operators. The elaborator statically discharges the common case (type-classification atoms whose target is introduced by a fixed metatype — membership constant, RFD 0027 D6) and routes the remainder to a modal reasoner over std::kripke. Modal atoms are admitted only at tier:modal; lower tiers reject them.
Restricted universals (forall v: T where Body, Head). In the where body the last atom is the consequent (Head) and all preceding atoms are the domain restriction (Body). The quantifier holds iff every domain element also satisfies the consequent — it lowers to the count-equality count{ v : Body, Head } == count{ v : Body } (the domain-and-consequent count equals the domain count). The type annotation T is the static sort of v; the runtime domain comes from the where Body, so the Body must restrict v with a membership or predicate atom (e.g. member(g, v) or v in g.items) — Body, not T, bounds the count. An empty domain (count{ v : Body } == 0) makes the universal vacuously true (0 == 0), deriving the head — classical restricted-∀ semantics. The encoding needs at least a domain atom and a consequent (≥2 where atoms); the paren restriction form forall(path, T), the exists-binder form, and a single-atom where refuse with OE1315 (App. C).
Allen interval relations (before, meets, overlaps, during, starts, finishes, and their reciprocals) are not substrate operators. They are provided by the std::allen library (RFD 0024), defined over the Date / Duration value layer (Temporal substrate) — ordinary predicates over interval endpoints, not parser-level syntax.
Three atom shapes in the grammar above parse-refuse rather than silently mis-derive:
- Role closure
field-path ('+'|'*') '(' … ')'(e.g.p.knows+(q: Person)). The base role-invocation formfield-path '(' Ident (':' type-expr)? ')'is accepted; the transitive (+) / reflexive-transitive (*) closure suffix refuses withOE0001. Express transitive reachability through a recursivederivehead. - Axis sugar
path '::' Ident(x :: T, i.e.meta(x) == T). Write themeta(x) == Tcomparison atom directly; the::rule-atom shorthand refuses. (In value positionEnumName::Variantis unaffected — see the payloadless-enum-constant note above.) - Range membership
field-path 'not'? 'in' <range>(p.v in 1..10). The set/list RHS ofinis accepted; alo..hirange RHS refuses (OE0001on..). Write the two-sided comparisonp.v >= 1, p.v <= 10instead.
Temporal rule atoms
Argon’s bitemporal substrate (Temporal substrate) admits metric temporal operators in rule bodies — the DatalogMTL fragment with stratified negation over the integer timeline.
Any atom may be qualified by a valid-time point or interval: atom at t (a single VT point), atom during [t1, t2] (a closed VT interval), atom since t (the open interval [t, ∞]).
Six prefix metric operators address past and future. Their interval bounds are durations (0, N with a unit ns…y, or inf/∞):
box_minus [a, b] (φ) // φ held at every past point in [a, b]
diamond_minus [a, b] (φ) // φ held at some past point in [a, b]
box_plus [a, b] (φ) // φ holds at every future point in [a, b]
diamond_plus [a, b] (φ) // φ holds at some future point in [a, b]
since [a, b] (φ, ψ) // φ has held since ψ within [a, b]
until [a, b] (φ, ψ) // φ holds until ψ within [a, b]
Two shortcuts expand to [0, ∞] intervals: ever atom (some past or future point; needs tier: expressive for the disjunction) and always atom (every point; tier: recursive).
Side-by-side modal (box/diamond) and metric temporal atoms in one body compose additively. Nesting one family inside the other is refused at tier: recursive and routed to tier: fol — see Tier ladder for the decidability rule (OE0712).
query
query-decl ::= attribute* 'pub'? 'query' Ident '(' param-list ')' '->' TypeExpr
('across' '[' standpoint (',' …)* ']')?
'{' query-body '}'
query-body ::= ('with' Ident '=' query-body ';')*
'select' projection
'from' rule-body // Rule-atom grammar
('optional' 'from' rule-body)*
('where' rule-body)? // additional atoms
('group' 'by' expr ('having' expr)?)?
('order' 'by' expr ('asc' | 'desc')?)?
('limit' Nat ('offset' Nat)?)?
projection ::= expr | '{' struct-init-list '}' | 'path' Ident | aggregate
role-step ::= Ident quantifier? ('(' role-binders ')')? ('as' Ident)? mode-spec?
quantifier ::= '+' | '*' | '{' expr (',' expr)? '}' // expr of type Nat
mode-spec ::= 'mode' ('walk' | 'trail' | 'acyclic' | 'simple')
| 'shortest' | 'all-shortest' | 'any' | 'k-shortest' Nat
Role-step path-traversal chains (a.role+(b), c.ParentOf{1, n}(p: Person)) are admitted as predicate-call atoms in the rule-atom grammar (Rule-atom grammar) and are not separately defined here.
The evaluated query-body is select … from … (where …)?. Every other form
parse-refuses loudly with a feature-named code — never a generic
OE0001, never silently ignored: the result-shaping clauses (group by … having, order by … asc|desc, limit … offset) with OE0007; the
with CTE clause with OE1344; the graph-/path-traversal projections
(select shortest path …, select path …, … as <name> mode …) with
OE1345; optional from with OE1346; and the result-table set
operators (union / union all / intersect / except) with OE1347.
pub query active_leases_for(t: Person) -> [Lease] {
select l from l: Lease, l.tenant == t where is_active(l)
}
pub query ancestors_within(p: Person, n: Nat) -> Set<Person> {
select a from p.ParentOf{1, n}(a: Person)
}
pub query shortest_route(a: City, b: City) -> Path<City, Road> {
select shortest path r from a.road+(b) as r
}
pub query avg_age_by_dept() -> Map<Department, Real> {
select { dept: avg(p.age) }
from p: Person, p.works_in(dept: Department)
group by dept
}
pub query rich_friends_of(p: Person) -> Set<Person> {
with rich = select x from x: Person where x.net_worth > 1_000_000;
select f from p.knows(f: Person) where f in rich
}
pub query agreed_across(p: Person) -> Truth4Of<Bool>
across [LegalGround, MedicalGround]
{
select consents(p)
}
Subquery forms. Brace-bracketed inner queries used as expressions:
subquery ::= subquery-kind '{' rule-body '}' // Rule-atom grammar; no `from` keyword
subquery-kind ::= 'collect' | 'set' | 'one' | 'count' | 'exists'
The subquery body is a bare rule-atom list — there is no from keyword inside the braces (that prefix belongs to the top-level query-body only; an inner from parse-refuses with OE0001).
| Form | Returns | Notes |
|---|---|---|
collect { … } | List<T> | Projection-carrying; supports order/limit. |
set { … } | Set<T> | Projection-carrying; deduplicated. |
one { … } | Option<T> | Projection-carrying; None if no match. |
count { … } | Nat | No projection (counts matched tuples). |
exists { … } | Bool | No projection (K3 fail-closed; false on unknown). |
The cardinality-fold kinds count and exists evaluate over the bare atom
list above (count { p: Person }, exists { p: Person }). The
projection-carrying kinds collect / set / one parse but refuse at
ox check / ox build (OE1312); they never silently lower to a
cardinality fold. Use a derive head plus a top-level query projection.
Set operations on result tables: union, union all, intersect, except.
Subquery aggregates (admitted as aggregate projections inside subquery bodies and as the subquery-form <aggregate> { atoms }): count, count distinct, sum, avg, min, max, collect, set_collect, string_join, percentile. The expression-level comprehension form sum(expr for x in coll where pred) (Expression grammar) admits a subset (sum, count, min, max, avg) for non-subquery contexts.
Default lattice context: K3. With across [...]: FDE.
mutate
mutate-decl ::= attribute* 'pub'? 'mutate' Ident '(' param-list ')' ('->' TypeExpr)?
'{' mutate-body '}'
mutate-body ::= ('require' '{' expr (',' …)* '}')?
stmt*
('return' expr ';')?
stmt ::= 'let' Ident (':' TypeExpr)? '=' expr ';'
| 'match' expr '{' arm (',' …)* '}'
| insert-stmt | update-stmt | delete-stmt | upsert-stmt | amend-stmt
| 'detach' 'delete' expr ';' // ‡ refused, OE1353
| emit-stmt
| 'for' Ident 'in' expr '{' stmt* '}'
| 'if' expr '{' stmt* '}' ('else' '{' stmt* '}')?
| expr ';'
insert-stmt ::= 'insert' TypeExpr '{' field-init-list '}' ';' // typed-literal insert
| 'insert' TypeExpr '{' '..' expr (',' field-init-list)? '}' ';' // spread: seed from a struct value
| 'insert' Ident ':' TypeExpr '{' field-init-list '}' ';' // ‡ named typed-literal — OE0001
| 'insert' Ident 'into' expr ';' // insert binding into collection
| 'insert' predicate-call '{' field-init-list '}' ';' // relation insert with body
| 'insert' predicate-call ';' // relation insert, no body
update-stmt ::= 'update' (Ident | pattern) 'set' '{' field-assign (',' …)* '}' ('where' expr)? ';'
field-assign ::= Ident ('=' | '+=' | '-=') expr
delete-stmt ::= 'delete' predicate-call ';' // delete iof(…) / delete Rel(…)
| 'delete' (Ident | pattern) ('where' expr)? ';' // ‡ entity / bulk delete — OE0001
upsert-stmt ::= 'upsert' pattern ('as' Ident)? upsert-clause+ ';' // ‡ refused, OE1352
upsert-clause ::= 'on' 'insert' '{' field-assign (',' …)* '}'
| 'on' 'update' '{' field-assign (',' …)* '}'
emit-stmt ::= 'emit' Ident '{' expr '}' ';'
amend-stmt ::= 'amend' predicate-call ';' // belief-time withdrawal (RFD 0076)
| 'amend' predicate-call '=>' predicate-call ';' // atomic withdraw + correct
pub mutate sign_lease(t: Person, p: Property, rent: Money, term_days: Nat) -> Lease {
require { rent > 0, term_days > 0 }
let l = insert Lease(t, p) {
monthly_rent: rent,
start: today(),
end: today() + term_days.days,
status: Pending,
};
emit AuditLog { LeaseSigned { lease: l, at: now() } };
return l;
}
pub mutate hire_or_raise(p: Person, o: Organization, salary: Money) {
upsert p.works_at(o) as emp
on insert { emp.start_date = now(), emp.salary = salary }
on update { emp.salary = salary };
}
pub mutate rebrand(old: String, new: String) {
update c: Company set { name = new } where c.name == old;
}
An update of a bound target writes its mut fields directly. The
('where' expr)? filter (and the bulk/pattern target forms it implies, as
in rebrand above) is refused at ox check / ox build (OE1318), never
silently dropped.
The grammar shapes marked ‡ are loud refusals, not silent no-ops. Named
typed-literal insert (insert l: Lease { … }; use let l = insert Lease { … } instead) and entity/bulk delete of a bound target or pattern with an
optional where refuse with OE0001 — only delete predicate-call (delete iof(…) / delete Rel(…)) is admitted. detach delete refuses with OE1353
and upsert with OE1352.
delete Rel(…) and insert Rel(…) additionally pass the relation-end
mutability gates (RFD 0076, Relations):
a matched delete on a relation with any effectively-immutable end refuses at
the write path with OE1402 (an unmatched delete stays an idempotent no-op),
and an insert adding a new value at an immutable end whose endpoint fiber an
earlier transaction already initialized refuses with OE1403 — checked before
the OE1341 cardinality cap, atomically, so a refused body commits nothing.
The gates read the closed extent: an insert into a subrelation is gated
against every superrelation’s fiber too (still OE1403), and a premise write
whose rule-derived consequences would vary an immutable-ended declared
relation’s extent — retract a derived binding whose dependent context
survives, or grow an initialized derived fiber — refuses the whole body with
OE1397, even though no statement names that relation. The initialization
set itself remains governed by the declared cardinality: on an immutable end
the fiber’s value set freezes at its first asserting transaction, so a
declared minimum of two or more is decidable exactly there — a body that
initializes such a fiber with fewer distinct values refuses with OE1398
(the frozen set could never grow to complete it), whichever route
initializes it: a direct insert, a subrelation insert, a rule-derived
initialization, or a static fact set at build. A mutable end’s minimum
keeps the deferred OW1342 posture.
The relation-end mutability write gates — the ones that admit or refuse a
delete R(…) / insert R(…) against an immutable end, and the individual
retraction that would strand an immutable-ended binding (OE1402–OE1404) —
are enforced by oxc-runtime in its end_mutability module; see
relations for the rule they enforce.
The rebrand example assumes name is declared mut on Company. Per struct and enum — language built-ins (data declarations), fields are immutable post-construction unless explicitly marked mut:
pub type Company {
#[intrinsic] founded: Date, // construction-required; not updatable
mut name: String, // updatable via `update`-stmt
}
update-stmt admits writes only to mut fields. A field-assign targeting a non-mut field is rejected with OE0820 UpdateImmutableField. The elaborator validates each field-assign against the entity’s field-decl mut flag; rejection surfaces at build time, not at mutation invocation.
Insert from a pre-built value — the ..v spread
Concept construction is fused to insertion: there is no bare construction
expression of concept type. Constructor machinery therefore lives in fns
over struct values (struct and enum — language built-ins (data
declarations)) — and the spread form lets
such a value feed an insert without re-spelling every field:
pub type Person { name: String, age: Int }
pub struct PersonSeed { name: String, age: Int }
pub fn seed() -> PersonSeed = PersonSeed { name: "Ada", age: 36 };
pub mutate admit() -> Person {
let v = seed();
return insert Person { ..v, age: 37 }; // seed's fields, age overridden
}
The base v must evaluate to a struct value; its fields seed the
field-init set by exact field name against the target’s declared fields
(the whole <: chain — an inherited field is a match), and the explicit
field: value initializers override same-named seeds, in either textual
order. The merge happens before the construction gates, so the spread is
field-init sugar and nothing downstream can tell the forms apart: required-
field completeness (OE0207), primitive-refinement invariants (OE0668),
abstract-type refusal (OE0233), fixed classification (OE0234), the
value/individual boundary (OE0248), and declared-field validation all fire
over the merged set exactly as the longhand spelling, and identity is still
minted only by the event log at insert. There is no first-class unpersisted
individual: the struct value is pure data (no identity) until the
insert constructs the concept individual from it.
Field-name matching is total and loud, never lossy:
- A required target field that neither the base nor an explicit
initializer supplies refuses with
OE0207, same as omitting it longhand. - An optional target field (
T?) the base does not carry simply stays absent — no phantom write. - A source field with no declared target field (on the concept or any
<:ancestor) refuses withOE0261 InsertSpreadFieldUndeclared: data the source value carries never silently vanishes on insert. - A base that is not a struct value — a scalar, an individual, an enum
constant — refuses with
OE0260 InsertSpreadSourceNotStruct. - More than one spread in one init list refuses at
ox check/ox buildwithOE0263 StructLitMultipleSpreads(a second spread has no defined merge order); the same refusal governs the value struct literal’s functional-update spread. Combine the sources into one struct value first, then spread that.
The base is an ordinary body value position: a deductive read there
refuses with OE1371 and an unbound name with OE1383, exactly as a field
initializer would. mut on a target field is orthogonal — construction may
set mut and non-mut fields alike, spread-fed or explicit.
The relation-insert-with-body form (insert P(a, b) { … }) does not take a
spread while that host form itself refuses (OE0001); the field-init merge
extends to it when the body form lands.
Field updates lower to append-only event pairs on the underlying property axiom (Storage layer): a retract event for the prior value and an assert event for the new. The proposition’s logical identity is the (entity_id, property_id) pair; the value is what changes. Bitemporal queries reconstruct prior values per Temporal substrate.
Mutations are transactional within the body; the kernel may reject mutations that violate refinement, consistency policy, or stratification — rejection surfaces to the caller as Result<T, Diagnostic> from the host runtime.
Dynamic reclassification — insert iof / delete iof. Because iof is a first-class predicate (Reflection), mutate bodies can dynamically classify and declassify entities by inserting or deleting iof tuples through the existing insert predicate-call / delete predicate-call forms:
pub mutate enrol(p: Person) {
insert iof(p, Student); // p is now a Student
}
pub mutate expel(s: Student) {
delete iof(s, Student); // s is no longer a Student
}
The single-type insert iof(x, T) / delete iof(x, T) forms above classify and declassify an entity, subject to the gates below. Inserting an iof tuple over a relation concept — insert iof((p, into), EnrolledAt) for a named relation EnrolledAt — refuses: the tuple-argument form parse-refuses with OE0001 rather than silently doing nothing.
The substrate enforces two constraints on insert iof(x, T):
- Refinement gate (enforced). If
Tcarries a defined refinementiff { … }(Refinement) — for example,pub type ExtinctSpecies <: BirdSpecies iff { self.numberOfLivingInstances == 0 }— the predicate is the substrate’s source of truth for membership: the modeler updates the underlying state (here,numberOfLivingInstances) and the substrate derives the iof classification automatically, so explicitinsert iof(x, ExtinctSpecies)emitsOE0211 IofInsertOnDefinedand the mutation is rejected. If insteadTcarries a primitive refinementwhere { … }, membership is conferred by assertion, soinsert iof(x, T)is permitted — but the predicate is a necessary invariant: anxthat positively violates it (definitefalse, World assumptions (CWA / OWA)) is rejected withOE0668 RefinementInvariantViolated. The same invariant is checked when a primitive-wheremember is created by construction (insert T { … }) or when anupdatewrites a field the predicate reads. - Modifier gates (enforced — RFD 0027 D6). Re-classification is governed by the ontology-neutral
fixedmodifier on the target’s introducing metatype (metatype), never by any axis name:insert iof(x, T)/delete iof(x, T)whereTis fixed-introduced is refused withOE0234 FixedReclassification— classification under such types is decided at construction (insert <T> { … }remains legal; construction is not re-classification). Admission is the absence offixed: dynamic classification is the default, and a vocabulary opts its rigid metatypes into the restriction explicitly (pub fixed metatype kind = { … };) — an axis assignment likerigidity::anti_rigidis inert user vocabulary and grants nothing by itself. Likewiseinsert iof(x, T)against anabstracttype — a direct instance — is refused withOE0233 AbstractTypeConstruct; non-abstract subtypes are unaffected. Both gates fire atox check/ox buildwhere the target is statically known and at the runtime write path otherwise, rejecting the whole mutation atomically. The membership-constancy theorem (Argon.Runtime.ModifierGates.runMutation_fixed_iof_constant) is what grounds static modal discharge over fixed-introduced types (Modal operators). - Compatibility gate.
x’s existing classification chain must include some supertype thatTspecializes (e.g.Student <: Personrequiresx: Person).
After insert iof(x, T), the entity x is classified under both its prior types and T. After delete iof(x, T), x is no longer classified under T but retains all other classifications. The substrate maintains a single global iof relation; insert/delete operate transactionally.
Bitemporal mutation forms. Per the temporal substrate (Temporal substrate, Effective-dating: valid time and the two as_of axes), an insert may carry an explicit valid-time at <date> qualifier — the assertion’s valid time begins on that civil day (RP-004 mutate). This form executes, on every insert arm: the write threads the date onto the event’s bitemporal extent, and an as_of <#date#> query reads it back. A typed-literal insert (insert T { … } at <date>, with or without the ..v spread) stamps the constructed individual’s membership and every field assertion with the same valid time, and an op-level qualifier wins over the call-level default valid-time an embedder supplies for the whole mutation (explicit intent over the per-call backdate).
pub type T;
pub type Evt { tag: String? }
pub mutate enact(x: T, effective: Date) {
insert iof(x, T); // [VT: now → ∞] [TT: now → ∞] — atemporal default
insert iof(x, T) at effective; // [VT: effective → ∞] — effective-dated
insert Evt { tag: "x" } at effective; // membership + fields, all [VT: effective → ∞]
}
The window form during [t1, t2] and a valid-time-qualified delete (bitemporal retraction — closing a valid-time interval rather than opening one) refuse loudly (OE1330) rather than silently applying at all valid times; the since <date> open-interval form executes on inserts, equivalently to at (an open interval from the anchor):
pub type T;
pub mutate retro(x: T) {
insert iof(x, T) during #2020-09-01#; // window VT
}
Explicit erasure — forget. For compliance scenarios (GDPR right-to-erasure, sensitive-data redaction), forget removes the underlying tuple including its bitemporal history — removed from every served read and every write gate; the durable-replay journal retains the erased bytes closed (see the gate-and-read surface below):
forget x; // bound individual; capability-gated
Only the bound-individual form forget <expr> is admitted; the forget iof(x, T) and bulk forget … where … shapes refuse with OE0001.
forget is capability-gated at the source level: a mutate body containing forget refuses to build (OE0730 ForgetWithoutCapability) unless the enclosing mutate declaration grants #[allow_forget]. Cascading derived facts are revised via DRed overdelete.
Erasure is measured at the gate-and-read surface: after forget x, no
assert-polarity event on any plane or polarity introduces or references x
(relation arguments are walked into nested collection values), and x’s
pre-existing closing retractions are erased with their asserts. 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 retains the erased
history closed rather than expunging bytes. Neither replay artifact is
served by a read or consulted by a write gate.
The capability does not exempt forget from the relation-end mutability
gates (RFD 0076): erasing an individual bound at an
effectively-immutable relation end while that end’s dependent context (the
individuals at every other position) survives the transaction refuses with
OE1404 — erasure may not stand in for the refused cascade. The dependent
may cease in the same transaction through either channel (a logical
retraction or another forget), and the gate reads every standpoint plane,
exactly the planes the erasure sweeps. Erasing the dependent individual
itself is the legal direction and takes its incident bindings with it.
Reasserting a tuple after retraction produces distinct VT intervals on the underlying store — the substrate does not coalesce on adjacent boundaries. Historical retract/reassert is queryable via audit projections.
Individual retraction — retract. retract ends one or more individual
lifetimes and cascades their incident relation bindings. Two source forms lower
to the kernel RetractIndividuals effect (RFD 0076):
retract x; // a single individual
retract {x, y}; // an atomic set — one cessation set, one coverage gate
Every retract (and forget) target in a body joins ONE transaction-wide
cessation set, gated once for immutable-end coverage: for a both-immutable
binary relation, retracting only one endpoint refuses OE1404 (the surviving
endpoint is its dependent context), while retract {x, y}; — or two separate
retract statements naming x and y — covers both. A retract inside an
if branch, a for body, or a match arm joins that same union. Re-introducing
a retracted individual later in the same body — a re-classification, a relation
endpoint, or a property write — refuses OE1400; an assert-then-retract nets
clean and is admitted. Duplicate targets are idempotent.
Belief-time correction — amend. delete ends a binding’s valid time (it stopped being true); amend corrects the record (it was never true — a mis-recorded assertion, false ab initio). The two are deliberately distinct verbs so a correction can never be mistaken for a valid-time deletion. amend withdraws the assertion as false ab initio, releasing its contribution to any relation-end freeze (RFD 0076), while the bitemporal history retains what-was-believed-when — an auditable correction, not an erasure:
pub type Person;
pub rel bornTo(mut child: Person, mother: Person) [0..*] [1];
#[allow_amend]
pub mutate rescind(child: Person, wrong: Person) {
amend bornTo(child, wrong); // withdrawal — truth unknown
}
#[allow_amend]
pub mutate correct(child: Person, wrong: Person, right: Person) {
amend bornTo(child, wrong) => bornTo(child, right); // atomic withdraw + correct
}
The primitive amend R(args); withdraws positive evidence (the proposition returns to unknown, not refuted). The composite amend R(args) => R(args'); withdraws and asserts the corrected proposition against one transaction-time view, all-or-nothing (a refused corrected assertion commits nothing). Like forget, amend is capability-gated at the source level: a mutate body using it refuses to build (OE1405 AmendWithoutCapability) unless the enclosing mutate declaration grants #[allow_amend]; correction authority is further authorized per invoking principal, target-scoped, at the serving layer. Amendment is validated against one net corrected view of the transaction (withdrawals subtracted, corrected assertions and lifecycle effects overlaid), so it grants no coverage exemption to a companion cascade. Amending an established immutable fiber below its declared minimum refuses with OE1407; amending a purely rule-derived tuple (no asserted event to correct) refuses with OE1406. The composite corrects a fact of one relation: its withdrawal and correction must name the same relation, so a cross-relation amend R(old) => S(new) refuses at build with OE1409 (to change facts across two relations, delete one and insert the other). Correcting a refutation (not_fact) is deferred to the negative-facts mutation surface; amend here corrects positive relation assertions. An amendment releases exactly the one assertion it named — an assertion is an event, not a value-for-all-time — so a later re-assertion of the same tuple is a fresh initialization that re-freezes its immutable fiber: after amend R(a); insert R(a); a subsequent insert R(a') refuses (OE1403) exactly as it would with no amendment, whereas amend R(a); insert R(a'); directly (no intervening re-assertion) admits.
Body semantics
A mutate body lowers to the Operation IR and runs with all-or-nothing atomic commit: a failed require guard, or any error, emits nothing (RFD 0015). require preconditions, let bindings, typed-literal entity construction with system-minted identity, projection navigation (a.b.c) over committed state, collection inserts (insert … into …) with for iteration, sum / count aggregate guards over comprehensions, index projection (coll[i]), and effectful if/else (control flow is expression-valued; branches are blocks) all execute, returning the body’s tail value.
Name resolution in value positions. A bare identifier in a body value position — a relation-tuple argument, a constructor or update … set field value, a let right-hand side, a require guard operand, or the tail — resolves in order: a parameter or let / loop / match binder in scope first; then a declared type name (concept / trait / metatype / metarel / relation), which lowers to a TypeRef value exactly as it does in a rule body (Type-as-value) — the write side produces the same reference the read side dispatches on and the wire accepts as { "$typeRef": … }; then a module-declared individual (one introduced by a pub fact, resolved to its content-addressed identity). A binder therefore shadows both a declared type and a declared individual of the same name. A name that resolves to none of these is refused at ox check / ox build with OE1383 (no identity is ever synthesized for an unbound name), rather than building green and failing at the first run. A type-as-value LANDS only in a write position declared to hold one — a TypeRef / Metatype / TraitRef-typed relation endpoint or field; landing it in an entity/value position is refused with OE1384 at build where the declared signature is visible, and by the identically-worded runtime write gate otherwise. The same rules govern test bodies.
match in bodies. A value-position match (a let RHS, an update … set value, a tail / return value, or a require guard) matches over constant patterns (payloadless enum constants, literals, or-patterns, _; Pattern matching) and desugars to the same IfExpr chain a value if uses, with exhaustiveness enforced at ox check (OE0203). A statement-position match (arms running effects — the BPMN-gateway dispatch match d { Disposition::Clean => { update … }, Disposition::BreaksFound => { insert … }, _ => {} }) lowers to a right-nested Operation::If chain over the arm blocks’ operations (RFD 0015 Amendment 2): the same ordered first-match semantics, constant-pattern subset, and OE0203 exhaustiveness as the value desugar; arm blocks and if branches admit the full mutate statement set at any nesting depth. upsert, emit, and detach delete in a body refuse by name rather than dying generic or doing nothing silently: emit with OE1318, upsert with OE1352, and detach delete with OE1353.
Mutate-body arithmetic is exact (RFD 0016 / RFD 0029): require guards and let / field expressions route through one canonical evaluation core — pure-Int operands stay checked integers (overflow is a loud error, never a wrap), and any Real / Decimal / Money operand promotes to an exact rational (/ is the field operation, no truncation), so 0.1 + 0.2 == 0.3 holds exactly in a mutate body where it would fail in f64.
Reads see committed state. A body’s projections (
a.b.c) read the store as committed before the mutation; a body does not observe its own not-yet-committed constructions. One consequence: constructing a collection-valued field and theninsert … intothat same field in a single body does not compose (the append seeds from committed state).
Deductive reads are iterands, not value terms. A pub query / derived-predicate deductive-plane read is set-valued. Its sole admissible use in a mutate body is as the iterand of a for x in <query> { … } loop, which binds each projected row in turn (lowered to a reasoner snapshot read over committed state, the same no-read-your-writes plane as above). A nullary read (for r in applicable()) iterates the whole extent; a parameterized read (for r in applicable_for(arg)) passes its bound arguments as the query’s sideways read-goal bindings, iterating only the rows the parameters select.
pub mutate mark_all() {
for r in applicable() {
insert iof(r, Done);
}
}
A deductive read in a non-iterand term/value position — a let right-hand side, a require guard, an update … set value, a field value, or the tail — has no scalar value and is refused at ox check / ox build with OE1371 MutateQueryTermNotExecutable rather than passing the static gates and dying at the first runtime call:
pub mutate mark_term() {
let x = applicable();
}
check
check-decl ::= attribute* 'pub'? 'check' Ident '(' param-list ')' ':-' atom-list '=>' diagnostic-expr ';'
pub check no_overlapping_leases(t: Person, p: Property) :-
count { from l: Lease where l.tenant == t, l.property == p, is_active(l) } > 1
=> Diagnostic {
severity: Severity::Error,
code: "Lease::E001",
message: format!("Tenant {} has overlapping active leases on {}", t.name, p.address),
};
A check is a denial rule over the well-founded model: the body is the violation pattern,
the => Diagnostic payload is the per-violation report. Checks never contribute facts (Cat3 —
observers, not derivers). check is the only producer for the diagnostic stream from user code.
Discharge — the body’s vocabulary decides, not the author
You never declare a check “compile-time” or “runtime.” Discharge is staged by what the body reads (RFD 0025), the same staging discipline as refinement predicates (Refinement) and modal static discharge (Modal operators):
| Body vocabulary | Discharges at |
|---|---|
Catalog-level — every head parameter and body variable is reflective-sorted (TypeRef, TraitRef, Metatype — RFD 0025 D1 as amended by RFD 0026 D6); atoms read declaration structure (specializes, implements, the type column of iof/meta) | ox check, ox build, and live in the editor — the LSP runs the same discharge and publishes firings as you type. The declared catalog is closed at build, so evaluation there is total and final. |
| Instance-level — any variable ranges over individuals | The runtime, at every mutation boundary and on demand — and also at build (and live in the editor, via the LSP), over whatever EDB the package itself declares (pub facts, seeded individuals). |
Catalog-level checks are the user-extensible compiler-diagnostic mechanism: a theory package
ships its modeling constraints (OntoClean-style rigidity lints, MLT well-formedness, trait
conformance) as check rules and they surface at ox check, ox build, and live in the
editor — the LSP runs the same discharge — like any compiler diagnostic, under the package’s
own codes.
The #[static] convention. Compile-time-intent checks carry #[static] by convention.
The attribute does not change where discharge happens (the vocabulary does); it asserts the
intent — if the body later drifts to instance vocabulary, the build fails with OE1322 instead
of silently reclassifying the check to runtime discharge.
A firing Severity::Error check at build is a build failure: ox build writes no artifact,
same discipline as every other loud gate. Warning/Info render and pass.
Runtime semantics — guard on the delta
At each mutation boundary the runtime evaluates the instance-level checks on the committed
state (pre) and on the transaction’s overlay (post = committed + the body’s buffered
events, mutate):
Severity::Errorguards: if the mutation creates violations —violations(post) ∖ violations(pre)is non-empty — the whole body is rejected atomically (nothing flushes,mutate’s atomicity) and the rendered diagnostics return to the caller. This generalizes the built-in write gates (OE0668refinement invariants,OE0211defined-concept inserts) to user-authored constraints.- Pre-existing violations never block. A violation that predates the mutation (a stricter artifact over old data) is reported through the observe channel but does not reject writes — the guard is over the delta, not the absolute state.
Warning/Infoobserve: collected and surfaced — on mutation results, in dispatch responses (thediagnosticssection), and on demand.#[observe]on anErrorcheck opts out of guarding (report-only, legitimate during migration).
The mechanized contract is spec/lean/Argon/Reasoning/Checks.lean: static_discharge_sound
(a catalog-closed body evaluates identically at build and against any store extending the
catalog) and guard_iff (the guard passes iff violations(post) ⊆ violations(pre)).
The Diagnostic payload
Diagnostic and Severity are check-surface syntax interpreted by the compiler (like the
#[default] defeat-plane directive) — no import declares them. Exactly three fields, validated at
ox check (OE1323); code: and message: are always required, and severity: is required
unless the check implements a trait member whose signature pins it
(check Member(Self) => Severity::…; — Trait atom, RFD 0026 amendment, issue
#230), in which case omitting severity: inherits the pinned one (a divergent restatement is
OE0676):
severity:—Severity::Error|Severity::Warning|Severity::Info(closed set).code:— a string literal, namespaced:::-qualified ("Lease::E001","OntoClean::C1"), with the compiler’sOE/OWprefixes reserved (OE1324).message:— a string literal, orformat!("…{}…", args)with positional{}placeholders only; each argument is evaluated per violation tuple and must resolve against the body’s bindings (OE1325).{{/}}escape literal braces.
at: is reserved for span attribution and refused.
Three-valued honesty
Check bodies evaluate over the well-founded model in K3 and fire on definite (is)
violations only — undefined does not fire. Precisely: a violation requires the body to be
well-founded true, so a negated body atom not p(x) contributes only when p(x) is
well-founded false (definitely absent), not merely not-true — an undefined p(x)
arising from recursion-through-negation does not satisfy the negation. A body that would have
fired only because a NAF subject was undefined is surfaced on the observe channel marked
[undefined], never as a violation and never to the delta guard, regardless of the check’s
declared severity. Negation in a check body is negation-as-failure over the well-founded model (the CWA
default, World assumptions (CWA / OWA)): not statute(c.cite) means “no
derivably known statute,” not a claim about reality. Surfacing can-grade (possible)
violations is out of scope.
Documentation gap — the in-body determinedness test is implemented but not yet specified here. The compiler accepts two check-body atoms,
determined(A)andundetermined(A), over a membership atomA: they read the converged model and hold whenA’s verdict there is definite, respectively not definite. A verdict is left indefinite by a well-founded standoff (can) or — for a concept marked#[world(open)]— by absence of evidence that nothing refutes, the same two cases the four-valuedassert [not] derivableverdict distinguishes (World assumptions (CWA / OWA)); under the closed-world default absence is definite-false and only the standoff remains. They let a check body dispose of an undecidable membership per path instead of taking a severity-derived default. Outside acheckbody they are refused (OE1431), and a check body that uses either while leaving some membership atom’scanpath unrouted warns (OW0744) — see Diagnostic codes. No chapter specifies them yet, and this section does not: two surface questions in the design record (RFD 0090) are open — whether the test generalizes to per-atom granularity inside a compound membership or stays at whole-membership granularity, and what surface the per-check inferred-disposition report takes (build-time inventory,oxsubcommand, or both). A chapter written before those settle would document a form that is expected to move. The chapter also owes the underlying gate the tests build on: how a blocking check disposes of a membership it cannot decide, which is likewise unspecified here today. Until then the mechanized denotation is the reference:undeterminedAtom/determinedAtominspec/lean/Argon/Reasoning/Checks.lean.
Delivery contract: a fired check is an emission to the reserved typed sink
Diagnostics: Diagnostic (Sinks). The => Diagnostic { … } consequence is
exactly => emit Diagnostics { Diagnostic { … } } — the Diagnostics-channel spelling of the
general => emit Sink { … } rule consequence (emit) — and check is
its sole producer (the diagnostic stream is never fed by a hand-written emit Diagnostics). Each
firing carries polarity (Firing): a new violation binding emits an Asserted
diagnostic and a violation resolved by a later mutation emits a Retracted one — the diagnostic
clears, the dual of the delta guard above. Delivery is direct — the
build diagnostic stream for static discharge, mutation results and the dispatch-response
diagnostics section for runtime discharge.
emit from any rule mode
emit publishes a typed value to a sink — a named, write-only egress channel. It is the language’s hook into the system’s pub/sub layer: a published value flows to whatever external observers a sink is wired to, and nothing else happens to the knowledge graph. An emit adds no axiom, derives no tuple, and can never change the queryable extent — it is an observation, not a mutation. Because it is extent-neutral, emit is admitted in every rule mode (fn, query, derive, mutate, check), not only the side-effecting one; a fn that emits is still pure in the sense the purity ladder cares about — its return value and the extent are unchanged.
Sinks
A sink is the typed publication channel emit targets. It names a channel and the type of value that channel accepts; it carries no body and no target:
sink-decl ::= attribute* 'pub'? 'sink' Ident ':' TypeExpr ';'
pub sink AuditLog: LeaseEvent;
pub sink HitlQueue: ReviewTask;
pub sink Notify: Notification;
There is exactly one sink form. A brace-bodied form pub sink Name { … } is not a sink declaration (a sink has no in-language body — where a value is delivered is configuration, not source); it is refused with OE1356. Delivery — log file, webhook, message queue, HITL ticket — is a runtime concern configured outside the source (the egress dual of the read-side placement routing); the language guarantees only that an emitted value matches the sink’s declared type.
Two spellings, one mechanism
emit means the same thing everywhere — publish value v to sink S — but the rule modes come in two body shapes, so it has two surface spellings.
Imperative bodies (mutate, fn block) have a statement position, so emit is a statement:
emit-stmt ::= 'emit' sink-path '{' expr '}' ';'
sink-path ::= Ident ('::' Ident)*
pub type Person { name: String }
pub sink AuditLog: String;
pub mutate sign_lease(t: Person) {
// … the write …
emit AuditLog { t.name };
}
Declarative bodies (derive, query, check) are clause bodies (select … from …, head :- atoms) with no statement position, so emit rides a consequence arrow => — the same arrow check already uses. In fact check’s => Diagnostic { … } is an emit to the reserved Diagnostics sink; it is the Diagnostics-channel special case of the general consequence form:
pub type Person { age: Int }
pub sink SeniorFeed: Person;
// derive: publish on each newly-derived tuple (the signed derivation delta)
pub derive senior(p: Person) :- p: Person, p.age >= 65 => emit SeniorFeed { p };
A check’s => Diagnostic { … } payload is the same construct: it is sugar for => emit Diagnostics { Diagnostic { … } }, an emit to the reserved typed sink Diagnostics: Diagnostic (check). emit Diagnostics { … } is otherwise reserved — the diagnostic stream is produced only through check, never by a hand-written emit. As a check consequence it carries polarity like any other derive-class sink: a new violation emits an Asserted diagnostic and a resolved violation emits a Retracted one (the diagnostic clears).
Firing — once per fresh delta, with polarity
A sink is fed once per fresh firing, where the firing unit is the natural one for the mode. Re-deriving, a cache hit, or re-reading an unchanged result does not re-emit. Each firing carries a polarity: Asserted (a value newly published) or Retracted (a previously-published delta withdrawn).
| Mode | Spelling | Fires once per… | Polarity |
|---|---|---|---|
mutate | emit S { e }; | committed write, in statement order — nothing is emitted on abort (a failed require publishes nothing, consistent with the all-or-nothing transaction) | Asserted |
fn | emit S { e }; | fresh evaluation; a cache hit on (args, state_version) does not re-emit | Asserted |
query | => emit S { e } | fresh evaluation | Asserted |
derive | => emit S { e } | head tuple crossing a truth boundary | Asserted on newly-true (+1 delta), Retracted on newly-false (−1 delta) |
check | => emit S { e } / => Diagnostic { … } | violation binding crossing a truth boundary | Asserted on a new violation, Retracted when one is resolved |
So mutate/fn/query fire per execution/evaluation and derive/check fire per binding — but in every case the rule is “fire on each fresh occurrence, deduplicated by the cache/delta,” which is why the published stream tracks the system’s evolution rather than how often something is asked.
The delta model. The two delta-firing modes carry both signs. A derive sink sees Asserted on the +1 delta when a head tuple becomes true and Retracted on the −1 delta when a previously-true tuple becomes false; a re-derivation that nets to zero emits nothing. A check sink sees Asserted on a new violation binding and Retracted when a previously-firing violation is resolved. Emitting only the positive delta and dropping retractions would be a silent egress lie: a consumer would learn that an entitlement was granted but never that it was revoked. Polarity closes that — the egress stream is faithful to the extent’s evolution in both directions.
The imperative and pull modes are Asserted-only by construction, not by omission. A mutate emits per committed write and a committed write is never un-happened — an abort emits nothing rather than a later Retracted. A fn emits per fresh evaluation, and query is pull-based: a pull produces a result, never a standing delta there is anything to withdraw. There is no retraction to carry because these modes have no negative delta, so they are complete.
Emission and delivery
An emission is a triple { sink, value, polarity }. The value is the evaluated typed value — the same value carrier the language uses for struct and enum values, so a sink’s payload type is an ordinary type and the emitted value is checked against it. The polarity is the firing sign above.
Emissions ride the commit’s observation channel — the same receipt channel that carries #[observe] and Warning / Info check diagnostics — not the axiom/event log of facts (Storage layer). This is why emit is model-neutral: an emission is never an axiom, so it cannot enter the well-founded model, and its durability does not change the queryable extent.
Delivery is at-least-once with idempotency — effectively-once effect, not effectively-once delivery, which is unattainable across an independent process boundary (the two-generals impossibility). The mechanism is a transactional outbox over the append-only commit log (Richardson, Microservices Patterns; the partitioned-store reasoning of Helland, “Life beyond Distributed Transactions”): an emission is recorded atomically with the commit that produced it, then delivered; a redelivery after a crash or timeout is deduplicated downstream by an idempotency key derived from the emission and its originating commit. A consumer that applies emissions idempotently observes each effect once.
Routing is configuration. Which physical destination a sink’s values reach — a log file, a webhook, a message queue, a human-in-the-loop ticket — is runtime configuration (ox.toml), the egress dual of the read-side [placement] routing. It never appears in source: a sink declaration names the channel and its payload type and nothing more, and emit guarantees only that the value matches that type.
Status
emit is settled surface and executes in all five rule modes. The mutate and fn statement forms publish per fresh execution/evaluation — Asserted, nothing on abort, and a fn cache hit on (args, state) does not re-emit. The derive => emit consequence publishes the signed derivation delta at the commit boundary: Asserted on a newly-true head tuple, Retracted when a previously-true one becomes false, nothing on a net-zero re-derivation. The check => emit consequence publishes from the violation delta (Asserted on a new violation, Retracted when one resolves). The query => emit consequence publishes one Asserted value per fresh evaluation (a cache hit does not re-emit). Every position type-checks the value against the sink (OE0201 on a mismatch), refuses an undeclared sink (OE0101), and refuses the reserved Diagnostics sink to a hand-written emit (OE1368 — the diagnostic stream is produced only through check’s => Diagnostic { … }). An emit-bearing fn inlined into a rule body is refused (OE0227 — a rule body has no egress surface). A check => emit that fires at ox check / ox build has no receipt at build, so it surfaces a non-blocking OW1369 (its egress is a runtime-only effect, never silently dropped). Egress is model-neutral throughout: it asserts no axiom and never changes the queryable extent — emissions ride the commit’s observation channel (mutate / derive / check on the mutation receipt) or the read’s dispatch response (query), never the axiom log. Delivery and routing (log file, webhook, message queue, HITL ticket) are runtime configuration — the egress dual of read-side placement. The one sink-declaration form is pub sink Name: Type;; a brace-bodied pub sink Name { … } is refused by name (OE1356) — a sink has no body.
Pattern matching
match-expr ::= 'match' expr '{' arm (',' …)* ','? '}'
arm ::= pattern ('if' expr)? '=>' expr
pattern ::= '_'
| literal
| Ident // binder
| TypeExpr '(' pattern (',' …)* ')' // variant
| TypeExpr '{' field-pattern-list '}' // record
| Ident ':' TypeExpr // type test
| 'is' reasoning-outcome
| pattern '|' pattern // OR
reasoning-outcome ::= 'unknown'
| 'ambiguous' '(' Ident ')'
| 'timeout' '(' Ident ')'
| 'both' '(' Ident ',' Ident ')'
pub fn classify(l: Lease) -> Status =
match l {
ResidentialLease(r) if r.bedrooms > 4 => Status::Large,
ResidentialLease(_) => Status::Standard,
CommercialLease(_) => Status::Commercial,
_: TerminatedLease => Status::Closed,
is unknown => Status::Pending,
is ambiguous(x) => Status::Ambiguous(x),
_ => Status::Unknown,
};
Exhaustiveness checked; non-exhaustive matches require _ or fail with OE0203.
Match semantics
match evaluates — in value position and statement position — as ordered
first-match over constant patterns:
| Pattern form | Admitted | Notes |
|---|---|---|
wildcard _ | ✓ | Matches everything. |
literal (Int / String / Bool / Date) | ✓ | Value equality (Int widens per Engine / Module / Store factoring). |
payloadless enum constant path (Status::Active) | ✓ | Resolved through the enum resolver; canonical-CBOR equality at runtime. |
or-pattern of the above (A | B => v) | ✓ | Expands to consecutive arms sharing the body. |
binder Ident | refused OE1319 | |
variant payload Type(p) (one positional binder) | ✓ value position | Binds the payload over the arm body; refused OE1319 in a statement-position effectful arm (below). A zero- or multi-binder arm is OE0257; the record form Type { … } is refused. |
type test x: T | refused OE1319 | |
is reasoning-outcome | refused OE1319 | |
arm guard pat if cond | refused OE1319 |
A value-position match (a fn body, a rule-body comparison operand, a
mutate let RHS / update … set value / return value / require guard)
desugars at elaboration to a right-nested conditional chain over
scrutinee == constant tests — if s == P1 { v1 } else if s == P2 { v2 } else { v_last } — so it executes on the existing IfExpr paths in the
reasoner and the mutate runtime; semantics are first-match in arm order,
and the final arm’s value is the else-branch. Exhaustiveness is
required: a final _ arm, or full coverage of the scrutinee’s enum
variants when statically known — otherwise OE0203 at ox check.
Statement-position match (arms running effects inside a mutate
body) executes with the same semantics (RFD 0015
Amendment 2): the arms are blocks
running mutate statements (update / insert / delete / forget /
require / nested for / if / match), the match lowers to a
right-nested Operation::If chain over the arm blocks’ operations —
ordered first-match, exactly one selected arm’s effects run, the arm
value is discarded — and the same exhaustiveness rule applies (_ => {}
with an empty block is the idiomatic catch-all; OE0203 otherwise).
Or-patterns expand to consecutive conditions sharing the arm’s
operations.
A single-binder payload destructuring (Some(v) => …) executes in
value position — let r = match opt { Some(v) => v, None => 0 }; and
every value-position site above: the arm’s condition tests the variant
tag and its body sees the binder substituted by the decoded payload. A
payload-binding arm in statement position (an effectful arm inside a
mutate statement-match) refuses with OE1319: bind the payload in
value position first (let v = match opt { … };), then run the effect. A
bare binder Ident, a type test, an is-outcome arm, and an arm guard
refuse with OE1319 in both positions.
Rule-body arm projections must be total. In a rule body (derive /
check / query / bridge), the relational lowering hoists every field
projection inside the conditional chain — including arms the scrutinee
never selects — into an eager, unconditional $field:: join, because
the relational plan has no lazy join. $field::<f> has no row when an
individual lacks f, so an arm projecting an optional (T?) field
would silently filter out every row missing that field, even rows whose
matching arm never touches it. Rather than mis-evaluate, ox check /
ox build refuse such a rule with OE1320: every field projected inside
a conditional arm (a value-if branch or match arm — both lower
identically) must be total, i.e. declared required on every visible
type that declares it. Workarounds: split the conditional into one rule
per arm (each joining only the fields that arm needs), or declare the
field required. Projections in always-evaluated positions (the rule body
proper, the outermost condition / scrutinee test) and fn / mutate
bodies (which evaluate branches lazily) are unaffected.
Trait atom
A trait is a behavioral contract: a named bundle of obligations over a Self type, discharged
by impl Trait for Type blocks. The boundary with the concept lattice is doctrinal: concepts say
what something is (ontologically); traits say what something can do — trait hierarchies never
enter the <: concept lattice, traits cannot be specialized or inherited, and classification never
belongs in a trait (Modeling guidance: traits versus phases and categories). The full design is RFD 0026;
the trait surface lands in stages, and what runs end-to-end is exercised by the packages in
examples/.
Surface
trait-decl ::= attribute* 'pub'? 'trait' Ident generic-params?
(':' TypeExpr ('+' TypeExpr)*)? -- supertraits (requires-constraints)
('{' trait-item* '}')? | ';' -- empty body = marker trait
trait-item ::= fn-signature ';' -- invocation-plane member ('self' leads)
| 'mutate' Ident '(' 'self' (',' member-params)? ')' ';' -- invocation-plane member
| 'derive' Ident '(' member-params ')' ';' -- rule-plane member
| 'check' Ident '(' member-params ')'
('=>' 'Severity' '::' Ident)? ';' -- rule-plane member; optional
-- severity pin (issue #230)
| 'query' Ident '(' member-params ')' '->' TypeExpr ';' -- rule-plane member
-- 'type' Ident ';' / 'const' Ident ':' TypeExpr ';' remain reserved
impl-block ::= 'impl' generic-params? TypeExpr ('for' TypeExpr)? ('{' … '}')? | ';'
trait-impl-item ::= fn-decl | mutate-decl -- full bodies
| derive-decl | check-decl | query-decl -- full bodies (rule body; select
-- body for query), Self admitted
When 'for' TypeExpr is present the construct is a trait impl (impl Renewable for Lease)
and its items are trait-impl-items; when absent it is a bare impl (impl Lease { … }),
whose items parse per impl Type { … } — grouping but are gated at
elaboration (OE1326) until inherent members execute.
Conformance (Conformance) governs trait impls only. Impls of either form are standpoint-global:
declaring one inside a standpoint { … } block is refused at elaboration (per-standpoint impls
are an open design question RFD 0026 deliberately does not decide).
Selfresolves only inside trait and impl bodies: in a trait item it is the obligation’s type parameter; in an impl item it denotes the impl’s target type. Misuse is OE0675. A trait member signature must mentionSelfin at least one parameter position (a member that is not about the implementing type belongs at module level — OE0675 says so). MultipleSelfpositions are legal.- Supertraits use
:(pub trait Repaintable: Drawable), Rust’s spelling, because they are Rust’s semantics — requires-constraints, not subsumption (Resolution).<:is reserved for the concept lattice. - Generic parameters remain unbounded-only (Generic parameters); generic members, bounded generics, and trait-side default bodies refuse loudly, never dropped.
Member semantics: two planes
Members split by how they are consumed; both planes elaborate by monomorphization — each impl
member lowers to an ordinary rule (Self ↦ target type) flowing through the standard pipeline
(classifier, stratifier, evaluability gate, reasoner) with no new evaluation machinery.
Rule plane (derive, check, query) — clause union
The trait declares the predicate; each impl contributes a type-guarded clause:
pub trait Adulthood {
derive Adult(Self)
}
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 }
elaborates to two ordinary rules with one head, Adulthood::Adult — Datalog unions same-head
rules natively, and the head-parameter guards close over <: (World assumptions (CWA / OWA)). Dispatch is derivation:
an atom Adult(p) in
pub derive CanVote(p: Person) :- Adult(p), p.registered
holds per-individual under whichever impl covers the individual’s actual type — the relational
transposition of dyn dispatch, with the no-overlap rule (Conformance) keeping the selection
single-valued per declared type (per-individual multiple classification is defined in Conformance). A
query member carries the full query signature — Self in parameter positions, a Self-free
return type (return-position Self is OE0675: it would need union types or bounded generics)
— and each impl provides a full select … from … body; the trait signature fixes the
projection shape. Query members are declared, conformance-checked, and lowered per impl, and
they surface through the same per-impl query dispatch top-level queries use — each impl’s clause
is queryable under its per-impl identity Trait::member@Target. They are not rule-body
atoms: a query-member atom in a derive/check body is refused loudly at ox check, the same
envelope discipline fn members get (Resolution) — top-level queries are not rule-body predicates
either, and the member form inherits exactly that. A single dispatch endpoint returning the
union of per-impl results is reserved. A check
member monomorphizes to ordinary RuleMode::Check rules; discharge, payload, and
delivery follow RFD 0025 unchanged. A check member’s
signature may pin its severity with the payload-arrow suffix
(check OverLimit(Self) => Severity::Error; — RFD 0026 amendment 2026-06-11, issue #230):
the pin makes blocking behavior part of the trait contract. Under a pin, an impl’s
Diagnostic { … } payload may omit severity: (it inherits the pinned one), may restate
the same severity (harmless), and is refused with OE0676 when it states a different one —
a contract whose blocking behavior varies by implementor is a weak contract. Only the severity
is pinnable: code: and message: stay per-impl, and a full Diagnostic { … } payload on the
trait-side signature is refused (OE1323). An unpinned member keeps per-impl severity freedom.
#[observe] on an impl member whose pinned severity is Error remains legal (a discharge-mode
opt-out, not a severity change).
Invocation plane (fn, mutate) — receiver resolution
A body is a single value or a single transaction; resolution is to exactly one impl. An
invocation-plane member leads with the self receiver — its first parameter must be self
(OE0675 otherwise; further Self-typed parameters are admitted after it) — because the receiver
is what dispatch selects on. A mutate member —
pub trait Closable {
mutate close(self, reason: String);
}
impl Closable for SavingsAccount {
mutate close(self, reason: String) {
update self: SavingsAccount set { status = "closed" }
}
}
— monomorphizes per impl (full RFD 0015 imperative bodies; self is the receiver variable inside
the body, Self splices to the target type) into the mutation catalog under the per-impl identity
Closable::close@SavingsAccount, callable as Closable::close: invocation dispatches on the
receiver individual’s actual committed type to the unique covering impl (<:-aware — an impl
for Account covers a SavingsAccount receiver). An ambiguous receiver — classified under two
<:-incomparable covered targets — and a receiver with no covering impl are both loud runtime
refusals naming the impls / the receiver’s types (Conformance), never an arbitrary pick. Invocation
flows through the standard mutation-dispatch surface (CLI harness / serve / SDK); the receiver is
passed as the self argument (an individual reference), exactly like any RFD 0019 entity-ref
parameter. fn members elaborate and dispatch the same way through the compute surface (Resolution has
the precise execution envelope). Mutations do not call mutations; member mutates do not change
that.
Generic parameters
Unbounded generics parse and travel the wire; bounded generics are rejected
(OE0667). The conditional-impl coherence theorem (Argon.TypeSystem.Conditional) is mechanized.
Resolution
Calling convention (rule plane). Trait::member(args) qualified is always legal. Bare
member(args) is legal when exactly one provider of that name and arity is in lexical scope;
ambiguity is an error naming the candidates. There is no postfix p.member() sugar in rule
bodies — p.x is field-access space (Rule-atom grammar).
The coverage gate. A bare member atom requires the argument’s static type to be fully
covered — you cannot call what isn’t implemented, statically. Coverage is computed over the
workspace-closed catalog via the abstract modifier (metatype, RFD 0027 D6): a static type T is
fully covered iff every instantiable (non-abstract) declared S ⊑ T (including T itself
when non-abstract) is ⊑ some impl target. Abstract types admit no direct instances
(OE0233 refuses construction) — every individual is classified through some non-abstract
subtype — so they need no covering themselves; their non-abstract descendants do. The
instantiability oracle is a pure wire-flag read (Argon.TypeSystem.Conformance. instantiableOfAbstract); no axis vocabulary is consulted — a sortality::non_sortal
binding is the package’s own ontology and exempts nothing by itself. The default is
instantiable: an unmodified declaration (the neutral std::core::type baseline, any
pub metatype m = { };) obligates coverage — the gate can only over-refuse, never silently
admit a vacuous member call, and the exemption is the explicit abstract opt-in. Partial
coverage is legal only under an explicit conformance guard:
pub derive CanVote(p: Person) :-
implements(meta(p), Adulthood), -- the visible branch
Adult(p), p.registered
Uncovered or unguarded-partial atoms are OE1327, whose help names the uncovered types and
offers both fixes (add the impl, or write the guard). Dispatch that can fail is always visible in
the source. Under multiple classification meta(p) is multi-valued (Reflection); the guard is
existential by construction — meta is a relation and the conjunction is a join, so the
guard holds iff some <:-minimal classifier of p is covered, and the clause that fires is
the one whose target that classifier satisfies.
Invocation-plane dispatch. Invocation-plane members are end to end through the execution surfaces Argon has — the dispatch surfaces, where the only type information is the receiver individual’s committed classification:
- A member is invoked by its callable name
Trait::member(qualified path, or any unambiguous::-suffix — the same resolution rules as mutations and rule heads; ambiguity is an error naming the candidates), or pinned to one impl by its per-impl identityTrait::member@Target. The pinned form is an explicit escape hatch: it bypasses receiver dispatch and does not re-verify that the receiver is covered by that impl’s target (parity with ordinary mutations, which do not re-verifyupdate x: Tagainstx’s classification). - The
selfargument carries the receiver individual; dispatch selects the unique impl whose target covers the receiver’s actual committed type (<:-aware). No covering impl, and a receiver under two<:-incomparable covered targets (the Conformance residual), are loud refusals naming the types / the impls — never an arbitrary pick. - mutate members run through every mutation surface (the CLI demo harness, serve’s
/v1/dispatch/mutation, the generated SDK’s member endpoint); fn members run through the compute surface (/v1/dispatch/compute, SDK) — exactly the surfaces where top-levelfndeclarations execute.
Source-level call sites are not an execution surface — for any fn. Top-level fn
declarations cannot be called from rule bodies or from fn/mutate bodies (term-position
user-function application does not resolve); method-call syntax x.foo() parses but has no
semantics — the call expression builds, resolves to nothing, and derives nothing (the same
silent-vacuity bucket as term-position application; tracked for a loud gate). fn
members inherit exactly that envelope — they are declared, conformance-checked, lowered, and
dispatchable wherever top-level fns are, and nowhere else. Source-level calls
bind to the rules this section reserves: build-time resolution (first x’s inherent impls, then
trait impls; one concrete body, no vtables), Trait1::foo(x) (UFCS) on collision, bare calls
legal where exactly one trait in scope provides the member, and the receiver’s static type
subject to the same coverage gate.
Supertraits are requires-constraints. pub trait Renewable: Resource makes
impl Renewable for Lease well-formed only if impl Resource for Lease exists (OE0674).
Nothing is inherited — no member bodies, no identity, no lattice position.
Orphan rule. An impl Trait for Type must live in the package declaring Trait or the one
declaring Type (OE0672), so every workspace’s impl set is unambiguous.
Conformance
Enforced at ox check and ox build over the closed catalog (elaboration-time diagnostics, not
check rules):
| Obligation | Diagnostic |
|---|---|
| Impl provides every declared member | OE0670 ImplMemberMissing |
| Impl member not declared by the trait / signature mismatch (incl. parameter-name mismatch on invocation-plane members — wire arguments bind by name across every impl of a callable) | OE0671 ImplMemberExtraneous |
| Orphan rule | OE0672 OrphanImplViolation |
Two impls of one trait with <:-comparable targets, or targets sharing a declared common descendant | OE0673 ImplTargetsOverlap |
| Supertrait impls present | OE0674 SupertraitUnsatisfied |
Self discipline (incl. the invocation-plane receiver rule: fn/mutate members lead with self) | OE0675 SelfMisuse |
| Genuinely unsupported member shapes: trait default bodies, inherent (bare-impl) members, unrecognized forms | OE1326 TraitMemberNotYet |
| Bare member atom over an uncovered type | OE1327 TraitMemberUncovered |
No-overlap (OE0673) instantiates the mechanized coherence contract by construction — a nominal
target is a TraitBound with satisfied T := T ⊑ target — so at most one impl covers any
declared instantiable type and there is never a specificity question. Both arms matter:
impl Adulthood for Person + impl Adulthood for USPerson is OE0673 (the “default + override”
pattern is rejected by design — write disjoint targets, or one impl whose body branches), and
impl T for Person + impl T for Customer is OE0673 when some pub type Employee <: Person, Customer is declared (the common descendant would sit under two clauses). One residual cannot be
closed statically — the set of an individual’s runtime classifications is not known at compile
time: an individual dynamically classified under two <:-incomparable
covered targets with no declared common descendant. Its semantics is defined, not accidental — in
the rule plane the clauses are independent sufficient conditions (both may fire; the union is
their disjunction); in the invocation plane an ambiguous receiver is a loud runtime refusal.
Reflective conformance: TraitRef and implements
TraitRefis a reflective sort parallel toTypeRef(RFD 0023): values are handles to declared traits (carrierValue::Name). It is a sibling sort underEntity— deliberately not on theMetatype <: TypeRefchain, because traits are not types.implements(t: TypeRef, tr: TraitRef) -> Boolis the fifth reflection intrinsic (Reflection), materialized as the RT-closed reserved-head relation$implementsfrom impl declarations, with supertrait closure and<:upward target-coverage folded in (an impl forPersoncoversUSPerson, World assumptions (CWA / OWA)-coherent).$implementsis catalog-closed, sonot implements(…)is stratification-safe, and — unlike the type-position arguments of the other four intrinsics — both positions ofimplementsare exempt from OE0212 and may be free: enumeration is relational (implements(t, Adulthood)with freetyields the implementing types;implements(meta(p), tr)with freetryields the traits ofp’s type). The expression plane gets the boolean form (if/matchscrutinees) andimplementors(Trait) -> Set<TypeRef>.- Conformance checks are catalog-level vocabulary under RFD 0025 D1 as amended by RFD 0026
(catalog-level iff every variable is reflective-sorted:
TypeRef,TraitRef, orMetatype), so they discharge statically atox check:
#[static]
pub check EveryPersonKindHasAdulthood(t: TypeRef) :-
specializes(t, Person),
not implements(t, Adulthood)
=> Diagnostic {
severity: Severity::Error,
code: "Onto::E001",
message: format!("{} must implement Adulthood", t),
}
Wire and Lean correspondence
Member rules are emitted as ordinary RuleDecl/QueryDecl events; invocation-plane members as
ordinary MutationDecl/ComputeDecl events keyed Trait::member@Target — no new AxiomKind.
TraitDeclBody.methods carries member signatures (all five planes); ImplDeclBody.items carries
member provenance (both fields carry it — the wire is built for this).
spec/lean/Argon/Substrate/Trait.lean carries the @[language_interface] shapes;
Argon.TypeSystem.Conditional states the coherence contract that OE0673 enforces. The Lean
obligations for this design (monomorphization conservativity on the Reasoning/Datalog/ spine;
coherence instantiation; structural-tier conformance) are listed in RFD 0026.
Modeling guidance: traits versus phases and categories
If it classifies, it is a concept; if it obligates behavior, it is a trait. “Adult” as a
classification is an anti-rigid phase of Person — model it as
pub type Adult <: Person iff { age >= 18 } and the substrate derives membership. Reach for a
trait when otherwise-unrelated types must satisfy a common contract of derivations or operations
(Closable, Auditable, per-jurisdiction rule families) — the contract is about what the types
provide, not what their instances are. A trait carrying a single unary Self-member named
like a classification is usually a phase in disguise.
Examples
-- Marker trait.
pub trait Renewable;
-- Rule-plane contract; per-jurisdiction clauses.
pub trait Adulthood {
derive Adult(Self)
}
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) :-
implements(meta(p), Adulthood), Adult(p), p.registered
-- Invocation-plane contract. The receiver leads; invocation passes
-- `self` (an individual ref) and dispatches on its actual type.
pub trait Closable {
mutate close(self, reason: String);
}
-- Supertrait as requires-constraint (Rust's `:`). Return-position
-- `Self` is OE0675 (it would need union types / bounded generics);
-- return a concrete type.
pub trait Repaintable: Drawable {
fn repaint_cost(self, color: Color) -> Int;
}
-- Bare impl (inherent grouping): parses per `impl Type { … }` — grouping, but its members are
-- OE1326-gated at elaboration — inherent members do not execute yet
-- (the RFD 0026 invocation plane covers trait members only).
impl Lease {
fn extend(self, days: Nat) -> Lease;
}
Macro atom
The macro atom is the fifth substrate atom (meta-calculus, constructs, rule, trait, macro) and the extensibility layer the ontology-neutral doctrine depends on: it is how a vocabulary library grows the surface without a compiler change. Its design is settled by RFD 0037; this chapter is the surface specification.
The declarative expansion engine — pub macro declarations expand at EXPAND (hygiene, repetition, attribute/decl-position invocation, cross-module imports), and their output re-elaborates through the ordinary path to events. The procedural layer (#[procmacro] pub fn, Procedural macros) provides concat_idents, quote/${} antiquotation, decl-reflection, and a total non-recursive body — the mechanism that re-homes #[irreflexive]/#[asymmetric]/#[functional] from compiler builtins to library procedural macros (#[functional] was the last builtin retired, Migration: re-homing the hard-coded surface).
The shape of the design. A macro expands to surface syntax, which is re-parsed and re-elaborated through the one existing path to events — never to events directly. Expansion is a phase between parse and resolve, run to a fixed point; the resolver, type checker, Name resolution ontology-neutrality gate, and decidability-tier classifier all run on the post-expansion program, so a macro is invisible to none of them. The declarative layer (pub macro, pattern→template) is the foundation; the procedural layer (#[procmacro], computation over reflected syntax) is layered on top (Procedural macros).
Declarative macros
macro-decl ::= attribute* 'pub'? 'macro' Ident '{' macro-rule (';' macro-rule)* ';'? '}'
macro-rule ::= '(' macro-pattern ')' '=>' '{' template '}'
macro-pattern ::= /* token tree with $name:SPEC metavariables and $(...)* / $(...)+ / $(...)? repetitions */
A declarative macro is a sequence of (pattern) => { template } rules. A pattern matches a token tree; metavariables are tagged with a fragment specifier that fixes the syntactic category matched and the binding space the bound name lives in. The template is surface syntax, with metavariable substitution and $( … )*/+/? repetitions (optional separator: $( … ),*).
pub macro vec {
() => { Vec::empty() };
($head:expr $(, $tail:expr)*) => { /* … */ };
}
let xs = vec!(1, 2, 3);
Fragment specifiers. The syntactic categories $x:expr, $t:ty, $i:ident, pat, stmt, block, item, literal, path, tt; plus four Argon-specific specifiers, each a binding-space-targeted parse — $c:concept parses a concept-introducing declaration and binds in the concept space, $r:rel a relation declaration (rel space), $m:metatype a metatype introducer, and $u:rule a rule-body atom (carrying its own rule-variable sub-bindings). The set is closed; a standpoint specifier and a user-extensible registry are out of scope (Out of scope). Because a concept/metatype specifier only binds — the Name resolution gate runs later, at resolution (Hygiene) — and no specifier carries a tier, ontology-neutrality and tier-honesty hold by construction.
The expansion model
Expansion is a distinct compilation phase:
lex → parse → expand* → resolve → check → instantiate (lower to events) → tier-classify → discharge
- Expand to surface, re-elaborate. A macro produces surface syntax that is spliced into the module and re-parsed; it then flows through the same resolve → check → lower path as hand-written source. The content-addressed
AxiomKindevent wire is produced only by the unchanged lowering, so a macro inherits the substrate’s identity and drift guarantees for free. A macro never emits events directly. - Fixed point. A macro whose output contains further invocations re-expands until none remain, bounded by a fuel cap that errors on exhaustion.
- Emit into the invocation module. A macro expands at its use site and its output lands in the invoking module, where rules it emits compose with the user’s by the same-module same-head union (
derive). It cannot contribute to another module’s heads. - Classifier-last gives tier-honesty. The decidability-tier classifier runs after expansion on the lowered events, so the program lands on its true tier — a macro that emits a recursive rule is classified exactly as if the rule were hand-written, and cannot smuggle the program across a tier boundary.
- Determinism. The declarative layer is strongly normalizing by construction, has no I/O, and emits collections in a canonical order. Rule variables are alpha-canonicalized at lowering (Hygiene), so a macro’s choice of fresh variable names never affects the
.oxbincontent hash. Expansion is therefore a deterministic function of its input, and reproducible builds need no author discipline.
Hygiene
Macro-introduced binders neither capture nor are captured by names at the use site, across all of Argon’s binder namespaces (rule variables, concept/type, rel/metarel, metatype/metaxis + axis values, individuals, trait members). The model is scope-sets with binding spaces (Flatt 2016 + Racket binding spaces): each namespace is an interned scope inside one scope-set, resolved by the unchanged maximal-subset rule. Hygiene is white-box — the quotation applies a fresh macro scope to the identifiers a macro introduces.
The Name resolution ontology-neutrality gate composes as a commuting pass: scope-set resolution (syntactic) produces a candidate binding, then the gate (semantic) checks that a macro-introduced concept resolves to a visible pub metatype — it may reject, but never re-points a reference. Macro-introduced vocabulary is not exempt from the gate.
Two regimes follow from what reaches event identity:
- Rule variables are clause-local and alpha-canonicalized at lowering, so hygiene need only guarantee non-capture; their names are irrelevant to identity.
- Introduced vocabulary (a concept/relation a macro declares) has a semantic, referenceable name, so hygiene gives it a content-derived, stable identity.
Macros are hygienic-only: there is no unhygienic!. A $crate-style self-reference plus syntax-parameter keywords cover the real needs; any future raw escape must name the binding space it breaks into.
Imports and the shared #[…] namespace
A pub macro foo { … } declaration occupies a single symbol in the module namespace. use mod::foo; brings it into scope; foo!(args) invokes it at expression position and #[foo(args)] at declaration position. The ! and #[…] sigils are invocation markers, not separate-namespace tags — there is no distinct macro namespace, and no #[macro_use] form; macro imports follow ordinary item-import rules (Imports).
User macros and compiler-known directives share the #[…] namespace. Resolution is by uniqueness at registration: a name resolves to exactly one implementation, and a user macro colliding with a built-in directive is an OE0705-class error, never a silent shadow (matching the loud-refusal discipline of the directive registry, Directive surface and the MLT decorator path). Identity is keyed to a stable handle, not the spelling, so re-homing a directive (Migration: re-homing the hard-coded surface) does not change how a #[name] resolves.
Procedural macros
A procedural macro computes its expansion by running code, rather than rewriting a pattern into a template. It is not a fn over a TokenStream: the procedural layer is a total, structurally-recursive meta-language over reflected syntax, not general-purpose computation. Totality is what the substrate’s own doctrine requires — it is strongly normalizing and deterministic by construction, and (being a total function over an inductive syntax type) mechanizable in Lean (Lean correspondence).
A procedural macro is a #[procmacro] pub fn in the one macro namespace:
#[procmacro]
pub fn irreflexive(item: Decl) -> Syntax {
let head = concat_idents("__", item.name, "_irreflexive");
quote {
$item;
pub check $head(x: $item.params[0].ty) :- $item.name(x, x)
=> Diagnostic { severity: Severity::Error, code: "Argon::OE1359",
message: "relation `${item.name}` is #[irreflexive] but relates an element to itself" };
}
}
The body is a total, non-recursive fragment: let bindings over the construction builtins and reflection projections, then a trailing quote. The surface is:
quote { … }— aQUOTE_EXPR; its body is re-parsed through the ordinary elaboration path (never emitted as events directly). Splices:$name(alet-bound value),$item(the whole reflected declaration, re-emitted verbatim), and the reflection projections below.- Reflection projections — the declaration’s resolved structure, complete and client-agnostic. On the item:
item.name,item.kind("rel"/"metarel"/"type"/"metatype"),item.arity(decimal string;"0"for a non-relation),item.attr,item.arg,item.param_list(verbatim),item.params,item.fields. Per parameterp(in$( for p in item.params )repetitions):p.name,p.ty,p.is_list,p.is_optional,p.is_last. Per fieldf(in$( for f in item.fields )repetitions):f.name,f.ty, cardinalityf.is_list/f.is_optional, thefrom-clause sourcef.rel/f.role(empty when the field has nofromclause), modifiersf.is_mut/f.is_pub/f.is_intrinsic, the verbatim attribute textf.attrs, andf.is_last.${ if <bool> { … } else { … } }is a total conditional over the boolean projections. Field defaults are deliberately not reflected: a field default is refused at parse (OE0237), so no well-formed default node exists to project. - Resolved-schema projections — the declaration’s structure resolved across the package:
item.all_fields(own and inherited fields via the<:chain, nearest declarer first, de-duplicated so the most-specific declarer wins;item.fieldsstays own-only),item.stored_fields/item.nav_fields(the same set partitioned on whether the field carries afromclause), and per fieldf.declaring_concept,f.has_from, andf.is_forward(endpoint direction:trueiff the projected role is not the backing relation’s first endpoint — the bit that decides scalar wiring vs a loop over the reverse endpoint in a constructor). These resolve from a parse-only, cycle-safe schema graph over the current package’s module tree (EXPAND precedes name resolution, so no elaborated types are consulted; a<:cycle is folded once, matching the substrate’s concept-cycle semantics). An ancestor or backing relation declared outside the package is beyond the phase’s visibility: a macro consumingitem.all_fieldson such a declaration declines the whole expansion loudly (a residual invocation,OE0723, no artifact) — it never silently omits an out-of-package ancestor’s fields. ${expr}antiquotation — an in-string splice (e.g. inside a diagnosticmessage:), resolved by the macro renderer at EXPAND. It is render-time, not runtime string concatenation (the runtime has noText + Text— that is a separate arc).concat_idents(a, b, …)(paste) — builds a raw definition-site identifier from pieces; the single capability the splice-only declarative layer lacks. The result is not freshened — the macro intends it as the stable public head (__{rel}_{prop}); splices are substitution barriers carrying their own use-site identity.
Bodies are non-recursive (trivially total). #[procmacro] resolves as a macro (DefKind::Macro), runs at EXPAND by a dedicated evaluator, and its #[procmacro] directive executes at fn-decl position. #[irreflexive] and #[asymmetric] (Migration: re-homing the hard-coded surface) are genuine procedural macros written exactly this way.
Full structural recursion over reflected Syntax — a totality checker and the classify ∘ expand tier-honesty theorem — is out of scope (Out of scope). Field reflection and resolved-schema reflection (inherited fields and relation endpoint direction, the projections above) are in scope and shipped; the resolution scope is the current package, with out-of-package ancestors refusing loudly as described above.
Directive surface and the MLT decorator path
Every #[…] attribute must resolve to a registered compiler directive or an in-scope pub macro (e.g. #[transitive], re-homed to std::rel, Migration: re-homing the hard-coded surface / RFD 0037 D8 — brought in by use). The directive registry (oxc-instantiate’s DIRECTIVE_REGISTRY) is the single declarative table of every directive the toolchain reads or reserves, with its argument shape, valid positions, and status. There are no silently-ignored attributes: an attribute that is neither a registered directive nor an in-scope macro refuses with OE0705 UnknownDirective (nearest-known suggestion), documented-but-unbuilt directives with OE0706 DirectiveReservedUnimplemented, misplaced directives with OE0707 DirectiveInvalidPosition, argument forms on bare-form directives with OE0709 AttributeArgsNotYetImplemented, and conflicting families with OE0714 DirectiveConflict.
The relation-property directives are library macros, split by mechanism (Migration: re-homing the hard-coded surface):
- Relation-property directives (RFD 0031) —
#[transitive]is a declarativepub macroinstd::rel(closure deriveR(x, z) :- R(x, y), R(y, z)into the relation’s own head);#[irreflexive]/#[asymmetric]/#[functional]are genuine procedural macros (#[procmacro], RFD 0040) that re-emit the decorated declaration ($item) and paste a guardingcheckvia structural reflection.#[functional]dispatches onitem.kindthenitem.arity: BOTH a binaryreland a binarymetarelre-emit$itemand paste a__{rel}_functionalcheck that fires when a source maps to two distinct targets (OE1361). It is binary-only — a functional object property is binary by definition, so a non-binary#[functional]matches no arity arm and the invocation is refused (OE0723; #1273). Arelwhose explicit target cardinality permits more than one target contradicts#[functional]and is refused (OE1378). All three re-home from compiler builtins (#[functional]was the last, soOE1362is retired and an unimported#[functional]refuses asOE0705, like its siblings); each formats Argon source and re-parses it through the ordinary derive/check pipeline. - MLT decorators —
#[categorizes(T)],#[partitions(T)],#[subordinate_to(M)],#[power_type_of(T)], and#[order(N)]are no longer compiler directives at all. They are genuine library#[procmacro]s in the unprivilegedmltpackage (notstd): each re-emits the decorated declaration and emits aMetaPropertyon the metarel (or, for#[order], the metaxis) it shares its name with — reading the argument’s name (or, for#[order], an integer), not its structure. Nothing is ambient: a consumer listsmltinox.tomland imports the vocabulary (use mlt::*;/use mlt::{categorizes};), and an unimported decorator is an unknown attribute macro, refused by the no-silent-attribute gate (OE0705). The emittedMetaPropertyis re-checked at the emission boundary and again at load —#[order]’s value is validated against themlt::ordermetaxis’s declaredNat where { _ <= 16 }domain by the self-validating.oxbinload re-check (OE0623), so the bound holds without any compiler privilege. The reasoner-side enforcement of the MLT closure rules over the emitted metaproperties is authored as themltpackage’s own rules/checks over the neutral substrate (Phase 2).
Directives as library macros
The hard-coded directive surface is library macros, with the user-facing syntax byte-identical to a privileged-path synthesis (RFD 0009 RP-003 GAP-3). The mechanism is decided by the directive’s emit target:
- Relation-property emits ordinary rules (which have surface), so it lives in
std::rel, split by what each member needs:#[transitive]is a declarativepub macro: the macro template produces lowered events byte-identical to a direct synthesis (verified differentially).#[irreflexive]/#[asymmetric]are genuine procedural macros (#[procmacro] pub fn … -> Syntax, RFD 0040): each re-emits the decorated declaration ($item) and pastes apub checkwhose head isconcat_idents("__", item.name, "_{prop}")— the unique ident the splice-only declarative layer cannot build. Their import gate isOE0705(a bare unimported#[irreflexive]is an unknown attribute macro, liketransitive).#[functional]is a genuine procedural macro (#[procmacro], RFD 0040 P2, importable fromstd::rel,OE0705if not — retiring the last#[builtin]and itsOE1362). It dispatches onitem.kindthenitem.arity; BOTH therelandmetarelarms re-emit$itemand paste a__{rel}_functionalcheck that fires when a source maps to two distinct targets (OE1361) — the same check-based enforcement as#[irreflexive]/#[asymmetric](#1377 A2, retiring the former[0..1]-cap reconstruction that silently dropped stacked siblings, user brackets, generics, and bodies). It is binary-only: a non-binary relation matches no arity arm and the invocation is refused (OE0723; #1273), because a functional object property is binary by definition. Arelwhose explicit target cardinality’s upper bound exceeds one contradicts the constraint and is refused (OE1378).
- MLT emits
MetaPropertyevents. Its surface is the unprivilegedmltpackage (RFD 0043): every decorator — the relational#[categorizes]/#[partitions]/#[subordinate_to]/#[power_type_of]and the metaxis-valued#[order(N)]— is a genuine#[procmacro]emitting a re-checkedMetaPropertythrough the emission boundary (Procedural macros). There is no compiler builtin, nostd::mlt, and no reserved MLT diagnostic code; the expander is the package’s own procmacro, not a privileged compiler path. MLT is one higher-order type theory among several (potency, ML2, powertype), so the substrate stays neutral about which one a model commits to. std::temporal/std::lifecyclebecome declarative library macros; whether each operator ships native or library is a scoping choice once the engine exists.
// std/temporal.ar — a declarative desugaring to rule-body atoms (no computation)
pub macro since {
( $lhs:rule since [ $lo:literal , $hi:literal ] $rhs:rule ) => {
@meta_window($t, $lo, $hi), $rhs at $t, holds_throughout($lhs, $t, now)
};
}
Lean correspondence
The substrate at spec/lean/Argon/Substrate/Macro.lean mechanizes the shape of macro declarations — MacroAtom = declarative MacroDecl | procedural ProcMacroDecl as @[language_interface] carriers policed by the drift gate. Per RFD 0037 D7, the substrate boundary is held at the typed AST: macro expansion is untrusted and Rust-side, and assurance comes from re-checking its output — the tier classifier runs last, the content hash is re-derivable, and the drift gate covers the MacroAtom shape. Mechanize the re-checker, not the producer.
The tier-honesty theorem — that classify ∘ expand lands a program on the same decidability class as its expansion — is statable only once expand is a total Lean object over an inductive Syntax type (RFD 0040 D7). The scope-set hygiene algebra and a full expansion-preservation theorem are out of scope.
The MLT-relational kinds — instanceOf, specializes, categorizes, partitions, subordinates — live in spec/lean/Argon/MetaCalculus/MLTKinds.lean, with the round-trip theorem classify_declOfKind; the elaborator emits canonical metarel_decl events via declOfKind.
Testing
A test declares an in-language unit test: a named imperative block run against a fresh world by ox test. Its name is a string literal; its body is the mutate-body statement set (mutate) plus the assert statement. An assert mirrors require, except a failed assert records a pass/fail outcome and execution continues (rather than aborting). Three assertion forms exist: the value/boolean form assert <expr>;; the derivability form assert [not] derivable F(args);, which tests membership / non-derivability of a fact against the materialized extent with world-honest four-valued outcomes keyed on the tuple’s truth value — present unrefuted (is) ⇒ PASS/FAIL, explicitly refuted (not) ⇒ FAIL/PASS under either world, well-founded-undefined (can) ⇒ a loud INCONCLUSIVE for BOTH polarities (an undefined atom is never a definite not; the exact pin is a scenario truth = "Can" expect row), asserted-and-refuted (both) ⇒ PASS/FAIL (positive support exists), absent under closed-world ⇒ FAIL/PASS, absent under open-world ⇒ a loud INCONCLUSIVE (absence is unknown, not false, World assumptions (CWA / OWA)); and the rejection form assert rejects [( Pkg::Code )] { … }, which asserts that a write block is refused by a write-path guard (a where-invariant, a check delta-guard, a group axiom, …) — a genuine guard rejection PASSes (matching a pinned code if given), an accepted write FAILs, and a non-guard error ERRORs loudly so a broken test never masquerades as a passing rejection. derivable and rejects are contextual keywords (leading position only). The runner, the full assertion semantics — including reads over the deductive plane, the outcome tables, and the guard-vs-non-guard classification — isolation, and the scope are specified in The test atom — ox test.
test-decl ::= attribute* 'test' String '{' test-stmt* '}'
test-stmt ::= mutate-stmt // `mutate`: let / insert / update / delete / for / require / calls
| 'assert' expr ';'
| 'assert' 'not'? 'derivable' IDENT '(' (expr (',' expr)*)? ')' ';'
| 'assert' 'rejects' ('(' path ')')? '{' mutate-stmt* '}' ';'
(The fixture / expect block forms are reserved and do not parse.)
pub type Account { name: String }
// A test constructs a world top-to-bottom and asserts over it; `ox test`
// runs each test against its own fresh store and reports PASS / FAIL / ERROR.
test "a constructed account carries its name" {
let cash = insert Account { name: "Cash" };
assert cash.name == "Cash";
}
Discovery. ox test runs test declarations from mod-reachable modules and auto-discovers every *.ar anywhere under the package’s tests/ directory (recursive, any depth — so no test under tests/ silently fails to run), elaborating them as part of the package — so a test placed in the reserved tests/ directory runs without mod-wiring, symmetric with scenarios/*.toml. This is test-mode only: tests/*.ar never enters ox build’s artifact. See Runtime contract.
Isolation. Each test runs against its own fresh store seeded with the package’s declared facts; no test sees another’s writes, so test ordering is unspecified and irrelevant. Within a test, the body’s writes are visible to later assertions, including the facts they derive (read-your-writes over the committed + deductive state). See Runtime contract for the ox test runner and the assertion semantics in full.
Scenario harness — ox run-scenario
An .ar package declares the ontology; it does not carry data. So a built .oxbin queried with no fixtures answers against an empty store. A scenario is a small .toml script that seeds the store and then reads it back, so a model ships with realistic, reproducible state — and, with assertions, with executable checks.
A scenario file lists an ordered sequence of steps. Each [[step]] is tagged by do:
[[step]]
do = "mutate" # invoke a declared mutation
path = "norms::register"
args = { p = "alice", age = 25 }
[[step]]
do = "query" # run a declared query, print its rows
path = "norms::findEmployeeNamesOfCompany"
args = { c = "acme" }
label = "employees of acme" # optional, echoed in the output
[[step]]
do = "derive" # dump a derived predicate's extent
name = "spouses" # the rule head's short name
explain = true # optional: annotate each tuple with its proof tag
[[step]]
do = "compute" # invoke a declared `fn`, print its single result
path = "norms::getSpouse"
args = { p = "alice" }
Document order is execution order. A scenario interleaves writes and reads: populate, look, mutate again, look again — each read sees the state at that point. The four kinds map to the same operations as the standalone commands: a mutate step runs a declared mutation (with the same check-guard handling as a write over HTTP); a query step runs a declared pub query and prints its rows; a derive step dumps a derivation’s extent by its rule-head short name, the read ox derive <oxbin> <rule> performs (with explain = true adding the Rule atom — fn, derive, query, mutate, check proof tag per tuple); a compute step evaluates a declared pub fn and prints the single value it returns.
A query step’s args is what makes a parameterized declared query do a keyed lookup. A pub query findEmployeeNamesOfCompany(c: Company) -> … whose body binds c looks c up only when c is supplied; with no args, the parameter degrades to a free join variable and the query enumerates the whole extent. A derive step takes no args — a derivation has no parameters, so it always dumps the full extent.
Argument typing
Each args table maps a parameter name to a TOML value, typed into the runtime’s value space by one shared rule (the same path expect tuples use, below):
| TOML form | Runtime value |
|---|---|
bare string "alice" | an individual (hashed by name, identical to pub fact P(alice);) |
{ individual = "alice" } | the same individual (explicit form) |
{ text = "active" } | a String value (use this for a string-typed field, since a bare string is an individual) |
integer 25 | Int |
boolean true | Bool |
{ decimal = "0.22" } / { money = "100.50" } / { real = "…" }, or a bare float 0.22 | an exact Decimal / Money / Real, parsed exactly (never via f64) |
{ date = "2024-01-01" } | a Date |
A bare string defaulting to an individual is the load-bearing convention: the same name in the source and in the scenario refers to the same entity. Array values and a non-finite float are rejected with a clear diagnostic.
Assertions — expect
Add an expect table to a step and it stops being a printer and becomes a test: its result is diffed against the expectation and a PASS/FAIL verdict is reported. Which sub-keys apply depends on the step kind:
[[step]]
do = "query"
path = "norms::findEmployeeNamesOfCompany"
args = { c = "acme" }
expect = { equals = [ [{ text = "Dave" }], [{ text = "Alice" }] ] }
[[step]]
do = "derive"
name = "isDisenfranchised"
expect = { empty = true }
[[step]]
do = "compute"
path = "norms::getSpouse"
args = { p = "alice" }
expect = { value = "dave" } # a single expected value
[[step]]
do = "mutate"
path = "norms::register"
args = { p = "bob", age = 12 }
expect = { rejected = "Norms::E001" } # assert a check guard rejects it
- A
queryorderivestep takes the row-set sub-keys:rows = N(exact count),contains = [tuple, …](every listed tuple is present, count-sensitive),equals = [tuple, …](the result set matches exactly, order-insensitive multiset equality),empty = true(no rows). - A
computestep takesvalue = <toml value>: a single expected value. - A
mutatestep takesrejected = "<CODE>": assert the mutation is rejected by acheckguard carrying that user code. A rejection by that code is a PASS and the run continues (the rejected transaction committed nothing); a success or a rejection by a different code is a FAIL. Withoutrejected, a guard rejection halts the scenario.
The correctness crux is that an expected value is typed through the same rule as the step args, and the diff is by value identity — never by rendered string. So an entity-valued result column matches a bare-string or { individual = "…" } expectation, but never a { text = "…" } one; a Text "dave" is never equal to the individual dave. A sub-key applied to the wrong kind (value on a query, rows on a compute, an unknown key) is a loud scenario error, not a silent no-op.
Files, discovery, and running
A package carries scenarios as a sibling demo.toml and/or any number of files under a scenarios/ subdirectory:
ox run-scenario <pkg> # run every discovered scenario
ox run-scenario <pkg> --scenario voting # run scenarios/voting.toml (or a sibling voting.toml)
ox run-scenario <pkg> --scenario path/to.toml
ox run-scenario <pkg> --extent norms::Voter # also print a concept extent after the run
With no --scenario, run-scenario discovers every scenario — the sibling demo.toml plus every scenarios/*.toml, filename-sorted for determinism — and runs them all. Each scenario runs against its own freshly-loaded, freshly-seeded store re-derived from the artifact bytes, so a mutation in one scenario cannot leak into another’s reads and the order across files is irrelevant. --scenario runs exactly one: a path verbatim, or a bare name resolving to scenarios/<name>.toml then a sibling <name>.toml. --extent <concept> prints that concept’s extent once the scenario has run.
run-scenario collects every failed assertion (it does not stop at the first) and exits non-zero if any assertion — or any scenario — failed, naming the offending file. A scenario therefore doubles as a CI-usable check.
A single file uses either [[step]] or the older flat [[mutate]] list (a pure population script, applied in order), never both — the whole point of [[step]] is interleaving, which a split across two arrays cannot express. The flat [[mutate]] form is also what ox query and ox derive apply when they find a sibling demo.toml: they seed from [[mutate]] fixtures, then run their own read. A [[step]] scenario beside one of those commands is not run by them (they have nowhere to surface the interleaved reads); they print a warning pointing at ox run-scenario.
The scenarios/ directory is the integration-.toml surface, distinct from tests/, which holds the in-language test atom (The test atom — ox test). The two are complementary: scenarios/ scripts the runtime from outside the language with a .toml harness; the test atom expresses checks inside the language.
The test atom — ox test
A test is a named imperative block, run top-to-bottom against a fresh store. Its name is a string literal; its body is the mutate-body statement set (let / insert / update / delete / for / require and declared mutation/fn calls — mutate, RFD 0015) plus the assert statement:
test "accountBalance derives the posted amount per account" {
let cash = insert Account { name: "Cash" };
let rev = insert Account { name: "Revenue" };
let e = insert Entry { memo: "sale 1" };
postPair(cash, rev, cash, rev, e, 100.50);
assert accountBalance(cash) == 100.50; // reads the derived value
assert accountBalance(rev) == -100.50;
}
An assert mirrors require exactly, with one difference: a failed require aborts the body; a failed assert records a pass/fail outcome and execution continues. true is a PASS, false a FAIL, and an evaluation error (an unbound reference, a non-boolean result, a missing field, a value not derivable in the current state) is an ERROR (the test errors; it never panics). Setup and assertions interleave naturally, so there is no separate fixture block.
Asserting over the deductive plane. A condition that names a derived predicate or a declared pub query — accountBalance(cash), or the nav-method form x.active_leases() — is evaluated against the reasoner’s materialized extent at the fixpoint of the current committed state, the same read path ox query / ox derive and the for x in <derive> snapshot use. A keyed call (accountBalance(cash)) selects the row whose leading columns match the argument and returns its head-carried value; with no matching row the assert ERRORs (the value is not derivable), and an ambiguous match is a loud error too. Scalar comparisons, field access, arithmetic, and aggregates evaluate as in a require guard. This is the point of testing a reasoning system: an assert checks what the rules derive, not just what was stored.
Asserting derivability — assert [not] derivable F(args). The value-read assert above answers what value a keyed derive carries; the derivability form answers whether a fact holds at all — membership and, crucially, non-derivability. assert derivable F(a, …) passes when a row whose leading columns equal the resolved args is in F’s materialized extent; assert not derivable F(a, …) is its negation. The predicate F resolves the same way the value-read assert and for x in <derive> do — a derive head, a declared pub query, or a base rel. The predicate slot accepts a qualified path (assert derivable students::adult(p), assert not derivable pkg::accounts::adult(p)), resolved by the same ::-suffix matching the rest of the runtime uses: a bare leaf resolves when it is unambiguous, and qualifying disambiguates a name carried by two heads in different modules — that case stays a loud ERROR for the bare form, naming every candidate, so qualification is the way to pick one. Partial args test prefix membership (derivable amtOf(acme) over amtOf(account, money) = “some row has account == acme”); zero args test extent non-emptiness (derivable F()). A predicate that names nothing is a loud ERROR, not a silent outcome. derivable is a contextual keyword — recognized only in leading position after assert / assert not, so it stays usable as an ordinary identifier everywhere else.
The outcome is world-honest and four-valued: the read consults the matching tuple’s truth value — the definite extent, the head’s well-founded-undefined companion, and the explicit refutation extent (pub not_fact) — and only a tuple absent from all three falls to the predicate’s world assumption (World assumptions (CWA / OWA)):
| truth of the asserted tuple | assert derivable F | assert not derivable F |
|---|---|---|
is — a matching row is present, unrefuted | PASS | FAIL |
not — explicitly refuted, no positive support | FAIL | PASS |
can — a well-founded-undefined standoff | INCONCLUSIVE | INCONCLUSIVE |
both — asserted and refuted (a native contradiction) | PASS | FAIL |
absent, F is closed-world (absence ⇒ not) | FAIL | PASS |
absent, F is open-world (absence ⇒ unknown) | INCONCLUSIVE | INCONCLUSIVE |
The can row is the load-bearing case on the deductive plane. An atom the well-founded semantics leaves undefined (a recursion-through-negation standoff, truth values) is neither definitely true nor definitely false — so neither polarity is assertable: a definitely-false atom and an undefined one must never be conflated at this surface, and assert not derivable over an undefined atom is a distinct, loud INCONCLUSIVE, never a silent pass. The teaching detail names the value and the exact-pinning vehicle — a scenario expect row { tuple = [ … ], truth = "Can" } (Scenario harness — ox run-scenario), which pins the third truth value where the polarity form cannot. The both row is the opposite discipline: an overdetermined atom is not unknown — positive support exists, so derivable passes and not derivable fails, the detail naming both. The not row reads an explicit refutation as definite-false under either world assumption: a refutation is present evidence, not open-world absence. The per-tuple classification joins the extents exactly as federation’s information-join does (truth values, can ⊕ not = not): an explicitly refuted standoff is not — the refutation supersedes the undefined companion (and it is not both: a standoff supplies no definite positive support). When a partial-args (prefix) assertion matches several rows, an unrefuted positive match keeps the assertion at plain is (a contradictory sibling row does not mask definite support), and a refutation decides the prefix claim only as far as it reaches: a full-arity refutation refutes exactly the asserted tuple (definite not under either world), while under an open-world predicate a refutation of one tuple beneath a partial prefix leaves the remainder unknown — assert not derivable F(prefix) stays INCONCLUSIVE, the detail naming the near-miss (under a closed world the remainder’s absence is already definite, so the prefix decides).
The open-world absent row is the same soundness case it always was. Under the open-world assumption absence of evidence is not evidence of absence: a missing row is unknown, not false, so closed-world non-derivability is not assertable and must never silently pass. The runner reports a distinct, loud INCONCLUSIVE carrying a teaching detail that names the open-world cause and the two ways forward (assert a positive outcome instead, or mark the concept #[world(closed)] if it should be closed). This composes with the world-honest negation-as-failure of the rule engine: a derivability assert reads the same world each negated atom does.
Asserting rejection — assert rejects [( Pkg::Code )] { … }. Argon is a constraint language; a model’s most important property is which writes its where-invariants and check delta-guards turn away. The two forms above assert positive outcomes — the negative-enforcement half is assert rejects { <mutate-body-stmts> }, which asserts that the block’s write is refused by a write-path guard. The block admits the full mutate-body statement set (insert / insert iof / update / delete / mutation calls); it runs against an isolated copy of the test world, so it commits nothing back regardless of outcome. An optional code pin assert rejects(Pkg::Code) { … } requires the rejection to carry that exact diagnostic code. Like derivable, rejects is a contextual keyword — recognized only in leading position after assert — so it stays usable as an ordinary identifier elsewhere.
The outcome is three-valued, and the guard-vs-non-guard distinction is load-bearing:
| block outcome | assert rejects | assert rejects(Code) |
|---|---|---|
| a write-path guard refuses the write | PASS | PASS iff the rejection carries Code, else FAIL (wrong reason) |
| the block commits with no rejection | FAIL (the write was accepted) | FAIL (the write was accepted) |
| a non-guard error (unbound var, type error, unevaluable) | ERROR | ERROR |
A guard rejection is a genuine constraint refusal on the write path — a where-invariant violation (OE0668), a check delta-guard refusal (which is also how a group axiom — disjoint / complete / partition — surfaces, carrying the check’s own Pkg::Code), an insert iof onto a defined (iff) concept (OE0211), a write onto an abstract or fixed-introduced type (OE0233 / OE0234), a construction-completeness refusal (OE0207 / OE1014), a property write to an unclassified individual (OE0238), a relation write over a non-existent endpoint or past a declared cardinality (OE0232 / OE1341), or a typed-slot write whose individual is classified under a concept a declared disjoint / partition group makes disjoint from the slot’s declared concept — refused on both slot planes, a relation endpoint (OE1428) and a concept-typed field (OE1435), because classifying that individual under the slot’s concept would itself violate the group, so the write can never be satisfied in this state or any later one (Group axioms). Only these satisfy rejects.
The declared-disjointness pair is admitted at its write-time codes only. The same disjointness is also refused one plane earlier, at elaboration, wherever the written value’s concept is statically known — the annotated-parameter spelling, where a mutation parameter’s declared concept already contradicts the slot’s (OE1426 endpoints, OE1434 fields). Those build-plane refusals are not observable through rejects at all, and no widening of this set could make them so: they refuse at ox check, so the package produces no artifact and the test run never happens. A reader who spells the disjoint write with an annotated parameter and expects a passing rejection test gets a compile error instead — the fix is to write the block over a value whose conflicting classification is only knowable at write time (an individual bound by an earlier insert in the test body, as the runtime codes above describe), or to pin the build-plane refusal the way every other ox check refusal is pinned, as a refusal fixture rather than a test. A non-guard error — a typo, an unbound reference, a type error, an unevaluable term, a stray require failure — is reported as a loud ERROR, never counted as a rejection: a broken test must never masquerade as a passing rejection test. An accepted write is a FAIL (and is rolled back, like any other block outcome, so it does not pollute the rest of the test). Use the positive forms to test that a legal write is accepted.
Isolation. Each test runs against its own fresh store seeded with the package’s declared facts (the same fresh-store-per-run discipline as the scenario harness, Scenario harness — ox run-scenario). No test sees another’s writes, so tests are order-independent. Within one test, the body’s writes are flushed and the read-model maintained before each assert, so an assert observes every earlier write — including the derived facts they entail (read-your-writes over the committed + deductive state).
Discovery + running. ox test runs every test from two sources, unioned: (1) any test declaration in a mod-reachable module (e.g. tests/mod.ar reached by mod tests;), and (2) every *.ar anywhere under <pkg>/tests/, which ox test auto-discovers (recursive, any depth — a nested tests/sub/foo.ar is discovered too, so no test under tests/ silently fails to run — filename-sorted) and elaborates as part of the package, so each sees the package’s full vocabulary. This mirrors how ox run-scenario auto-discovers scenarios/*.toml: a test placed in the reserved tests/ directory runs without mod-wiring. A tests/ file that is also mod-reachable is folded exactly once (its tests never double-run). A discovered tests/foo.ar references package items by their package-absolute path (pkg::…). This discovery is test-mode only: ox build / ox check and the shipped .oxbin never include tests/*.ar — tests do not ship. Tests are inert at query / serve time; only ox test enumerates and runs them:
ox test <pkg> # build the package (+ tests/), run every test, report
ox test <pkg> --filter NAME # run only tests whose name/path contains NAME
A tests/*.ar with a parse or resolve error fails loudly (ox test aborts with the diagnostic), never a silent skip; a package with genuinely no tests anywhere prints “no tests found” and exits 0.
ox test builds the package (so the run reflects current source), runs every discovered test, and prints a PASS / FAIL / ERROR / INCONCLUSIVE line per test — a failed or inconclusive assertion shows its rendered source text and why it could not pass — followed by an N passed, M failed, K errored, I inconclusive summary. A test’s rollup status takes the strongest of its assertions in the precedence Error > Fail > Inconclusive > Pass. It exits non-zero if any test fails, errors, or is inconclusive (a test you cannot decide is not green), so ox test doubles as a CI-usable gate (like ox run-scenario).
Assertion forms. Three forms are admitted. The value/boolean form is assert <bool-expr>; — scalar comparisons (==, !=, <, <=, >, >=), field access on bound entities (x.field, chained x.a.b), arithmetic on those, the aggregate/comprehension forms, and the deductive-plane reads above (a derived-predicate / pub query keyed call, and the nav-method x.method() form). The derivability form is assert [not] derivable F(args); with the world-honest four-valued semantics above. The rejection form is assert rejects [( Pkg::Code )] { <mutate-body-stmts> } — the negative-enforcement half above. All three are test-only: a stray assert (any form) in a mutate / fn body lowers to an unsupported form and ox check / ox build refuse it up front with OE1318 (the runtime also rejects it, as defense-in-depth) — it is never a silent no-op.
Only the three assert forms above are admitted; anything else is a loud refusal, never a silent pass.
Truth values
Types
Two related types, exported from std::core and in the prelude:
pub enum Truth4 { Is, Not, Can, Both } // substrate, no payload
pub enum Truth4Of<T> { Is(T), Not, Can, Both(T, T) } // parameterized lift
The unparameterized Truth4 is the substrate bilattice carrier — what the Lean spec mechanizes and what Federation.infoJoin, Consistency.append, and Projection.project operate over. The parameterized Truth4Of<T> is the stdlib presentation type used at query-result boundaries that have already produced typed projections — Is(T) carries the witness for the is verdict; Both(T, T) carries the two disagreeing witnesses.
Locked-substrate semantics (per Foundation/Truth4.lean, Standpoint/Consistency.lean, Standpoint/Federation.lean):
- K3 single-standpoint queries return projections containing only
Is,Not,Can. - FDE
across [...]queries may returnBoth(a, b)when standpoints disagree. #[consistency(paraconsistent)]standpoints admitBothinternally.
Default visibility and K3 projection
Default query return type is the projection type (e.g., Bool, [Lease]); Truth4Of<T> is introduced only when:
- The query has
across [...]. - The return type is explicitly
Truth4Of<…>. - A paraconsistent standpoint admits internal
Both.
K3 fail-closed projection. When a single-standpoint K3-context query projects to a non-Truth4 type, Can is folded into the projection per Pietz–Rivieccio Exactly-True semantics:
- Boolean projections:
Can→false. OnlyIs(true)is designated. - Set / list / record projections:
Can-valued cells are omitted from the result set. - Scalar projections:
Cancollapses fail-closed (omitted for collection-shaped results,falsefor Boolean predicates).
To preserve the distinction between Not and Can at the surface, declare the return type as Truth4Of<T> explicitly.
The compiler emits Info-level diagnostics on every across query reminding the modeler that Both may arise.
World assumptions (CWA / OWA)
A world assumption governs how the absence of evidence is interpreted:
- Closed (CWA) — if a fact isn’t derivable, it’s false. Negation-as-failure is total.
- Open (OWA) — if a fact isn’t derivable, it’s unknown. Negation requires positive disproof.
The blessed default is CWA; OWA is a live per-concept opt-in. Every concept Argon evaluates is closed-world unless it carries a
#[world(open)]mark: negation-as-failure over the stratified / well-founded fixpoint (Rule atom —fn,derive,query,mutate,check, truth values) treats an unmarked concept’s absent membership as definite-false. The package-leveldefault_world(CWA unless a manifest sets it otherwise) governs every concept without an explicit mark. This default is a deliberate choice: an Argon program is a data system whose extents are the authority for what is true, and CWA is the regime that makes “not in the extent” mean “false”. A modeler who wants the OWL/DL open-world reading for a specific concept opts that concept in with#[world(open)], described below (RFD 0045, #787/#796).
CWA is the operative default because Argon’s primary use is application data — counts, payments, schedules, classifications — where the stored extent is the closed world. Concepts that model genuinely-incomplete knowledge (where “absent” means “unknown”, not “false”) opt into the open-world reading per the surface below.
Per-concept OWA opt-in (#[world(open|closed)]) — live. The surface is a #[world(...)] attribute on an ontologically-classified concept:
#[world(open)]
pub type LegalAgent { mut name: String } // OWA — `unknown` is distinct from `false`
The world directive is executed (RFD 0045 D2): the elaborator reads the policy word — open or closed, any other word is a loud refusal — records the concept’s world assumption on the per-concept world map (keyed by qualified concept path), and the reasoner stamps that map onto every evaluation catalog so each negated concept-membership atom is read under its own concept’s world. The attribute is concept-only (OE0707 on other placements). Concepts without the attribute inherit the package default_world (CWA by default).
The OWA semantics. The world a concept declares changes how the substrate reads its absence:
Interaction with negation-as-failure (Rule atom — fn, derive, query, mutate, check). Under CWA, not P succeeds whenever P is not derivable. Under OWA, an absent P(x) is unknown, not a definite not — so a closed-world not H whose derivation depends on an open-world negation does not get to read that unknown as false. Rather than silently over-asserting the head, the substrate refuses that genuinely-unsound shape at ox check / ox build (OE1367 NegatedOpenWorldDerived): mark H’s concept open-world too (so not H tolerates the unknown), or restructure so the open-world negation does not flow into a closed-world-negated head. This is world-honest negation-as-failure: the engine honors default_world and each concept’s mark instead of evaluating closed-world unconditionally. R(args) is unknown reads R’s completed well-founded-undefined companion; it does not turn ordinary open-world absence into rows. Recursive companion reads refuse with OE1441.
Interaction with the write side. A concept’s world governs its compiler-synthesized structural checks. Under the closed-world default a complete/partition covering check fires when no member membership is derivable (OE0241) and a disjoint overlap check refuses a double-classification (OE0240). Under an open-world concept the covering check softens to refuse-on-K3-not — refuse only on a definite violation, tolerating the merely-unknown case — while disjointness stays world-invariant (a positive overlap is definite under any world). This is the RFD 0045 D1/D2 design.
Interaction with the test atom. assert [not] derivable F(x) (The test atom — ox test) is four-valued: it reads the matching tuple’s truth value first, and F’s world only for a tuple absent from every extent. A present, unrefuted row is is (PASS for derivable, FAIL for not derivable); an explicitly refuted row (pub not_fact) is not — definite-false under either world, a refutation being present evidence rather than absence; a well-founded-undefined atom is can — neither polarity is assertable, so the runner reports INCONCLUSIVE for both and points at the scenario truth = "Can" pin; an asserted-and-refuted row is both — positive support exists, so derivable passes. Only genuine absence falls to the world assumption: under CWA it is definite non-derivability (FAIL for derivable, PASS for not derivable); under OWA it is unknown, so closed-world non-derivability is not assertable — the runner reports a distinct, loud INCONCLUSIVE, never a silent pass.
Interaction with refinement (Refinement). The world a concept declares makes iff-derived membership three-valued — unknown does not grant membership, and a primitive where invariant is the dual where only a definite false denies a write — whereas under CWA unknown collapses to false in both directions. The three-valued refinement field-access discipline is a separate mechanism: a refinement predicate reading a field with no recorded value evaluates to unknown rather than failing — the K3 fail-closed boundary of Refinement, orthogonal to the concept-level world assumption. A missing field makes a predicate unknown; the world assumption then decides whether that unknown collapses to false (CWA) or is preserved (OWA).
Required-field completeness under CWA. Under the closed-world default a required field that is unasserted is a schema violation, not an unknown. OE1014 RequiredFieldUnasserted fires for the in-body construction case — a mutate body that classifies an individual with insert iof and populates its fields in the same body — at body end / commit time (see mutate). The construction-time gate for brace construction (insert T { … } missing a required field, OE0207) is unconditional. Under an #[world(open)] concept an unasserted required field is unknown rather than a violation.
Cross-world conservativity (mechanized). A CWA-true result lifts safely into an OWA consumer — positive evidence stays positive when more information may yet arrive. The base transfer theorem is mechanized at TypeSystem/Soundness/CwaOwa.lean (cwa_owa_transfer): a refinement membership established under CWA remains valid when re-evaluated under OWA, and cwa_isCwa_preserved_under_info_increase extends this to a more-informative consistent (K3) state. The reverse is provably not sound (owa_to_cwa_not_sound): a CWA conclusion that holds only via the closed-world collapse of an unknown to false does not transfer. The module is deliberately parameterized over the WorldAssumption — the CWA default is a surface/engine policy, not a substrate theorem, so the mechanization arbitrates the relationship between the two regimes without privileging either. The bitemporal lift of the transfer is in Locality/Temporal.lean.
Module-extraction interaction. Cross-module imports preserve CWA conclusions about the imported signature via ⊥-local module extraction; the Σ-scoped conservativity theorems are in Module extraction (tree-shaking) (Locality/ScopedConservativity.lean, Locality/DomainConservative.lean).
Per-(standpoint, concept), not per-(concept, time). A world assumption is declared per-(standpoint, concept), never per-(concept, time). A modeler needing different closure disciplines across valid-time would declare two or more standpoints with VT-scoped activation conditions and compose them via standpoint federation (Standpoints and modal operators); the substrate WA-map signature has no time argument. Attempting a per-concept closure with an explicit VT scope without standpoint decomposition is refused with OE0810 WorldAssumptionTemporalIndex.
Reasoning
A derive program is a set of rules (derive); its meaning is
the set of tuples those rules derive. This chapter fixes that meaning: the least-fixpoint model of
the positive program, the stratification that orders negation, the well-founded semantics that
handles recursion through negation, and the defeasibility layer that compiles overrides onto all of
it. The surface — what a derive rule looks like and what each clause is allowed to say — is in
derive; this chapter is what the engine computes from it.
Least-fixpoint model
A positive derive program (no negation, no aggregation) denotes a single model: the least
fixpoint of its immediate-consequence operator. Start from the ground facts, apply every rule
once to derive new tuples, repeat until nothing new appears. Over a finite domain this terminates,
and the result is unique — independent of the order rules fire. Multiple derive clauses with
matching head name and arity union into one relation node, so a seed tuple and a recursive clause
over the same head share a fixpoint (seeds ∪ derived-closure).
Aggregation extends the operator without breaking uniqueness, provided each aggregate reads a strictly lower stratum than its head writes (the stratification below). The aggregate fold then sees a converged input relation before it runs, so the fixpoint over the layered program is still unique.
Stratified negation
Negation breaks the monotonicity the least fixpoint relies on: a tuple that adds to a positive
relation can remove a tuple that a not atom guarded. Stratification restores order. The
rules are partitioned along their axis dependency graph — which axes a rule’s body reads against
which axis its head writes — and each layer is evaluated to fixpoint before the layer above it
reads its negation. A rule’s body negation must read a relation already converged in a lower
stratum.
The graph must be acyclic for this layering to exist. The Lean substrate proves (Theorem 2) that a cycle in the axis dependency graph admits a Cat1/Cat2 rule pair whose stratified fixpoint is order-dependent — there is no canonical layer assignment, so the program has no single stratified model. Cross-stratum, acyclic negation is ordinary stratified negation and is evaluated by this layering.
A negation cycle that no stratification can layer — p :- not q, q :- not p — is not
rejected. It is dispatched to the well-founded semantics below. (The diagnostic that once rejected
such a cycle, OE1309, no longer fires from stratification; it survives only as the dispatch seam.)
Well-founded semantics
Negation reads against the well-founded model, computed by the Van Gelder–Ross–Schlipf 1991 alternating fixpoint. WFS is the default because the well-founded model always exists and is unique — every program has exactly one, with no choice for the engine to make. The headline consequence: recursion through negation is accepted. A negation cycle that strict stratification cannot layer is evaluated by the alternating fixpoint rather than refused.
The alternating fixpoint is three-valued: every atom is true, false, or undefined. The
engine materializes the definitely-true extent as the head’s rows, and an atom the alternating
fixpoint cannot pin to true or false — a genuine standoff — is carried alongside as the head’s
undefined extent. The query surface is four-valued: a definitely-true tuple is an Is
row, and an undefined tuple surfaces as a Can row, by default — never silently omitted,
because an omitted standoff is byte-indistinguishable from a definite Not (the epistemic failure
the Truth4 discipline exists to prevent). 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.
For conditional obligations this keeps the sound projection while restoring the honesty: an
obligation whose trigger is genuinely undecided neither fires nor is denied — and the read says
so (Can) instead of omitting it.
The envelope contract: the four-valued agent view carries Can rows inline and counts them in
hidden.can; the human fail-closed K3 projection (opt-in) drops Can rows from the row list but
still reports them in hidden.can — drop-but-count, never a silent collapse. ox derive
prints the undefined extent after the definite tuples (silent when empty). The defeat-plane
evaluator is two-valued wherever the defeat graph is acyclic — where no defeat target depends
back on its own attacker, every head is definitely true or definitely false and contributes no
undefined extent. A genuine mutual-defeat cycle — a target whose attacker’s derivation depends
in turn on that same target (a rule-dependency cycle through the attacker’s derivation, not a
defeat-graph identity cycle, which OE0718 refuses at build time) — has no stable two-valued
answer: those heads are undefined under the well-founded model, surfaced as Can like any
other undefined extent, and are never resolved by evaluation order. The legacy strength-stratified
evaluator remains two-valued by contract.
#[brave] / stable-model semantics — multiple two-valued models, with credulous and skeptical
readings — is out of scope (Out of scope). WFS is
the single evaluation discipline.
Defeasible reasoning
Real ontologies have exceptions and overrides; classical Datalog does not. Argon’s defeasibility (RFD 0028) makes them first-class without letting any rule lie about what it derives. The design has three commitments:
- Honest heads. A rule derives exactly what its head says. An unmarked
deriverule is strict — classical Datalog, exactly asderive; its conclusions cannot be overridden. - The attack is a directive, not grammar. Whether one rule displaces another is a statement about rules, carried in the directive plane above the rule, never a clause inside its body.
- Strategy is a compilation scheme. The meaning of a defeasible program is the meaning of its compilation onto the core stratified/WFS semantics. No separate reasoner runs; the engine stays strategy-blind.
The directive vocabulary
| Directive | On | Meaning |
|---|---|---|
#[default] | a derive rule | this clause is overridable — it holds unless an applicable attacker blocks it (the Rust default fn reading) |
#[defeats(target(args))] | a derive rule | when this rule’s body fires, it blocks the targeted conclusion for the bound tuples |
#[label(name)] | a derive rule | gives the clause an identity, referenced as head.label |
The canonical example — adults can vote by default, felons are disenfranchised, special-class members vote regardless — reads true at every line:
// strict = unmarked: special-class members vote, period (unattackable)
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);
No rule spells the head it denies. The exception lives under its own name (disenfranchised);
the attack rides the directive plane. The pure “block without asserting” defeater (Governatori’s
⇝) is the degenerate case — a #[defeats(…)] rule whose head no one reads.
Targeting
A #[defeats] target is resolution-checked at elaboration (goto-def-able); an unresolvable
target refuses loudly. A target resolves over the attacking module’s import-scoped catalog —
its own rules plus the pub rules reachable through its use imports (RFD 0082),
so an exception can live in another file and name what it overrides under its own honest name.
Scope follows ordinary name resolution exactly: importing the rule (a use leaf, glob, or
alias) puts its bare short name in scope; importing only its module binds the module name, so
its members are addressable by the qualified-path spelling, not by bare name.
Three targeting forms ship, plus qualified-path disambiguation:
- 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. This is lex specialis — the specific rule defeating the general clause’s label, an explicit module-stable edge rather than a pair of magic priority integers. - Trait-qualified —
#[defeats(Vote::can_vote(p) @ A)]: attacks a trait member’s clause at a given impl target, using the qualified catalog naming (Rule-atom grammar). - Qualified-path disambiguation —
#[defeats(other_module::can_vote(p))]: when a bare short name is visible from more than one imported module it is ambiguous and refuses (OE0736); the qualified path resolves the head against exactly the named module. An own-module declaration shadows imported ones, like every other resolution position.
Cross-module targeting has three bounds. Defeat visibility equals ordinary name visibility
(OE0737): a rule the attacking module cannot name is not a legal target — a non-pub rule is
attackable only from its own module and that module’s descendants (the ordinary
descendant-private rule), never from a distance. #[default] is the consent token: a rule can be overridden from another file
only if its own author marked it #[default] (OE0717, unchanged). The edge stays inside one
package (OE0738): a package is a trust and versioning boundary, and a dependency update must
not silently change a program’s conclusions — cross-package composition goes through ordinary
use of public predicates (honest heads), never through a defeat edge.
Arguments resolve against the decorated rule’s variables. #[defeats(can_vote(p))] on
disenfranchised(p) blocks can_vote exactly for the p the attacker derives — per-tuple
blocking, not head-wide suppression. A #[defeats] argument that binds in neither the head nor
the body of the rule is a loud error (OE0721), never a fresh variable.
Discipline
- Strict conclusions are unattackable (
OE0717). A#[defeats]target resolving to a head or clause not marked#[default]refuses: adding rules to a classical program can only add conclusions. - Defeat-graph cycles are refused (
OE0718). The graph over resolved rule identities is acyclic by build-time check; cyclic attack structures are where the well-behaved compilation stories diverge, and Argon refuses rather than picking one silently. - Defeated defeaters are legal — a
#[defeats]rule may itself be#[default]and be attacked in turn (the exception to the exception), as long as the chain bottoms out. - Team defeat, ambiguity blocking. A tuple is in the head’s extent iff some clause not
attacked on that tuple derives it — an unbeaten teammate keeps the conclusion. A blocked tuple
is simply absent from the extent; it does not propagate a third truth value downstream. This
clean present/absent verdict holds wherever the defeat is acyclic. It does not hold on a
genuine mutual-defeat cycle — a target whose attacker’s derivation depends back on that same
target (a rule-dependency cycle, distinct from the defeat-graph identity cycle
OE0718refuses) — where the tuple is neither definitely present nor definitely absent but undefined under the well-founded model (surfaced asCan, above); there is no order-of-evaluation answer. - Downstream rules read warranted extents. Once a tuple is blocked, every later positive body
atom reads the post-defeat extent, including through any number of strict intermediate heads.
Marking a downstream reader
#[default]makes that reader overridable; it does not switch its inputs back to the pre-defeat support catalog. If no rule attacks that reader, it derives the same tuples as an otherwise identical strict reader. - Duplicate labels per head refuse (
OE0719); labels are per-head identities.
Worked: acyclic defeat vs a mutual-defeat cycle
When the attacker’s own derivation never reads back through its target, every head settles — two-valued, no third truth value anywhere. A defaulted discount, attacked by a fraud hold whose body reads only base facts:
#[default]
pub derive discount(o) :- Large(o);
#[defeats(discount(o))]
pub derive fraud_hold(o) :- Flagged(o);
A flagged large order’s discount is definitely false — absent from the extent, not
undefined. The hold fires, the default is blocked, and nothing downstream ever sees a Can.
Now let each attacker’s body read the other default’s head:
#[default]
pub derive keys_a(p) :- Employee(p);
// a's disqualifier holds exactly when b's access does
#[defeats(keys_a(p))]
pub derive conflict_a(p) :- keys_b(p);
#[default]
pub derive keys_b(p) :- Employee(p);
// ... and b's disqualifier reads a's access back
#[defeats(keys_b(p))]
pub derive conflict_b(p) :- keys_a(p);
The defeat graph itself is still acyclic — conflict_a attacks keys_a, conflict_b attacks
keys_b, no attacker is anyone’s target — so the program is admitted, not refused. But the
rule-dependency graph now cycles through negation: keys_a holds unless conflict_a blocks
it, conflict_a holds exactly when keys_b does, and symmetrically back. Neither head can
settle: granting keys_a blocks keys_b, which withdraws the very conflict that would have
blocked keys_a — and the mirror-image assignment is equally supported. There is no stable
two-valued answer, and no evaluation order is allowed to invent one. Both keys_a and keys_b
surface undefined (Can) for every employee, per the envelope contract above.
Because undefined is a defined outcome here — not an error — the elaborator does not refuse this
program; it warns. OW0740 (mutual-defeat-cycle-undefined) fires at elaboration whenever the
resolved defeat plane forms such a rule-dependency cycle through defeat, naming the mutually-defeating
public heads (here keys_a and keys_b) so an author who expected a definite verdict learns those
heads read Can. The lint is representation-independent — it inspects the resolved defeat edges and
the lowered rule bodies, not whether the plane is compiled to strict rules or evaluated by the runtime
resolver — and fires only for this shape: never for the OE0718 identity cycle (refused earlier),
never for a defeat-free recursion-through-negation cycle (the ordinary well-founded treatment above),
and never for the acyclic stratified-defeat fragment (provably two-valued). To recover a definite
classification, break the cycle: remove or redirect one attack so no attacker’s derivation reads back
through its own target. A strict base clause is a partial remedy only — it settles the tuples it
covers (those become definite, and definite verdicts cascade around the cycle for them), but the
heads stay cyclic for every other tuple and the warning stands.
Strategy #1: Governatori with explicit superiority
The strategy is Governatori-style defeasible logic with explicit superiority and ambiguity blocking, specified as its Maher-2021 three-stratum compilation onto the core stratified/WFS semantics:
- Support — run every clause (strict + default, plus the attacker rules’ own honest heads) to fixpoint over the materialized base, and attribute each clause’s contribution over that converged catalog — a recursive clause sees its own prior tuples, so transitive closure does not under-derive.
- Blocking, from surviving attackers — project each attacker’s surviving extent (resolved in defeat-graph topological order, which exists because the graph is acyclic) through its edge argument binding to the blocked target tuples (per-tuple). A defeated defeater contributes nothing — it no longer blocks on the tuples where it was itself defeated.
- Team-defeat fold — strict clauses contribute unconditionally; a default clause contributes the tuples no surviving edge blocks.
Clause attribution composes these strata rather than exposing raw support as a downstream read model. When attributing one defeasible head, the evaluator keeps that head’s own converged support available for recursive clauses, replaces other defeasible heads with their warranted extents, and recomputes strict intermediate heads over those warranted extents. This preserves recursive closure without allowing a downstream default clause to recover a tuple defeated upstream.
The strategy id is recorded in the .oxbin so an artifact is honest about which compilation gave
it its meaning. Future strategies (default logic, courteous LP, argumentation, ASP preferences)
arrive as use-imported macro-vocabulary packages selected per module; the engine never changes.
Proof tags
Every derived fact carries one of four tags (Rule-atom grammar),
surfaced through the provenance channel (ox derive --explain):
+Δdefinitely provable — supported by a strict (unattackable) clause.−Δdefinitely refuted.+∂defeasibly provable — a surviving#[default]clause supports it after defeat resolution.−∂defeasibly refuted — every supporting clause was attacked.
$ ox derive examples/legal_norms_can_vote can_vote --explain
+Δ (dave) // strict special-class clause
+∂ (alice) // surviving adult default
For an artifact whose defeat plane was compiled at elaboration (the strict-WFS lowering
recorded in the artifact’s defeat-provenance section), --explain additionally prints the
compilation header. The header is artifact-scoped — the carrier records the whole module’s
compiled plane, whatever rule is queried — and lists the strategy that gave the compiled rules
their meaning and, per source clause, its #[default]/strict kind, #[label], emitted internal
support relation, and the #[defeats] edges that clause declared (a clause-level target keeps
its .label grain):
artifact: defeat plane COMPILED at elaboration (strategy: Governatori with explicit
superiority); 2 source clause(s) lowered to strict WFS rules
#[default] m::can_vote #[label(adult)] → support $defeat::support::m::can_vote::adult
strict m::disenfranchised → support $defeat::support::m::disenfranchised::#0
defeats m::can_vote.adult
The Lean substrate proves the defeat algebra over each clause’s converged contribution: the
team-defeat fold realizes the declarative per-tuple warranted set
(Argon.Reasoning.Defeasibility.Transform.compiled_extent_eq_warranted), strict conclusions are
unattackable (strict_clause_unattackable), ambiguity blocking holds
(all_supporting_clauses_attacked_absent), and — modelling the blocking sets as derived from
the attackers’ surviving extents rather than as opaque inputs — a defeated defeater no longer
blocks (defeated_defeater_does_not_block, block_mono_in_survivors, pardoned_target_survives).
The safe interaction with occurrence typing carries over — only narrowings established by
strict (non-#[default]) rules are preserved under defeasible attack
(Argon.TypeSystem.Soundness.Defeasibility).
Migration from the strength triple
The pre-RFD-0028 surface — #[strict] / #[defeasible] / #[defeater], #[priority(N)], and
pub priority blocks — is removed, and refuses loudly (OE0722) with no silent aliasing.
The map:
#[defeasible]→#[default].#[defeater](which spelled the head it denied) → an ordinary rule under its own honest head carrying#[defeats(target(args))]; the targeted clause must be#[default].#[strict]→ delete it; unmarked rules are already strict.#[priority(N)]/pub priority→ an explicit#[defeats]edge. 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.
Standpoints and modal operators
Standpoint declaration and federation
standpoint-decl ::= attribute* 'pub'? 'standpoint' Ident ('<:' TypeExpr (',' …)*)?
'{' mod-item* '}'
pub standpoint USFederalTax {
pub type TaxableIncome <: Money;
pub derive owes_tax(p: Person) :- p.income: TaxableIncome, p.income > 0;
}
pub standpoint California <: USFederalTax {
pub derive state_owes_tax(p: Person) :- /* … */;
}
pub standpoint ContestedClaims { /* … */ }
A standpoint is a namespace-bearing module: items declared inside are accessible as StandpointName::ItemName (e.g., USFederalTax::owes_tax). The <: relation between standpoints declares lattice membership (federation ancestor for across [...] queries), not namespace inheritance — a child standpoint does not automatically see the parent’s items; explicit use is required to bring items into scope. Cross-standpoint federation via the across [...] annotation on queries; results may carry Truth4 values.
Nested standpoint declarations are not admitted — lattice edges come from <:, not from textual containment. Submodules (mod) and impl blocks inside a standpoint are permitted.
Federation conflict policy is chosen per query, not per standpoint: a federated across [...] query is paraconsistent by default — disagreement surfaces as Both (see truth values) — and may request strict projection. Conflict policy is a per-query projection, not a standpoint-level declaration.
Visibility composition. Facts are scoped by where they are asserted. A fact/not_fact declared inside a standpoint s { … } block is local to s; one declared at file/module scope, outside any standpoint block, sits in the DEFAULT (unscoped) layer. The two compose by the sheaf reading (Sheaf-theoretic semantics for standpoint federation):
- The DEFAULT layer restricts into every view. An unscoped assertion is visible to an unfederated query, to every standpoint-scoped query, and to every standpoint in an
across [...]federation — it is the broadest perspective, and the sheaf restriction maps carry it into every stalk. - A standpoint-scoped fact is invisible outside its standpoint. It does not appear in an unfederated (unscoped) query, nor in a query scoped to a different standpoint. Reading a scoped fact requires explicitly selecting its standpoint — via
across [...],box/diamond(Modal operators), or a bridge rule (Bridge rules).
So an unfederated query (pub query q() -> P;) reads exactly the DEFAULT layer; a single-standpoint federation (across [s]) reads the DEFAULT layer unioned with s’s own facts; a multi-standpoint federation info-joins those per-standpoint views (Sheaf-theoretic semantics for standpoint federation). Derive rules, checks, and the mutation delta-guard all evaluate over the DEFAULT-layer (base) view — a scoped fact never silently becomes global truth. A federated query that names a standpoint with no declaration is refused (OE1103 UndeclaredStandpointInAcross) rather than contributing an empty extent that would mask the typo. Mechanized at spec/lean/Argon/Standpoint/Visibility.lean: view_of_base_eq_default_layer (base = DEFAULT layer), scoped_view_eq_default_union_own (scoped = DEFAULT ∪ own standpoint), default_visible_everywhere, scoped_invisible_to_base, and scoped_invisible_to_other.
Modal operators
box(P) and diamond(P) are rule atoms (Rule-atom grammar) admitted at tier:modal. They are interpreted over a Kripke frame; the frame is determined by the surrounding context.
Frames. Argon recognizes two intrinsic frames:
- Standpoint frame. Worlds are standpoints; the accessibility relation is the reflexive-transitive closure of
<:over the standpoints in scope (extended by the explicit set of any enclosingacross [...]clause).box(P)at standpointsholds iffPholds at everys'reachable fromsalong the frame. - Classification frame. Worlds are configurations in which an individual exists (store states reachable by mutations). A type introduced by a
fixedmetatype (metatype, RFD 0027 D6) carries forward rigidity: once an individual is a member, themutatemutation gate preserves that membership forward along accessibility (Argon.Runtime.ModifierGates.runMutation_fixed_iof_constant). This is one-directional, not two-way constancy — a current non-member can still later be constructed into afixedtype, sofixedpins membership going forward but says nothing about future gains. A type introduced by an unmodified (dynamic) metatype carries no such guarantee. This is the standard Guizzardi/UFO modal characterization of rigidity, realized operationally by the ontology-neutralfixedbit rather than by reading any vocabulary’srigidityaxis.
diamond(P) is dual: it holds when P holds in at least one accessible world.
Static discharge. When P is a positive type-classification atom over a target introduced by a fixed metatype, forward rigidity preserves membership across accessible worlds and the elaborator can discharge the modal at compile time without consulting a reasoner (Argon.Reasoning.StaticDischarge.box_fixed_discharge). The discharge is polarity-asymmetric: forward rigidity grips box(x : T) but gives no grip on the negation box(¬(x : T)), since a current non-member may still be constructed into the type later (Argon.Reasoning.StaticDischarge.isRigidIn_does_not_discharge_box_neg).
| Atom | Target | Reduces to |
|---|---|---|
box(x : T) | fixed-introduced | x : T |
diamond(x : T) | fixed-introduced | x : T |
box(¬(x : T)) | fixed-introduced | not discharged — needs declared disjointness |
box(x : T) | dynamic (the default) | modal reasoner |
diamond(x : T) | dynamic (the default) | exists w: World, iof(x, T, w) |
box(P) | non-classifier P | modal reasoner |
Atoms that survive static discharge are routed to a modal reasoner over the std::kripke carrier; the choice of backend (tableau, resolution, bounded model-checking) is implementation-defined.
std::kripke. Exported from the stdlib for explicit modal modeling — e.g., writing forall w: World where accessible(current, w), P@w inside unsafe logic { }. The carrier is minimal:
// in std::kripke
pub struct World;
pub struct Entity;
pub fn iof(e: Entity, t: Top, w: World) -> Bool; // instance-of-in-world
pub fn accessible(w1: World, w2: World) -> Bool; // frame accessibility
pub fn current() -> World; // the current world
The surface operators box(P) and diamond(P) are syntactic forms — not function calls — and desugar to FOL over World:
| Surface | Desugared |
|---|---|
box(P) | forall w: World where accessible(current(), w), P@w |
diamond(P) | exists w: World where accessible(current(), w), P@w |
Where P@w denotes the rule atom P evaluated at world w (i.e., relativized to w’s extension). Modelers using only the surface operators need not import std::kripke directly; the desugaring threads through the elaborator. There are no Box / Diamond runtime functions — modal operators reduce to quantification over worlds.
Federation interaction. Inside a query annotated across [s₁, s₂, …], the standpoint frame is extended to the named set; box(P) reads “P holds in every listed standpoint” and may return Both, Can, or Not per truth values when the standpoints disagree. Classification-frame box/diamond atoms compose with federation by being evaluated independently at each listed standpoint, then info-joined via the FDE lattice.
Lowering. box(P) and diamond(P) lower to AtomIR::Modal and classify at tier:modal. Cross-nesting a modal with a temporal operator is refused by the tier classifier. examples/modal_box_diamond.ar exercises both operators end-to-end.
Bridge rules
Bridge rules thread information explicitly between standpoints. Where across [...] composes standpoints lattice-wise (FDE info-join over <:), a bridge rule is a directional, named, typed inference that moves a conclusion from one standpoint to another, optionally applying a domain mapping.
Status. pub bridge is refused at ox check / ox build with OE1102 (audit doc-04 / issue #151). Bridge rules parse, lower to wire events, resolve, and pass well-formedness checks — but the federation fixpoint never fires them, so a built artifact’s bridge bodies would silently never contribute to their target standpoint. Rather than ship that inert surface, a bridge declaration is refused loudly; model the cross-standpoint inference with an explicit derive / pub fact for now. Bridge evaluation is the post-stable flagship standpoint deliverable: the parse / lower / wire / decode surface is retained intact (Module::bridges_targeting, the BridgeDecl codec) so the work resumes without a grammar or wire-format change. The semantics below is the committed design.
bridge-decl ::= attribute* 'pub'? 'bridge' Ident
'(' standpoint-ref '->' standpoint-ref ')'
'{' bridge-rule (';' bridge-rule)* ';'? '}'
bridge-rule ::= rule-body '=>' bridge-head
bridge-head ::= predicate-call // direct assertion into target
| predicate-call 'mapping' mapping-clause // with domain map
mapping-clause ::= '{' (Ident '=' expr (',' …)*)? '}' // src-var ↦ dst-expr
standpoint-ref ::= path // resolves to a standpoint
pub bridge USCitizens(US::Tax -> International::Legal) {
Person(p), p.citizenship == "US"
=> LegalEntity(q) mapping { q.jurisdiction = "US", q.person_ref = p };
Person(p), p.passport != null
=> Verified(p);
}
The -> between standpoint references in the bridge head encodes the direction; existing -> operator (function return arrow) is reused with no new lexer token.
Directionality (key property). A bridge from s₁ to s₂ does not imply a bridge from s₂ to s₁. The asymmetry is what makes bridges different from equality / equivalence axioms; without it, bridge composition would collapse standpoints and destroy their contextual scoping. This is the standard discipline across DFOL (Ghidini-Serafini), DDL / C-OWL (Borgida-Serafini-Bouquet), and MCS (Brewka-Eiter) — see the vault notes on bridge-rule semantics for the comparative landscape.
Body and head. The rule body uses the standard rule-atom grammar (Rule-atom grammar) evaluated at the source standpoint. The head is a predicate call evaluated at the target standpoint. If source and target have different domains (e.g., a tax-jurisdiction Person vs. an international-law LegalEntity), an explicit mapping { … } clause specifies how source variables map to target-domain values; without a mapping clause, the bridge is identity (source variable = target argument).
Negation-as-failure in bridge bodies. Bridge bodies admit NAF (not atom) over the source standpoint’s predicates — matching the MCS form. The bridge graph (nodes = standpoints; edges = bridges) must be stratified for the federation fixpoint to be unique; cycles in the bridge graph emit OE1101 BridgeCycle.
Composition with across [...]. A query annotated across [s₁, s₂] composes lattice federation (via <:) and any bridges between s₁ and s₂ in scope. Both contributions are info-joined via the FDE lattice; conflicts surface as Both per truth values.
Mechanization. The standpoint-frame sheaf semantics (Sheaf-theoretic semantics for standpoint federation) treats bridges as restriction maps on the perspectival sheaf — see that section. Diagnostic codes for malformed bridges live in the OE11xx range (Standpoints / perspectival config).
Bridges under temporal extent (Temporal substrate). When bridges cross modules with bitemporal extent, the substrate admits only temporal-uniform bridges at tier: recursive: a bridge rule consults facts only at the same valid-time at which it fires. Bridges that consult facts at different times (bridge m s t reading s _ t' with $t’ \ne t$) require tier: fol and an explicit temporal-bridge-monotonicity axiom in the module’s prelude. The sheaf-equivalence theorems of Sheaf-theoretic semantics for standpoint federation lift pointwise under temporal-uniform bridges with no new axioms; non-uniform bridges require a new axiom and are out of the conservativity envelope. Diagnostic: OE0720 NonUniformBridgeRefused. Mechanized at spec/lean/Argon/Locality/Temporal.lean (sheaf_equivalence_bitemporal_uniform).
Sheaf-theoretic semantics for standpoint federation
The standpoint lattice carries a sheaf-theoretic semantics — the formal grounding of why across [...] federation, bridge rules (Bridge rules), and box/diamond modal operators all compose coherently.
The sheaf. Treat the standpoint lattice as a base for a Grothendieck topology: open sets are downward-closed subsets of the <: lattice. The perspectival sheaf 𝓕 assigns to each open set U a stalk 𝓕(U) — the local knowledge (set of derivable facts, three-valued atoms, evidence accumulations) available to the standpoints in U. Restriction maps 𝓕(U) → 𝓕(V) for V ⊆ U encode how broader perspectives specialize to narrower ones; explicit bridge rules (Bridge rules) lift to additional restriction maps wherever the bridge graph reaches.
| Modal / standpoint construct | Sheaf-theoretic reading |
|---|---|
Standpoint s | Open set in the lattice topology |
box_s(P) | Section of 𝓕 over s’s open set (P holds across the perspective) |
diamond_s(P) | Local non-zero stalk witness (P holds somewhere in s’s open) |
<: between standpoints | Open-set inclusion |
| Bridge rule (Bridge rules) | Restriction map 𝓕(U) → 𝓕(V) along the bridge direction |
Consistent federation (across [s₁, s₂] returns Is(v)) | Global section over U(s₁) ∪ U(s₂) |
Irreducible disagreement (Both(a, b)) | Non-trivial first cohomology H¹(𝓕) ≠ 0 |
Substrate theorems. The correspondence is proved over a finite model at spec/lean/Argon/Locality/SheafEquivalence.lean — no axioms, no sorry: bottom_up_is_equilibrium, equilibrium_is_global_section, and equilibrium_is_minimal_section. Stated informally: the bottom-up evaluation of the federated standpoint system produces an MCS-style equilibrium, and that equilibrium is a minimal global section of the perspectival sheaf. The full categorical construction — a genuine Grothendieck topology with general restriction maps — is open research (it would unify two independent formalizations of perspectival reasoning; see the vault note “Topology of Understanding”). The finite-model theorems are the substrate commitment.
Consequence (H¹ interpretation). When across [s₁, s₂] yields Both(a, b) (the FDE conflict outcome), the first cohomology H¹(𝓕) carries a non-trivial generator. The two witnesses a, b are cohomologically distinct — they witness an irreducible perspectival disagreement that no global section can unify. This is the formal content behind the #[consistency(paraconsistent)] policy: the substrate makes the disagreement explicit rather than collapsing it.
Consequence (sheaf cohomology as diagnostics). Future tooling may surface H¹(𝓕) generators as a diagnostic class — “your federation disagreement is reducible (cohomologous to zero, fixable by a bridge rule) or irreducible (a non-trivial cohomology generator, requiring paraconsistent acceptance).” This is research-grade; the substrate axioms are the commitment.
Lift under bitemporal extent. When the standpoint federation runs over modules with bitemporal extent (Temporal substrate), the three sheaf-equivalence theorems lift pointwise without new axioms — provided bridges are temporal-uniform (Bridge rules). The bitemporal BeliefAssignment, BridgeEval, and LocalFixpoint become time-indexed functions; at each time slice, the canonical theorem applies. Mechanized at spec/lean/Argon/Locality/Temporal.lean (bottom_up_is_equilibrium_bitemporal, equilibrium_is_global_section_bitemporal, equilibrium_is_minimal_section_bitemporal, bundled as sheaf_equivalence_bitemporal_uniform). Non-uniform bridges (consulting facts at different VTs) require a new temporal-bridge-monotonicity axiom and are out of the conservativity envelope.
Decidability
Tier ladder
| Tier | Adds | Cost |
|---|---|---|
structural | subsumption (<:), disjointness, role hierarchies, partition | polytime |
closure | transitive closure, role composition, functional/inverse, reflexive/irreflexive | polytime |
expressive | qualified cardinalities, full negation, class expressions | decidable, EXP worst case |
recursive | Datalog with negation; recursion-through-negation evaluated under WFS (default) | decidable |
fol | full FOL via unsafe logic { } | semi-decidable |
modal | modal/standpoint over std::kripke | modal+FOL |
metaorder | unbounded multi-level / higher-order instantiation (Higher-order modeling, theories ship in stdlib) | bounded decidable; unbounded not |
Default module tier: structural.
Temporal sub-tier (orthogonal axis). Every program has a tier pair (main, temporal). The temporal sub-tier ladder:
| Sub-tier | Admits | Data complexity |
|---|---|---|
none | no temporal operators | n/a |
snapshot | bare iof(x, T) with bitemporal storage; no temporal operators in rules | n/a |
acyclic-stratified | MTL-acyclic stratified DatalogMTL¬ on ℤ | P |
stratified | full stratified DatalogMTL¬ on ℤ | PSPACE |
forward-propagating | forward-propagating stratified DatalogMTL¬ on ℤ | PSPACE (no past-future mixing) |
unstratified | WFS through unstratified MTL recursion | OPEN; admitted only at tier: fol via unsafe logic |
The classifier accepts a program if both axes are tractable and the program respects additive composition between modal (box/diamond) and metric temporal operators — no nesting of one family inside the other. Cross-mixed nesting box(box_minus[a,b](C)) is refused at tier: recursive (OE0712 ModalTemporalCrossNestRefused) and routed to tier: fol. The grounding is Demri-Wałęga 2024 (Standpoint LTL): satisfiability is EXPSPACE-complete unrestricted, with a PSPACE fragment under interplay restriction that Argon’s rigidity-of-standpoints commitment satisfies.
Annotation:
#[dec(tier: recursive, temporal: stratified)]
mod my_module { … }
If only one axis is annotated, the other is inferred from the module’s content.
Mechanized at spec/lean/Argon/Decidability/Temporal.lean (TemporalSubTier lattice + additiveComposition predicate + admitsAt_admits soundness lemma).
Annotations
#[dec(tier:<name>)]
#[budget(heartbeats: Nat)]
#[scope(max: Nat)]
#[brave] // stable models (reserved)
#[coinductive] // greatest fixpoint
#[constructive] // required at tier 5+ for non-ground NAF
#[shape(<recognized-shape>)]
#[lattice(K3 | FDE | Boolean)]
#[consistency(strict | paraconsistent)] // on standpoint
#[world(open | closed)] // on concept (World assumptions (CWA / OWA); live — marks the concept's world, default CWA)
#[default] // on derive — overridable clause (Defeasibility, RFD 0028)
#[defeats(target(args))] // on derive — declares an attack (Defeasibility, RFD 0028)
#[label(name)] // on derive — clause identity, head.label (Defeasibility)
#[intrinsic] // on iof-body field
#[unproven] / #[assumed] // on test
#[language_interface] // on Lean-spec-bound items
Theory-specific annotations (e.g., #[order(N)] from the mlt package, #[potency(N)] from a potency package) are library macros in unprivileged theory packages, not language-level attributes — see Higher-order modeling and Stdlib (selected).
Reserved attribute names. The compiler-known attribute names listed above are reserved. A user pub macro <name> { … } or #[procmacro] pub fn <name>(…) declaration whose name collides with the reserved set is rejected at the declaration site with OE0708 ReservedAttributeName. The full reserved set:
brave, budget, coinductive, consistency, constructive, dec, default,
defeasible, defeater, defeats, derive, intrinsic, inverse, label,
language_interface, lattice, managed, no_implicit_prelude, priority,
procmacro, scope, shape, unproven, assumed, world
(defeasible, defeater, priority are removed surfaces — they stay reserved so a typo refuses with a migration hint, RFD 0028 D9 — and default, defeats, label are the replacements.)
This list is fixed; adding to it requires an edition bump (the edition = N field in ox.toml). Theory-specific attribute names (categorizes, partitions, subordinate_to, power_type_of, order, complete, potency, @n, …) live in their own unprivileged theory package (mlt, potency, …) as #[procmacro] / pub macro declarations, not as language-reserved attributes — see Higher-order modeling and Stdlib (selected).
unsafe logic
The only escape to full FOL:
unsafe logic {
#[scope(max: 10)]
forall p: Person where exists q: Person where ParentOf(p, q)
}
The body admits the rule-atom grammar of Rule-atom grammar; FOL binding quantifiers (forall x: T where … and exists x: T where …) are accepted without tier-cap rejection. No new connectives or syntax are introduced — only the elaborator’s tier classifier differs. The tier classifier statically routes the block to a backend (native / SMT / ASP / FOL prover) and emits a per-block verdict: TERMINATES, TERMINATES UNDER BOUND (requires #[scope] / #[budget]), or REJECTED.
Higher-order modeling
Multi-level / higher-order theories live in ordinary first-party packages with zero compiler privilege — never in the language core and never in std. The substrate provides the primitives — iof (Reflection), <: (Vocabulary concepts and the generic type metatype), the meta-calculus (Meta-calculus atom), well-founded instantiation, stratified aggregates over the iof DAG (derive) — and theories layer on top. The substrate is deliberately neutral about which higher-order theory a model commits to, so none of them belongs in std (RFD 0043).
The mlt package’s ability to treat iof and specializes as first-class predicates — passing, counting over, and quantifying across type-values — rests on the substrate reflective sort TypeRef (Reflection), whose values are references to declared types; this realizes RP-003 GAP-1. The substrate TypeRef sort is the neutral handle layer and is distinct from any library construct built atop it: it is not an MLT* orderless Type term (whose powertype/order semantics live in that package) nor a vocabulary’s own Type_ concept (an ordinary declared type). TypeRef carries no order, potency, or categorization — those remain the theory packages’ concern.
Higher-order theories ship as unprivileged packages at tier:metaorder (Tier ladder), e.g.:
| Package | Theory | Surface keywords |
|---|---|---|
mlt | Multi-Level Theory (Carvalho-Almeida-Fonseca-Guizzardi) | categorizes, partitions, subordinate_to, power_type_of, order |
mlt::star | MLT* orderless extensions | Type, orderless sortality |
potency | Deep Instantiation / Potency (Atkinson-Kühne, MetaDepth) | potency, clabject, @n annotations |
Each package provides:
- its metaxes (e.g., the
mltpackage declarespub metaxis order for metatype = Nat where { _ <= 16 }), - its metarels (e.g., the
mltpackage declarespub metarel categorizes(higher: type, base: type)), - the decorator forms as library
#[procmacro]s (e.g.,#[categorizes(Animal)]re-emits the decorated declaration and emits a re-checkedMetaPropertyon thecategorizesmetarel;#[order(2)]does the same on theordermetaxis), - derived level functions over the iof graph,
- check rules enforcing the theory’s axioms (e.g., the
mltpackage enforces Carvalho-Almeida S1–S5), - the theory’s constraints as its own rules/checks over the neutral substrate — never compiler-reserved diagnostic codes; the substrate ships no theory-namespaced codes.
Intrinsic-by-default for higher-order types. When the mlt package’s cross-level decorators (#[categorizes], #[partitions], #[subordinate_to], #[power_type_of]) are applied to a type declaration, the type’s fields default to #[intrinsic] — every iof-instance must supply a value (field = value at kind level) or carry a from-navigation source (struct and enum — language built-ins (data declarations)). A field with neither emits OE1908 IntrinsicPropertyMissing against every iof-instance. Modelers opt a field out by declaring it T? (optional, yields None when absent). The field-default opt-out (field: Type = expr) refuses with OE0237 FieldDefaultUnsupported (see struct and enum — language built-ins (data declarations)). Parallel decorators in a potency, ml2, or user theory package may declare their own intrinsic-default rules.
A modeler picks a theory by listing its package in ox.toml and importing it: use mlt::*; brings MLT’s vocabulary into scope; a third-party vocabulary package (e.g. an externally-authored UFO package) may re-export mlt::* plus its own metatypes (kind, role, phase, …) on top.
The std::level::Stratified trait gives consumers a theory-agnostic level handle:
// in std::level
pub trait Stratified {
type Level;
fn level(self) -> Self::Level;
}
Each theory implements Stratified for its entities; consumers depend on the trait, not the theory.
See the stdlib reference (Stdlib (selected)) for full package documentation. Decidability tier metaorder (Tier ladder) admits unbounded multi-level / higher-order instantiation; theory packages may surface their own bounded variants (e.g., a package may cap MLT order to lower the module to a polynomial tier).
Prior versions of this specification embedded MLT directly in Higher-order modeling with built-in decorators and OE19xx diagnostic codes. That commitment is superseded; see
spec/research/RP-003-mlt-as-library.mdfor the feasibility study and migration rationale.
Build pipeline and .oxbin
An Argon workspace compiles to one content-addressed file: .oxbin. This chapter is the build contract: what ox build accepts and rejects (ox build is fail-closed), the pipeline that produces the artifact (Pipeline), how a composition gets its cryptographic identity (Composition signature), the artifact’s section model (Section model), versioning (Versioning — four orthogonal axes), content-addressing (Content-addressing), and load-time validation (Load-time validation — two layers). The byte-level wire format is documented in the oxc-oxbin crate (compiler/crates/oxc-oxbin/FORMAT.md); the axiom-ADT body variants the events section carries are catalogued in Axiom-ADT variant catalog.
ox build is fail-closed
A built artifact evaluates every rule it contains, or it does not exist. ox build runs three gates in order; any gate that fails aborts the build and writes no .oxbin. A wrong artifact is worse than no artifact.
-
Parse gate. Elaboration is error-tolerant — it recovers from malformed input by dropping the broken construct. Left unchecked, a parse error would silently produce an
.oxbinmissing whatever the parser choked on. Soox buildfirst collects every error-severity parse diagnostic; if any exist, it prints them and exits 1 before resolution runs (oxc-driver/src/lib.rs:759). -
Resolution and type gate. It then runs the full resolve/typecheck pass (
collect_check_diagnostics). Any error-severity diagnostic — an unresolved predicate, a type error — aborts the build with no artifact. -
Un-evaluable-rule oracle gate. The lowering pipeline admits some rule shapes the runtime’s rule compiler then refuses. Such a rule is silently dropped from evaluation, so an artifact containing one returns wrong (under-derived) query answers with no runtime error. To prevent that,
ox buildloads the elaborated artifact and re-runs the runtime’s owncompile_ruleas an oracle over every rule (Module::uncompilable_rules). If any rule will not evaluate, the build fails loudly and writes nothing.ox checkenforces the same oracle through the driver’s internalvalidate_runtime_evaluable_rulespath, so editor/CI validation cannot green-light a package thatox buildlater rejects. This intentionally makesox checkdo the extra work needed for artifact-parity validation: package elaboration, stdlib loading, comptime lifting, event encoding, andModule::load. Refused shapes:Shape Diagnostic Unsafe rule — a head / negated / comparison / compute variable not bound by a positive body atom (range-restriction) OE1303Nested aggregate ( count { count { … } > N })OE1311Collection/string aggregate kind ( collect/set_collect/string_join/percentile); the folding kindscount/sum/min/max/avg/count_distinctevaluateOE1312notwrapping an aggregate subqueryOE1313Empty aggregate body ( count { })OE1314Unsupported quantifier shape ( existsbinder,forall(path, T), under-specifiedforall)OE1315Rule bodies the semi-naive executor does not compile — a type-test atom or a meta-equality (
==onmeta(x)) — are recorded the same way and surface as the same load-time refusal (oxc-runtime/src/lib.rs:299).The oracle reports executor-capability failures. A hard
Module::loadfailure surfaces through normal artifact load/build diagnostics.The fix is always to rephrase the rule into a supported shape — typically by exposing the offending subterm through an intermediate
pub deriverule. The supported universalforall v: T where Body, Head(the lastwhereatom is the consequent, the rest the domain) does evaluate; it lowers to a count-equality.
Pipeline
ox build <dir> searches upward from <dir> for the nearest ox.toml and treats that directory as the package (oxc-workspace::find_nearest_package_dir), the same cargo-style discovery ox check and ox lsp use. Module resolution then handles real nested package layouts — pkg::, super::, self:: path roots and pub use re-exports (see Modules and packages; not repeated here).
Output location. Build products never sit next to sources — cargo-style, the artifact is written under a target/ directory: <root>/target/<name>.oxbin, where <root> is the package directory (the one holding ox.toml) and <name> is the resolved entry’s file stem (so a package whose entry is root.ar yields target/root.oxbin). For a standalone .ar file with no enclosing manifest, target/ is created beside the file. target/ is created on demand and is git-ignored. -o/--out overrides the path entirely. The artifact-loading commands (ox query, ox derive, ox run, ox run-scenario, and ox runtime serve --oxbin) accept a package directory or source .ar and resolve the same default target/ location, or an explicit .oxbin path.
The pipeline has a sharp boundary: oxc is the per-package compiler; ox is the workspace orchestrator. Every cross-package concern lives in ox.
per-package source
↓ oxc elaborate ← oxc owns (sync; salsa-cached)
per-package CoreIR
↓ oxc instantiate ← oxc owns; consumes wiring
per-package .oxc cache
↓ ─┐
(workspace metadata + lockfile) │ ox owns:
↓ ox build │ • compute wiring diagram
↓ │ • compose standpoint lattice
↓ │ • check tier cap
↓ ─┘ • merge .oxc → .oxbin
workspace .oxbin
↓ runtime backend (Runtime contract)
answers / docs / mutations / …
Three artifacts, three typed transitions:
- Source — the
.arsurface text plus the per-package manifest. .oxccache — one file per package: the elaborated, instantiated serialization of that package’s contribution relative to this composition. Same section model as Section model, smaller scope..oxbin— the workspace artifact: one file perox build, content-addressed, consumed by a runtime backend (Runtime contract).
oxc never sees the workspace. It elaborates a single package against a wiring diagram ox supplies; it cannot resolve cross-package references, merge standpoint lattices, check tier consistency across packages, or emit the workspace .oxbin. ox is the only command modelers invoke directly (ox build, ox run, ox query, ox check, ox test, ox doc, ox inspect, ox runtime serve, per reserved keywords); oxc instantiate / oxc emit core-ir / oxc dump are tooling-internal.
Build scope (RFD 0030). ox build produces a single, monolithic .oxbin per invocation covering the entry’s dependency closure: each [dependencies] path dependency’s modules are loaded and folded into the consumer’s workspace (exactly as the embedded stdlib is folded), elaborated into the same event stream, and embedded in the one artifact — so a dependency’s content participates in the artifact’s content hashes (Content-addressing). Cross-package references across a path dependency resolve at build time. The registry/git dependency surface (a bare version requirement, the version/git/branch/tag/rev/registry keys, ox.lock) is out of scope (Out of scope) and refuses with OE1240. The separate per-package .oxc cache and workspace-merge orchestrator below (Composition signature) describe the composition model the registry story realizes.
Composition signature
The wiring diagram is the workspace-level resolution of every cross-package symbol reference — the closed-world view of how packages compose. ox computes it from the package graph and the lockfile.
WiringDiagram :=
map from (consumer_package, ref_site)
to (target_package, target_qualified_path, target_stable_id)
The composition signature is the cryptographic identity of one composition:
composition_signature := BLAKE3-256(
wiring_diagram_hash ∥ // BLAKE3-256 over the canonical resolved symbol table
standpoint_lattice_hash ∥ // BLAKE3-256 over canonical-form composed lattice
tier_ladder_version ∥ // u32 LE (preamble, Versioning — four orthogonal axes)
runtime_contract_version // u32 LE (preamble, Versioning — four orthogonal axes)
)
The hash is BLAKE3-256 throughout the content-addressing story (RFD 0001 Phase 2 replaced SHA-256). ox build computes wiring_diagram_hash as a BLAKE3 over the canonically-sorted (axiom-kind, content-id) of the structural axiom events — the schema-defining declarations, axioms, and rule-modes — so it is sensitive to a relation/concept rename or a rule change but stable under instance-data churn (asserted facts carry no structural weight); standpoint_lattice_hash is the same construction over the standpoint and bridge declarations.
The wiring diagram source is not preserved in .oxbin — it is a transient build-time input. Only the four sub-hashes survive (wiring_diagram_hash and standpoint_lattice_hash in global-control; the two version u32s in the preamble). ox build checks that the wiring is correct; the runtime re-verifies at load that the stored sub-hashes have not been tampered with. Two semantically equivalent compositions produce byte-equivalent .oxbin files — the central content-addressing invariant (Content-addressing).
Section model
A .oxbin is a preamble followed by a sequence of typed sections. Ten section types are defined — five mandatory, five optional:
| Section | Mandatory | Load discipline | Holds |
|---|---|---|---|
global-control | ✓ | synchronous | Section directory; the two sub-hashes; tier caps |
symbol-table | ✓ | synchronous | Every reference resolves through it |
events | ✓ | synchronous (mmap) | Source of truth for axioms, retractions, derived events |
standpoint-lattice | ✓ | synchronous | The lattice each event row’s standpoint_id indexes into |
tier-table | ✓ | synchronous | Per-rule tier-pair classification consumed by Layer 2 (Load-time validation — two layers) |
fork-lineage | optional | sync when present | Omitted by artifacts without forks (most published packages) |
projection-cache | optional | LAZY mmap | Maintained projections; runtime re-saturates if absent |
arrangement-section | optional | LAZY mmap | DBSP-shaped Z-set arrangements; forward-compat for IVM |
pathology-flags | optional | sync when present | Compile-time-detected reasoning pathologies |
doc-blocks | optional | LAZY per-item | Doc strings; the runtime works without them, ox doc requires them |
Optional and LAZY are independent: optional governs presence in the artifact; LAZY governs whether the runtime mmaps the section on first access rather than reading it in the synchronous load prefix. Section type-ids live in one uint8 namespace — 0..99 standard, 100..199 Argon-future, 200..255 user-custom (unknown custom sections are ignored on load unless MANDATORY-flagged). The section directory in global-control indexes every section; its order is canonical load order. The runtime tolerates a mandatory section that is present but empty. Byte layout of the preamble, per-section sub-headers, and per-section encodings is documented in the oxc-oxbin crate.
Versioning — four orthogonal axes
The preamble carries four independent uint32 version axes, each bumped on its own:
| Axis | Bumped when | Minor / major |
|---|---|---|
oxbin_format_version | Section model or encoding changes | Minor: additive section; major: breaking section/encoding |
core_ir_version | The CoreIR axiom-ADT variant set changes | Minor: additive variant/field; major: removal/breaking |
tier_ladder_version | The decidability ladder definition changes | Major only — tiers are semantic, not additive |
runtime_contract_version | The OxbinRuntime trait surface (Runtime contract) changes | Minor: additive method; major: breaking signature |
Robustness: producers strict, consumers liberal, per axis.
- A consumer accepts a future minor bump on any axis: load proceeds and unknown additive things degrade gracefully (an unrecognized non-
MANDATORYsection is skipped). - A consumer refuses a future major bump on any axis: load fails with
OE1202 IncompatibleVersionAxis(axis, want, got). - A consumer refuses any
MANDATORY-flagged section it does not recognize, regardless of axis:OE1203 UnknownMandatorySection(section_type).
Within one oxbin_format_version major, an artifact MAY carry two representations of the same logical content side by side (e.g. a DRedc-shaped projection-cache and a DBSP-shaped arrangement-section); each runtime picks the section it understands.
There is no kernel_api_version axis — the runtime contract is the OxbinRuntime trait (Runtime contract), versioned by runtime_contract_version. A backend’s internal schema version (e.g. the Postgres backend’s migrations, Storage layer) is not carried in the preamble; backends version their own schemas.
Content-addressing
The artifact-level hash is a Merkle root over the composition and the section directory:
artifact_hash := SHA-256(
composition_signature ∥ // Composition signature
section_directory_canonical_form // deterministic-CBOR section directory
)
Each section additionally carries a content_hash (SHA-256 over its encoded body) when CONTENT_HASHED-flagged. Two semantically equivalent compositions produce byte-equivalent .oxbin files, hence an identical artifact_hash. This holds because every section uses deterministic encoding (RFC 8949 deterministic CBOR for structural sections) plus per-section canonical-form rules (canonicalization is part of the oxc-oxbin format contract). The payoff:
- Distributed caching — re-running
ox buildon the same inputs is a cache hit onartifact_hash. - Selective invalidation — a TBox change touches only the
eventscontent hash; downstream caches invalidate at section granularity. - Content-addressed distribution — an
.oxbincan be named by itsartifact_hashin a CAS/Nix/IPFS-style registry. - Tamper detection — any byte changed after
ox buildis caught at load.
Load-time validation — two layers
The runtime contract requires two validation layers.
Layer 1 — closed boolean. Computed once global-control is parsed, before any other section body is read; constant-time per artifact:
tier_valid(.oxbin, runtime)
:= global_control.max_tier_claimed ≤ runtime.max_tier_supported
∧ global_control.max_temporal_claimed ≤ runtime.max_temporal_supported
max_tier_claimed and max_temporal_claimed reflect the highest tier pair (per Decidability and RP-004) of any rule in the artifact. Failure refuses the artifact with OE1204 TierMismatch(want, got).
Layer 2 — per-section verifier. Run after Layer 1 passes:
| Invariant | Section(s) | Diagnostic |
|---|---|---|
| Symbol resolution | references in events/rules/queries | OE1210 SymbolResolutionFailed |
| Standpoint lattice acyclicity | standpoint-lattice | OE1211 LatticeCycle |
| Provenance well-formedness | derivation columns in events | OE1212 ProvenanceNotDNF |
| Composition-signature consistency | preamble + global-control | OE1213 SignatureMismatch |
| Tier consistency | tier-table vs max_tier_claimed | OE1214 TierTableInconsistent |
| Doc-block well-formedness | doc-blocks (when present) | OE1215 DocLinkUnresolved |
Policy is per runtime kind, made explicit at OxbinRuntime::load (Runtime contract):
- Sandboxed runtimes (untrusted content) default strict — any Layer 2 failure refuses the load.
- Trusted runtimes (in-process from a trusted
ox build) default lenient — non-load-bearing failures (e.g.OE1215) warn and proceed; symbol-resolution and lattice failures are always fatal.
RP-004 hooks
The bitemporal substrate of RP-004 (Types and refinement, Rule atom — fn, derive, query, mutate, check, Decidability, Standpoints and modal operators) flows through .oxbin natively:
- The
eventssort key is(tenant_id, fork_id, standpoint_id, predicate, valid_time, tx_time)— bitemporal-native ordering, no Layer-2 rewrite for temporal queries. global-controlcarries bothmax_tier_claimedandmax_temporal_claimed; Layer 1 checks both.tier-tablerows carry a(main_tier, temporal_tier)pair per rule, mirroringDecidability/Temporal.lean’sclassifyPair. Cross-nest violations (OE0712 ModalTemporalCrossNestRefused) are rejected at compose time, before the artifact is emitted.pathology-flagsincludes an “additive-composition violation detected at compose time” flag, distinguishing provably-correct artifacts from trusted-by-compiler ones.
Runtime serving command
ox runtime serve exposes the Runtime contract serving surface around one compiled artifact. It does not compile source — a caller runs ox build first and passes the resulting .oxbin.
ox runtime serve \
--project . \
--oxbin target/main.oxbin \
--host 127.0.0.1 --port 7780 \
--storage mem
Postgres-backed serving uses the same .oxbin plus a durable ABox event log (--storage pg --database-url "$DATABASE_URL"). Watch-mode compilation, if tooling enables it, is a separate process that rewrites the artifact; the serving process hot-reloads only after the Runtime contract compatibility checks pass.
Mechanization
The wire shape, validation predicates, composition_signature, and four-axis versioning are mechanized in spec/lean/Argon/BuildArtifact/: Section.lean (section type + flags, @[language_interface]), Header.lean (preamble + version axes), Oxbin.lean (the Oxbin record + section invariants), ContentHash.lean (Merkle-of-sections), Validation.lean (Layer 1 + Layer 2 predicates), CompositionSignature.lean, and Versioning.lean (four-axis robustness). The Rust crate oxc-protocol::oxbin mirrors the @[language_interface]-tagged inductives; CI fails on drift.
Editor extensions — oxup extension
The Argon editor integration is installed and kept in sync by the toolchain manager (oxup), not by hand. oxup extension (alias ext) manages it, abstracted over editors (RFD 0032).
oxup extension install [--editor <vscode|cursor|codium|code-insiders|code-server|neovim|vim|emacs>] [--archive <path.vsix>]
oxup extension uninstall [--editor <name>]
oxup extension list
install (no --editor) auto-detects every installed VS Code-family editor (by its CLI on PATH) and installs the extension matching the active toolchain version into each. The version coupling is real: the published .vsix is stamped with the toolchain version and addressed at an immutable CDN path, https://install.argon-lang.org/editors/vscode/<version>/argon-<version>.vsix. oxup resolves the active toolchain’s concrete version (from its manifest.toml), fetches that .vsix, sha256-verifies it against the published sidecar (the same fail-closed discipline as the toolchain fetch), and hands it to the editor CLI’s --install-extension. --archive <path.vsix> installs a local .vsix offline instead of fetching.
oxup init (non-minimal) and oxup update auto-install / refresh the extension after the toolchain is placed; oxup init --no-extension skips it, and update re-installs only when the toolchain version changed (the installed version is recorded in settings.toml). A failed extension install is a soft failure — the toolchain is what matters — and “no editor detected” is a quiet one-line note, not an error.
Editor support matrix
Two install mechanisms, by the contract the editor exposes. The user-facing detail — the language-server capability set, and the file-placement managed-config block — is in Toolchain → Editor support.
| Editor | Install mechanism |
|---|---|
VS Code (code), Cursor (cursor), VSCodium (codium), VS Code Insiders (code-insiders), code-server (code-server) | version-matched .vsix via the editor’s --install-extension CLI |
| Neovim, Vim, Emacs | file placement — embedded plugin files + an idempotent oxup-managed config block |
Neovim, Vim, and Emacs are recognized — naming one with --editor produces a specific, loud refusal pointing at manual setup and the tracking issue, never a silent no-op.
Coding agents (distinct from editor extensions): the toolchain also ships
ox mcp, an MCP server that drives Argon from any MCP-capable agent — Claude Code, codex, Cursor, opencode, VS Code / Copilot. Per-host registration and the shared four-valued (Truth4) epistemic discipline live underagents/(start at itsSETUP.md); a dedicated guide is forthcoming. Any host also gets the discipline with zero setup — it rides theargon_*tool descriptions.
Runtime contract
A .oxbin is consumed by a runtime backend. The runtime contract defines what a backend promises: how to load an artifact, how to bind it to per-tenant state, how to answer queries against bitemporal data, how to apply mutations, and how to enforce the capability surface that gates compliance-sensitive operations.
The contract specifies a three-role factoring (Engine / Module / Store factoring), a trait surface (The OxbinRuntime trait surface), a lifecycle (Lifecycle), bitemporal AsOf semantics (AsOf semantics — bitemporal point), a capability surface (Capability surface), and the ox runtime serve HTTP surface (Serving surface).
Engine / Module / Store factoring
The runtime decomposes into three roles:
- Engine is the shared, immutable base — the shared schema (std + any base packages the workspace declared — including any vocabulary packages — plus the meta-axes those packages declared). The Engine is
Arc<SchemaStore>, mmap-friendly, and shareable across processes and threads. - Module is a typed, loaded
.oxbin. It owns the artifact’s structural content (symbol table, standpoint lattice, tier table, rules, queries, mutations) and is immutable for its lifetime. Many Stores may use one Module concurrently. - Store is the isolated execution context where facts live. One Store is one (tenant, fork) pair: it accumulates events through
mutateand answersquery.
A Store sees facts according to two filters: (1) tenant + fork scope — only events tagged with this Store’s tenant_id and fork_id, under the fork’s copy-on-write lineage rules (AsOf semantics — bitemporal point); (2) standpoint perspective — when a query names a standpoint, only events visible from that standpoint by the lattice ancestor relation.
The OxbinRuntime trait surface
A backend implements:
#![allow(unused)]
fn main() {
pub trait OxbinRuntime: Send + Sync {
type Module: 'static + Send + Sync;
type Store: 'static + Send + Sync;
type Error: std::error::Error + Send + Sync;
/// Load an .oxbin into a Module. Layer 1 validation runs synchronously
/// here (RP-004 hooks); Layer 2 runs in the prefix before this returns Ok.
fn load(
&self,
oxbin: &Oxbin,
validation: ValidationPolicy,
) -> Result<Self::Module, Self::Error>;
/// Open a Store for one (tenant, fork). May create the fork if it
/// doesn't exist (subject to capability checks per the backend's IAM).
fn open_store(
&self,
module: &Self::Module,
tenant_id: TenantId,
fork_id: ForkId,
) -> Result<Self::Store, Self::Error>;
/// Run a query at a bitemporal point (default `AsOf::Now`). Standpoint
/// scope is part of the query, not this argument.
fn query(
&self,
module: &Self::Module,
store: &Self::Store,
query: &CoreIRQuery,
as_of: AsOf,
) -> Result<Answer, Self::Error>;
/// Apply a mutation; returns the TT at which it became effective.
/// Mutations are transactional; partial mutations are not observable.
fn mutate(
&self,
module: &Self::Module,
store: &mut Self::Store,
mutation: &CoreIRMutation,
) -> Result<TxTime, Self::Error>;
/// Fork an existing (tenant, fork) at a given TT. The new fork inherits
/// parent state up to fork_point_tt; later mutations diverge (CoW, `AsOf` semantics — bitemporal point).
fn fork(
&self,
module: &Self::Module,
parent: &Self::Store,
new_fork_name: &str,
fork_point_tt: TxTime,
) -> Result<Self::Store, Self::Error>;
/// Capability-gated physical erasure (Capability surface). Requires both
/// `#[allow_forget]` on the target concept and the runtime principal
/// to hold the `forget` capability.
fn forget(
&self,
module: &Self::Module,
store: &mut Self::Store,
targets: &[AxiomId],
actor: PrincipalId,
reason: &str,
) -> Result<ForgetReceipt, Self::Error>;
}
}
The trait is Send + Sync; a backend’s concurrency discipline is its own concern. The ValidationPolicy argument to load distinguishes sandboxed Strict (any Layer-2 failure refuses the load) from trusted Lenient (Layer-2 failures emit warnings; non-load-bearing issues are tolerated).
AsOf, Capability, and ValidationPolicy are @[language_interface] types mirrored by oxc-protocol::runtime; CI fails on drift.
Lifecycle
A normal lifecycle: at startup, runtime.load(oxbin, policy) returns a Module (Layer 1 tier check synchronously, Layer 2 per-section invariants in the prefix); per request, open_store then query / mutate; at shutdown, Stores drop, then the Module, then the Engine.
A second load returns a new Module. The runtime MAY share the first Module’s Engine if the base schemas are byte-identical (Arc identity), otherwise the new Module gets a fresh Engine. Hot replacement of a live Module is not required by the in-process trait; the serving helper (Serving surface) implements a guarded form that loads the next Module, checks compatibility against live ABox state, and swaps atomically only if compatible. The Engine/Module split is what makes this possible without redesigning Store semantics.
AsOf semantics — bitemporal point
Every query reads facts at a specific bitemporal point. The AsOf type carries both axes:
#![allow(unused)]
fn main() {
pub enum AsOf {
/// Default: VT = now, TT = now — "what is currently true, as currently believed."
Now,
/// "What was valid at VT = t, as currently believed?" Historical-fact view.
AtVt(DateTime),
/// "What was believed at TT = t about VT = now?" Audit view.
AtTt(DateTime),
/// Full bitemporal: "what was believed at TT = tt about VT = vt?"
At { vt: DateTime, tt: DateTime },
}
}
The default AsOf::Now answers about current state; historical and audit queries are opt-in.
The data point and the rule valid-time
A read of derived facts names two valid-time points over one shared transaction time: the data point — which facts held — and the rule valid-time — which rules were in effect (see Rule validity over time). The transaction time is not split: it is a single viewpoint applied to facts and rule records alike.
By default the rule point tracks the data’s valid time. Reading facts valid at t evaluates them under the rules in effect at t — “as of that point, under the rules as they then stood.” This is the reconstruction default and needs no extra annotation. An explicit rule valid-time decouples the two for counterfactuals:
#![allow(unused)]
fn main() {
pub struct ReadPoint {
pub data: AsOf,
/// Rule valid-time. `None` ⇒ track the data point's valid time
/// (the reconstruction default). `Some(t)` ⇒ evaluate under the
/// rules in effect at `t` — e.g. past facts under today's rules.
pub rule_vt: Option<DateTime>,
}
}
A bare read is the snapshot at now: facts valid now (VT_start ≤ now < VT_end), believed now (the current transaction-time watermark), under the rules in effect now. A derived read carries the same bitemporal read-point a base-fact extent does — it is not a separate mode. Reading the past pins the data point (AtVt/AtTt/At) and, by the default coupling above, the rule point with it. So future-dated and expired facts and rules do not appear in a bare read; they appear exactly at the points whose window contains them.
rule_vt = now over a past data point asks the counterfactual “what would the current rules derive over the facts as they then stood?”; tracking (the default) asks the faithful “what did we derive then, under the rules then in effect?”. The engine realizes a read-point by filtering the base facts (event_visible_at(vt, tt)) and selecting the rules in effect (rule_vt) before evaluation.
Now is frozen at the boundary. Valid-time now is the wall clock; transaction-time now is the published watermark. The serving boundary (ox runtime serve, the embeddable Connection, the ox driver) captures both once per request and threads a concrete point into the runtime — the runtime reads no clock. A recorded read-point therefore replays identically, which is what keeps incremental maintenance, checkpoint recovery, and the embedded↔standalone equivalence reproducible. A bare read’s snapshot is valid until the next validity boundary; the read-model cache rebuilds when wall-clock time crosses one, so a now read is never served a stale model.
Forks interact with AsOf through the copy-on-write invariant. A child fork inherits parent state up to fork_point_tt. A query against the child at AsOf::AtTt(tt): if tt < fork_point_tt it reads the parent’s events at tt; if tt ≥ fork_point_tt it reads the parent’s pre-fork events plus the child’s own events up to tt. The parent’s pre-fork state is shared with all children; each child sees its own divergence. A backend MAY materialize the inheritance at fork-creation or compute it lazily on read; both satisfy the contract.
Capability surface
Three capabilities gate dangerous operations:
forget— physical erasure of axiom events for compliance (RP-004 Temporal substrate). The source-level gate: a mutate body containingforgetrefuses to build (OE0730) unless the enclosingmutatedeclaration grants#[allow_forget]. The serving layer refusesforgetoutright (Runtime contract limits below).unsafe_logic— execution oftier:folrules underunsafe logic { }. Granted to the workspace atox buildtime. A query whose dependency closure touches anunsafe_logicrule may run unbounded; runtimes MAY apply a per-query wall-clock timeout and returnOE1301 UnsafeLogicTimeout.fork— creating new forks, per-(tenant, parent_fork). A principal without it canopen_storeagainst existing forks but cannot create new ones. The trunk fork’s owner holdsforkautomatically.
Capabilities flow through the runtime as part of the PrincipalId argument to forget and the principal context of open_store. How a runtime authenticates a principal is backend-specific (the Postgres backend uses Postgres roles; the in-memory backend trusts the calling process).
Incremental evaluation
Argon has two incremental systems with one deterministic artifact boundary:
| Change domain | Incremental substrate | Retained unit |
|---|---|---|
| source and package inputs | Salsa query graph | file, declaration, package summary, artifact section |
| committed runtime facts | DBSP circuit (target; see status note below) | relation-tuple deltas, traces, and arrangements |
They are deliberately separate. Source compilation is demand-driven and definition-granular. Runtime evaluation is transaction-stream-driven and tuple-granular. Salsa identities never enter an artifact; runtime tuples never invalidate compiler queries.
Incremental compilation
A long-lived compiler session retains source-file and workspace input identities. Changing a file updates its existing input. Declaration identity is semantic — file, name, declaration kind, visibility, and deterministic same-class occurrence — while its byte range is revision-local.
Parsing, declaration extraction, resolution, checking, macro expansion, elaboration, and dependency analysis are tracked at the narrowest sound boundary. An unchanged declaration subtree retains its downstream results even when earlier text moves its byte position. Package discovery still checks authoritative manifests and filesystem shape; it reuses a workspace input only when the resulting files, root, and preludes are unchanged.
Each build produces one .oxbin covering the entry package’s dependency
closure: every reachable source file is elaborated and its events are merged into
that single artifact. Artifact assembly is whole-package — an incremental build
reuses parse, resolution, and check results and then reassembles the artifact
from them — and the result must be byte-identical to a clean build of the same
inputs.
Status: target architecture. Multi-package composition — deterministic per-package contributions combined by a workspace orchestrator, with unchanged encoded sections reused rather than re-encoded — is not shipped. Section-level reuse is what would make assembly itself incremental; today assembly is a whole-artifact cost paid on every build, and it is what dominates an edited one.
Incremental runtime evaluation
Status: target architecture. The sections below describe the intended DBSP runtime. The current runtime maintains the derived read-model with a delta-seeded semi-naive insert path (monotone programs) and DRedc two-counter retraction support, falling back to the batch stratified fixpoint elsewhere; the circuit/trace/frontier machinery described here is not yet shipped. The Incremental compilation section above is present-tense except where it carries its own status note.
The event log is the source of truth. A mutation advances the maintained model by the delta its transaction implies; the rule program is not re-run over the complete event history. This is the DBSP model described in the full technical paper and the SIGMOD overview.
The delta algebra, the Z-set representation, and the per-operator
incrementalization (join arrangements, distinct zero-crossing, stratified
negation, recursion) are runtime internals. The design record owns them:
Incremental System Architecture
§3.1–3.2.
Scope, frontier, and atomicity
Each (tenant, fork, module commitment) owns an independent evaluation scope
with a monotonically published transaction frontier. Its retained state —
traces, arrangements, dictionaries, and the commitments they are valid under —
is enumerated in the design record
(Incremental System Architecture
§3.3).
A successful mutation appends one atomic event batch, advances the circuit with the corresponding signed delta, applies its derived delta, then publishes the new frontier. A read pinned to a published frontier observes base and derived state from exactly that frontier. No thread observes a frontier before both the event batch and maintained read model are ready.
If maintenance fails after the event batch is durable, the new frontier remains unpublished. The circuit epoch is poisoned and cannot accept another delta. Recovery restores the last valid arrangement checkpoint and replays the event suffix, or rebuilds from the event log through the batch evaluator. Partially advanced traces are never served.
The backend generation counter remains an atomic, monotonic coherence signal: Postgres increments it in the event transaction; the in-memory backend changes it under the same write lock as its indexes. Generation is not the evaluation algorithm. It detects stale cache epochs and coordinates recovery while the DBSP circuit performs tuple-level maintenance.
Bitemporal reads
The continuously maintained circuit serves the current published transaction
frontier. A transaction-time historical read uses a checkpoint at or before its
requested point and replays only the required event prefix, or falls back to a
batch reconstruction. It does not rewind or mutate the live current-frontier
circuit. Valid-time filtering remains part of the concrete read point and the
rule-validity contract described in AsOf semantics.
Fallback and correctness
The batch evaluator remains the cold fallback and independent oracle. An unsupported circuit operator, incompatible arrangement section, poisoned epoch, or disabled incremental path rebuilds explicitly; no rule is silently omitted. Derived traces and arrangements are replaceable caches over the event log.
For every transaction prefix:
integrate(incremental output deltas) = batch evaluation at that prefix
The equality covers tuple weights, undefined companions, temporal extents, dictionary identities, and provenance projections. Compiler incrementality has the analogous obligation: incremental diagnostics and artifact bytes equal a fresh compilation.
Serving surface
The in-process trait is the semantic contract. ox runtime serve is the first-party helper around it for tools that need a process boundary, specified by RFD-0014. It exposes a versioned HTTP surface:
| Route | Meaning |
|---|---|
GET /v1/health | service status, storage mode + reachability, active module hash, artifact schema hash |
GET /healthz | liveness — process is up (does not probe storage) |
GET /readyz | readiness — probes storage; 503 when the backend is unreachable |
GET /v1/module | active module status, source path, diagnostics, artifact schema hash |
GET /v1/schema | generated-schema-equivalent TBox metadata, plus the artifact schema hash a generated SDK checks for version skew |
POST /v1/dispatch/query | execute a declared pub query descriptor |
POST /v1/dispatch/mutation | execute a declared pub mutate descriptor |
POST /v1/dispatch/compute | execute a supported pure pub fn descriptor |
POST /v1/batch | execute an ordered array of mutation dispatches atomically |
POST /v1/forks/{fork}/dispatch/* | fork-scoped descriptor dispatch |
GET /v1/forks, POST /v1/forks | list and create forks |
GET /v1/forks/{fork} | inspect one fork record |
DELETE /v1/forks/{fork} | abort a fork |
GET /v1/forks/{fork}/diff/{other} | compare visible ABox events |
POST /v1/forks/{fork}/promote | merge a fork’s signed net delta into main — the fork’s new asserts AND its retractions (a delete/forget in the fork propagates as a superseding retraction, not asserts-only) |
POST /v1/forks/{fork}/derive | run buffered derive projection |
POST /v1/forks/{fork}/derive/trace | return buffered derive trace events |
GET /v1/snapshot | materialize TBox, ABox (paginated), and optional derivations |
GET /v1/checks | enumerate currently-violated checks in a scope |
GET /v1/derived/individuals/{id} | derived facts mentioning an individual |
GET /v1/derived/individuals/{id}/explain | explanations for that individual’s derived facts |
GET /v1/derived/facts/{fact_id}/explain | explanation for one derived fact |
Dispatch routes take a runtime context from trusted headers (X-Tenant-Id, X-Fork-Id, X-Principal-Id, X-Request-Id, X-Standpoint) or the request body; headers win when both are present. Missing tenant defaults are a CLI/helper concern; missing fork defaults to main.
Storage modes. mem keeps Store state in process memory — useful for local experiments, tests, and short-lived sandboxes; durability ends with the process. It is the default runtime backend. pg is an append-only axiom event log in Postgres (oxc-storage-pg): scoped tenant/fork/standpoint/module scans, AsOf::Now/AtVt/AtTt/full-bitemporal reads, durable fork records and lineage, generation counters bumped atomically with appends, projection-cache invalidation in the same transaction, and decoded body-field lookups for iof assertions and relation tuples. ox runtime serve --storage pg selects it. The source of truth in either mode is the event log; snapshots, derived facts, and projection caches are recomputable views over .oxbin plus visible events.
Ad-hoc query/mutation submission (RFD 0033). Beyond declared-descriptor dispatch, the API accepts arbitrary query/mutation bodies as source text — POST /v1/query/adhoc and POST /v1/mutation/adhoc (plus the per-fork analogs). A submitted body is parsed, fully type-checked against the loaded module (the same checker the build runs, via the runtime Schema backend — an ill-typed body is refused with ARGON_RUNTIME_ADHOC_TYPE_ERROR and never executes), lowered, and run through the same path declared pub query/pub mutate use; the declared forms are a named convenience layer over this generic path. Ad-hoc invocation carries the same context/tenant/fork/standpoint scoping as declared dispatch — there is no ad-hoc-specific restriction on what it may read or write, with one capability exception: forget (physical erasure) requires the build-time #[allow_forget] grant, which a runtime-submitted body cannot self-confer, so an ad-hoc forget is refused (OE0730). Ad-hoc submission is on by default; a deployment may lock down to declared invocables only (--no-adhoc / --no-adhoc-mutation), in which case the endpoint returns ARGON_RUNTIME_ADHOC_DISABLED.
The ad-hoc read-point receipt. Every /v1/query/adhoc response carries a mandatory readPoint object — { "effective": { "kind", "vt"?, "tt"? }, "source": "caller_override" | "declared" | "default_now", "declaredAsOf", "declaredShadowed" } — naming the concrete bitemporal point the rows were computed at and which precedence rule selected it (request asOf override → the submitted source’s own as_of pin → the default snapshot-at-now, its valid-time frozen to the request’s admission instant; belief resolves at the published watermark). A request asOf re-selects over a source-declared as_of on the same request — the shadowed pin is reported (declaredShadowed: true), never silently dropped — and a read-point at which no fact holds returns an ordinary empty value with the receipt (“nothing held then”), never an error. On a paged walk the page-1 receipt’s point rides the cursor (a valid-time-only point is additionally pinned to the serving watermark’s transaction time), so every later page reads the identical view. The MCP argon_ad_hoc_query envelope carries the same readPoint shape.
Generic entity writes remain declaration-typed. There is no untyped entity-write surface (no POST /v1/entities): every write — declared or ad-hoc — routes through the typed pub mutate / Operation mechanism, never a raw entity blob. Compute dispatch supports the pure CoreIR subset evaluable without Store effects (literals, parameters, tuples, lists, boolean/int/text operators, membership, conditionals, ascription); calls, projections, and Store-dependent terms are refused until their runtime substrate is specified.
Entity-reference arguments — new vs existing. A pub mutate parameter typed as a declared concept/struct is an entity reference. Over HTTP such an argument takes a JSON string in one of two forms, and the form the client chooses decides the meaning:
- An
#i<N>string (e.g."#i7") names an existing individual — one already minted in this scope (typically returned by an earlier dispatch). It is validated against the store’s next-fresh floor: a reference at or above the floor cannot name an existing individual and is refused (ARGON_RUNTIME_VALIDATION_FAILED). - Any other string (e.g.
"alice") is a client-supplied symbolic name for a NEW individual. The server mints a fresh#i<N>identity for it inside the per-scope critical section, runs the mutation with that identity substituted, and returns the mapping on the receipt undermintedEntities: an array of{ "name", "id", "concept" }records. The client uses the returnedidto refer to that individual on subsequent dispatches.
This is what makes the flagship models writable over HTTP: a mutation such as openRentObligation(landlord: Person, tenant: Person, …) whose body classifies its parameters via insert iof(landlord, Person) is dispatched by supplying symbolic names ("alice", "bob"), and a later recordPayment(satisfactionAccount: SatisfactionAccount, payment: SatisfactionRecord, …) supplies the minted #i<N> for the existing account and a fresh symbolic name for the new payment. The in-process / demo.toml path mints symbolic names deterministically from the name (so the same name is the same individual); the HTTP path mints fresh per request with no idempotency-by-name — the same name in two requests is two individuals.
Atomic batches. POST /v1/batch executes an ordered array of mutation dispatches as one unit: { "context": …, "mutations": [ { "qualifiedPath", "args" }, … ] }. Every step runs in one per-scope critical section against a single hydrated store, so step N sees the effects of steps 0..N and the check delta-guard evaluates each step against the accumulating state. On success the union of emitted events is persisted in one transaction; the response carries committed: true, a per-step steps array (each with its result / mintedEntities / diagnostics), and eventsCommitted. Any step failure rolls the whole batch back — nothing is persisted — and the error envelope reports the failing step under error.details.batchStep. The request body cap and the result-row cap apply to the batch as a whole. New-entity symbolic-name minting is per step: a symbolic name repeated across two steps mints two individuals (each step is a distinct mutation). To thread one new individual through several steps, mint it in the first step and reference the returned #i<N> in the later ones.
Named individuals in outputs. Individuals declared by pub fact carry their source name into the artifact’s name-index, and every entity rendered by a query / extent / snapshot / dispatch result includes that name under $name alongside the opaque $id (so a served scope reads with its modeler-chosen names). HTTP-minted individuals (above) carry their client label out on the dispatch receipt’s mintedEntities; the durable identity is always the #i<N> id.
Runtime error codes. Failures on the /v1 surface use a stable error.code string in the ARGON_RUNTIME_* family (distinct from the compile-time OE#### catalog of Appendix C). Every 4xx/5xx response uses the same structured envelope — {error:{code,message,details},requestId,moduleHash} — including routing, method, and request-body parse failures (a malformed JSON body is ARGON_RUNTIME_VALIDATION_FAILED, an unknown route is ARGON_RUNTIME_UNKNOWN_ROUTE, a wrong method is ARGON_RUNTIME_METHOD_NOT_ALLOWED), so a client keys on error.code uniformly for every failure class. The full set of codes, their HTTP statuses, and their meanings is the published failure reference in Appendix E, checked against the runtime’s canonical registry. A batch-step failure rides the same envelope with the failing step index under error.details.batchStep.
Pagination. A list-shaped read (pub query, /v1/snapshot) larger than the result-row cap is read page by page rather than refused. A query takes an optional page: { limit, cursor } in its request body; /v1/snapshot takes ?limit=&cursor=. The response carries a page object — { total, returned, nextCursor } (the snapshot’s under abox.page) — and a nextCursor of null marks the last page. The cursor is an opaque server-issued token: pass back the previous page’s nextCursor verbatim, do not parse or construct it (a foreign cursor is refused with ARGON_RUNTIME_INVALID_CURSOR). Page size is min(limit, cap), so one page never exceeds the cap; an un-paged read still refuses (ARGON_RUNTIME_RESULT_TOO_LARGE) when the whole result is over the cap — a truncated answer is indistinguishable from a complete one.
The cursor pins a read point, so a multi-page walk is consistent against one fixed view rather than re-evaluated against live state each page. Page 1 reads at the request’s asOf (or Now); the server resolves Now to the transaction time of the store that served the page and embeds it in the nextCursor. Every later page inherits that pinned point from the cursor (it overrides the request’s asOf), so the offset is always taken against the same bitemporal snapshot. A concurrent insert lands past the pinned point and stays invisible to the walk; a concurrent retraction before the cursor stays visible (it existed at the pinned point) — so the walk neither skips nor duplicates a row, and total is stable across pages. To page over a historical point, pass asOf (the bitemporal read point of AsOf semantics — bitemporal point) on the first request; the whole walk then follows it.
/v1/snapshot carries the same pin in its cursor. On the durable pg backend the pinned page re-hydrates the ABox at the pinned transaction time, so the snapshot walk is consistent under concurrent writes. The in-process mem backend keeps only current-state events (it has no historical scan), so a mem snapshot walk is a live positional read; production serving is pg-backed.
Checks surface. GET /v1/checks enumerates the checks currently violated in a scope, evaluated against live state — the compliance/violations view, complementing the per-mutation observe channel (which only fires on a write). The response carries violated (any blocking Error firing), violationCount, diagnosticCount, and a diagnostics array of {code,message,severity,check,args} (the same shape a mutation response’s observe-channel diagnostics use). ?check=<name> filters to one check (an unknown name is ARGON_RUNTIME_UNKNOWN_CHECK). The generated SDK exposes this as client.checks(), and the mutation receipt now carries the observe-channel diagnostics (previously dropped for typed-SDK consumers).
Liveness and readiness. GET /healthz is liveness — 200 while the process is up and serving; it deliberately does not probe storage, so a transient storage outage does not trigger a process restart. GET /readyz is readiness — it probes the storage backend (SELECT 1 on pg; always ready on mem) and returns 503 ARGON_RUNTIME_STORAGE_UNREACHABLE when storage is down, so a load balancer stops routing to a dead-storage instance. GET /v1/health folds both into one human/debug body and is honest about storage: ok:false + 503 when the backend is unreachable. See the deployment runbook for how an orchestrator should wire these.
Hot reload. The serving layer may watch the active .oxbin path. On a changed artifact it loads the new bytes into a fresh Module, validates, computes the new module hash and declaration signatures, probes live ABox state, accepts additive schema changes, and rejects declaration removal or changed signatures when live ABox state exists — keeping the previous Module active and recording diagnostics on failure. Failed reloads never discard ABox state.
Capability and provenance limits. The serving layer enforces fork on nested forks. forget mutations are refused at the serving layer; unsafe_logic queries are refused (lowering emits unsafe_logic = false for supported rules). The derive/explain endpoints expose buffered rule/tuple provenance only; full PosBool DNF witness trees are reserved for the formal provenance substrate and must not be implied by the response shape.
Defaults
Runtimes default several axes — these live in the ox CLI driver and library helpers, not in the runtime trait: tenant → a singleton UUID (multi-tenant configs override); fork → trunk at tenant creation; standpoint → tenant-wide :root; AsOf → Now; ValidationPolicy → Lenient for ox run, Strict for sandboxed contexts.
SDK generation
ox gen emits a typed client SDK from a built .oxbin so application code calls declared pub query / pub mutate / pub fn descriptors with checked argument and result types instead of hand-writing HTTP requests against the /v1 surface.
Mechanization
The runtime contract is mechanized in Lean as abstract signatures (parallel to std::kripke’s interface-only mechanization): Engine/Module/Store types, the OxbinRuntime trait surface, the bitemporal AsOf type, the three-capability surface, and the generation-monotonicity / cache-correctness invariant, under Argon/Runtime/. oxc-protocol::runtime mirrors the @[language_interface]-tagged types; CI fails on drift.
Deployment and trust model
The server has no authentication. ox runtime serve consumes X-Tenant-Id / X-Principal-Id (and the other context headers) at face value — it does not verify who the caller is or whether they may act as the named tenant or principal. Tenant data is isolated at the storage layer (one tenant cannot read another’s individuals, verified), but the gate deciding who may claim a tenant is absent by design. A caller that sets X-Tenant-Id: acme reads and writes acme’s data regardless of any Authorization header.
This is an honest internal-service posture, not a bug — but it has a hard deployment consequence: the server MUST be fronted by a gateway and MUST NOT be directly reachable by untrusted clients. The default loopback-only bind (serve refuses a non-loopback host) enforces “behind a proxy on the same host or network namespace”; do not relax it without a fronting gateway.
The deployment contract:
- Front it with an authenticating gateway. A reverse proxy / API gateway terminates client auth (mTLS, OIDC, an API-key service — backend-specific), and only after authenticating injects the trusted
X-Tenant-Id/X-Principal-Idheaders. The runtime trusts these because the gateway is the only thing that can reach it. - Strip inbound trust headers at the edge. The gateway MUST overwrite (not pass through)
X-Tenant-Id/X-Principal-Id/X-Standpointfrom the client request, so a client cannot forge a tenant. Treat these as gateway-internal headers. - Network-isolate the runtime. Bind loopback (the default) or a private interface, and use network policy / security groups so only the gateway can open a connection. The runtime has no rate limiting or load-shedding beyond the per-request timeout, body-size cap, and result-row cap (Serving surface) — the gateway owns rate limiting and client back-pressure.
- Wire liveness/readiness to the orchestrator. Point the orchestrator’s liveness probe at
GET /healthz(restart on failure) and its readiness probe atGET /readyz(remove from the load-balancer pool on failure — e.g. when Postgres is unreachable). Do not gate liveness on storage: a storage outage should drain traffic (readiness), not restart pods (liveness). - Schema-change gate. Cross-version artifact loads over live data refuse by default (
ARGON_RUNTIME_SCHEMA_MISMATCH, R-B7); an operator opts into serving a changed schema over an existing scope withARGON_ACCEPT_SCHEMA_CHANGE=1. Treat that as a deliberate migration step, not a default.
forget (physical erasure) is refused outright over HTTP and there is no per-principal authorization inside the runtime (Capability surface, Serving surface) — all authorization is the gateway’s responsibility.
The operator procedure that stands this topology up — reference container and systemd artifacts, probe wiring, the schema-change gate, secrets, and a one-box walkthrough — is the Deployment runbook.
Deployment runbook
This is the operator procedure for running the Argon runtime as a served
platform — the reference artifacts and runbook of RFD 0057 D5
(deployment packaging). It assumes the deployment and trust model
— read that first: it fixes the one topology in which serving is safe, and
this runbook is how you stand that topology up. Reference artifacts (a
Dockerfile, a systemd unit, an env-file template) live in the repository’s
deploy/ directory; they are templates you adapt, not a turnkey stack.
The topology, in one picture
┌──────────────────────────────────────────────────┐
client ───►│ Authenticating gateway (same host / namespace) │
(mTLS / │ • terminates client auth │
OIDC / │ • OVERWRITES X-Tenant-Id / X-Principal-Id / │
API key) │ X-Standpoint (never passes the client's) │
│ • rate-limits, back-pressures │
└───────────────────────┬──────────────────────────┘
│ loopback / private only
▼
┌──────────────────────────────────────────────────┐
│ ox runtime serve 127.0.0.1:7780 │
│ • trusts the three headers at face value │
│ • /healthz (liveness) /readyz (readiness) │
│ • network-isolated: only the gateway reaches it │
└──────────────────────────────────────────────────┘
Two processes, co-located. The gateway is the only thing that can open a connection to the runtime, and it is the only thing that sets the trust headers. If a client can reach the runtime port directly, it can read and write any tenant’s data — the runtime has no auth to stop it. This is the whole reason for the loopback bind (the runtime refuses a non-loopback host) and the network isolation.
Wiring the orchestrator to the probes
The runtime splits liveness from readiness on purpose (serving surface). Wire them to the two distinct orchestrator actions:
| Probe | Endpoint | Probes storage? | Orchestrator action on failure |
|---|---|---|---|
| Liveness | GET /healthz | No | Restart the process |
| Readiness | GET /readyz | Yes (bounded SELECT 1 for pg) | Drain — remove from the load-balancer pool |
The discipline that matters: do not gate liveness on storage. A transient
Postgres outage should drain traffic (readiness fails, the LB stops routing
here), not kill and restart the process (which fixes nothing and amplifies the
outage). /healthz answers 200 whenever the request pipeline is up; /readyz
answers 503 when storage is unreachable. Both bypass admission control, so
they keep answering under load — the point of the split is that an overloaded
server is still live, and readiness reports the overload rather than being
shed by it.
A Kubernetes example, for orientation only (adapt timings to your fixpoint cost):
livenessProbe:
httpGet: { path: /healthz, port: 7780 }
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet: { path: /readyz, port: 7780 }
periodSeconds: 5
failureThreshold: 2
timeoutSeconds: 2 # readiness must answer fast on a HUNG backend
The cross-version schema-change gate
When a freshly built .oxbin is loaded over a scope that already holds data,
the runtime compares the artifact’s schema identity against the scope’s recorded
identity. A mismatch is refused by default (ARGON_RUNTIME_SCHEMA_MISMATCH,
R-B7) — serving a changed schema over existing individuals risks reading them
back under the wrong shape. To proceed deliberately, set
ARGON_ACCEPT_SCHEMA_CHANGE=1 for that load:
ARGON_ACCEPT_SCHEMA_CHANGE=1 ox runtime serve --project . --oxbin target/main.oxbin \
--storage pg --database-url "$ARGON_DATABASE_URL"
Treat it as a migration step, never a standing default — leaving it set defeats the guard on every subsequent deploy.
Secrets
The runtime takes one secret: the Postgres --database-url (only when
--storage pg). The runtime has no credentials of its own — there is nothing
else to provision. Keep the URL out of image layers, out of the unit text, and
out of the process table where you can:
- systemd — put it in a root-owned, group-readable
EnvironmentFile(/etc/argon/runtime.env, mode0640), referenced by the unit; seedeploy/runtime.env.example. - containers — feed it as an orchestrator secret mounted to the env, not
baked with
COPYorENV.
Config surface
The runtime is configured by a layered surface — flags > env > file >
defaults (RFD 0057 D4). Pass a TOML file with --config; a
value there is the baseline an ARGON_* env var overrides, and a CLI flag
overrides both. Omit --config and the surface is the historical `flags > env
defaults
. Unknown keys in the file are a loud parse error (a typo never silently no-ops); a referenced-but-unset${ENV}secret is a loud refusal. The committedserve.config.example.toml(in theoxc-serve` crate) documents every section.
| Knob | Flag / env / file | Notes |
|---|---|---|
| Config file | --config | the TOML baseline (RFD 0057 D4) |
| Project root | --project / ARGON_SERVE_PROJECT / project | process context |
| Artifact | --oxbin / ARGON_SERVE_OXBIN / oxbin | the compiled .oxbin to serve |
| Bind | --host / --port (or ARGON_SERVE_HOST / ARGON_SERVE_PORT / file) | non-loopback host refused |
| Storage | --storage mem|pg / ARGON_SERVE_STORAGE / [storage] | pg needs a database-url |
| DB URL | --database-url / ARGON_DATABASE_URL / [storage] database_url{,_file} | the one secret — by reference, never inline |
| Ad-hoc lockdown | --no-adhoc / --no-adhoc-mutation / [adhoc] | ad-hoc is ON by default (RFD 0033) |
| Telemetry | --otel-endpoint / ARGON_OTEL_ENDPOINT / [telemetry] otel_endpoint | OTLP collector; off when unset (see below) |
| Hot-reload | --no-watch / ARGON_SERVE_WATCH / watch | watcher ON by default (see below) |
| Limits | [limits] / [admission] | request timeout, body/row caps, admission/fairness |
| Log filter | RUST_LOG | structured request logging; default info |
| Schema gate | ARGON_ACCEPT_SCHEMA_CHANGE=1 | deliberate cross-version load |
| Connectors | ARGON_DEPLOY_CONFIG / argon.deploy.toml | foreign-connector config (RFD 0036) |
Telemetry
Observability rides the runtime’s existing tracing bridge (RFD 0057 D2): set an OTLP collector endpoint and the runtime exports
vendor-neutral traces and metrics over OTLP/gRPC. It is off by
default — with no endpoint configured nothing is exported and the
instrumentation is inert (no exporter, no background pipeline, no measurable
overhead), so you opt in by configuring the endpoint, not by a feature flag.
ox runtime serve --project . --oxbin target/main.oxbin \
--otel-endpoint http://otel-collector:4317
# or: ARGON_OTEL_ENDPOINT=http://otel-collector:4317, or [telemetry] in the file.
- Traces — each request is a root span; the handler phases (
dispatch,reason,persist) attach as child spans, so a slow request is attributable to a phase. A trace crossing the gateway joins its span via propagated context. - Metrics — request latency by path + tenant, mutation throughput, check-violation count, store/event-log growth, IVM-rebuild frequency, and reasoner-budget hits.
It is OTLP, not Prometheus-direct: one bridge gives both traces and metrics, vendor-neutrally. A Prometheus consumer runs an OTLP→Prometheus collector.
Lifecycle: graceful drain and hot-reload
Graceful drain on stop (RFD 0057 D3). On SIGTERM (an
orchestrator stop) or SIGINT (Ctrl-C) the runtime stops accepting new
connections and drains in-flight requests — bounded by the per-request
deadline, so the drain is finite — then exits clean. A request in flight at the
signal is finished or deadline-cut, never severed mid-commit. ox runtime serve
returns only after the drain. The systemd unit’s default SIGTERM stop is
therefore a clean drain; set TimeoutStopSec a hair above the request deadline
(the reference unit uses 35s for the 30s default) so the longest in-flight
request can finish before systemd escalates to SIGKILL.
Hot-reload (RFD 0057 D3). With the watcher on (default), the
runtime watches the served .oxbin and, on change, validates the new
artifact and performs an atomic module swap with no dropped in-flight
requests — new requests bind the new module, in-flight requests complete
against the one they started on. The cross-version schema-change
gate still applies to a reload: an additive change
reloads freely; a type-incompatible change against a live A-box is refused
(ARGON_RUNTIME_MODULE_INCOMPATIBLE) unless ARGON_ACCEPT_SCHEMA_CHANGE=1 opts
in. Reload never silently accepts a model that would invalidate persisted state.
Disable the watcher (--no-watch, or watch = false) to fix the model for the
process lifetime and roll a model only by restart.
Deploy on one box
A minimal end-to-end stand-up, mem storage, behind a co-located gateway.
-
Build the artifact.
ox build # produces target/<name>.oxbin -
Run the runtime on loopback. Either directly:
ox runtime serve --project . --oxbin target/main.oxbin \ --host 127.0.0.1 --port 7780 --storage memor via the reference systemd unit:
sudo install -m 0755 "$(command -v ox)" /usr/local/bin/ox sudo install -d -o argon -g argon /srv/argon # + place the .oxbin under it sudo install -m 0640 -o root -g argon deploy/runtime.env.example /etc/argon/runtime.env sudo cp deploy/argon-runtime.service /etc/systemd/system/ sudo systemctl daemon-reload && sudo systemctl enable --now argon-runtime -
Front it with a gateway that authenticates the client and overwrites the trust headers. A minimal nginx sketch (auth elided — wire your own
auth_request/ mTLS / OIDC):server { listen 443 ssl; # ... terminate client auth here ... location / { # OVERWRITE the trust headers — never pass the client's through. proxy_set_header X-Tenant-Id $authenticated_tenant; proxy_set_header X-Principal-Id $authenticated_principal; proxy_set_header X-Standpoint $resolved_standpoint; proxy_pass http://127.0.0.1:7780; } } -
Verify the probes.
curl -fsS http://127.0.0.1:7780/healthz # → 200 {"status":"live"} curl -fsS http://127.0.0.1:7780/readyz # → 200 {"status":"ready"} (or 503 if storage is down) -
Confirm the isolation. From outside the box, the runtime port (
7780) must be unreachable — only443(the gateway) is exposed. If a remote client can hit7780, the deployment is unsafe regardless of the gateway.
Embedding contract (in-process)
oxc-runtime and oxc-serve are publish = false: there is no crates.io artifact, no semver guarantee, and no docs.rs. Embedding the runtime in-process from Rust means a git dependency on the workspace against APIs that are documented at intent level (the crates’ AGENTS.md nodes) but are not a stable, versioned contract. The semantic contract is the OxbinRuntime trait (The OxbinRuntime trait surface); the serving surface (Serving surface) is the supported boundary for everyone else. Prefer the HTTP surface unless you specifically need in-process embedding.
The in-process Store is the mem backend: durable Postgres serving is reached through the served async adapter (PgOxbinRuntime), via ox runtime serve --storage pg, not by constructing a Store and selecting pg. The HTTP serving surface (Serving surface) sits above the in-process surface — it uses the served pg adapter rather than in-process Store pg — so an integration over HTTP is the lower-risk path.
Storage layer
Argon’s storage layer is a trait abstraction (The storage trait) with two backends behind it: an in-memory backend for tests and REPL, and a Postgres backend. Both implement the same trait so the rest of the runtime is backend-agnostic.
The architectural decision that frames every other choice is the rejection of triple-decomposition: storage is a single append-only log of axiom-ADT events, not entity-attribute-value triples (The single-event-log architecture). The single log carries every cross-cutting concern — discriminator axes, bitemporality, defeasibility, provenance, capability gates — once, instead of per-relation.
The contract is ontology-neutral. No UFO axis is a column. Rigidity, sortality, identity-provision, and any vocabulary-specific axis live as meta_property events whose body declares (axis, target, value). A UFO vocabulary package ships its axes as data, not as type-system primitives.
The single-event-log architecture
Storage is one canonical relation: axiom_events. Every kernel write — concept declaration, subsumption axiom, relation declaration, rule declaration, type assertion, relation tuple, property assertion, meta-property, standpoint declaration, module declaration, retraction — inserts exactly one row. Rows are never updated in place except for the transaction-time-end field on supersession. Rows are deleted only via the capability-gated forget operation (Capability surface).
Each row carries:
- Identity — a UUIDv7 event identifier (time-orderable, monotonic) and a stable
axiom_idUUID identifying the logical axiom across edits. - The axiom-ADT body — a variant tag (one of the 26 axiom kinds catalogued in Axiom-ADT variant catalog) plus a canonical-CBOR payload mirroring Argon’s CoreIR 1:1. The
content_idisSHA-256(body), so identical bodies deduplicate. - Discriminator axes — tenant, fork, standpoint, module. Always present; default singletons in single-tenant deployments.
- Bitemporality — a valid-time range and transaction-time begin/end (Temporal substrate, RP-004).
- Polarity — assert (
+) or retract (-); retractions point at the assert they supersede. - Decidability classification — the tier-pair
(main_tier, temporal_tier)per Tier ladder and RP-004. - Defeasibility proof tags — Governatori-Rotolo 4-slot tags per defeasible reasoning.
- Provenance — PosBool(M) DNF for derived events (absent for axioms).
The four discriminator axes plus the bitemporal pair give the 4-axis versioning contract: any event is located by (tenant, fork, standpoint, module) in space and by (valid-time, transaction-time) in time. A query at an AsOf point (AsOf semantics — bitemporal point) reads the events live in scope at that bitemporal coordinate.
Why one log, not many tables. A split design (mutable normalized TBox tables plus an append-only ABox log) cannot answer “what was the TBox as of TT₀” without rebuilding state from a separate ledger, and forces every discriminator axis to be added redundantly to every TBox table. A unified log factors these axes once. Event sourcing is the correct foundation for bitemporal retraction, copy-on-write forking, and PosBool(M) derivation provenance; any of these over a mutable schema introduces impedance mismatches that surface as edge-case correctness bugs.
Why ADT-shaped bodies, not RDF-style decomposition. Reifying an axiom into triples loses the ADT pattern-matching benefit, inflates row count 5–50×, and adds a join layer on every read. The empirical case is clear: horned-owl (Phillip Lord, Newcastle) achieves 1–2 orders of magnitude faster parsing and ~20× less memory than the OWL API’s triple store by using Rust enums for OWL axioms; Konclude and Tawny-OWL converge on the same shape. Pattern matching on the ADT is reasoning in the matching-logic sense (Roșu 2017, LMCS).
The body encoding is canonical CBOR (deterministic per RFC 8949 §4.2.1). The body schema is the variant’s body shape from the axiom-ADT variant catalog.
The storage trait
Backends implement:
pub trait StorageBackend: Send + Sync {
type Error: std::error::Error + Send + Sync;
/// Open or create storage for a (tenant, fork). Includes any
/// per-backend schema migrations.
fn open(
&mut self,
tenant_id: TenantId,
fork_id: ForkId,
) -> Result<(), Self::Error>;
/// Append one axiom event (assert or retract).
fn append(&mut self, event: &AxiomEvent) -> Result<TxTime, Self::Error>;
/// Append a batch of axiom events as one transaction.
fn append_batch(
&mut self,
events: &[AxiomEvent],
) -> Result<TxTime, Self::Error>;
/// Physically erase axiom events for compliance. Capability checked
/// by the caller; the backend logs to its forget log.
fn forget(
&mut self,
targets: &[AxiomId],
actor: PrincipalId,
reason: &str,
) -> Result<ForgetReceipt, Self::Error>;
/// Scan live events of a kind in scope at a bitemporal point.
/// Returns events with op='+' and (tx_to absent OR tx_to > as_of.tt).
fn scan_live(
&self,
scope: Scope,
kind: AxiomKind,
as_of: AsOf,
) -> Result<EventCursor<'_>, Self::Error>;
/// Lookup events by extracted body field. Backends with the right
/// index serve this in O(log n); others fall back to scan.
fn lookup_by_field(
&self,
scope: Scope,
kind: AxiomKind,
field: BodyField,
value: BodyValue,
as_of: AsOf,
) -> Result<EventCursor<'_>, Self::Error>;
/// Per-axiom-id history (across retractions / reasserts).
fn axiom_history(
&self,
scope: Scope,
axiom_id: AxiomId,
) -> Result<EventCursor<'_>, Self::Error>;
/// Apply CQRS projection maintenance after a batch of appends.
fn maintain_projections(
&mut self,
delta: &EventDelta,
) -> Result<(), Self::Error>;
}
Scope carries (tenant_id, fork_id, standpoint_id_set, module_id_set). The standpoint_id_set is typically a single standpoint plus its lattice ancestors (pre-computed at compose time, cached in global-control).
Backends manage their own schema versioning. The schema version is not carried in the .oxbin preamble.
The Postgres backend
The Postgres backend (oxc-storage-pg) materializes the event log as a single canonical axiom_events table plus a set of CQRS projection tables (modules, concepts, relations, meta-axes, standpoints and their ancestry, per-(standpoint, concept) world assumption, forks, and a forget-audit log). The event log is the source of truth; projections are recomputable views maintained incrementally by the Argon runtime calling maintain_projections after each batch (so projection logic stays in Rust and is shared with the in-memory backend) rather than by DB triggers.
Body fields are extracted into DB-enforced generated columns (STORED GENERATED ALWAYS AS) via an oxc-pgx pgrx extension that reads canonical CBOR; because deterministic CBOR is byte-deterministic, these extractions cannot drift from the body. Hot-path access patterns — live-in-scope-by-kind, axiom-id history, valid-time and transaction-time ranges, and per-variant / per-relation hot fields — are served by partial, GiST, and BRIN indexes that ox build auto-generates from the declared schema; modelers never write storage indexes by hand.
The concrete Postgres DDL — table definitions, index definitions, the cbor_extract_* extension signatures, and the projection catalog — is an operational artifact of this one backend, not part of the language surface. It is not mechanized (see Mechanization) and lives in the oxc-storage-pg crate. ox kernel init installs the extension and runs migrations.
The Postgres backend crate exists but is not the default runtime path; the in-memory backend is what the runtime exercises by default (see Serving surface).
The in-memory backend
The in-memory backend (oxc-storage-mem) is the live backend — for tests, REPL, and ephemeral runtimes. It implements StorageBackend with BTreeMap-backed indexes plus an interval tree for O(log n) bitemporal range queries, sharing all query and mutation code with the Postgres backend. It is the default for cargo nextest run -j 4 (tests need no Postgres). It has no durability (drops on shutdown), no IAM (trusts the calling process; capability checks pass unconditionally), and no partitioning.
Mechanization
The storage layer is mechanized in Lean at the contract level — abstract over backend, ontology-neutral, axiom-ADT-shaped:
Argon/Storage/Trait.lean— abstractStorageBackendsignature (no DDL)Argon/Storage/AxiomEvent.lean— the canonical event row shape (@[language_interface])Argon/Storage/AxiomKind.lean— the 26 variants as a Lean inductive (@[language_interface])Argon/Storage/AxiomBody.lean— per-variant body payloads (@[language_interface])Argon/Storage/CatalogProjections.lean— projection shapes
oxc-protocol::storage mirrors the @[language_interface]-tagged inductives; CI fails on drift. The Postgres DDL is not mechanized — it is an operational artifact of the Postgres backend. The trait admits future backends (analytical / RDF-bridge / property-graph / object-store) without changing the canonical axiom_events abstraction; none are standard backends, and none would change the event-log contract above.
Stdlib (selected)
Standard packages
The standard library is five packages. The compiler embeds each one’s root.ar via include_str! (oxc-instantiate/src/lower.rs) and elaborates it before every user build, so use std::… resolves to real declarations.
| Package | Provides |
|---|---|
std::core | Top, Bot, the generic metatype type, the generic metarel rel. Truth4 / Truth4Of<T> are compiler-injected (in scope without a use). |
std::rel | The relation-property library macros (transitive, irreflexive, asymmetric, functional) — see Migration: re-homing the hard-coded surface. |
std::kripke | World, Entity, accessible decls — vocabulary for the modal evaluator (see Modal operators). |
std::path | Path<NodeT, EdgeT>, Edge<A, B>, Weighted trait — type decls (see The Path type). |
std::temporal | A Temporal trait anchoring the namespace (see Temporal rule atoms). |
Higher-order type theories are not in std. MLT ships as the unprivileged first-party package mlt (packages/mlt, RFD 0043): its own metarels (categorizes, partitions, is_subordinate_to, power_type_of), the order metaxis, and the decorator #[procmacro]s, with zero compiler privilege — a consumer lists it in ox.toml and imports the vocabulary. Parallel theories (potency, ML2, …) ship the same way; the substrate stays neutral about which one a model commits to.
Per-package worked programs live in examples/, compiled and run in CI.
Note std::math is not a package. The numeric and temporal types (Nat, Int, Real, Decimal, Money, Date, …) are compiler-injected primordials resolved directly by the resolver, not imported from .ar source. The fixed-width primitives (i8–i128, u8–u128, f32, f64) are likewise compiler-provided under the std::math::primitive name, requiring explicit use.
Primordial types (lexically always in scope, no use required):
Nat, Int, Real, Decimal, Money, Date, Time, DateTime, Duration,
Bool, String, Top, Bot
These match how ontologists think — counts, measurements, monetary amounts — rather than machine-level widths. Fixed-width variants (i8–i128, u8–u128, f32, f64) ship in std::math::primitive for FFI, performance-critical paths, and binary-protocol interop; explicit use required.
The prelude is a package feature, not a compiler default (RFD 0038). There is no auto-imported std::prelude; a package’s [package].prelude (ox.toml, an array of use-tails) auto-prepends uses to its own modules, empty by default, opt-out per-module with #![no_implicit_prelude]. What is always in scope without any use is the substrate — the language primitives, not a library:
- Primordial types (above) and builtin type forms:
Option<T>(Some/None),Result<T, E>(Ok/Err),Ordering(Less/Equal/Greater),Truth4,Truth4Of<T>,List<T>,Set<T>,Map<K, V>,Range<T>, plus the check surfaceDiagnostic/Severity. These are compiler-provided (is_builtin_type_form) and resolve directly — in scope everywhere, no import, no prelude.
type/rel are not ambient. Opt into the no-commitment baseline with use std::core::{type, rel} or a [package].prelude entry (RFD 0038) — exactly as you bring in any vocabulary. Every ontological commitment is visible in the source: vocabulary classifiers (UFO’s kind, BFO’s class, …) and the baseline type/rel alike resolve only against pub metatype / pub metarel declarations in scope — declared in the package or imported from an external vocabulary package (Name resolution, Vocabulary concepts and the generic type metatype); an unresolved introducer is OE0605 / OE0606. No vocabulary — not even the baseline — ships ambient. A vocabulary package may re-export std::core::rel (and/or type) in its own prelude.ar as a convenience.
Numeric tower and coercion
Modeling-surface numeric types (primordial, always in scope):
| Type | Meaning |
|---|---|
Nat | unsigned integer; arbitrary-precision logically, runtime-chosen width |
Int | signed integer; arbitrary-precision logically, runtime-chosen width |
Real | real number; exact arbitrary-precision rational (RFD 0016). Inexact floats are the opt-in f32/f64 in std::math::primitive. |
Decimal | arbitrary-precision decimal (exact arithmetic) |
Money | currency-typed decimal, exact (cent precision by default) |
All numeric primitives are signed where signedness is meaningful. The runtime chooses an implementation width based on refinement bounds: Int where _ < 256 may compile to i16; unbounded Int defaults to arbitrary-precision.
Fixed-width primitives (std::math::primitive, requires explicit use):
i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64
For FFI, performance-critical paths, and binary-protocol interop.
Money arithmetic (per D-069):
| Operation | Result |
|---|---|
Money + Money | Money |
Money − Money | Money |
Money × (Nat | Int | Real | Decimal) | Money |
(Nat | Int | Real | Decimal) × Money | Money |
Money ÷ (Nat | Int | Real | Decimal) | Money |
Money ÷ Money | Decimal |
Scalar multiplication and division accept any numeric type as the non-Money operand (the scalar widens implicitly to Decimal for the arithmetic). Money-to-Money addition and subtraction are currency-checked at elaboration when the operands have refined currency annotations. No implicit conversion Nat → Money or Decimal → Money; construct via Money::from_cents(n) or Money::from_decimal(d, currency).
Temporal arithmetic:
| Operation | Result |
|---|---|
Date + Duration | Date |
Date − Date | Duration |
Date − Duration | Date |
DateTime + Duration | DateTime |
DateTime − DateTime | Duration |
Time + Duration | Time |
Temporal literals. A temporal value is written hash-delimited: #…#. The hashes are
load-bearing — they distinguish a date from arithmetic. A bare 2024-01-01 is integer
subtraction (2024 − 1 − 1 = 2022), not a date; written against a Date-typed position it is
refused with OE1340 and a directed hint toward the #…# form
(the #date# silent-Int landmine, fixed in arc5). There is one delimiter; the inner ISO-8601 /
RFC-9557 shape totally determines the carrier (RFD 0067) — a total, static, refusing
kind function, never a coercion:
| Literal | Carrier |
|---|---|
#YYYY-MM-DD# | Date (civil, day) |
#HH:MM:SS[.fff]# | Time (civil wall-clock) |
#…T…# (no zone marker) | DateTime (civil, zoneless) |
#…T…Z# or #…T…±HH:MM# | Instant (absolute UTC point) |
#…T…±HH:MM[IANA/Zone]# | ZonedDateTime |
The inner text is validated at compile time (month 01–12, day in range, leap years exact; time and
zone fields per RFC 3339 / RFC 9557). A trailing Z denotes an Instant (UTC = an absolute
point), following ISO/RFC-9557, not a civil DateTime; a bare #…T…# with no zone marker is the
zoneless civil DateTime. There is no mandatory Z. Of the carriers above, Date and civil
DateTime are executable today; the Time, Instant, and ZonedDateTime literal forms are
described here and recognized by the surface grammar ahead of the value layer landing them (issue
#1285). Duration is not a #…# literal — it is a length, not a point (see below).
pub type Invoice { mut issued: Date, mut paid: Date }
// `<int>.days` / `<int>.weeks` are duration sugar: a fixed whole-day count
// (`30.days` ⤳ 30, `2.weeks` ⤳ 14). `Date ± Duration` evaluates exactly.
pub derive overdue(i: Invoice) :- Invoice(i), i.paid > i.issued + 30.days;
// `today()` reads the evaluation's current valid-time (a `Date`).
pub derive past_due(i: Invoice) :- Invoice(i), i.paid < today();
Duration construction. A duration is a count of whole days in this increment. The fixed-length units <int>.days and <int>.weeks lower at rule-term resolution to a Duration literal — no impl is needed, and they work in rule bodies today. The calendar-relative units .months / .years have no fixed day count (a month is 28–31 days depending on the anchor), so they are loud-refused with OE1337 rather than silently approximated; compute the target date explicitly instead. Sub-day units (.seconds / .minutes / .hours) await the sub-day Time value layer.
today() — the current valid-time of the evaluation, as a Date — is the one nullary date intrinsic that evaluates today (now() awaits the sub-day layer). It is fixed for the whole evaluation, so a rule stays a pure function of its snapshot; a served model’s deadline arithmetic advances with the wall clock across requests rather than baking a build-time date.
Operator semantics — division. / is exact field division on the numeric tower, on every plane (checker, runtime, reasoner). Statically, Int / Int : Real — the quotient of two integers is a rational, and the type says so; 7 / 2 evaluates to the exact rational 7/2, never a truncated 3. The inference is uniform: 20 / 5 : Real statically even though the value collapses at the carrier (next sentence) — soundly, since Int <: Real. At the value carrier, integral results collapse to Int per the tower-wide convention (20 / 5 is Int(4); integral Real values are Int at the carrier). All other arithmetic operators on Int × Int stay Int. Truncating integer division does not exist; if it is ever wanted, it will be an explicit operator, never /.
Coercion rules:
- Implicit widening:
i32 → i64 → Int,Nat → Int, andInt → Real. SinceRealis exact (RFD 0016),Int → Realis exact-into-exact (Int ⊆ Realmathematically):require perPeriodValue > 0compares theRealagainst theIntliteral0without an explicit0.0, andInt + Realyields aReal; a pure-Intexpression staysInt— except/, which isReal(operator semantics above). The inexact floatsf32/f64do not implicitly widen intoReal— a float reaches the exact tier only via an explicit, lossy conversion (widening an approximate value into the exact tier must be a visible choice). - Explicit narrowing:
i64::try_into::<i32>()returningResult<i32, OverflowError>. - No implicit truncation, ever.
Per RFD 0016, Real/Decimal/Money are exact (arbitrary-precision rational); the reasoner folds them exactly (so avg over a Real collection is exact). Custom-precision fixed-point and refinement-driven width selection are out of scope (Out of scope).
Effective-dating: valid time and the two as_of axes
Argon’s substrate is bitemporal (RP-004): every assertion carries two orthogonal time axes.
- Transaction time (TT) — when the system was told. The append-only event log’s stamp; an
as_of <int>query reads “what the system would have said at TT = t” (the tx-counter axis). Audit/replay. - Valid time (VT) — when the fact held in the world. Independent of when it was recorded; an
as_of <#date#|#instant#>query reads “what was VALID at VT = t, as currently believed.” Effective-dating.
The two are independent and composable: a query may pin TT alone, VT alone, or both (the full bitemporal point). They answer different questions — “what did we believe last Tuesday” (TT) versus “what rate was in effect on 2024-04-15” (VT) — and a model that conflates them (a hand-rolled effectiveFrom: Date field) cannot express the second cleanly.
Asserting at a valid time. A write carries an optional at <t> qualifier; the assertion’s valid
time begins at t rather than at write time. The valid-time axis is nanosecond-resolution, not
day-granular (RFD 0067): at accepts an Instant (sub-day) as well as a #date#, so
one substrate serves legal effective-dating at day precision and agent/robot state at nanosecond
precision, differing only in which literal precision the modeler writes. A #date# is the
convenience form — its valid time begins at that civil day’s midnight UTC. Both the imperative
mutate-body form and the declarative pub fact form take the qualifier:
pub type StandardRate;
// effective-dated enactment: the membership holds FROM `effective` onward
pub mutate enact(r: StandardRate, effective: Date) {
insert iof(r, StandardRate) at effective;
}
// the declaration-form twin — a top-level effective-dated assertion
pub fact StandardRate(rate2024) at #2024-01-01#;
The at <t> expression must evaluate to a temporal point — a #date# or #instant# literal, or a Date/Instant-typed value; a bare arithmetic value is refused (OE1339). A valid time whose epoch-nanoseconds fall outside the bitemporal axis is loud-refused, never silently clamped (RFD 0067, following the out-of-range-literal precedent). The window form during <window> and bitemporal retraction (a valid-time-qualified delete) refuse loudly (OE1330) rather than silently applying at all valid times.
Reading at a valid time. An as_of <#date#|#instant#> clause on a query projects the extent
valid at that point — a civil day, or (sub-day) a nanosecond Instant. Snapshot selection is a
granularity-free integer comparison, so the same clause serves both precisions. The axis is chosen
lexically: as_of <int> selects transaction-time; as_of <#date#|#instant#> selects valid-time.
pub type Rate;
// "what rate(s) were in force on this filing day?" (the VALID-time axis)
pub query rate_on_filing_day() -> Rate as_of #2024-04-15#;
// "what the system believed at TT = 5 about VALID = now" (the TX-time axis)
pub query rate_at_tx_5() -> Rate as_of 5;
A Rate enacted at #2024-01-01# is invisible to an as_of #2023-…# read and visible to an as_of #2024-…# read — the effective-dating round-trip. See examples/effective_dated_tax_v0 for the full rate-table transcript.
Reserved keywords
Lexer-level reserved words cannot be used as identifiers. The table below is the complete set, generated from the grammar’s keyword table; a keyword carrying a clarifying note shows it inline.
| Keyword | Note |
|---|---|
abstract | RFD 0027 D6 — substrate-neutral declaration modifier: no direct instances. Legal on metatype declarations (every introduced type is abstract) and per-type on concept declarations (pub abstract type Vehicle). Instantiability for the OE1327 coverage gate is NOT-abstract — the compiler reads this flag, never a user axis name. |
across | |
acyclic | |
all_shortest | |
always | |
ambiguous | |
amend | RFD 0076 — belief-time fact correction: withdraws a recorded assertion as false-ab-initio (retaining the audit history) and, in the composite amend R(old) => R(new) form, asserts the corrected tuple against the corrected view. Requires the enclosing mutate to declare #[allow_amend] (OE1405), mirroring forget/#[allow_forget]. |
any | |
as | |
as_of | Bitemporal time-travel query clause. pub query foo() -> T as_of <expr> filters events by transaction time at query execution. Wire-encoded as an Option<CborValue> on QueryDeclBody. |
asc | |
assert | |
async | |
at | |
await | |
both | |
box | |
box_minus | |
box_plus | |
bridge | |
by | |
check | |
const | |
delete | |
derive | |
desc | |
detach | |
diamond | |
diamond_minus | |
diamond_plus | |
do | |
during | |
else | |
emit | |
enum | |
ever | |
except | |
exists | |
expect | |
fact | |
false | |
fixed | RFD 0027 D6 — substrate-neutral declaration modifier: classification decided at construction. Metatype-level only (pub fixed metatype kind = { … };). insert iof / delete iof against a type introduced by a fixed metatype refuses (OE0234); construction is unaffected. Dynamic classification is the default; fixed is the opt-in restriction. |
fixture | |
fn | |
for | |
forall | |
forget | |
from | |
group | |
having | |
if | |
iff | |
impl | |
in | |
insert | |
intersect | |
into | |
iof | |
is | |
k_shortest | |
let | |
limit | |
logic | |
macro | |
mapping | |
match | |
meta | |
metarel | |
metatype | |
metaxis | |
mod | |
mut | |
mutate | |
not | |
not_fact | RFD 0010 — strong (classical) negation of facts. Distinct from not (NAF in rule bodies, no wire trace) — pub not_fact P(x) is a declaration that lowers to an IofRefutation / RelationTupleRefutation axiom event. |
offset | |
on | |
optional | |
or | |
path | |
pub | |
query | |
require | |
retract | |
return | |
select | |
Self | |
self | |
set | |
shortest | |
simple | |
since | |
sink | |
specializes | |
standpoint | |
struct | |
test | |
timeout | |
trail | |
trait | |
true | |
union | |
unknown | |
unsafe | |
until | |
update | |
upsert | |
use | |
via | RFD 0084 D6 — the family-query via sugar: Parent(args) via k in a rule body desugars to the generalized specializes(k(_fresh…), Parent(args)) family query, binding the witness k to the answering family member. Reserved rather than contextual because the collision check (RFD 0084 D6) found via free in every rule-body position; from/[k] were rejected for live collisions. |
walk | |
where | |
with |
Reserved for future use (parse but currently unimplemented): async, await, do, or.
Plus type-level constants and primordial types (always in scope, see the standard library): Top, ⊤, Bot, ⊥, Nat, Int, Real, Decimal, Money, Bool, String, Date, Time, DateTime, Duration.
Formerly reserved. mode and ordered are no longer keywords (RFD 0031 D7): both were vestigial reservations with no parser rule consuming them, and mode blocked a common vocabulary word (pub metatype mode). The documented graph-traversal mode clause (mode-spec, the rule atom) is recognized contextually in the role-step position when that surface lands, never as a reserved word. The Allen interval relations (before, meets, met_by, overlaps, overlapped_by, contains, starts, started_by, finishes, finished_by) are no longer keywords; Allen interval algebra ships as std::allen library vocabulary (RFD 0024), freeing those words for modelers. during remains reserved — it is the temporal-qualifier keyword (at | during | since), not an Allen operator.
Not reserved at lexer level — introducer names are ordinary identifiers: type, rel, and any vocabulary-introduced metatype or metarel name (UFO’s kind, subkind, role, phase, category, relator, mixin, mediation, material, BFO’s class, …). These are contextual identifiers resolved per name resolution against the pub metatype / pub metarel declarations visible in scope — names come from vocabulary packages, declared in the using package or imported from an external one; an unresolved introducer is OE0605 / OE0606. type and rel are not ambient: they are declared in std::core (see the standard library) and brought into scope by use std::core::{type, rel} or a [package].prelude entry (RFD 0038), exactly like any vocabulary. No vocabulary — not even the std::core baseline — ships ambient.
Compiler-known attribute names (reserved against macro shadowing, but not lexer keywords) live in a separate list — see annotations.
Out of scope (v0)
Deliberately deferred:
- Set/map literal syntax (
{1, 2, 3},{"a": 1}). - Registry and VCS dependencies.
ox buildresolves[dependencies]path dependencies only (Build pipeline); a bare version requirement (ufo = "1.0"), theversion/git/branch/tag/rev/registrykeys, and theox.locklockfile refuse withOE1240. - Federated
service <endpoint> { … }external-store calls. - Managed bridge rules — the
#[add]/#[delete]/#[revise]/#[override]operators (Brewka et al. 2011) that withdraw or rewrite target-standpoint conclusions, and the#[managed]escape from the bridge-graph acyclicity requirement (Bridge rules). - MLT* orderless types.
#[brave]/ stable-model semantics — multiple two-valued models with credulous and skeptical readings, as an alternative to the default well-founded semantics (derive).- User-defined operators.
- Async / await.
- Const generics.
- Higher-kinded types.
- Runtime trait objects (
dyn Trait). - Topology declarations inside
metarelbodies. - Refinement-driven width-selection on primitive types (current behavior: arbitrary-precision; explicit
i32etc. for fixed-width). - Raw-identifier escape for hard-reserved keywords (e.g., a Rust-style
r#prefix). Today, vocabulary authors may freely use contextual identifiers (type,rel,kind,role, …) because they are not lexer-level reserved — see reserved keywords. Hard-reserved keywords (derive,query,mutate,check,fn, …) cannot currently be used as identifiers. If demand surfaces from vocabulary authors, anr#escape is the obvious extension. - Hypergraph path traversal. v0
Path<NodeT, EdgeT>(ThePathtype) is over binary relations only; n-ary relations (pub rel Transaction(seller, buyer, item)) are queryable as predicates but not path-traversable. A principled hypergraph traversal semantics — specifying which endpoint the path enters and exits through — is deferred. - Temporal proof tags — the 4-slot Governatori-Rotolo schema $\pm\partial^{t_d}@{t_r}, l_{t_l}$ over the bitemporal substrate (Temporal substrate, Defeasibility).
- Alternative defeasibility strategy vocabularies — ambiguity propagation, stricter team-defeat variants, dynamic preferences, MKNF hybrid knowledge bases, rational closure / KLM typicality. These arrive as
use-imported strategy packages, not switches on the Governatori strategy (Defeasibility).
These may land in later language versions.
Diagnostic codes
The compiler crate oxc-diagnostics holds the authoritative diagnostic catalog
(generated.rs), regenerated from the [[diagnostic]] entries in
oxc-syntax/grammar.toml. This appendix is generated from the same entries
— it cannot drift from the catalog (CI enforces it via cargo xtask check-drift).
Where prose elsewhere in the book disagrees with a row here, this table wins.
A code is O[E|W|I]NNNN — an O-prefix (legacy Ontolog/Oxide; carries through
pending a v0.3 rename), a severity letter, and a four-digit number.
The catalog has 388 codes (367 OE + 19 OW + 2 OI), of which 350 are live (a real emitter exists) and 38 are reserved for planned later stages.
| Severity | Meaning |
|---|---|
OE | Error: blocks elaboration or load |
OW | Warning: surfaces, does not block |
OI | Info: a note; nothing is blocked |
The Status column distinguishes a live code (a real emitter exists in the
toolchain) from a reserved one (declared up-front for a planned later stage,
no emitter yet — audit dc-04). A reserved code never fires today; it is listed
so the number is claimed and the planned gate is documented. There are no
library-namespaced codes (mlt::, potency::, ufo::) in the catalog: a
theory package authors its constraints as its own rules/checks over the neutral
substrate, never as reserved compiler codes. The MLT reasoning codes that once
sat in 19xx are gone for exactly that reason; the 19xx codes that remain are
the cross-level instantiation-body field-coverage gates (§5.2), which are
substrate-level, not theory-specific.
Range allocation
The number’s leading digits group codes by subsystem. These are conventions for allocation, not a hard partition; ranges with no codes today are omitted.
| Range | Subsystem |
|---|---|
| 00xx–01xx | Parse / syntax; name resolution and imports |
| 02xx | Type / elaboration / predicate-atom validation; membership-write gates |
| 05xx | Stratification |
| 06xx | Meta calculus, refinement, comparison handling, trait conformance |
| 07xx | Modules, attributes, temporal/modal, forget |
| 08xx | Field-modifier gates, world-assumption temporal index, positional construction |
| 09xx | World-assumption conservativity and placement |
| 10xx | CWA evaluation, field intent |
| 11xx | Bridges |
| 12xx | Build composition + .oxbin validation (§16) |
| 13xx | Runtime contract + rule-evaluation gates (§17) |
| 14xx | Individual lifecycle (retraction cascade) + write-path stability gates |
| 19xx | Cross-level instantiation bodies |
| 24xx | Collection operators |
Parse and name resolution (00xx–01xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0001 | Error | Live | UnexpectedToken | Parser found a token it did not expect at the current position. |
| OE0002 | Error | Live | UnterminatedString | String literal missing its closing ". |
| OE0003 | Error | Reserved | InvalidNumberLiteral | Numeric literal could not be parsed. The INT/REAL literal grammar is §2.5. |
| OE0004 | Error | Live | InvalidEscapeSequence | Unrecognized escape sequence in a string or char literal. |
| OE0005 | Error | Live | UnterminatedBlockComment | Block comment /* … */ missing its closing delimiter. |
| OE0006 | Error | Live | NonAssociativeChain | Comparison (== != < <= > >=) and membership (in, not in) operators are non-associative (§6.6): a bare chain like a < b < c is refused at parse. Pre-refusal the parser folded chains left-associatively, so a < b < c became the Bool-vs-Int comparison (a < b) < c — type-mangled, and in a rule body silently never-true (every row dropped under a green check). Write the conjunction explicitly (a < b && b < c); explicit parentheses ((a < b) == c) are accepted as written. |
| OE0007 | Error | Live | QueryClauseNotYetImplemented | A query select-body uses a result-shaping clause — group by … having, `order by … asc |
| OE0008 | Error | Live | ModifierMisplaced | A declaration modifier appears on a form that does not admit it. abstract attaches to metatype declarations (every type the metatype introduces is abstract) and per-type to concept declarations (pub abstract type Vehicle { … }); fixed is metatype-level ONLY (pub fixed metatype kind = { … }; — classification-mutability is a property of the behavior bundle, not of one type). Any other placement — abstract struct, fixed before a concept declaration, a modifier on enum/rel/trait/rule forms, or a repeated modifier — is refused at parse rather than silently dropped: a swallowed modifier would let the program LOOK constrained while the substrate enforces nothing. |
| OE0009 | Error | Live | EnumPipeFormRemoved | An enum declaration uses the `= variant |
| OE0011 | Error | Live | MissingTerminator | A bodyless declaration is missing its terminating ;. A declaration is terminated by exactly one of: its body’s closing } (pub category Animal { … }), its cover’s closing } (pub kind Vehicle { Car, Truck }), or — when it carries neither — a required ; (pub category Top;). This is the same item-termination rule as Rust (struct Foo; needs the ;, struct Foo { … } does not). The terminator used to be optional: with no ; and no body the parser could not tell where the declaration ended, so a following #[…] attribute was silently re-attributed to the prior declaration and mis-expanded, surfacing a wrong, unrelated error far from the cause. Add the ; at the end of the unterminated declaration. |
| OE0012 | Error | Live | CoverPipeFormRemoved | A concept declaration uses the `= A |
| OE0013 | Error | Live | RelationEndMutMisplaced | A mut relation-end modifier appears where it is not admitted. mut is legal only immediately before a named end of a concrete relation declaration (rel R(mut end: T, …)); it fires here for mut before an anonymous end (rel R(mut T) — there is no end name to attach the modifier to; name the end) or inside a metarel signature (metarel m(mut a: T, …) — mutability is declared per concrete relation end, never on the metarel; move the mut to the concrete relation declarations the metarel classifies). |
| OE0014 | Error | Live | ListCardinalityNotYetImplemented | A list type carries a cardinality bound ([T; n..m], [T; >= n], [T; n..m, ordered] — the built-in type forms table and the anonymous-field cardinality default document the surface) that does not enforce yet. The bound parsed but was SILENTLY DISCARDED — the type lowered as a plain unbounded [T] list, so a model could LOOK width-constrained while any width committed with no diagnostic. Refused by name at parse until write-path enforcement lands. Use a plain [T] list for now. |
| OE0101 | Error | Live | UnresolvedName | Bare identifier did not resolve to any in-scope declaration. |
| OE0102 | Error | Live | ReservedModuleName | std is a reserved top-level module name; the compiler embeds the stdlib at this namespace. User-declared modules named std would shadow stdlib imports and are refused. Rename the module. |
| OE0103 | Error | Live | UnresolvedUseImport | A use a::b::C; import does not resolve to any item in scope. Before this gate a broken use was accepted clean and the only error was a misleading downstream OE0605/OE0101 far from the cause; now the use itself is refused, with a did-you-mean suggestion over the names visible in the target namespace (a dependency package’s pub surface declared in [dependencies], a sibling module, an intra-package pkg::/self::/super:: path). A glob use pkg::*; brings in the target’s pub surface and is never a silent no-op; a glob whose prefix is itself unresolved is the same OE0103. |
| OE0104 | Error | Reserved | GlobImportUnsupported | Reserved (no emitter): a glob use path::*; whose shape is not yet supported. The supported glob — re-export-aware import of a module’s pub surface — resolves; this code is held so a future unsupported glob variant refuses feature-named rather than silently importing nothing. |
| OE0105 | Error | Live | DuplicateImportBinding | Two use imports bind the same local name to DIFFERENT definitions — use va::Kind; use vb::Kind; (with or without an as alias on either) — so a bare use of the name resolved silently to whichever import the resolver consulted first, and its meaning flipped with import order. This is refused loudly: a local import name must resolve unambiguously. Fix it by giving the imports distinct as aliases, or by dropping the redundant use. Re-importing the SAME definition under one name is idempotent and allowed; a single import that binds one name across multiple namespaces at once (for example a shared relation and its decorator macro) is likewise not a conflict. When the conflicting imports are metatype, metarel, or metaxis introducers, the ambiguity is reported instead by the introducer/binding diagnostic at the point of use, which names both candidate definitions; name clashes introduced by a glob use m::*; follow separate shadowing rules and are reported at the use site. |
| OE0106 | Error | Live | GlobImportAmbiguity | A name brought into scope by two or more glob imports (use m::*;) resolves to DIFFERENT items, and no explicit import or local declaration disambiguates it, so a use of the name is ambiguous. This is refused at the use site: the resolver never silently picks one glob’s item over another’s, and its meaning must not depend on import order. Fix it by importing the intended item explicitly (use m::Name; — an explicit import silently shadows the globs), by qualifying the reference (m::Name), or by dropping one of the conflicting globs. An unused ambiguous name is not reported; two globs that re-export the SAME item are not a conflict; an explicit import or a same-module declaration always shadows a glob import of the same name. |
| OE0107 | Error | Live | NavViewFilterUnboundName | A bare value name in the where filter of a navigation-view field (f: [T] from Rel.endpoint where <predicate>) is not in scope. The filter binds exactly one local name: the endpoint name written after the relation in the from clause — supported in from Supports.supported. Module-scope declarations (concepts, relations, rules, constants) remain visible, so where supported : Dependent, where under13(supported) and where supported.age < 13 all resolve. Neither self nor the relation’s other endpoints, including the holder whose field is being computed, are available in the filter. The message names the local that is available without assuming the unresolved name was a typo. Fix a typo by using that local. If the condition genuinely needs the holder or another endpoint, express it in a pub derive rule and call that rule with the available local. Reported instead of the generic unresolved-name refusal only for an unresolved bare value inside a navigation-view where filter. An unresolved callee, qualified path, or name outside such a filter keeps the generic code. |
| OE0150 | Error | Live | RelationSubsumptionArityMismatch | Relation R1 <: R declaration where R1 and R have different parameter counts. |
| OE0151 | Error | Live | RelationSubsumptionEndpointVariance | Relation R1 <: R declaration where an endpoint position’s type in R1 does not specialize (<:) the corresponding position in R. |
| OE0152 | Error | Reserved | RelationSubsumptionCardinalityViolation | Relation R1 <: R declaration where a slot’s cardinality [c..d] does not refine the parent’s [a..b]. Refinement requires c >= a and (d <= b or b = *). |
| OE0153 | Error | Live | RelationSubsumptionMetarelMismatch | Relation R1 <: R declaration where meta(R1) != meta(R) (MVP rule — cross-metarel subsumption is deferred). |
| OE0154 | Error | Live | RelationSubsumptionCycle | Relation subsumption graph contains a cycle (R1 <: R2 <: ... <: R1) — the substrate requires an acyclic subsumption DAG. |
| OW0010 | Warning | Live | NonCanonicalClauseOrder | A concept declaration writes the : iof-instantiation clause before the <: specialization clause. Both orders parse and mean the same thing — the two clauses are independent (parents are an unordered set on each axis) — so this is a style WARNING, not an error: it never blocks ox check/ox build. The canonical spelling places <: before : (pub subkind Penguin <: FlyingAnimal : Vertebrate_Species { … }), mirroring the order arithmetic — specialization parents sit at the concept’s own order, instantiation parents one above. oxfmt will normalize the order for you once the CST-driven formatter lands; reorder the clauses to silence the warning in the meantime. |
Type, elaboration, predicate atoms, membership writes (02xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0201 | Error | Live | TypeMismatch | Expected one type but found a different one — expected X, found Y. |
| OE0202 | Error | Live | ArityMismatch | Function call has the wrong number of arguments. |
| OE0203 | Error | Live | NonExhaustiveMatch | match arms do not cover every constructor of the scrutinee type. |
| OE0204 | Error | Live | UnknownField | Field access x.f — in a derive/check/bridge rule body or a fn/mutate body — where the receiver’s established type, a struct or concept (pub type ..., or a vocabulary introducer) including inherited fields and any operand : T type-test narrowing in scope (a rule-body type-test or a comprehension where e : T), declares no field named f. In rule bodies, reported only on provable absence: f must be missing from every established concept of the receiver, since an Argon individual may be classified under several concepts (a field on any asserted concept is admitted). Evidence-gated: a base with no established concept (an unbound variable, or a qualified path to a declared type/enum such as Color.red) is skipped. |
| OE0207 | Error | Live | RequiredFieldMissing | insert T { … } omitted a field that T or one of its <: ancestors declares as required at construction (intent Required — not T? / Truth4Of<T> — with neither a default nor a from-navigation source). Emitted by the runtime construct path (oxc-runtime) as a loud refusal with nothing committed, rather than silently minting an incomplete individual. Distinct from OE1014 RequiredFieldUnasserted, the CWA evaluation-time case; the OE0207 number is a known wart pending the v0.3 renumber. |
| OE0208 | Error | Live | AmbiguousFieldFromMultipleParents | A concept inherits SAME-NAMED fields from two <:-incomparable ancestors (a field diamond — §5.2): no unique binding exists for the name, and the substrate offers no automatic merge, override-by-position, or last-wins resolution. Before this gate the collision silently resolved rightmost-parent-wins at every flatten site (ox gen schemas, the nav carrier scan) — swapping the parent order swapped the field’s type, with zero diagnostics. A LINEAR declarer chain is NOT a diamond: a unique most-specific declarer (every other declarer among its ancestors) is the field’s binding, so a subtype redeclaring an inherited field stays admitted. Fix it by renaming one of the colliding fields, or by redeclaring the field on the child to bind it explicitly. The per-parent field qualifier (bar.Parent::field) is the designed future disambiguation surface and does not parse yet. In-file <: graph only (cross-module parents defer, like OE0242/OE0243). |
| OE0210 | Error | Reserved | IofInsertOnRigidType | Reserved (designed, not yet wired — §7.5): a dynamic insert iof(x, T) reclassifying an individual into a rigid type (rigidity::rigid — kind, subkind, category) rather than an anti-rigid one (role, phase, mixin). To create a new individual of a rigid type, use insert T { … }. The rigidity gate is not yet wired into the runtime iof path; this entry reserves the code for it. |
| OE0211 | Error | Live | IofInsertOnDefined | Manual membership write into a defined concept whose membership is derived, not asserted: insert iof / delete iof on an iff or shape-defined concept (or one with a defined <:-ancestor), and insert C { … } construction of a shape-defined concept or a subtype beneath one. Update the underlying state instead; the substrate auto-classifies. Emitted by the runtime mutate path (oxc-runtime): IofInsertOnDefined, the upward-closed ancestor case (IofInsertOnDefinedAncestor), and the construct door (ShapeDefinedConstruct). |
| OE0212 | Error | Reserved | MetaArgUnbound | Reserved refusal for an unbound argument position of the type-position reflection intrinsics (meta/iof/specializes/extent). implements is EXEMPT by design: $implements is a finite catalog-closed relation, so both of its positions may be free — enumeration (free t: the implementing types; free tr: a type’s traits) is the intended use. The current engine answers the other intrinsics’ free positions relationally too, by enumerating their materialized $-relations, so this code has no emission site today; it is reserved for argument shapes a future evaluator cannot satisfy relationally. |
| OE0213 | Error | Live | IofInsertIntoBot | insert iof(x, Bot) (or delete iof(x, Bot)) targets Bot/⊥, the empty concept (lattice bottom), which by definition has no instances — the assertion is a contradiction and is refused at ox check / ox build. A sound empty-category refusal that follows from Bot’s definition alone, NOT from concept-vs-concept disjointness (which stays reserved, OE1904). The dual Top/⊤ is the lattice join — every individual is an instance of it — so a rule body Top(x) ranges over the whole domain. |
| OE0214 | Error | Live | CoverClauseUnsupported | A concept declaration carries a cover body (pub category Pet { Dog, Cat }) — the surface parses, but the elaborator does not yet thread cover members into the subkind hierarchy, so the body would assert NOTHING: no classification of the members under the declared type, no partition/exhaustiveness axiom, zero diagnostics. It reads exactly like it should mean something, so silently accepting it flattened whole taxonomies. The threading of cover members into the subkind hierarchy is designed but not built; until it lands, state the intent explicitly: declare each member a subtype (pub type Dog <: Pet;) and, for the cover/exhaustiveness semantics, add a complete/partition group axiom over the parent (partition Pet { Dog, Cat } — OE0242/OE0243 validate it). |
| OE0220 | Error | Live | FactReferencesUnknownPredicate | pub fact P(...) references a predicate name that does not resolve to a concept or relation in the module’s fact-head scope (same-file declarations, plus imported concepts/relations the file’s use/prelude scope reaches). When the name IS declared elsewhere in the workspace but is out of this scope, the message says so and names the working pattern: mint the individual where the concept is declared (pub fact P(x); in the declaring module) and reference it cross-module by its bare name — individual identity is the global bare-name hash — or bring the concept into scope with a module-qualified use. |
| OE0221 | Error | Live | FactArgArityMismatch | pub fact P(args) argument count doesn’t match the declared arity of P (1 for concepts; the relation’s parameter count for relations). |
| OE0222 | Error | Live | FactArgTypeMismatch | pub fact P(args) argument shape doesn’t match a declared parameter type (e.g., a literal in a slot that expects an individual). |
| OE0223 | Error | Live | RuleReferencesUnknownPredicate | A rule body or head (derive / check / bridge) references a predicate atom P(...) whose head name does not resolve to a declared concept, relation, derive/query/check head, or reflection intrinsic in scope (§7.3.1). Predicate vocabulary must be declared; OWA governs the truth value of instances of a declared predicate, not whether the predicate name itself exists. |
| OE0224 | Error | Live | PositionalConceptInsert | A positional insert C(...) in a mutate body where C resolves to a concept constructs nothing — parenthesized insert asserts a relation tuple, and the head is a concept, not a relation. Use the brace form insert C { field = value, ... } to construct an individual. Construction is brace-only; parens are reserved for relation-tuple assertions. |
| OE0225 | Error | Live | RuleAtomArgArityMismatch | A predicate atom P(args) in a derive / check / bridge rule body or head supplies the wrong number of arguments for the declared arity of P (1 for a concept-as-predicate; the relation’s parameter count for a relation; the head arity for a derived predicate). The non-ground analogue of OE0221 (pub fact), split by context per the OE0220→OE0223 precedent. |
| OE0226 | Error | Live | RuleAtomArgTypeMismatch | An argument to a predicate atom P(args) in a derive / check / bridge rule body or head has a type incompatible with P’s declared parameter type at that position (e.g., a logic variable bound elsewhere to String used in a slot declared Int, or a literal of the wrong primordial). Reported only when both the argument’s type and the parameter type are concretely known. The non-ground analogue of OE0222 (pub fact). |
| OE0227 | Error | Live | RuleValuePathUnresolved | A multi-segment path (a::b) in a rule body resolves to nothing under the unified value-position resolution order (bound rule variable, enum constant, axis value, type reference, declared individual — and, in predicate position, rule heads / concept extents / relations first). Before this gate the lowering silently turned an unresolved path into a VARIABLE named by the joined path — rigidity::anti_rigid then tripped OE1303 as an “unbound variable”, and a path in predicate-argument position bound everything and over-derived. Refused at ox check / ox build instead, naming the path and the namespaces searched. Also fires when a bare A::B rule atom resolves to a CONSTANT (a constant is a value, not a predicate; the path :: Ident metaEq sugar applies only when the left side is a bound term AND constant resolution declines the path), and when a qualified path matches several declarations (qualify further). Fix the spelling, declare the missing symbol, or qualify the reference. |
| OE0228 | Error | Live | RuleValueNameAmbiguous | A bare identifier in a rule-body value position is BOTH a rule variable (a head parameter, or a comprehension/quantifier/aggregate binder) AND resolves to a declared constant (a declared individual). Refused naming both candidates rather than silently picking one — the lesson of Rust’s bindings_with_variant_name lint, promoted to a refusal: silently reading it as a variable shadows the constant (the pre-S0 behavior that made knows(x, carol) over-derive); silently reading it as the constant changes the rule’s arity semantics. Rename the variable, or qualify the constant (module::name). A bare identifier that occurs ONLY in body positions and resolves to a declared constant IS that constant — no ambiguity, no error. |
| OE0229 | Error | Live | UnknownFieldWrite | A mutate-body write names a field the target concept does not declare: a constructor field (insert C { f: v }), an update … set { f = v } assignment, or an insert … into x.f target, where f is missing from C and from every ancestor in its <: chain. Reported only on provable absence — the concept and its full ancestor chain must be declared in the elaborating file; cross-module targets defer to runtime. Without the gate a typo’d field name persists under a property id no read path resolves — silent data loss. |
| OE0230 | Error | Live | InsertIntoNonCollection | insert <elem> into x.f targets a field whose declared type is not a collection ([T] / List<T> / Set<T>). insert into appends to collection fields; on a scalar field it would silently replace the committed scalar with a one-element list, corrupting the field’s type and detaching every refinement and derive that reads it. Use update … set { f = … } for scalar writes. |
| OE0231 | Error | Live | PropertyIdCollision | Two distinct Type::field pairs fold to the same property NameRef under the hash-derived property_id_for_field stand-in. Property ids are masked into the INT4 wire band [1, 2^31-1] (the high bit is reserved); the 31-bit space leaves room for collisions at workspace scale (birthday bound ≈ 2^15.5 field identities), and a collision is silent data aliasing — the two fields would share one storage column, the later declaration’s reverse-lookup entry shadowing the earlier. Refused at module load: the closed-set ConceptDecl walk that builds the resolution map is the injectivity oracle, so the collision converts to a build/load refusal naming both pairs instead of corrupting writes. Rename one field, or rebuild once the sequential interning table retires the hash stand-in entirely. |
| OE0232 | Error | Live | RelationEndpointMissing | A write places an individual into a typed slot — a relation endpoint or a concept-typed field — that no committed event introduces: a dangling reference. The ruling: such writes REFUSE missing individuals at write time; an individual EXISTS iff some committed event names it (an iof assertion OR a property assertion — full classification is not required, preserving §3.5 domain-conservative ghost-individual semantics). Fires on three surfaces. STATIC: a pub fact R(…, e, …) whose identifier endpoint e is in no concept-membership pub fact C(e) in the elaborating file (build-time, all information in source; cross-module endpoints defer to runtime). RUNTIME (relation): an insert R(…) in a mutate body (or CLI/serve write) whose individual endpoint resolves to an id no iof/property assertion in the store names. RUNTIME (field): an insert C { f: o } / update t set { f = o } whose concept-typed field f is given an individual o that no committed event names — the field-side counterpart of the relation endpoint floor, since a concept-typed field is a typed slot exactly as an endpoint is. Both runtime surfaces are checked against the overlay’s read-your-writes view, so an individual constructed or classified earlier in the SAME body counts. Without the gate, a relation’s declared signature (or a field’s declared type) is decorative: derives joining serve rows whose endpoints are in no extent, and a forged below-floor #i reference would slip past the serve floor-bound. Atomic — raised before the tuple/field is buffered, so the whole body commits nothing. Existence is the FIRST gate; TYPING is the layer above it (OE0258): once the individual exists, a closed-world target concept refuses a write over an UNclassified individual (CWA cannot silently expand a closed extent), while an open-world concept admits it. delete iof-driven cascade is a recorded non-decision (deferred). Construct or classify the individual first, or fix the reference. |
| OE0233 | Error | Live | AbstractTypeConstruct | The program asserts a direct instance of an abstract type: insert T { … } where T is declared abstract (or is introduced by an abstract metatype), a direct insert iof(x, T) classifying an existing individual under it, or a positive pub fact T(x) seeding one at elaboration. abstract means NO DIRECT INSTANCES — abstract types structure the hierarchy (and are exempt from trait impl-coverage obligations, OE1327) precisely because every individual is classified through some non-abstract subtype. Assert or classify under a non-abstract subtype instead; subtypes are unaffected, and pub not_fact T(x) (refuting membership) stays legal. Emitted at ox check/ox build where the target is statically known; the runtime enforces the same refusal at the write path (atomically — the whole mutation rejects) for targets only resolvable at execution, and at artifact load for hand-built IofAssertion events that would bypass the elaborator. |
| OE0234 | Error | Live | FixedReclassification | A mutate body re-classifies under a type introduced by a fixed metatype: insert iof(x, T) or delete iof(x, T) where T’s introducing metatype is declared fixed. fixed means classification is DECIDED AT CONSTRUCTION — membership of fixed-introduced types never changes over an individual’s lifetime, which is what makes static check discharge over them sound (§10.2: rigid designation = fixed classification). Construction (insert T { … }) is NOT re-classification and is unaffected; types introduced by non-fixed metatypes classify and de-classify freely (dynamic is the default — the restriction is the opt-in, the same posture as check). Emitted at ox check/ox build where the target is statically known; the runtime write path enforces the same refusal (atomically) otherwise. Importing a vocabulary can never change mutation semantics by axis NAME — only the declared fixed modifier carries this behavior. |
| OE0235 | Error | Live | GenericTypeApplicationUnsupported | A generic type application carries arguments the elaborator does not interpret. User-declared concept generics are decorative: a concept’s <T> parameters parse but reach no storage slot (ConceptDeclBody has no type-parameter column), so an application like Boxy<Int> / Boxy<Int, String, Bool> / a bare Boxy (arity 0 against a 1-param concept) / NotGeneric<Int> (arguments on a non-generic concept) was accepted with the arity and applicability silently discarded — the §6.2 ‘generic applications subtype only by equality’ rule and the §6.1 bounded-generic grammar both imply the arguments are semantically real. The library generics with a real construction surface are unaffected (List<T>, Set<T>, Option<T>, Result<T, E>, Range<T>, Truth4Of<T>, and the live reflective TypeRef<C>); their argument shapes ARE interpreted. Refused at ox check / ox build at the use site until parameterised user concepts are real (the type-parameter plane is V1 bounded-generics territory). Drop the arguments, or use one of the supported library generics. |
| OE0236 | Error | Live | MutateFieldValueTypeMismatch | A value position in a mutate body is assigned to a declared field whose type it does not match: an update target set { f = v } whose v is a value of a primordial incompatible with f’s declared type (a String into an Int field, an Int into a Date field, a structured CBOR record into a scalar field). Pre-refusal these assignments bypassed any runtime re-check (the write path’s coerce_value_for_declared_type was temporal-coercion-only, falling through other => Ok(other) for the mismatch), so the wrong-typed value committed and downstream rules derived silently-wrong rows (an Int in a Date field made d < <date> unconditionally true under the cross-type comparison fallback — see OE0630). Emitted at ox check / ox build for the statically-decidable, sound subset — a primitive literal whose primordial type UNCONDITIONALLY contradicts the field’s declared primordial type (the constructor insert C { f: lit } and update target: T set { f = lit } paths), refused before a doomed artifact is built rather than late at query/serve. The static gate is a STRICT SUBSET of the runtime gate: value-dependent pairs the runtime may coerce (a String literal into a parse-coercible temporal / exact-numeric field, an Int into the numeric tower or Duration day-count, an integral Real into Int/Nat) stay deferred to the runtime mutate value path, which remains the backstop for those and for cross-module field-type chains. The Value::Cbor carrier seam is bounded by tag inspection: a tag-30 exact rational is checked against the numeric tower (accepted into Real/Decimal/Money/Int, refused into Date/Duration/String/Bool), and a structured map/array/enum-payload carrier is refused into the NATIVE primordials that never ride a structured carrier (Int/Nat/Real/Date/Duration/String/Bool) — a Money struct into Int is the canonical refusal. Money and Decimal are EXEMPT: today they are represented AS opaque/structured CBOR carriers, so a structured value is their legitimate form; an indecodable opaque blob or a concept-typed field also passes through. The field-coercion path (Text→Date/Duration/Real parse, Int→Duration day-count) stays accepted where the surface defines it. |
| OE0237 | Error | Live | FieldDefaultUnsupported | A field declaration carries a default-value clause field: T = expr. Book §5.1 specifies full default semantics (lazy evaluation at access, self resolution, resolution order), but the elaborator never reads the expression — the parser consumed it as token soup and the lowering hardcoded default: None, so the default silently never applied: a required field with a dropped default still refused construction (OE0207), and an optional field T? = expr constructed successfully with the default absent (silently wrong extents downstream). Refused at parse until the lazy-default evaluator lands rather than accepting a clause the substrate ignores. Drop the = expr and supply the value at construction, or use a from-navigation source (field: T from Rel.endpoint) for a derived value. The from-clause form is unaffected — it is parsed and elaborated. |
| OE0238 | Error | Live | UpdateOnUnclassified | update … set { … } targeted an individual that is an instance of no concept at all — its classification set is empty. A primitive (where) concept’s necessary invariant is keyed on a classifier, so an unclassified target evaluates none of them: the field write proceeds wholly unchecked and the corrupt value silently resurfaces (bypassing the invariant) if the individual is later classified into a concept the value violates. Refused loudly by the runtime mutate path (oxc-runtime) rather than written — classify the individual first (insert iof(x, T) or construct it with insert T { … }), then update. Atomic: nothing flushed. Scoped fix; re-validating all properties at classification time is a separate follow-up. |
| OE0239 | Error | Live | FactReferencesDerivedPredicate | pub fact P(...) names P, a predicate defined by a pub derive / pub query rule (an intensional / IDB head), not a base predicate. pub fact asserts into an EXTENSIONAL relation (a concept-as-classification or a pub rel); a derived predicate’s extent is COMPUTED by rule derivation, and a pub fact seed would key a different relation node than the rule head reads, so it silently DROPS OUT of the fixpoint (no error, wrong answer). Refused at ox check / ox build rather than emitting an artifact that mis-derives. To give a derived predicate ground tuples that participate in its derivation, use one of the two sanctioned forms: (1) a bodiless pub derive P(args); clause — a derive head with no :- ... body over CONCRETE arguments is itself a ground fact that unions with P’s other derive clauses and rules over the same head; or (2) declare a base relation the rule reads — pub rel Base(...), seed it pub fact Base(...), and add a pub derive P(...) :- Base(...) clause. Either keeps P a single-origin intensional head; pick the bodiless-derive form to seed P directly, the base-rel form when the seed tuples are themselves a reusable extensional predicate. |
| OE0240 | Error | Live | GroupAxiomDisjointOverlap | A disjoint { A, B, … } group axiom declares its members pairwise non-overlapping, but a write would classify one individual into two of them at once — membership evaluated in the fixpoint (concept atoms Ai(x) and Aj(x)), so asserted and derived membership count alike, including shape-defined members whose extent is never asserted. The membership is refused at the write rather than admitted, since an individual in two declared-disjoint siblings makes the partition contradictory and every downstream query over either sibling silently wrong. Carried by a compiler-synthesized check (one per unordered member pair) whose runtime delta-guard refuses the offending insert. Instance-level under the closed-world default (§6.9). #[world(open)] is a live concept attribute, but this disjoint delta-guard evaluates closed-world regardless of the concept’s world — the open-world tolerate softening is designed and not yet wired (the runtime guard does not read the per-concept world map). Retract the conflicting membership, or remove the member from the disjoint set if the overlap is intended. |
| OE0241 | Error | Live | GroupAxiomUncovered | A complete Parent { A, B, … } (or partition) group axiom declares its members jointly exhaustive over Parent, but a write would create a Parent instance that is a member of none of them (Parent(x) with not Ai(x) for every cover member — membership evaluated in the fixpoint, so asserted and derived coverage count alike, including shape-defined members and parents whose extents are never asserted). Under the closed-world default (§6.9) the negation-as-failure covering guard fires when no child membership is derivable, and the write is refused rather than admitting an uncovered Parent instance the cover claims cannot exist. Carried by a compiler-synthesized check whose runtime delta-guard refuses the offending insert. #[world(open)] is a live concept attribute, but this covering delta-guard evaluates closed-world regardless of the concept’s world — the open-world tolerate (permit-as-unknown) softening is designed and not yet wired (the runtime guard does not read the per-concept world map). Classify the instance into one of the cover members, or widen the cover. |
| OE0242 | Error | Live | GroupAxiomUncoveredSubtype | A complete Parent { A, B, … } (or partition) group axiom declares its members jointly exhaustive over Parent, but the schema declares a NON-abstract concept S <: Parent (transitively) that is PROVABLY uncovered — provably disjoint from EVERY cover member, so no individual can be both an S and a member (e.g. a disjoint/partition declaration separates S from each member, or the cover is empty). Any instance of S is then a Parent instance in no cover member, so the cover is statically incomplete. A subtype that is merely not a static <:-subtype of a member is NOT refused here: under multiple classification its instances can still be classified into a member at write time, so its covering is deferred to the runtime OE0241 guard (the same deferral applied to a subtype declared in another module). Refused at build by a direct elaboration pass over the <: graph (not a reasoner check — instantiable has no catalog atom to range over). An ABSTRACT subtype is exempt: it has no direct instances, so it cannot witness an uncovered instance. Cover the class (add S or one of its supertypes to the cover, or interpose a cover member S <: Ai), mark S abstract, or drop the disjointness that forces S outside every member. |
| OE0243 | Error | Live | GroupAxiomNonSubtypeChild | A complete Parent { …, S, … } (or partition) group axiom names a cover member S that is not a subtype (<:, transitively) of the declared Parent. A cover’s members partition the parent’s extent, so a non-subtype member could classify individuals outside Parent — the covering / disjointness guards would then range over the wrong extent. Refused at supertype resolution. disjoint { … } has no parent and imposes no such constraint. Declare S <: Parent, or remove S from the cover. |
| OE0244 | Error | Live | FromClauseUnknownRelation | A navigation-view projection field (field: T from Rel.endpoint) names a relation Rel that does not resolve to a declared pub rel in scope. The from clause projects the field’s value from one endpoint of that relation, so a relation that is not in scope leaves the field projecting nothing — and the clause was previously never checked: only the runtime read from_relation, so a bogus relation built clean and silently projected an empty view. The relation path is now resolved at ox check (the same name-resolution gate concept/relation references pass), and an unresolved or non-relation head is refused. Declare the relation, import it (use), or correct the path. |
| OE0245 | Error | Live | FromClauseUnknownEndpoint | A navigation-view projection field (field: T from Rel.endpoint) names an endpoint accessor endpoint that is not one of the relation Rel’s declared endpoint names (nor the positional domain/range aliases for the first/last endpoint of a binary relation). The endpoint selects which relation position the field projects; an accessor that matches no position projects nothing — and was previously unchecked (only the runtime read it, so a bogus endpoint built clean and silently projected an empty view). The accessor is now validated against the relation’s declared positions at ox check. Use one of Rel’s declared endpoint names, or domain/range. |
| OE0246 | Error | Live | FromClauseEndpointTypeMismatch | A navigation-view projection field (field: T from Rel.endpoint) declares a field type T whose concept is <:-incomparable with the type E of the relation endpoint it projects — neither is a subtype of the other. A navigation view is type-directed: it projects the endpoint and keeps the members that are iof T, so the view is sound for any <:-comparable (T, E) pair (identity when T == E, a no-op widening when E <: T, a real iof T filter when T <: E). But when T and E are unrelated, no endpoint value can be iof T: the view is statically empty and the declaration is type-incoherent. (The narrowing case T <: E — e.g. circles: [Circle] from Contains.shape over a Shape endpoint — is now ACCEPTED and filters to the T-typed members; it no longer errors here.) Widen the field type to a supertype of E, or project an endpoint whose type is <:-comparable with T. |
| OE0247 | Error | Live | FromClauseFilterContradiction | A navigation-view projection field (field: T from Rel.endpoint where <predicate>) carries an explicit where type-test endpoint : C whose concept C is <:-incomparable with the field’s element type T. The type-directed view already keeps only the iof T members; a where endpoint : C restates or narrows that selection and must be CONSISTENT with it. When C and T are unrelated, the conjunction iof T && iof C is unsatisfiable: the view is statically empty. A consistent restatement names T itself (where shape : Circle on a [Circle] field) or a <:-related concept; a value predicate (where shape.radius > 0) imposes no such constraint. Drop the contradictory type-test, or align it with the field type. |
| OE0248 | Error | Live | FieldValueNotIndividual | A write to a concept-typed field supplies a primitive value where an individual is required: a constructor field (insert C { f: <literal> }) or an update … set { f = <literal> } assignment whose declared field type f: D is a declared CONCEPT, given a primitive literal (a string / integer / decimal / boolean). The value-vs-individual mismatch is the one HARD-CATEGORY refusal sound under Argon’s open-world, multiply-classified semantics (the same arm as OE0222 for pub fact relation endpoints): a value is never an individual of any concept, so it can never inhabit a concept-typed slot — storing it would key a property the read path resolves to a non-individual, silently violating the declared shape. The symmetric arm (an individual written to a primitive-typed field) is also refused. This is NOT a concept-vs-concept disjointness check: an individual of any concept supplied to a concept-typed field is ACCEPTED (it may be reclassifiable; two concepts are never provably disjoint without a declared partition). Supply an individual reference (a constructed binding, a parameter, or a declared individual) for the concept-typed field, or give the field a primitive type if a value was intended. |
| OE0249 | Error | Live | StructFieldMissing | A struct VALUE literal T { … } omits a field that T declares: a struct is pure data with structural equality (§5.1), so a literal states every field — there is no construction-time default (field: U = expr defaults are a separate, refused surface). The omitted field’s name and declared type are named. Add the missing field: value, or, when constructing from an existing value of the same struct, use functional update T { ..base, field: value } so the base supplies the rest. |
| OE0250 | Error | Live | StructFieldExtra | A struct VALUE literal T { … } supplies a field that T does not declare. A struct is closed data (§5.1): only declared fields may be initialized, and an unknown field is a typo or a stale field name rather than an extension point. Remove the field, or correct it to one of T’s declared field names. |
| OE0251 | Error | Live | StructFieldTypeMismatch | A struct VALUE literal T { field: value } initializes a field with a value whose type does not match the field’s declared type. A struct is pure data with structural equality (§5.1); storing a mistyped field would make two values compare unequal-by-type or derive a silently-wrong projection, so the mismatch is refused at build time, naming the field, its declared type, and the supplied type. Supply a value of the declared field type. |
| OE0252 | Error | Live | InsertOfStruct | insert T { … } names a struct, but insert constructs a concept INDIVIDUAL — it mints an identity and records an iof classification. A struct is pure data with no metatype and no identity (§5.1): it is constructed as a plain VALUE T { … } (e.g. a let binding, a field value, an argument), never inserted. Drop the insert keyword to construct the struct value, or name a concept type if an individual was intended. |
| OE0253 | Error | Live | MutOnStructField | A struct field is declared mut. A struct is an IMMUTABLE pure value (§5.1): its fields are fixed at construction and updated only by functional update T { ..base, field: new }, which yields a NEW value rather than mutating in place. The mut modifier marks a concept field updatable by an update … set { … } statement — a notion that applies to identity-bearing concept individuals, not to struct values. Remove mut from the struct field. |
| OE0254 | Error | Live | StructFieldAccessUnknown | Field access v.f where v is a struct VALUE whose declared struct type has no field f (§5.1). A struct projects field-wise off its declared fields; an absent field is a typo or a stale name. Use one of the struct’s declared field names. |
| OE0255 | Error | Live | StructValueIntoNonStructField | A struct VALUE is supplied where a field of a different type is declared: a struct value S { … } written to a field whose declared type is a primitive, a concept, or a different struct/enum type. A struct value has structural equality and no identity (§5.1), so it inhabits only a field declared with its own struct type — storing it into an incompatible field would key a value the read path cannot interpret. Supply a value of the declared field type, or change the field’s type to the struct type if the struct value was intended. |
| OE0256 | Error | Live | EnumPayloadArity | An enum payload-variant construction Variant(args) supplies a number of arguments other than one. A payload variant carries a single value (§5.1) — the tag-1011 value carrier holds one payload slot — so Some(7) is well-formed while Some() (zero) and Some(1, 2) (two) are not; the surplus or missing argument is refused rather than silently dropped or truncated. Supply exactly one payload value, or construct a payloadless variant (None) with no parentheses. |
| OE0257 | Error | Live | EnumPayloadPattern | A payload-binding match arm Variant(binder) does not bind exactly one fresh variable: a zero-binder Variant(), a multi-binder Variant(a, b), a nested non-binding sub-pattern, or the record form Variant { … }. A payload variant carries one value (§5.1), so its destructuring binds exactly one fresh variable positionally — write Variant(x) => …. Rewrite the arm with a single binder, or match the variant without binding by hoisting the test. |
| OE0258 | Error | Live | RelationEndpointTypeClosed | A write places an individual into a typed slot — a relation endpoint (insert R(…)) or a concept-typed field (insert C { f: o } / update t set { f = o }) — that EXISTS (OE0232 passed) but is classified under NOTHING — a bare/ghost individual known only by a property assertion — while the slot’s declared concept T is governed by the CLOSED-world assumption (§6.9). Under CWA concept membership is closed (absence of evidence is evidence of absence), so admitting the write would silently mint the first membership claim about the individual and expand T’s closed extent — refused, the typing layer above OE0232’s existence-only gate. The world assumption DECIDES this arm and nothing else: an OPEN-world T admits the same write (membership assumed-as-unknown), with no diagnostic. Membership is the individual’s full classification (asserted iof UNION derived iff, <:-closed — the decision query_extent/$meta/dispatch share), read against the overlay’s read-your-writes view, so classifying the individual earlier in the SAME body satisfies the gate. Only genuine CONCEPT slots are checked — a primordial position (rel R(a: String, …)) or a primitive field has no extent and is skipped (a primitive into a concept field is the distinct OE0248 value-vs-individual refusal). The Reserved boundary (OE1904) demarcates this gate precisely: when the individual IS classified, but under a concept disjoint from T only by intent (the reversed case — o is iof Pet, the slot wants Person), the write is ACCEPTED, because two concepts are never provably disjoint without a declared partition. Atomic — raised before the write is buffered, so the whole body commits nothing. Classify the individual under {concept} first (this same mutate body counts), or declare {concept} open-world if its membership should be assumable from a write. |
| OE0259 | Error | Live | RuleComparisonOperandMismatch | A comparison lhs cmp rhs (==, !=, <, <=, >, >=) in a derive / check / bridge rule body relates two operands of provably disjoint sorts — a hard-category primordial mismatch such as String against Int, or Bool against a number. No value inhabits both sorts, so the comparison can only ever be false (equality) or is undefined (ordering); before this gate it passed ox check and was caught — if at all — only at the runtime comparison operator. The static analogue of the expression-level OE0201 comparison check, carrying the rule-body logic-variable binding context (head-parameter annotation, predicate-argument slot, operand : T narrowing). Reported only on PROVABLE disjointness: two concepts are never flagged (an individual may classify under both), the numeric tower (Int/Real/Decimal/Money) shares one category and is never flagged, and Unknown / unbound operands carry no certainty. Temporal-vs-mismatch is handled on its own stricter path. Fix the operand types or the comparison. |
| OE0260 | Error | Live | InsertSpreadSourceNotStruct | The spread base of an insert T { ..v, … } did not evaluate to a struct value. The spread form seeds the construction’s field initializers from the base struct’s fields by exact field name (explicit field: value initializers override them), so the base must be a value of a declared struct — a let-bound struct, a struct-returning fn call, or a struct-typed parameter. An individual, a scalar, an enum value, or an unresolved value carries no field map to seed from. Bind a struct value and spread that, or spell the field initializers explicitly. |
| OE0261 | Error | Live | InsertSpreadFieldUndeclared | A field of the spread base in insert T { ..v, … } is not declared on the target concept or any <: ancestor. Spread matching is by exact field name: every source-struct field must land on a declared field of the target, and a field with no match refuses rather than being silently ignored (data the source value carries must never vanish on insert). Remove the field from the source struct type, rename it to the declared target field it should feed, or construct with explicit field: value initializers instead of the spread. |
| OE0262 | Error | Live | RuleBinderTierCollision | A bare identifier stands as the argument of a user concept / relation / rule — a VARIABLE-expecting predicate slot — but resolves to a declared type / relation tier (a type-as-value). This is the POSITION analogue of OE0228 (a bare name that is both a binder and a declared individual): in a variable-expecting slot a bare declared-name is genuinely ambiguous — is it a body-local join variable that should enumerate the predicate’s partners, or the declaration used as a type-as-value constant? The value-position type-as-value coercion runs before the full binder set is known and, with only the head parameters excluded, silently PINNED the argument to the declaration’s TypeRef (lowering it as a constant handle rather than a join variable). That is the same silent-flip class OE0228 refuses, in the tier direction: a lowercase relation name (whose naming convention overlaps variable convention) used as a join variable was pinned to the relation with no diagnostic, so the rule joined against a constant where it meant to bind. Refused rather than pinned. The type-EXPECTING positions keep coercing and are never flagged: the reflective heads rel(r, m) / arm(r, pos, t) / iof(c, T) / specializes(T1, T2), a meta(x) == T / comparison / ascription type operand, and a fn argument (a type passed to a pure function). A QUALIFIED name (module::name) is unambiguous and coerces to the type-as-value in any position. Fix: rename the variable so it does not collide with a declared type or relation, or qualify the name to use it as a type-as-value constant. |
| OE0263 | Error | Live | StructLitMultipleSpreads | A struct literal or insert T { … } field-init list carries more than one ..base spread. One spread seeds the field map and the named initializers override it; a second spread has no defined merge order and was previously dropped silently (first spread wins, the rest vanished). Combine the sources into one struct value first (let merged = S { ..a, field: b.field };), then spread the merged value. |
| OE0264 | Error | Live | StructLitConceptHead | The head T of a struct VALUE literal T { field: value, … } resolves to a declared concept, relation, trait, or other non-struct declaration. A brace literal constructs pure DATA — a value of a declared struct, with structural equality and no identity (§5.1) — so its head must name a struct. A concept declares identity-bearing INDIVIDUALS, and an individual is created only by insert T { … } inside a mutate (or test) body, which mints the identity and records the iof classification; this is the same struct/concept boundary OE0252 enforces in the other direction (insert refuses a struct head). Previously the literal passed ox check and died at first evaluation with a generic uncoded runtime error. Declare T as a struct if a plain value was intended, or construct the individual with insert T { … } in a mutate body. An enum-headed brace literal is not this refusal: enum values construct through their variant paths. |
| OE0265 | Error | Live | FnBodyInsert | A fn body contains an insert … expression. A fn is the pure plane of the rule ladder: it lowers to pure compute, is cacheable on its arguments, and is callable from any mode — including rule bodies evaluated inside the reasoner’s fixpoint — so it must produce a value with no side effect. insert constructs an individual in the store (an identity-minting write) and belongs to the transactional planes: a mutate body, or a test body (which executes mutate-shaped statements against the test’s scratch store). Before this gate the form passed ox check and then misbehaved by position: a bare or block-tail insert died at first call with a generic uncoded error, and an insert bound by an unused let was dropped silently — the write the author spelled never happened. Move the construction into a mutate (or test) body and pass the constructed value to the fn as an argument, or construct a struct VALUE T { … } if pure data was intended. |
| OE0266 | Error | Live | MutateInsertUndeclaredRelation | A trait-impl mutate member’s insert P(...) / delete P(...) tuple write names a relation P that no declaration anywhere in the program defines. The runtime resolves a tuple-write target against the whole-program artifact — every package’s relation declarations, matched by qualified path or bare-name suffix, imports and package boundaries ignored — so a target no package declares can never succeed: it aborts the mutation at the rule’s first firing (“relation not declared in module”), latent until data first makes the rule applicable. Declare the relation (pub rel P(...)) in the package that owns it, or correct the target name. Unlike the scoped module-level mutate gate (OE0223), this gate mirrors the runtime resolver program-globally, so a relation declared in any package resolves and is not refused here. |
| OE0267 | Error | Live | ImmutableRelationEndNonIndividualContext | Relation ends are immutable unless marked mut. An immutable end is a promise: each fact’s bond at this end is permanent — no write can rewrite or delete it; it dissolves only when an individual at one of the relation’s OTHER ends is deleted (the deletion cascades and removes the fact). That promise is fulfillable only when every other end holds identity-bearing individuals — things with a create/delete lifetime. Here it is not: either another end is typed by a value that is never created or deleted — a primordial (Int, String, …), a struct/enum value, or a type/trait reference (TypeRef / Metatype / TraitRef) — or the relation is unary and has no other end at all. Had the declaration been accepted, every fact would be frozen forever — impossible to update or retract — and the individuals it mentions would become undeletable, since deleting an individual requires releasing every fact that mentions it and this end’s release condition can never fire. Marking the VALUE end mut instead does not help, and that declaration is refused by this same check: the value end’s mut would only let NEW values accumulate beside the old ones (an insert adds a fact; removing one requires every end mut), while the flagged end’s permanent bond still holds every fact in place — each value ever recorded would stand forever, and the individual would still be undeletable. Fix: write mut before the flagged end’s name (e.g. pub rel hasAge(mut p: Person, n: Int)), then choose the reading you mean: with mut p alone the value is constitutive — a person’s n is fixed at its first assertion and the fact exits only when the person is deleted; if the value should change over time, mark BOTH ends mut (updating is delete + insert, and a matched delete requires every end mut, OE1402). mut changes only the facts’ lifecycle, never the individuals’: retracting a fact never deletes the individuals it mentions. Keep an end immutable exactly when the honest exit of the fact is the disappearance of a participant (like the mother in bornTo(mut child: Person, mother: Person)). Fires at ox check / ox build on the declaration; an endpoint whose type does not resolve is handled by the ordinary resolution diagnostics instead. |
| OE0268 | Error | Live | RelationEndMutabilityWeakening | A relation declares an end mut while some transitive superrelation (via <: / specializes) keeps the same position immutable. Effective mutability at a position is conjunctive over the relation and every transitive superrelation: a child may tighten a parent mut end by omitting mut, but declaring mut where any applicable parent end is unmarked is refused at ox check / ox build — a child-admitted retraction would remove a subsumed parent row at an end the parent declared lifetime-bound. Remove the child mut, or mark the parent end mut at its own declaration. |
| OW0269 | Warning | Live | IndividualCapturesJoinVariable | A bare name used across multiple atoms of one rule body — the way a join variable is used — resolves to a declared individual, so every occurrence is that CONSTANT, not a shared fresh variable: the atoms are each pinned to the individual instead of being joined, which silently over-restricts the rule. This is the documented resolution order (a non-binder bare name that matches a fact-declared individual is that individual, workspace-wide), so the binding itself is correct; the warning flags the collision because the multi-atom shape is variable-typical and the restriction is otherwise invisible. The message names the individual, the module whose fact declares it, and the rule. Two fixes, by intent: if a join variable was meant, rename it to a name that is not a declared individual; if the constant was meant in several atoms, make the intent explicit — bind a fresh variable and pin it with an equality (s == theIndividual) — or suppress the lint on the rule. Deliberately narrow: a single-occurrence constant argument (knows(x, carol)), an equality pin, a qualified spelling (people::alice), and individual references in fact declarations, mutate bodies, and test bodies never fire. The single-occurrence collision (a would-be variable used in exactly one atom) is indistinguishable from a deliberate constant pin and is accepted silently — this warning covers the join shape only. |
Stratification (05xx)
Reserved range — the stratifier’s pre-runtime aggregate diagnoses. The runtime equivalents fire as OE13xx rule-compile gates today.
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0510 | Error | Reserved | NonStratifiedAggregate | Reserved (designed, not yet emitted as OE0510 — §7.2): a stratified aggregate that would loop through an aggregation step on its own stratum (no well-defined fixpoint). The runtime today refuses recursion-through-aggregation as OE1317 RecursionThroughAggregation at the rule-compile gate; this 05xx code is reserved for the stratifier’s pre-runtime diagnosis of the same shape. |
Meta calculus, refinement, comparison, traits (06xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0605 | Error | Live | UnknownMetatype | A concept declaration’s introducer (the identifier before the concept name — pub <introducer> Name) does not resolve to a pub metatype visible in scope. Introducers are not keywords: per §3.4 the leading identifier resolves against pub metatype declarations — declared in this package or brought into scope by a use/the package prelude (§5.2). Nothing is ambient: even the baseline type (and rel for relations) must be in scope — add use std::core::{type, rel}; or list prelude = ["std::core::{type, rel}"] under [package]. Two fixes: bring the baseline into scope and use the ontology-uncommitted pub type Person;, or declare/import the vocabulary the introducer belongs to (pub metatype kind = { }; makes pub kind Person legal). Vocabulary classifiers (UFO’s kind, BFO’s class, …) are package vocabulary, not language surface — std::core’s own docs: type is the no-commitment baseline; vocabulary packages ship alternative metatypes. |
| OE0606 | Error | Live | UnknownMetarel | A relation declaration’s introducer (pub <introducer> Name(...)) does not resolve to a pub metarel visible in scope. Per §3.4 the leading identifier resolves against pub metarel declarations — declared in this package or brought into scope by a use/the package prelude (§5.2). Nothing is ambient: even the baseline rel must be in scope — add use std::core::{type, rel}; or list prelude = ["std::core::{type, rel}"] under [package]. Two fixes: bring the baseline into scope and use the neutral pub rel Owns(owner: Person, thing: Asset);, or declare/import the vocabulary the introducer belongs to (pub metarel mediation<E1, E2>(e1: E1, e2: E2); makes pub mediation Involves(...) legal). Relation-vocabulary classifiers (UFO’s mediation, material, …) are package vocabulary, not language surface. |
| OE0612 | Error | Reserved | OptionComparisonRequiresHandling | Rule body contains Option<T> comp_op T (or comp_op Option<T>) without explicit None-handling. Use is Some(a), a comp_op ... / `.is_some_and( |
| OE0613 | Error | Reserved | Truth4ComparisonRequiresHandling | Rule body contains Truth4Of<T> comp_op T without explicit four-valued handling. Use is Is(a), a comp_op ... / is Not / is Can outcome suffixes, or match { Is(a) => ..., Not => ..., Can => ..., Both => ... }. |
| OE0620 | Error | Live | AxisValueNotInAxisDomain | A rule-body value position names an axis value (rigidity::anti_rigid — a path whose first segment resolves to a visible pub metaxis declaration) that the axis’s declared domain does not admit: either the value is not a member of the enumerated domain (the diagnostic lists the declared values), or the axis declares a TYPED domain (pub metaxis weight for metatype = Real;), whose values are literals with no enumerated-symbol spelling. A valid enumerated axis value never reaches this code — it lowers to a Value::Symbol constant (owner = the axis identity, ord = the chain position) and evaluates against the catalog-closed $axis relation. (Pre-S3 this code refused EVERY axis value as not-yet-evaluable; that scope is retired — the rule-body diagnostic that remains is exactly the not-in-domain / typed-domain-spelling case.) |
| OE0621 | Error | Live | UnknownMetaxis | A metatype / metarel body binds an axis (axis: value) whose axis name does not resolve to a visible pub metaxis declaration — or matches several, in which case every candidate is named and a qualified spelling (vocab::rigidity: rigid) disambiguates. Axes are not ambient vocabulary: declare the axis in this package (pub metaxis rigidity for metatype { anti_rigid < semi_rigid < rigid };) or import the vocabulary package that declares it. Pre-S1 these bindings were parsed and silently DROPPED; now every binding resolves to the declaring metaxis’s identity and is persisted on the wire, so an unresolvable axis is a hard refusal, never a phantom. |
| OE0622 | Error | Live | AxisValueNotInDomain | An axis binding’s value is not a member of the axis’s declared domain. For enumerated domains ({ a, b } unordered or { a < b < c } chain) the axis: value binding must name one of the declared values — the diagnostic lists them. Binding an enumerated axis with a literal (axis: 1.5), or a typed axis with a bare symbol (axis: rigid), is the same refusal: the value shape follows the domain (a bare symbol for enumerated domains, a literal for typed ones). |
| OE0623 | Error | Live | AxisLiteralTypeMismatch | A typed-domain axis binding’s literal does not satisfy the axis’s declared TypeExpr (or statically fails its where { … } refinement) — e.g. weight = "heavy" against pub metaxis weight for metatype = Real where { _ > 0.0 };. Checked statically for the primitive domains (Int / Nat / Real / Decimal / String / Bool); a refinement comparing the placeholder _ against a literal is also discharged statically. Domains the elaborator cannot decide statically are not refused here (they validate when the $axis relation materializes, slice S3). |
| OE0624 | Error | Live | AxisTierMismatch | An axis is bound on a declaration tier its for targets do not include: a pub metaxis … for metatype axis binds only inside metatype bodies, for metarel only inside metarel bodies. Axes declared for individual / for macro currently bind NOWHERE — those binding surfaces do not exist yet, so a metatype/metarel binding against such an axis is refused with this code rather than silently accepted against a tier that can never consume it. Fix: bind an axis whose targets include this declaration’s tier, or extend the axis’s target list (pub metaxis a for [metatype, metarel] { … };). |
| OE0625 | Error | Live | DuplicateAxisBinding | A metatype / metarel body binds the same axis twice (e.g. { rigidity: rigid, rigidity: anti_rigid }) — malformed per the §4 distinct-axes well-formedness constraint (Argon.MetaCalculus.Wellformed.MetatypeDecl.axesDistinct). Axis assignment is functional per (declaration, axis): a second binding could only restate or contradict the first, so both occurrences are named and the declaration is refused. Two spellings of one axis (bare rigidity and qualified vocab::rigidity) count as duplicates — identity is the RESOLVED axis. |
| OE0626 | Error | Live | MetaxisDeclMalformed | A pub metaxis declaration’s shape is malformed: an enumerated body mixes , and < separators (a domain is either unordered { a, b } or a chain { a < b < c }, never both); the body is empty or absent (an axis with no domain admits no value, so every binding against it would be refused — declare the values or the typed domain); a value is repeated; the for target list is empty or names something other than metatype / metarel / individual / macro. The declaration is refused at ox check / ox build so the wire never carries a domain the validator cannot interpret. |
| OE0627 | Error | Live | SymbolComparisonUnordered | An ordered comparison (< <= > >=) over symbol values that carry no common chain order: the operands belong to DIFFERENT owners (different metaxes / different enums — chain order is per-owner), the owner declares an UNORDERED domain ({ a, b } — declare a chain { a < b < c } to compare by order), or a symbol is ordered against a non-symbol value. Emitted at ox check / ox build when both operands are statically known (axis-value / enum constants, or an axis projection whose axis identity is known); the reasoner and the mutate path refuse the same comparison at runtime under the same code — never the enum-variant-order fallback, never a silent row drop. Equality (== / !=) stays defined everywhere: symbols are equal exactly on (owner, name) identity. |
| OE0628 | Error | Live | ReflectiveProjectionSortAmbiguous | A field projection base.name the sort-directed reading cannot dispatch soundly: (a) name resolves to a visible pub metaxis but the SORT of base is statically UNDETERMINED (or the body pins it to two contradictory sorts) — a Metatype-sorted base reads the metaxis through the $axis relation (meta(t).rigidity with t: TypeRef), while a TypeRef- or individual-sorted base reads a declared FIELD named name (the §5.2 kind-field walk-up); two different relations, so guessing would silently answer the wrong question; fix it by making the base’s sort explicit — annotate the head parameter (t: TypeRef / m: Metatype), or bind the base with a sort-determining catalog atom (m : Metatype, iof(t, kind), an equality with a sorted variable — Var == Var equalities propagate sorts). (b) the base IS Metatype-sorted but no visible metaxis is named name — metatypes carry axis bindings, not fields, so the $field:: join would be silently empty; declare/import the metaxis, or project the field from a type or individual. A projected name matching SEVERAL visible metaxes on a Metatype-sorted base is refused through the axis resolver’s ambiguity arm (OE0621). |
| OE0629 | Error | Live | AxisAssignmentConflict | Two same-precedence $axis assignments disagree for one (target, axis) pair at module load (the effective axis relation is functional — a metatype binding may be overridden by a per-target meta_property event, but two conflicting assignments at the SAME precedence level have no defined winner, so the module refuses to load rather than silently pick one). No surface form double-asserts today (duplicate axis bindings refuse at OE0625; duplicate #[order] refuses at elaboration); this code is the artifact-trust backstop for hand-built or corrupted event logs. Emitted by oxc-runtime at load; declared here so appendix C and ox diagnostics carry it. |
| OE0630 | Error | Live | IncomparableValueTypes | An ordered comparison (< <= > >=) is evaluated between two values whose types carry no common order: e.g. an Int against a Date, a number against a Text. Pre-refusal the reasoner’s value_order fell back to the derived Ord of the Value enum — comparing by variant discriminant — so a mistyped field (an Int stored in a Date field, OE0236’s runtime cousin) made field < <date> unconditionally true and field > <date> unconditionally false regardless of the actual values, silently deriving wrong rows. Now a loud ReasoningError, parallel to the existing temporal-mismatch (TemporalTypeError) and unordered-symbol (OE0627) refusals, raised in both the rule evaluator and the mutate value path — never the enum-variant-order fallback, never a silent row drop. Equality (== / !=) stays defined across types (distinct types are simply unequal) and never routes through the ordering refusal — only the four ordering operators can raise OE0630. Numeric ordering across the exact-numeric tower is defined for the Int/Real carriers (and Decimal/Money operands that ride a Value::Real/literal at evaluation): both promote to a common BigRational and compare by value. A Money/Decimal value stored as an opaque Value::Cbor carrier does NOT decode to a rational at the ordering site — it byte-compares as an unordered structured carrier and is refused (OE0630), not silently mis-ordered; ordering stored exact-decimal carriers is the recorded follow-on, not a v0 guarantee. |
| OE0631 | Error | Live | MetarelEndpointMetatypeMismatch | A relation declared via a metarel introducer (pub mediation Involves(m: Marriage, p: Person)) has an endpoint whose metatype does not match — and is not a sub-metatype of — the metatype the classifying metarel declares at that position (§4.3). A pub metarel mediation(mediator: relator, mediated: kind) requires position 0 to be relator-sorted and position 1 kind-sorted; an endpoint introduced under any other metatype (or a non-sub-metatype) is refused. The §4.3 promise — “the elaborator verifies the relation’s endpoint metatypes match the metarel’s positions” — was previously prose only: the position metatype names were parsed and discarded, so a wrong-sorted endpoint (Gustavo’s inheresIn(v: Vehicle, …) where an ability must inhere via an aspect, not a kind) checked clean. The check is catalog-driven and axis-name-agnostic — it compares resolved metatype identities, never a metatype/axis name string. A metarel position naming a primordial or a generic parameter (the stdlib rel<E1: metatype, E2: metatype>) imposes NO constraint; the gate fires only when both the position metatype and the endpoint metatype are known and incompatible. |
| OE0632 | Error | Live | ReflectionOnUnclassified | A reflection intrinsic (meta / iof / specializes / extent) is applied to a struct- or enum-declared carrier — language-level DATA, not an ontologically-classified concept (§4.4.2). Language data carries no metatype, so meta(p) == Point over a pub struct Point is a category error, parallel to OE1016 (Truth4OfOnStruct) for the Truth4 surface. The discipline §4.4.2 leans on (“calling meta() on a struct/enum-declared value is a category error … emits a diagnostic at elaboration”) was previously unenforced — the call checked clean and a vocabulary author writing nonsensical reflection over plain data got zero feedback. Reflection applies only to concepts introduced under a vocabulary or stdlib metatype; classify the carrier under a metatype, or drop the reflection. |
| OE0633 | Error | Live | MetarelEndpointArityMismatch | A relation declared via a metarel introducer (pub mediation Involves(m: Marriage, p: Person)) has a different number of endpoints than its classifying metarel declares positions (§4.3). A pub metarel mediation(mediator: relator, mediated: kind) declares two positions, so a relation introduced under mediation must have exactly two endpoints — a ternary mediation R(a, b, c) and a unary mediation R(a) are both refused. The §4.3 endpoint-verification step zipped the relation’s endpoints against the metarel’s positions and silently continued past any position beyond the metarel’s arity, so an over- or under-arity relation checked clean (its surplus or missing endpoints were never validated). The guard is arity-EQUALITY against the metarel’s declared endpoint count — the generic stdlib rel<E1: metatype, E2: metatype> declares two positions and still accepts exactly its two endpoints; a metarel whose positions are all unconstrained (None) still requires the matching count. It is not relaxed to “accept anything”: a metarel imposing no per-position metatype constraint still pins the arity. Forward-compatible — only relaxes if metarel genericity (variadic positions) later lands. Mirrors arityEqual (Argon.Substrate.RelationSubsumption) and the OE0150 relation-arity precedent. |
| OE0634 | Error | Live | AxisSetFormMismatch | A metaxis binding uses the wrong form for its domain’s arity. A set-valued domain (pub metaxis restrictedTo for metatype = [ Nature ]) binds ONLY with the set form axis: [a, b]; a scalar domain (enumerated { … } or single Typed) binds ONLY with a single axis: value. Crossing the two — a scalar value against a set domain, or […] against a scalar domain — is refused here. This consolidates what were divergent, non-agreeing refusals for one need: a set binding against a scalar domain previously tripped OE0626 (“expects a value, found [”), and a single value against a set domain tripped OE0622 — separate codes for “you used the wrong binding shape.” OE0634 is the single FORM gate; a set MEMBER that fails the element type / refinement is OE0623 (the per-element type check), not this. Emitted at elaboration and re-checked at artifact load (a crossed form reaching the runtime is a wire inconsistency). The set form is admitted only by a set-domain axis (AxisDomainBody.admits — Argon.Storage.AxisBindingValid). |
| OE0660 | Error | Live | RefinementUnsupportedForm | A where/iff refinement predicate — a concept refinement, or the value filter of a navigation-view from clause (f: [T] from Rel.endpoint where <pred>) — contains an expression form the refinement evaluator doesn’t support. Supported forms: literals, self/identifiers, field projection self.field, binary ops (arithmetic / comparison / && / ` |
| OE0661 | Error | Live | SubqueryInComputeBody | A fn body contains an aggregate/quantifier subquery over a relation extent — exists { Rel(…) }, count { … }, forall …, sum { … }. A fn is pure compute over its arguments and has no access to a relation’s extent, so the subquery ranges over nothing and the form evaluates SILENTLY to empty/false (the fn then misbehaves wherever it is used as a rule-body predicate atom). It is therefore refused at build time rather than evaluated. This is the subquery form ONLY: a COMPREHENSION over an argument-reachable collection or from-field — count(x for x in p.coll) > 0 — is bounded by an argument value, evaluates correctly, and is NOT refused. Express the extent-scanning condition on the rule plane, where relation extents are in scope: pub derive popular(p: Person) :- p: Person, HasFriend(p, _);. Refusing at build time is mandatory: dropping the subquery would silently yield a fn that returns false for everyone — the silent-wrong failure this code exists to prevent. The bounded-root-anchored design that will evaluate an argument-rooted subquery in a fn is a separate, larger change. |
| OE0662 | Error | Live | ShapeClauseUnresolvedDerive | A concept’s shape clause (pub type C <: P iff Shape(self, …), the derived-classification form) names a shape that does not resolve to a pub derive declared in the SAME module. The clause desugars to a synthesized concept-headed rule derive C(m) :- Shape(m, …), P(m); whose body reads the shape derive’s extent, so the shape MUST be a module-local pub derive — an imported derive, a private (non-pub) derive, or an undeclared name has no extent to read and would silently classify nobody (the silent-empty failure this gate refuses). Declare pub derive Shape(…) :- …; alongside the concept, or correct the name. Imported / cross-module shapes are not yet a legal shape target. |
| OE0663 | Error | Live | ShapeClauseTargetNotADerive | A concept’s shape clause (iff Shape(self, …)) resolves to a module-local declaration that is NOT a derive — a query, fn, check, mutate, concept, or relation of the same name. Shape membership is computed on the rule plane by the synthesized concept-headed rule that reads the shape’s extent, so the shape must be a pub derive (the one declaration kind that carries a derived extent the membership rule can read). No other declaration kind carries that extent. Point the clause at a pub derive, or rename the colliding declaration. |
| OE0664 | Error | Live | ShapeClauseArityMismatch | A concept’s shape-clause argument count does not match the shape derive’s head arity. iff Shape(self, w1, …, wn) requires the module-local pub derive Shape to declare exactly 1 + n head parameters: parameter 0 receives the classified individual (bound to the shape-clause self), and each of the n witnesses binds one further head column. A mismatched clause would either drop head columns silently or leave a synthesized-rule body variable unbound, so it is refused at build time rather than classifying against a malformed body. Pass one argument per shape-derive head parameter (the leading self plus one witness each). |
| OE0665 | Error | Live | ShapeClauseParamTypeMismatch | The shape derive’s head parameter 0 declares a type that is <:-INCOMPARABLE with the shape-classified concept’s declared parent — neither a subtype nor a supertype of it. iff Shape(self, …) binds parameter 0 to the classified individual, and the synthesized membership rule conjoins the declared parent (derive C(m) :- Shape(m, …), P(m);). A parameter-0 type comparable with the parent is fine: the PARENT itself or an ANCESTOR narrows the join to the parent, and a DESCENDANT of the parent already ranges within it (the conjunct is redundant, not empty). Only a type incomparable with every declared parent is refused — the join could be non-empty solely by multiple classification, almost always a modeling error that classifies nobody. An UNANNOTATED parameter 0 is accepted (nothing declared, nothing to contradict); a concept with no declared parent is accepted (nothing to bound against). Annotate parameter 0 with a type comparable to the concept’s parent, or drop the annotation. |
| OE0666 | Error | Live | ShapeProjectionHeadCollision | A rule head lands on an already-claimed shape-projection name. Each shape-clause witness synthesizes a projection derive named __classification_<Concept>_<witness>; that spelling is not injective over underscored identifiers (concept A with witness b_c and concept A_b with witness c both mint __classification_A_b_c), and Argon’s same-head union semantics would silently merge the two projections’ rows — a silent-wrong classification. The same channel opens if a hand-written derive/query claims a __classification_-prefixed head, so that prefix is reserved outright. Rename a concept, a witness, or the offending derive so every projection head is distinct. |
| OE0667 | Error | Live | TraitImplBoundsNotSupported | Generic-parameter trait bounds on pub trait or impl blocks are reserved syntax but not yet implemented in v0.1. The conditional-impl coherence theorem (Argon.TypeSystem.Conditional) admits multi-impl resolution under disjoint bounds; the elaborator side ships in V1. Use pub trait T<U> / impl<U> Type (without bounds) for now. |
| OE0668 | Error | Live | RefinementInvariantViolated | A membership write would place an individual in a primitive (where) concept whose necessary invariant it positively violates — the predicate evaluates to a definite false. unknown permits the write (information absence, not violation). Checked when a primitive-where member is created by insert iof, by construction (insert T { … }), or when an update writes a field the predicate reads. Emitted by the runtime mutate path (oxc-runtime); the same exact value tower as every other value position. |
| OE0669 | Error | Live | ShapeClauseDuplicateWitness | One shape clause names the same witness twice (iff S(self, p, p)). Each named witness mints one projection derive __classification_<Concept>_<witness>, so a duplicated name would mint the identical projection head twice and Argon’s same-head union semantics would silently merge the columns — and a duplicated binder adds no expressive power. Witness names must be distinct within a clause. To require two shape columns to be EQUAL, bind them to one variable inside the shape derive’s own body (pub derive S(c, p) :- Covers(c, p, p);) and name the witness once. |
| OE0670 | Error | Live | ImplMemberMissing | An impl Trait for Type does not provide a member its trait declares (completeness). Every trait member is an obligation: a missing rule-plane member would leave the member predicate without a clause for this target, so calls covered by this impl would silently derive nothing. Provide the member with a full body, or remove the impl. |
| OE0671 | Error | Live | ImplMemberExtraneous | An impl Trait for Type provides a member the trait does not declare, or provides it with a different signature — arity, parameter types modulo Self, return type, or plane (derive/check/query vs fn/mutate). The trait owns the member contract; an impl can neither extend it nor reshape it. Bare impls (impl Type { … }) are exempt — they carry no contract (§5.4). |
| OE0672 | Error | Live | OrphanImplViolation | An impl Trait for Type lives in a package that declares neither Trait nor Type (§12.4’s reserved orphan rule, now real). Allowing orphan impls would let two packages give one (trait, type) pair conflicting clauses with no owner to arbitrate. Move the impl into the package declaring the trait or the one declaring the type. v0.1 compiles a single user package plus the stdlib, so this fires today only for impls pairing a stdlib trait with a stdlib type; it activates fully with multi-package composition. |
| OE0673 | Error | Live | ImplTargetsOverlap | Two impls of one trait have overlapping targets: the targets are <:-comparable, or they share a declared common descendant (coherence). Overlap would put some declared type under two clauses, so clause selection would no longer be single-valued per declared type — and specialization (“more-specific impl wins”) is rejected by design. impl T for Person + impl T for USPerson is the comparable arm (write disjoint targets, or one impl whose body branches); impl T for Person + impl T for Customer with pub type Employee <: Person, Customer declared is the common-descendant arm. |
| OE0674 | Error | Live | SupertraitUnsatisfied | An impl Sub for T exists but a supertrait of Sub has no impl covering T. Supertraits are requires-constraints, Rust-exact: pub trait Sub: Super makes impl Sub for T well-formed only when some impl Super for U with T <: U exists. Nothing is inherited — add the missing supertrait impl. |
| OE0675 | Error | Live | SelfMisuse | Self is used outside the positions where it resolves. Self resolves only inside trait and impl bodies: in a trait member signature it is the obligation’s type parameter; in an impl member it denotes the impl’s target type. The full discipline: (1) Self outside a trait/impl body never resolves — it is not a module-level type; (2) a trait member signature must mention Self in at least one parameter position (a member that is not about the implementing type belongs at module level); (3) return-position Self on a query member is refused — it would make the dispatch endpoint’s type a union over impl targets, which needs union types or bounded generics (V1). Move the declaration into a trait/impl body, or replace Self with the concrete declared type. |
| OE0676 | Error | Live | ImplMemberSeverityDiverges | An impl’s check member states a severity different from the one its trait pins. A trait-side check member signature may pin its severity (check OverLimit(Self) => Severity::Error;); the pin makes the member’s blocking behavior part of the trait contract — a contract whose blocking behavior varies by implementor is a weak contract (an Error that guards mutations on one target but merely warns on another is not one obligation, it is two). Under a pin an impl may omit its payload’s severity: field (it inherits the pinned one — the ergonomic point of pinning) or restate the same severity (harmless); stating a different one is refused here, naming the trait, the member, the pinned severity, and the divergent one. Codes and messages stay per-impl. To keep per-impl severity freedom, remove the pin from the trait signature. |
| OE0677 | Error | Live | ShapeClauseMalformed | A shape clause (iff Shape(self, w…)) is malformed or misplaced. The shape-clause grammar: the clause is legal only at a concept declaration’s refinement position (pub type C <: P iff Shape(self);, or the standalone pub type C iff Shape(self);); the shape path is a single module-local name (no :: segments, self::-qualification included); the arguments are parenthesized; self (the classified individual) is exactly the FIRST argument; and every remaining argument is a plain identifier naming a witness. Each refusal is loud and the parser recovers past the clause so the rest of the file still reports — nothing lowers from a malformed clause (a silently dropped clause would leave the concept unconstrained, the silent-wrong this family refuses). Exactly one refusal is reported per clause; the first malformation wins. |
| OE0678 | Error | Live | ShapeMembershipHeadClaim | A hand-written rule head (a derive or check, module-level or standpoint-scoped) claims the membership rule of a shape-defined concept (pub type C <: P iff Shape(self, …)). That head’s only legal claimant is the synthesized membership rule, which conjoins the declared <: parent; under Argon’s same-head union semantics a user rule sharing the head would add members that never satisfied the shape or the parent bound, and the derived-read surfaces (derive, --extent, bodiless select) would serve them — a check-green source silently violating C <: P, with no loud writer (the write gate guards iof writes, not the rule plane). Membership of a shape-defined concept is single-source: only the synthesized rule may head it. Rename the rule’s head, or express the extra members inside the shape derive’s own body. |
| OE0683 | Error | Live | NavViewFilteredStandoffCapableElement | A where-filtered navigation view (f: [T] from Rel.endpoint where P) whose element type T has a SHAPE-DERIVED membership (iff Shape(self, …), a rule head the well-founded fixpoint can leave undefined) is refused at build, fail-closed. WHY: a filtered view stays store-local and evaluates its element-type membership eagerly, per candidate, at read-model seed time; but a shape-derived membership has no seed-time verdict, so a store-local read would either fabricate a two-valued extent (silently including or dropping a candidate whose membership is genuinely undetermined) or fail loud at runtime — a check-green / runtime-dead outcome this family never admits. The filter-free form of the same view is instead evaluated INSIDE the fixpoint, where an undetermined member surfaces as a standoff rather than being silently resolved. SCOPE: the trigger is the ability to STAND OFF, not a derived extent generally. A braced refinement (iff { P }) is derived but its predicate cannot call a rule (OE0660), so it is always two-valued and never stands off — a filtered view over such an element type is ACCEPTED and keeps its store-local materialization. REMEDY: either drop the where filter so the view lifts into the fixpoint, or make T a concept whose membership is a seed-time fact (an ordinary declared concept, a where refinement, or a braced iff), or move the narrowing the filter expresses into the element type’s own iff definition. |
| OE0684 | Error | Reserved | NavViewUnproductiveCycle | Reserved (designed; the productivity-graph emitter is a follow-on increment). An unproductive positive navigation-view cycle: a lifted view whose extent is provably empty for every store — a view-element dependency cycle with no external seed, or an acyclic dead-end that can never produce a member. WHY: a lifted view synthesizes a fixpoint rule f(self, x) :- C(self), Rel(self, x), T(x) whose element membership T(x) may itself read back through the same view; when that dependency graph has no productive entry — nothing outside the cycle can ever assert a member — the view is empty in every model, which is a modeling defect, not a runtime standoff (a standoff is a value the consumer adjudicates; an unproductive cycle is dead code). The validation graph carries view-element edges so the productivity check catches it at build rather than serving a permanently empty view. REMEDY: break the cycle by seeding at least one element type from outside it (an ordinary concept, an asserted fact, or a membership rule that does not itself depend on this view), or remove the view if its emptiness is intended. |
| OE0685 | Error | Reserved | ImperativeProjectionIncomparablePin | Reserved (designed and wired, but not yet reachable: the imperative read-goal path does not route through the shared rule-reference resolver, so no caller selects the imperative mode and this emitter cannot fire). An imperative projection (x.field in a mutate / test body) whose bare field name resolves to two or more distinct canonical cells with nothing determining which slot is meant — the pin-resolution ladder’s Incomparable outcome. WHY: same-named fields on distinct concepts are DISTINCT storage cells (each field’s identity hashes its declaring concept’s qualified path, so Taxpayer.balance and Trust.balance never alias); when the base variable is pinned to several concepts that declare field on incomparable branches of the <:-lattice — no single declarer is a <:-ancestor of the others, so there is no most-specific cell — a bare-name read would silently pick one branch’s cell and write or read the wrong value. An imperative body cannot defer this to the fixpoint; it must name one cell. REMEDY: pin the base to the concept whose field you mean (bind it with a C(x) extent atom or an iof(x, C) for the single declaring concept), or rename one of the colliding fields so the projection is unambiguous. |
| OE0686 | Error | Live | RuleBodyProjectionAmbiguousPin | A rule-body field projection (t.field in a derive / query / check body) whose base is pinned to several distinct concepts, none of whose <:-ancestry declares a field named field, so the ladder can key no cell — the pin-resolution ladder’s Ambiguous outcome (and the cross-module incomparable case a file-local oracle cannot answer, resolved through the workspace <:-ancestry walk). WHY: same-named fields on distinct concepts are DISTINCT cells (a cell is keyed by its declaring concept); the ladder keys a concept-pinned read to the single most-specific declaring concept in the base’s accumulated pins. When no pinned concept (nor any of its ancestors) declares the field, there is no cell to key and a bare-name read would join an empty or aliased relation — a silently wrong answer. The runtime and the build-plane validator consume the SAME resolved references the checker accepted, so this refusal never diverges cross-module. REMEDY: pin the base to a concept that actually declares field (a head parameter typed to that concept, or a body C(t) / iof(t, C) atom), or correct the field name. |
| OW0679 | Warning | Live | ShapeClassifierNegationCycle | A shape-defined concept (pub type C <: P iff Shape(self, …)) participates in a recursion-through-negation cycle across classifiers: its synthesized membership rule reaches its own extent through a negated atom (directly, or via another shape/derive whose body negates back into C). This is ALLOWED, not refused — the well-founded model gives such a cycle a value, and membership surfaces Can (well-founded-undefined) for the individuals inside the cycle rather than a definite Is/Not. The warning is loud so the modeler knows those individuals read Can (and a definite-only surface will refuse or drop them), not silently absent. This is the classifier-cycle analogue of the ordinary WFS treatment of recursion through negation; it fires ONLY for a shape-defined classifier in such a cycle, never for an ordinary rule-head concept. To get a definite classification instead, break the negative dependency between the classifiers (make the chain asymmetric / stratified). |
Modules, attributes, temporal, modal, forget (07xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0701 | Error | Live | InvertedDuration | A metric-temporal operator’s interval [a, b] is malformed (§7.3.2). The common case is an INVERTED interval — lower bound a exceeds upper bound b: a metric interval is a forward offset window on the valid-time axis, so an inverted interval denotes the empty window and the operator would be vacuous (emitted at lowering when both bounds are statically-known integer durations — a literal day-count, an <int>.days/.weeks sugar — and a > b; an inf/∞ upper bound is never inverted). Also covers an inf/∞ in the LOWER-bound position: only the upper bound may be unbounded. Swap the bounds, write [0, b] for an anchored window, or replace an inf lower bound with a finite offset. |
| OE0702 | Error | Reserved | MetricArityMismatch | Unary metric temporal operator applied to two operands (or binary applied to one) (§7.3.2). |
| OE0703 | Error | Reserved | MetricAtomBelowTier | Metric temporal atom appears under temporal: snapshot or temporal: none (§6.10.5). |
| OE0704 | Error | Live | ReservedIntrinsicName | A declaration is named after an IDENT-lexed intrinsic: a reflection intrinsic (implements, implementors, spec §4.4) or a determinedness test (undetermined, determined, which read whether a membership atom’s verdict was left undefined). Rule-body call forms of these names always denote the intrinsic — the reflection heads are claimed by name resolution, the determinedness tests by the atom lowering that runs before it — so the declaration would be silently unreachable, and its own call sites would be refused with a diagnostic describing the intrinsic rather than the program written. Rename the declaration. The keyword intrinsics (iof, specializes, meta, extent) cannot collide and need no gate. |
| OE0705 | Error | Live | UnknownDirective | An #[...] attribute names no registered compiler directive (the directive registry). Unknown attributes never silently no-op: a typo’d directive (#[defeasable]) would otherwise silently change rule semantics — the canonical case let a felon vote. The diagnostic suggests the nearest registered directive when one is within typo distance. Macro-invocation attributes (spec section 13) are not built; until they are, every attribute must be a registered directive. |
| OE0706 | Error | Live | DirectiveReservedUnimplemented | A directive the book documents but the toolchain does not implement (#[brave], #[intrinsic], #[coinductive], …). Refused loudly rather than silently ignored: a directive that parses green while doing nothing misrepresents program semantics. The message cites where the directive is documented and what to do instead. (Removed surfaces — the #[strict]/#[defeasible]/#[defeater] strength triple and #[priority] — refuse under OE0722 instead, with a migration hint.) |
| OE0707 | Error | Live | DirectiveInvalidPosition | A registered, executed directive attached to a declaration kind it does not apply to (e.g. #[static] on a derive rule, #[comptime] on a mutate). Previously ignored silently; the registry names the directive and its valid positions. |
| OE0708 | Error | Reserved | ReservedAttributeName | A user pub macro or #[procmacro] pub fn declaration shadows a compiler-reserved attribute name (§9.2). |
| OE0709 | Error | Live | AttributeArgsNotYetImplemented | Attribute argument form not yet implemented; only the bare form is admitted (e.g. #[comptime], not #[comptime(strict)]). The argument syntax is parser-reserved for a future release. |
| OE0710 | Error | Reserved | ModuleExtractionConservativityViolation | Module extractor cannot prove conservativity under the importing module’s world assumption (§3.5). |
| OE0711 | Error | Reserved | ModuleExtractionDefeatChainBroken | Defeat-aware extraction cannot close the defeat graph — a referenced defeater is excluded by the seed signature (§3.5). |
| OE0712 | Error | Reserved | ModalTemporalCrossNestRefused | Modal operator nests a metric temporal operator (or vice versa) at tier: recursive. Route to tier: fol (§9.1). |
| OE0714 | Error | Reserved | DirectiveConflict | Two directives from one mutually-exclusive family on a single declaration. Its only family was the rule-strength triple #[strict] / #[defeasible] / #[defeater], removed — each member now refuses individually as removed (OE0722), so a conflict never forms and this code has no live emitter. Reserved for a future exclusive-directive family rather than reused (registry hygiene). |
| OE0715 | Error | Live | ModuleHeaderReadingRemoved | A content-carrying file declares mod NAME; (no sibling NAME.ar, no inline { … } body) in the legacy module-header position — the reading where the mod renamed the file’s OWN namespace. mod NAME; has a single meaning, Rust’s: it declares a CHILD module backed by a sibling NAME.ar (or NAME/mod.ar). A file cannot rename itself. Refused loudly: the silent-rename meant a typo’d mod quietly relocated a whole file’s declarations under a phantom namespace while ox check stayed green. To name a package’s module, use the directory/file layout (the root file is module root); to declare a child module, create the sibling NAME.ar; to keep declarations at the current namespace, delete the mod NAME; line. |
| OE0716 | Error | Live | DefeatTargetUnresolvable | A #[defeats(target(args))] target resolves to nothing in the attacking module’s import-scoped catalog (its own rules plus the public rules reachable through its use imports): no derive head of that name, no clause carrying that #[label], or no trait member at that @ Type impl. The message distinguishes a misspelling (no such name visible anywhere) from a not-imported name (a matching public declaration exists in a module this one does not use — add the import, or write the qualified path module::head(args)). Defeat targets are resolution-checked at elaboration (goto-def-able) so a typo never silently changes which norm a program concludes. Also covers a malformed call-form target, a bare #[defeats] with no target, and a target/head arity mismatch. The accepted forms are head(args), head.label(args), module::head(args), and Trait::member(args) @ Type. |
| OE0717 | Error | Live | DefeatsStrictConclusion | A #[defeats(...)] target resolves to a head/clause not marked #[default]. Strict conclusions are unattackable: adding rules to a classical program can only add conclusions, and defeat exists only where overridability was declared. Mark the targeted clause #[default] to make it overridable, or remove the attack. |
| OE0718 | Error | Live | DefeatGraphCycle | The defeat graph (over resolved rule identities — clauses and heads) has a cycle, refused in v1. Cyclic attack structures are exactly where the well-behaved compilation stories diverge; v1 refuses rather than picking one silently. An exception-to-an-exception (a #[default] rule that is itself a #[defeats] target) is legal as long as the chain bottoms out. The message names the cycle rule by rule. |
| OE0719 | Error | Live | DuplicateClauseLabel | Two clauses of one head carry the same #[label(name)]. Labels are per-head identities — head.label must address exactly one clause — so a head cannot have two clauses labeled alike. Rename one of the labels. |
| OE0720 | Error | Reserved | NonUniformBridgeRefused | Bridge consults facts at a different valid-time than the bridge head’s; refused outside tier: fol (§10.3). |
| OE0721 | Error | Live | DefeatArgumentUnbound | A #[defeats] target argument binds in neither the head nor the body of the decorated rule, or binds only in the body (per-tuple defeat keys on the attacker’s HEAD tuple). A directive argument is never a fresh variable — the lesson that a silently-fresh variable matched everything. Name a variable the attacker derives in its head. |
| OE0722 | Error | Live | DeprecatedStrengthAttribute | The Governatori strength-attribute surface — #[strict] / #[defeasible] / #[defeater] and #[priority] / pub priority — is removed. It silently inverted a head’s polarity at a distance (a defeater spelled the head it denied) and numeric priority was non-compositional and silently dropped. The replacement is honest heads plus the defeat-directive plane: unmarked rules stay strict; #[default] marks an overridable clause; an exception is an ordinary rule under its own honest head carrying #[defeats(target(args))]; lex specialis is the specific rule defeating the general clause’s #[label]. No silent aliasing — migrate the program. The help text shows the new form for the specific attribute. |
| OE0723 | Error | Live | MacroInvocationDidNotExpand | A macro invocation name!( … ) survived the EXPAND phase: no pub macro name with a rule matching the invocation is in scope (the macro is undefined in this module, or none of its rules’ matchers bind these arguments). An unexpanded invocation is refused rather than silently accepted — an invocation the elaborator does not understand must not vanish from the artifact (the silent-wrong discipline forbids). Define the macro, fix the invocation to match a rule, or import the macro (cross-module macro use is a follow-up). |
| OE0724 | Error | Live | UnknownFragmentSpecifier | A macro metavariable $name:spec names a fragment specifier outside the closed v1 set. The admitted specifiers are the ontological concept / rel / metatype / rule and the syntactic expr / ident / ty / literal / path / tt; standpoint is cut from v1. Validated at expansion so a macro body never binds a metavariable against a parse category the expander cannot honor. Use one of the admitted specifiers. |
| OE0725 | Error | Live | ReservedHygieneIdentifier | A user-authored identifier contains the reserved hygiene marker · (U+00B7 MIDDLE DOT). That marker is reserved for the names macro hygiene mints when freshening a macro-introduced variable, giving those names a namespace provably disjoint from anything a user can write — the disjointness the hygiene non-capture guarantee rests on. Remove the · from the identifier. |
| OE0726 | Error | Live | DuplicateMacroDefinition | A module declares two macros with the same name. A macro name must have exactly one implementation per module (uniqueness-at-registration) — two macro name { … } declarations are a loud error, never a silent merge or shadow (the silent-wrong discipline forbids; the two rule sets would otherwise be invoked as if one macro). Rename or remove one of the declarations; a distinct cross-module macro of the same name is reached with a module-qualified use. |
| OE0727 | Error | Live | MacroExpansionDidNotConverge | A module’s macro expansion did not reach a fixed point within the fuel budget: a macro whose expansion re-introduces an invocation of itself (directly or through a cycle) diverges. The phase stops and refuses rather than looping forever — distinct from OE0723 (an invocation that no rule matches), since here the macro does match but its expansion never settles. Break the recursion (add a non-recursive base rule, or stop the rule from re-emitting its own invocation). |
| OE0728 | Error | Live | ExpansionSpanUnanchored | A diagnostic produced over macro-expanded text carried a span that the source-faithful expansion provenance map could not resolve to any location the user wrote. This is a compiler bug: every span over expanded input must resolve to real user source (a copied-through fragment) or to the invocation site of the macro that synthesized it. The compiler refuses to render text the user never wrote, so it surfaces this internal error loudly rather than pointing at a phantom location. Please report it with the input that triggered it. |
| OE0729 | Error | Live | ProcmacroBodyNotTotal | A procedural macro (#[procmacro] pub fn) body uses a construct outside the total fragment, so its termination cannot be guaranteed by construction. The procedural meta-language is deliberately a closed, strongly-normalizing fragment: let bindings over string literals / reflection projections (item.name, item.params[i].ty) / concat_idents(…) paste, a quote { … } (with $-splices and a bounded $( for x in item.fields ) { … } repetition over a reflected child-list), and a list of quote/typed-artifact elements. General control flow (if / match / for / while statements) and calls to anything other than concat_idents are refused HERE, at the declaration — totality is enforced, not trusted to a fuel cap at expansion time (distinct from OE0727, which catches a runaway at expansion). Rewrite the body within the total fragment; iteration over a declaration’s structural children is the bounded $( for … ) repetition inside quote, not an open loop. |
| OE0730 | Error | Live | ForgetWithoutCapability | A mutate body contains forget but the enclosing mutate declaration does not grant #[allow_forget]. forget physically erases axiom events including their bitemporal history; the capability must be granted explicitly at the declaration that wields it. The richer capability plane (ox.toml / backend principal / _forget_log audit) is a recorded follow-on. |
| OE0731 | Error | Reserved | ForgetLogTamper | Attempted forget on a _forget_log event. |
| OE0732 | Error | Reserved | RetentionViolation | Query targets transaction-time outside the declared #[retention(tx_time, ...)] window. |
| OE0733 | Error | Live | SchemaDeclInStandpoint | A vocabulary / schema-level declaration, or a query / mutate, appears inside a standpoint { … } block. A standpoint scopes a SOURCE’S VIEW, not a separate universe: the vocabulary it ranges over is shared and lives at module level. Standpoint-scopable is the view’s CONTENT — facts (pub fact / pub not_fact), the reading-relative rule form pub derive, and pub check (discharged in this standpoint’s view). A derive or check written lexically inside the standpoint is sugar for a module-universal rule evaluated within this standpoint’s view: it fires within this standpoint (exact-match on the standpoint id, plus DEFAULT-owned module rules) and is invisible to the base and to sibling standpoints, exactly as a module-level rule gated on a standpoint-local reading fact would. Still refused: the vocabulary declarations (pub type / pub rel / metaxis / metatype / metarel / enum / struct / trait / fn / macro), which describe the universe; query, redundant with the across [S] federated readout; and mutate, whose per-standpoint form is a recorded open question. (mod / use / test / a nested standpoint are governed by the parser.) The pattern for a refused form is unchanged: declare it at module level, then assert (pub fact) or read (pub derive / across [S]) it per standpoint. Refused at ox check / ox build, parallel to the impl-in-standpoint refusal in the same dispatch. |
| OE0734 | Error | Live | ProcmacroSignatureType | A procedural macro (#[procmacro] pub fn) signature names a type outside the reflection vocabulary. A procmacro is a macro definition, not a value-level function: its parameters reflect the macro’s input — Decl (the decorated declaration), Args (the attribute arguments), or Syntax (a function-like macro’s token input) — and it returns Syntax (a single emission) or Expansion (a list of emissions / typed artifacts). These reflection types are the only ones admitted in a procmacro signature; an ordinary value type (or a typo) is refused here so the macro author is not silently handed a meaningless signature. Use one of the reflection types. |
| OE0735 | Error | Live | DefeasibleScopedDerive | A standpoint-scoped pub derive carries a defeasibility marker (#[default] / #[defeats] / #[label] / #[defeasible] / #[defeater]). A scoped derive is evaluated within its standpoint’s view by the strict-classical per-standpoint path, which does NOT run the defeat-plane resolution: #[defeats] / #[label] are resolved globally (a whole-file pass with no standpoint axis, so an edge could cross standpoints), and the per-standpoint federated evaluation carries no defeat fold — so the markers would be SILENTLY INERT and the rule would fire un-defeated in its view (over-derivation). A scoped derive must be strict until per-standpoint defeat resolution is implemented; refused loudly rather than accepted as a hollow surface. Make the scoped derive strict, or move the defeasible rule to module level. |
| OE0736 | Error | Live | DefeatTargetAmbiguous | A bare #[defeats(head(args))] short name resolves to a public head in more than one imported module, so the attack does not determine which rule it overrides. The message names each candidate’s module; disambiguate by writing the qualified path module::head(args) (or module::head.label(args) for a clause-level target), which resolves the head against exactly the named module. Own-module declarations shadow imported ones, so ambiguity only arises between imports. |
| OE0737 | Error | Live | DefeatTargetNotVisible | A #[defeats] target resolves to a declaration that exists in another module but is not pub, so the attacking module cannot name it. Defeat visibility equals ordinary name visibility: the set of modules that can attack a rule is exactly the set that can name it, a bound the rule’s author already controls with pub. There is no action-at-a-distance on private rules — a private rule’s conclusions must never silently depend on code its author cannot see. Only the target’s own author can expose it by making it pub. |
| OE0738 | Error | Live | DefeatCrossPackageRefused | A #[defeats] target resolves into a different package. A defeat edge may cross module (file) boundaries only within one package: a package is a trust and versioning boundary, and a cross-package edge would let a dependency update silently change the program’s conclusions — a new version could add an edge overriding one of your defaults, or change which clause your edge resolves to, with no change to your source. Compose across packages through ordinary use of public predicates (honest heads), which is strategy-independent and unaffected by this rule. |
| OE0739 | Error | Live | DefeatComponentStrategyConflict | A connected defeat component spans modules that selected incompatible reasoning strategies. A connected defeat graph is compiled together by one strategy’s transform (single strategy per connected defeat graph — the invariant the file boundary used to guarantee by construction, now checked at workspace assembly and owned by the connected component). The message names the two modules and their strategies. With one strategy in the language this refusal is unreachable in practice; it is specified and enforced from day one so a second strategy arrives against an already-guarded grain. |
| OW0710 | Warning | Reserved | OrphanModuleFile | Reserved (no emitter): an .ar file under the package source root reached by no mod/use chain from the entry is not part of the package (Rust module semantics, §3.1) — not compiled, not checked, not linted. Argon matches Cargo and says NOTHING about such a file: an unreferenced source file is simply not compiled, not surfaced as a warning. The orphan set is still computed (the workspace records it; a future tool may surface it as an editor hint), so the code is held rather than retired. Declare the file with a mod item from a reachable module to include it. |
| OW0740 | Warning | Live | MutualDefeatCycleUndefined | A set of #[default] public heads forms 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 definite classification, break the cycle: remove or redirect one attack so no attacker’s derivation reads back through its own target. A strict base clause settles only the tuples it covers (those become definite, and definite verdicts cascade around the cycle for them); the cycle — and this warning — remain for every other tuple. |
| OW0741 | Warning | Live | CollisionCapableSiblings | Two sibling relation members translate identically onto the same parent family: both are subsumption children of one parent, and their resolved argument mappings onto it are byte-identical, so they share the same image under the mapping φ. A fact of one member and an identically-shaped fact of the other translate to the same parent tuple, and because a relation’s extent is a set, those images collapse to a SINGLE parent row — an aggregate over the parent counts the shared row once, not twice (two members each asserting (alice, 500) yield one parent row, so sum(amount) is 500, not 1000). This is the correct set semantics, so it is a WARNING, not an error; it is surfaced because it is almost always a modeling slip. The warning names both members and the parent. Fix, in order: (1) if the members are distinct families, give them distinguishing translations onto the parent — a constant pin that differs, or a discriminating parent end each fills differently — so their rows stay apart; (2) if they are the same family, declare one member, not two. |
| OW0742 | Warning | Live | DuplicateFactInFile | An identical pub fact — same relation, byte-identical argument list after canonical encoding — is asserted more than once in the SAME source file. A relation’s extent is a set: the second assertion adds nothing, the two identical assertions collapse to one row, so an in-file literal repeat is almost always a copy-paste error. This is a WARNING, not a refusal — the program is well-defined (the repeat is idempotent). Two remedies, in order: delete the duplicate assertion if it is a copy-paste error; OR, if the repeats are meant to record DISTINCT events (a fact stated twice because it happened twice), give each fact its own identity — add an event-id end so the arguments differ and both rows survive. (Argon facts assert propositions, not events; a bare proposition is true once, no matter how many times it is written.) The lint is deliberately narrow: it fires ONLY on same-file literal duplicates. It stays silent for the same fact asserted across two different files (a module legitimately restating a shared proposition) and for a duplicate arriving through a mutation (an insert of an already-present fact re-freezes the existing row — a defined no-op on the mutability plane, not an authoring slip). |
| OW0743 | Warning | Live | DeclarationShadowsImport | A derive, rel, or concept declaration’s bare name is already bound by an explicit use import in the same module, and that import resolves to a predicate declared elsewhere. Head declarations are module-local and bare-name resolution is local-first, so this declaration introduces a NEW module-local predicate that shadows the imported one: clauses on it contribute to the module-local predicate, NOT to the imported one — reads of the imported predicate silently miss the intended tuples, and an unqualified read from a third module refuses as ambiguous far from the cause. This is a WARNING, not a refusal — the program is well-defined — but the shape is almost always a modeling slip (the import of the very name being declared signals intent to share). The message names both fully-qualified predicates. Fix, in order: (1) rename the module-local predicate and wire it centrally — a rule beside the original declaration reads the renamed local predicate into the shared relation; (2) declare a distinct subrelation of the imported predicate (pub rel Local(…) <: Imported) and contribute clauses to that; (3) if a disjoint module-local predicate really is intended, remove the imported name from the use (or qualify reads of the foreign predicate) so the shadowing is explicit. The warning is deliberately narrow: it fires only when the same module both explicitly imports the name (a plain, non-glob, non-aliased use leaf) and declares it; the module declaring the original predicate never fires, and a local declaration with no matching import never fires. |
| OW0744 | Warning | Live | CheckNotExhaustiveOverUndetermined | A check body that uses the determinedness test undetermined(...) / determined(...) does not cover every membership atom whose UNDEFINED (Can) verdict can leave the check’s firing verdict undecided. The named membership atom’s Can path is routed by no body clause, so the honesty gate’s severity-derived default governs it: a Severity::Error check blocks that path (OE1430), a Warning/Info/#[observe] check reports it at undefined grade, and an => emit check does not emit it. The coverage analysis is syntactic and its scope is bounded: it inspects a check’s top-level body atoms and treats a membership as standoff-capable only when the concept is computed in the same file (a derive head or an iff concept). Inside that scope it warns whenever it cannot see a routing clause, so a warning costs at most one explicit clause; a membership over a concept computed in another module is outside the analysis and takes the default without a warning. The warning is an aid to reading a body — the gate’s enforcement does not depend on it. Remedies: add a clause disposing of the named atom’s undetermined case — undetermined(A) => ... to route it, or determined(A) as a positive guard so the check does not fire on its Can verdict; OR, if the gate’s default is the intended disposition for that path, suppress this lint. This warning never blocks a build. |
Field modifiers, world-assumption gates, construction (08xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0810 | Error | Reserved | WorldAssumptionTemporalIndex | #[world(...)] declared per concept with a valid-time scope but no standpoint context (§6.9). |
| OE0811 | Error | Reserved | PersistenceMarkerOnNonTemporalRule | Persistence marker applied to a rule outside the temporal substrate. |
| OE0812 | Error | Reserved | DefeasibleDefeaterOfDefeasibleTemporal | Defeasible defeater of a defeasible temporal rule; route to unsafe logic (§7.8). |
| OE0820 | Error | Live | UpdateImmutableField | An update target: T set { f = … } writes to a field f that T declares without the mut modifier. The elaborator validates each field-assignment against the field-decl’s mut flag and refuses at build time, not at mutation invocation. Emitted by the mutate-body lowerer (oxc-instantiate); a bare (un-annotated) target defers to the runtime rather than emitting here. |
| OE0822 | Error | Reserved | MutOnDerivedField | Reserved (designed — §5.1): a mut modifier on a from-navigation-derived field, which is contradictory — a derived field has no independent value to mutate. mut is otherwise orthogonal to the other field-decl modifiers (#[intrinsic], defaults, metatype rigidity). This entry reserves the code for that gate. |
| OE0831 | Error | Live | PositionalInsertArityMismatch | A positional insert C(a, b, …) targets a concept C. The mutate-body lowerer only routes the brace form insert C { field: … } to construction, so a positional concept insert silently constructs nothing; it is refused at check. The diagnostic names the required fields in constructor order and (on an arity mismatch) reports the argument-vs-field counts. Emitted by the driver’s mutate-body validation (oxc-driver). Use the brace form insert C { field: … } to supply fields by name. |
World-assumption conservativity (09xx)
Reserved range — the §6.9 world-assumption gates are designed but not yet wired.
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0901 | Error | Live | MixedWorldAssumptionConflict | A rule or query applies negation-as-failure (not R(..)) to a FOREIGN relation (one mapped to a store by [placement.R]) whose source is read open-world — the CWA-importing-OWA boundary. A foreign relation is materialized into the fixpoint as a frozen CLOSED-world slice (the frozen-EDB rule), so not R(..) reads absence-in-the-slice as definite-false; but absence of a tuple in an OPEN-world source is UNKNOWN, not false (§6.9), so the negation reads unknown as definitely-false and over-asserts its head. The closed-world lifting of the slice into the open consumer is not provable — the TypeSystem/Soundness/CwaOwa.lean conservativity theorem proves the CWA→OWA transfer sound in one direction only — so the negation is refused at ox check / ox build rather than silently evaluated, the C6×D4 hinge (a CWA relation admits NAF; an OWA one does not). To resolve: mark the relation’s source closed-world (so absence IS falsity and not R is sound), or restructure so the foreign relation is not negated. The native-concept analog — NAF over a #[world(open)] concept’s own derived extent — is the separate OE1367 gate (the evaluator threads it three-valued); this code is the federation boundary, where the frozen slice is a closed snapshot of an open source. |
| OE0903 | Error | Reserved | WorldAttributeOnStruct | Reserved — superseded by OE0707: #[world(...)] applied to a struct/enum, which carry no ontological classification — world assumption applies only to classified concepts. The world directive is now registered concept-only (positions: [CONCEPT_DECL]), so a struct/enum placement refuses via the generic placement gate OE0707; this bespoke code is retained as a reserved row and does not fire. |
| OE0904 | Error | Live | RecursionThroughForeignSource | A FOREIGN relation (one mapped to a store by [placement.R]) is consulted inside a RECURSIVE rule that also derives that relation’s own demand — recursion through the foreign source (the single-fetch federation boundary, §6.9). A foreign relation is materialized into the fixpoint by FREEZING it once at the EDB-loading edge: a single fetch, complete iff the relation’s extension is independent of the IDB computed in that fixpoint. When the relation appears in the body of a rule whose head is in a recursive strongly-connected component AND that body also references a predicate of the same component (the recursive back-edge), each fixpoint iteration derives new bindings that demand MORE of the foreign source than the single fetch retrieved — so the fixpoint silently under-derives. The freeze-soundness condition is mechanized as AvoidsVocab (Reasoning/Datalog/AnalyticalFreeze.lean): the producing program must not feed back into the frozen output. A non-recursive join with a foreign relation, or a foreign relation that only SEEDS a recursion (consulted once with ground demand, in a base-case rule with no back-edge), is sound and admitted. To resolve: restructure so the foreign source is a leaf strictly below the IDB it feeds (move it out of the recursive component), or wait for monotone bounded re-consultation (the orchestrator iterates demand rounds against an idempotent, demand-monotone connector — a named, gated follow-on, not a v1 capability). |
| OW0902 | Warning | Reserved | WorldAssumptionImplicit | Reserved (designed — §6.9): default_world omitted from ox.toml, so the substrate applied a default world assumption rather than an explicit modeler choice. The warning that would suggest making the choice explicit is unbuilt; this entry reserves the code. |
CWA evaluation and field intent (10xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1014 | Error | Live | RequiredFieldUnasserted | Under the closed-world default (book §6.9 — CWA everywhere today), a required field’s value is not derivable from any asserted axiom: the schema declares the field present but the KB does not assert it. Emitted by the runtime mutate path (oxc-runtime) at exactly one site: the in-body insert iof(x, T) construction-completeness gate — classifying x into T whose required fields x lacks, when the same body also populates fields of x, refuses atomically, judged at body end so insert iof; update set { … } in one mutation is complete (read-your-writes). Distinct from brace construction (insert T { … } missing a field → OE0207). Staged construction (classify in one mutation, populate later — no in-body field writes) currently emits NO diagnostic: the absent required field reads as K3-unknown and collapses to CWA-false at access. The field-access-time / evaluation-channel emitter that would surface staged incompleteness is designed and is not built in v0. #[world(open)] is a live concept attribute; the permit-as-unknown softening of this required-field gate under an open-world concept is designed and not yet wired. |
| OE1016 | Error | Reserved | Truth4OfOnStruct | Truth4Of<T> field type appears on a struct (no metatype classification). The four-valued surface is restricted to metatype-classified concepts (pub type ..., or a declared vocabulary introducer). |
| OW1015 | Warning | Reserved | OptionalFieldAmbiguousIntent | Reserved (no emitter): an optional field field: T? does not record whether its optionality is structural (the value may legitimately be absent) or epistemic (the value exists but is unknown), a distinction drawn from JPA’s TBox/SHACL split. The intent-marking surface and the warning that would request it are unbuilt — the #[intent(...)] attribute does not exist yet (the lint-level #[allow(...)] / [lints] suppression surface does, and this code is wired to the optional-field-ambiguous-intent lint, so the warning will be suppressible the moment it gains an emitter). This entry reserves the code for that future surface; today every T? field checks clean. |
Bridges (11xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1101 | Error | Reserved | BridgeCycle | The bridge graph (nodes = standpoints, edges = bridges) contains a cycle that prevents stratified evaluation. Cycles between standpoints under bridge composition would require fixed-point recomputation across the federation; v0.1 admits only acyclic bridge graphs. Either break the cycle (drop one of the bridges) or wait for the V1 #[managed] bridge surface (spec §10.3). |
| OE1102 | Error | Live | BridgeNotEvaluated | A pub bridge declaration (§10.3). Bridge rules parse, lower to BridgeDecl wire events, and resolve, but the federation fixpoint never fires them (Module::bridges_targeting has no callers) — a built artifact’s bridge bodies would silently never contribute to their target standpoint. Refused at ox check / ox build rather than building an artifact whose declared inference is inert. Bridge evaluation is the post-stable flagship standpoint deliverable; the parse / lower / wire / decode surface is retained so the work resumes without a grammar or wire-format change. Model the cross-standpoint inference with an explicit derive / pub fact for now. |
| OE1103 | Error | Live | UndeclaredStandpointInAcross | A pub query … across [ … ] clause names a standpoint that is not declared in the workspace. Under the standpoint visibility composition rule (§10.4), a federated query reads the named standpoints’ views info-joined with the DEFAULT layer; a name that resolves to no standpoint declaration would silently contribute the DEFAULT layer alone (an empty per-standpoint extent), masking the typo as a real — but wrong — federation result. The query is refused loudly at dispatch naming the unknown standpoint. Declare the standpoint, or correct the name in the across clause. Mechanization: Argon.Visibility (the visibility relation the federated materializer honors). |
| OE1104 | Error | Live | ModalNotEvaluated | A box / diamond modal atom (§11.2) that the engine cannot soundly evaluate. The forward-rigidity discharge box/diamond(x : T) → x : T is sound ONLY when T is introduced by a fixed metatype (§10.2: fixed classification is decided at construction, so membership is frame-rigid — box φ, diamond φ, and φ coincide). The modal wrapper is then dropped and the inner classification atom is evaluated directly. Every OTHER shape is refused at ox check / ox build rather than silently stripped to the inner atom: a non-fixed classifier (whose membership can change across worlds), a non-classifier predicate P, a negated inner box(not (x : T)), and any standpoint-frame box/diamond (worlds = standpoints, accessibility = the <: reflexive-transitive closure, §10.4). The standpoint-frame Datalog translation and the tableau model engine over a Kripke carrier (§10.3) are later slices; until they land a non-dischargeable modal atom is refused loudly, because stripping it to its inner atom would be a wrong answer for anti-rigid or dynamic classification. Either introduce the classified type via a fixed metatype (pub fixed metatype kind = { … }; then pub kind T;) so the discharge applies, or model the cross-world inference with an explicit derive / pub fact for now. |
Build composition and .oxbin validation (12xx, §16)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1201 | Error | Live | InvalidMagic | .oxbin magic header does not match \0oxbin\0\1. |
| OE1202 | Error | Live | IncompatibleVersionAxis | Major bump on one of the four version axes (oxbin_format_version, core_ir_version, tier_ladder_version, runtime_contract_version). |
| OE1203 | Error | Live | UnknownMandatorySection | .oxbin carries a MANDATORY-flagged section type the runtime does not recognize. |
| OE1204 | Error | Live | TierMismatch | Artifact’s max_tier_claimed or max_temporal_claimed exceeds the runtime’s supported maxima. |
| OE1205 | Error | Live | ArrangementSectionUnsupported | MVP runtime refuses an .oxbin carrying a DBSP-shape arrangement-section. |
| OE1210 | Error | Live | SymbolResolutionFailed | Layer-2 validation: a reference in events / rules / queries / mutations did not resolve to a symbol-table entry. |
| OE1211 | Error | Live | StandpointLatticeCycle | Standpoint lattice has a cycle (compose-time or load-time). |
| OE1212 | Error | Live | ProvenanceNotDNF | An event’s derivation column is not a well-formed PosBool(M) DNF. |
| OE1213 | Error | Live | CompositionSignatureMismatch | Stored composition_signature does not match what its four legs compute. |
| OE1214 | Error | Live | TierTableInconsistent | A rule’s tier in tier-table exceeds the artifact’s max_tier_claimed. |
| OE1215 | Error | Live | DocLinkUnresolved | A DocBlock cross-link does not resolve; strict policy fails, lenient warns. |
| OE1220 | Error | Live | UnknownAxiomKind | Events section carries a variant tag the runtime does not recognize. |
| OE1230 | Error | Live | TierCapExceeded | Compose-time: a declaration’s classified decidability tier exceeds the workspace ceiling set by [lattice].max_tier in ox.toml (§10). The ceiling is the highest of the seven ladder tiers (structural, closure, expressive, recursive, fol, modal, metaorder) a package admits; a rule the §10 classifier places above it is refused at ox check/ox build rather than emitted into an artifact that claims a tier it exceeds. Raise max_tier, or rephrase the declaration into the admitted fragment. |
| OE1240 | Error | Live | DependencyVersionUnsupported | An ox.toml [dependencies] entry uses a registry/VCS form — a bare version string (dep = "1.0") or one of the version/git/branch/tag/rev/registry keys — that is not yet supported. v1 supports path dependencies only (dep = { path = "../dep" }). The form is recognized and refused feature-named, not silently ignored: a version requirement that silently resolved to nothing (or to a path) would be silent-wrong. Use a path dependency, or wait for the registry story. |
| OE1241 | Error | Live | DependencyNameMismatch | An ox.toml [dependencies] key does not match the [package].name of the package at the declared path. The dependency name is the path root the consumer uses (use dep::X;, dep::X), and v1 keeps the dependency’s published name authoritative (Cargo’s default). Rename the [dependencies] key to the package’s published name, or fix the path. |
| OE1242 | Error | Live | DependencyCycle | The [dependencies] graph contains a cycle. Package dependencies must form a DAG; a cycle is refused loudly naming the cycle path (a -> b -> a). Break the cycle — extract the shared surface into a third package both depend on. |
| OE1243 | Error | Live | DuplicateDependencyName | A dependency name collides as a path-resolution root: either the same name resolves to two different package directories across the dependency graph, or it equals an intra-package top-level module name. A name is a single namespace root; it cannot denote two things, and a bare name::X must not silently prefer one. (Two declarations resolving to the SAME canonical directory are de-duplicated, not an error.) Rename the conflicting dependency or the local module. |
| OE1244 | Error | Live | DependencyPackageLoadFailed | A path dependency could not be loaded: the path does not point at a directory with an ox.toml, the dependency’s entry/module closure failed to resolve, or its manifest did not parse. The dependency is refused naming the path and the underlying cause rather than silently contributing nothing. |
| OE1245 | Error | Live | MappingSchemaDrift | A compiled placement/mapping artifact pins to a Schema composition signature that does not match the schema it is being loaded against. The mapping compiles to a content-addressed artifact hashed against the schema’s composition signature, so a schema change that the mapping was not recompiled against is a LOAD-TIME refusal rather than a runtime surprise: a relation could otherwise be mapped to a store under a shape the schema no longer has. Placement is versioned separately from the schema — a relation can move stores without a schema bump — but the mapping always pins to the schema hash it was compiled against. Recompile the mapping against the current schema (ox build). |
| OE1246 | Error | Live | ForeignRelationAlsoDerived | A relation is BOTH mapped to a foreign store by a [placement.<rel>] section AND produced by a derive rule. A relation’s extent must have a single origin — it is EITHER extensional (its rows live in the foreign store the placement points at) OR intensional (its rows are computed by rule derivation) — never both. Allowing both would silently produce an ambiguous extent: the foreign rows and the derived rows would compete, and which one a query reads would depend on evaluation order rather than on the model. The conflict is refused at build time naming the relation, rather than emitting an .oxbin whose foreign-vs-derived extent is undefined. Either drop the derive rule (the relation is foreign-owned) or remove the [placement] (the relation is rule-computed). |
| OE1247 | Error | Reserved | ConflictingPlacement | Reserved (designed, not yet wired): a relation has a placement declared from two sources at once — e.g. an ox.toml [placement.<rel>] section AND a source-level foreign!/#[foreign] macro, or the same relation placed twice. A relation may be bound to at most one store; two placements would compete for which store backs the relation’s extent, and which one wins would depend on merge order rather than the model. The placements are unioned at build time (MappingSections::merge_placements) and a relation appearing in more than one source is refused, naming the relation, rather than silently overwriting one with the other. The union seam exists; the build path that emits this code on a macro-emitted-vs-config collision is wired when the macro emission surface lands. Keep a single placement for the relation — either the ox.toml section or the macro, not both. |
| OE1248 | Error | Live | WorkspaceInheritanceUnavailable | A [package] field uses the workspace-inheritance form { workspace = … } but there is no enclosing [workspace] to inherit from, or the value is workspace = false. Per-field inheritance (version.workspace = true, edition.workspace = true) is meaningful only for a member of a workspace whose root declares the shared [workspace.package] field. Set workspace = true inside a real workspace, or write the value literally. |
| OE1249 | Error | Reserved | UnknownEdition | Reserved (no emitter) — edition-value validation deferred until editions gate surface syntax. Editions are non-negative integers, opt-in per package, parse-time-only and inert in v1: the resolved value gates nothing downstream, so any integer (given as edition = 1 or the legacy string edition = "1") is accepted. This code is held so that once an edition gates real surface syntax, an edition this toolchain does not understand can be refused feature-named rather than silently treated as the default. |
| OE1250 | Error | Live | WorkspaceMemberVersionUnsatisfied | A workspace member depends on a sibling member (or a path dependency) whose declared [package].version does not satisfy the depending package’s SemVer constraint. A member-to-member dependency resolves LOCALLY to the sibling member directory — never to a registry — so the sibling’s declared version must satisfy the constraint, or the workspace is internally inconsistent. Align the member version, or relax the constraint. |
| OE1251 | Error | Live | PatchTargetUnsupported | A root-only [patch.<source>] entry targets a registry/VCS source that is not yet supported. v1 patches only path/workspace sources; a registry patch is refused feature-named rather than silently ignored, since the registry does not yet exist. Patch a path/workspace source, or wait for the registry story. |
| OE1252 | Error | Live | WorkspaceMemberGlobEmpty | A [workspace] members glob matched no package directory. A member glob must resolve to at least one directory holding an ox.toml; an empty glob is refused rather than silently producing a member-less workspace (a typo in members = ["crates/*"] would otherwise drop every member without a word). Fix the glob, or remove it. |
| OE1253 | Error | Live | WorkspaceMemberGlobMalformed | A [workspace] members/exclude/default-members glob is malformed: an invalid glob pattern, or an unreadable matched path. The pattern is refused naming the glob and the underlying cause rather than silently expanding to nothing. |
| OE1254 | Error | Live | WorkspaceInheritedFieldMissing | A workspace member inherits a field with <field>.workspace = true (or <dep>.workspace = true) but the workspace root’s [workspace.package] / [workspace.dependencies] does not declare that field. Inheritance pulls the shared declaration from the workspace root; if the root does not declare it, there is nothing to inherit. Declare the field under [workspace.package] / [workspace.dependencies], or give the member a literal value. |
| OE1255 | Error | Live | WorkspaceDependencyOverride | A workspace member inheriting a dependency with <dep>.workspace = true overrides a field of the inherited [workspace.dependencies] declaration beyond what is permitted. A member may add only optional / features over the inherited declaration (additive); it may not redeclare the source (path/version/git/…). Drop the override, or stop inheriting and declare the dependency in full. |
| OE1256 | Error | Live | WorkspaceDefaultMembersEmpty | A [workspace] default-members list resolved to no package directory. A declared default-members must name at least one existing member; a list that matches nothing is refused rather than silently building an empty default set (a typo would otherwise drop every member without a word). Fix the default-members entries, or remove the key to default to all members. |
| OE1260 | Error | Live | LockfileMalformed | An ox.lock lockfile failed to parse. The lockfile is the machine-generated record of the resolved package graph; a hand-edited or truncated lock is refused naming the parse cause rather than silently regenerated, since silently overwriting a corrupt lock would hide a merge conflict or a botched edit. Delete the lock to regenerate it (ox build), or fix the malformed entry. The lockfile is auto-generated — do not edit it by hand. |
| OE1261 | Error | Live | LockfileContentHashMismatch | A package’s recomputed content_hash does not match the value recorded in ox.lock. The lock pins each resolved package to a deterministic BLAKE3 hash over its source (the .ar files plus a normalized ox.toml); a mismatch means the on-disk source changed under a committed lock. Outside --locked/--frozen the lock is refreshed automatically on the next resolve; under --locked/--frozen it is a loud refusal, because a CI build must not silently build different source than the lock pins. Re-run without --locked to refresh the lock, or restore the pinned source. |
| OE1262 | Error | Live | LockfileDrift | The resolved package graph does not match the committed ox.lock under --locked/--frozen: a package was added, removed, or its version/source/content_hash changed since the lock was generated. --locked requires the lock to already describe the resolution exactly and refuses to regenerate it, so drift is a loud refusal rather than a silent rewrite — the strict-CI contract that the build matches the committed lock. Re-run without --locked to update the lock, then commit the updated ox.lock. |
| OE1263 | Error | Live | LockfileMissing | No ox.lock exists at the workspace root, but --locked/--frozen was requested. These flags require a committed lockfile to verify the resolution against; in their absence there is nothing to honor, and silently generating one would defeat the strict-CI guarantee that the build matches a reviewed lock. Run ox build (or ox check) without --locked once to generate and commit ox.lock, then re-run with the flag. |
| OE1264 | Error | Live | UnresolvableDependencyGraph | The dependency graph has no solution: there is no single assignment of one version per package that satisfies every requirement. Argon resolves each package name to EXACTLY ONE version graph-wide — unlike Cargo, it never lets two semver-incompatible versions of the same package coexist under name-mangling, because two concepts at one qualified path cannot both be ‘the’ type (the nominal-identity invariant). So two requirements that pin the same package name to disjoint version ranges are an unsatisfiable conflict, refused loudly rather than silently mangled into coexistence. The diagnostic carries the PubGrub derivation chain — the root-cause sequence of ‘because A depends on B and C requires not-B’ steps that proves no solution exists. Relax a version constraint, align the conflicting requirements on a shared version, or drop one of the conflicting dependencies. |
| OE1265 | Error | Live | RegistryFetchFailed | A registry read failed: a transport/network error, an unreadable file:// index path, a malformed index record, or an offline build with no warm cache entry. The registry is a static content-addressed store (a config.json plus an append-only NDJSON sparse index over https:///file://); a read that cannot answer is refused loudly rather than treated as an empty result, because a silently-empty index would resolve a real dependency to nothing (silent-wrong). Check the registry URL/path is reachable, or build --offline against a warm cache. |
| OE1266 | Error | Live | RegistryContentHashMismatch | A fetched package artifact’s BLAKE3 hash does not match the cksum the registry index recorded. The content hash is the artifact’s content-address: a mismatch means the artifact is corrupt, truncated, or tampered. This is a FAIL-CLOSED refusal — the artifact is never cached or built, so a bad registry object cannot enter the build. Re-fetch; if the mismatch persists the published object is bad. (Cryptographic signatures over the artifact are out of scope; v1 integrity is the content hash only.) |
| OE1267 | Error | Live | NoRegistryConfigured | A registry dependency is declared but no registry is configured. A bare version requirement (dep = "1.0") or a { version = "1.0" } form that names no local workspace member is a REGISTRY dependency — it resolves against a configured sparse index. This REPLACES the old OE1240 ‘registry form reserved for later’ refusal: the form is supported now, it just needs an index. Add a [registry] table to ox.toml (index = "file:///path" or index = "https://…"), or set ARGON_REGISTRY; to depend on a local checkout instead, use a path dependency (dep = { path = "../dep" }). |
| OE1268 | Error | Live | RegistryPackageNotFound | The package — or a version satisfying the requirement — is not in the registry index. The sparse index has no record file for the package name, or the version requirement matches no published version. Refused loudly rather than resolved to nothing. Check the package name and the version requirement, and that the package has been published to this registry. |
| OE1269 | Error | Live | RegistryYankedVersionSelected | The only version satisfying the requirement is yanked, and the requirement is not an exact pin. A yanked version is withdrawn from new resolutions — it stays selectable only when pinned exactly (= x.y.z), e.g. from an existing ox.lock that already pinned it, so a yank never breaks a locked build but is not picked for a fresh resolve. Pick a non-yanked version, or pin exactly if you must depend on the yanked one. |
| OE1270 | Error | Live | RegistryArtifactTooLarge | A registry body (an index record or a package artifact) exceeds the transport’s hard size cap. A registry read buffers the whole body before its content hash can be checked (OE1266 only protects AFTER the body is in memory), so an unbounded body is a denial-of-service: a malicious or buggy registry can exhaust memory or hang the build on a slow drip. The cap (a build-time constant, 256 MiB — far above any plausible published SOURCE package) is a hard refusal, not a truncation, and is enforced both before the download (when the index declares a size over the cap) and during it (the read itself is bounded); the server’s Content-Length is never trusted. If a package is legitimately this large, the cap must be raised deliberately. |
| OE1271 | Error | Live | RegistryInvalidPackageName | A registry dependency name is not a valid package identifier. A package name is used VERBATIM as a filesystem path segment — the index shard path and the content-addressed cache path under ~/.argon — so it must match [A-Za-z0-9_-]+ (non-empty, ASCII alphanumerics plus _ and -): no path separators, no ./.. components, no empty string. A name like ../../tmp/x would otherwise escape the cache root and write outside it; this is validated at the manifest boundary, before the name reaches any path join, and refused loudly rather than silently traversing. Fix the dependency name in ox.toml. |
| OE1272 | Error | Live | PublishManifestInvalid | ox publish refused because the package manifest is not publishable. A published package’s ox.toml must declare a [package] table with a non-empty name matching the registry charset [A-Za-z0-9_-]+ and a parseable [package].version — the index keys a version record by exactly that (name, version). A missing/empty name, a name with a path separator or .. (which would escape the index shard / cache path), or an absent version is refused loudly rather than published under a guessed coordinate. Fix [package] in ox.toml. |
| OE1273 | Error | Live | PublishVersionExists | ox publish refused because <name>@<version> is already published to this registry. The sparse index is APPEND-ONLY and every published artifact is immutable (content-addressed): a version is published exactly once and never overwritten, so a re-publish of the same coordinate — which would silently change what a downstream lock already pinned — is a hard refusal, not an in-place mutation. Bump [package].version and publish the new version; to withdraw a bad version use a yank (an append-only event), never an overwrite. |
| OE1274 | Error | Live | PublishRegistryWriteFailed | ox publish could not write to the target registry. v1 publishes to a LOCAL / file:// registry only — it writes the content-addressed blob under blobs/, appends the version record to the package’s NDJSON index shard, and ensures config.json — so the target must be a writable local directory or file:// URL. Publishing to an https:// production CDN registry needs upload + auth and is out of scope for v1. Check the registry path is a writable local directory, or point --registry / [registry].index at a file:// path. |
| OE1275 | Error | Live | PublishEmptyPackage | ox publish refused because the package has no publishable source. A package directory must hold an ox.toml and at least one .ar source file — an empty package (no .ar files), or a path that is not an Argon package directory at all, is refused loudly rather than publishing an empty artifact that would resolve to nothing downstream. Add source, or publish the correct package directory. |
| OE1276 | Error | Live | PublishBreakingChange | ox publish refused because the new version makes a BREAKING change to its pub declaration surface without the breaking-tier SemVer bump. Argon recomputes the constructs semantic signature of both the version being published and the latest previously-published version of the same package, diffs them per declaration, and classifies each change against the compatibility taxonomy — proven sound in the scratch-Lean oracle (binary-compatibility, JLS Ch.13 analog): a COMPATIBLE change (a new pub decl, a widened bound/refinement, grown <: parents, widened cardinality) may publish as a patch, but a BREAKING change (removing/renaming a pub decl, narrowing a refinement — including adding a not P (NAF) clause —, an arity / position-type change, narrowing cardinality, or a not-clause-bearing closed→open (CWA→OWA) world flip) requires the breaking tier: a MAJOR bump in 1.x+, a MINOR bump in 0.x (Cargo’s 0.MINOR.PATCH rule — a 0.MINOR bump is the breaking tier pre-1.0). This is refused loudly, naming the offending declaration(s), the kind of break, and the required bump, rather than silently publishing a version a downstream ^-range would auto-adopt and then fail to resolve/check against. Bump [package].version to the required tier, or make the change compatible. The first-ever publish of a package (no prior version) is unconstrained. |
| OE1277 | Error | Live | RegistryCacheCoordinateMismatch | A cached registry package’s extracted [package] manifest does not match the registry coordinate the resolver selected: its name/version differs from the (name, version) the index resolved and the content hash was verified against. A cached directory is folded into the build like a path dependency, so a mismatch means the cache entry holds the WRONG package (cache poisoning, a content-hash collision, or a stale/corrupt extraction) — admitting it would silently compile a different package than the one resolution chose. The dependency is refused naming both coordinates rather than trusted blindly. Clear the package cache under ~/.argon/packages and re-resolve. |
| OE1278 | Error | Live | RegistryInvalidChecksum | A registry index cksum is not a valid content-hash. The cksum is the BLAKE3 content-address of a version’s artifact and is used VERBATIM as a filesystem path segment — the blob download location resolved from the dl template (typically blobs/<cksum> under ~/.argon) and the content-addressed cache directory name — so it must be a non-empty hex string ([0-9a-fA-F]+): no path separators, no ./.. components, no empty string. A cksum like ../../../../etc/hosts would otherwise escape the blob store and be opened, read, and hashed BEFORE the content-hash integrity check runs — an arbitrary-file-read / existence-oracle primitive on a hostile or MITM’d index (registry integrity rests on TLS until index signing lands in a future release). It is validated at the index parse boundary and re-checked before the blob location is built, and refused loudly rather than silently traversing. The index record is bad; if it persists the registry object is compromised. |
| OE1279 | Error | Live | BuildEventIdSpaceExhausted | The build minted more axiom events than the 20-bit per-build EventId sequence band holds (1,048,575): the workspace lowers more declarations, facts, and metaproperties than one .oxbin’s event-id space can name. The per-build counter deliberately spans the stdlib and every workspace file in one pass (EventId monotonicity across the composed event stream), so this is a whole-workspace ceiling, not a per-file one. Before this code the mint SATURATED at the ceiling — every subsequent event received the SAME clamped id, so the emitted artifact carried silently aliased EventIds (deterministically, on every rebuild): axiom histories merged, retraction lineage pointed at the wrong event, and the corruption persisted in the .oxbin itself. The build is fail-closed: it now refuses to emit the artifact instead. The ceiling is the MVP event-id layout (build mode packs tx_seconds=0, shard=0, leaving the 20-bit sequence); a wider band via the shard or tx_seconds fields is future work. Split the package into smaller modules built as separate artifacts, or move bulk fact data out of source into a runtime ingest path. |
| OI1207 | Info | Live | ProjectionCacheSkipped | ox build did NOT embed the section-6 projection cache (the persisted derived read-model) in the emitted .oxbin, and this note names why. The build-time cache is default-on but declines — honestly, with this diagnostic rather than a silent gap — any module outside the v1 fragment: (1) a DATED module (a fact or rule carries a bounded valid-time window, pub fact … at #date# — the model changes when the wall clock crosses a validity epoch with no storage change, and v1 does not persist epochs); (2) a CLOCK-READING module (a rule body — ordinary or enumerate-derive — reads the evaluation clock via today(): a day-N model read on day N+1 would serve stale clock-derived tuples that no content commitment can see); (3) a METRIC-TEMPORAL program (box_minus/box_plus/diamond_minus/diamond_plus/since/until atoms, enumerate-derive bodies included — the catalog wire form drops per-tuple temporal intervals, so a temporal model cannot round-trip); or (4) an encoded payload over the v1 cap (min(64 MiB, 4 x events-section bytes)). A skipped artifact is byte-compatible and behaves exactly as before this feature: every consumer re-saturates from the event log on first read (the cost is one fixpoint per cold process, not wrong answers). Each exclusion is a named predicate with a named upgrade path, not an implicit gap. --no-derived-cache skips emission silently by request. The emitter is the in-process build pipeline (oxc-workspace), so ox build, ox test’s test build, and every embedder render it. |
| OI1208 | Info | Live | ProjectionCacheServed | A read session was answered from the artifact’s section-6 projection cache (the persisted derived read-model): the consumer arm passed every gate (provenance admitted, pristine-seeded store, registry-free, in-fragment, tzdb/format/commitment/generation verified), decoded the payload, restamped the world-assumption map and metric resolution from the module, and served the model without running the fixpoint. This note is the OBSERVABLE half of the projection-cache conformance gate: the golden-anchored corpus entries (conformance/corpus/projection_cache_roundtrip) require it INDEPENDENTLY in ox test and ox run-scenario output, so either command silently falling back to recompute fails the gate rather than passing on correct-but-recomputed answers. Rendered once per session (per scenario store; once per ox test run when any per-test store seeded) by the two commands the conformance gate drives — it is a cache-route marker, not a warning: answers are identical either way (the differential oracle pins seeded ≡ recomputed). Its dual is the ProjectionCacheIgnored warning, which fires when a PRESENT section is refused as invalid; this info note marks when one is actually served. (This rendered text deliberately names the sibling by name, never by code: the help body prints on every marker, and a literal sibling code here would false-positive every grep-for-the-warning-code check over combined output.) |
| OW1206 | Warning | Live | ProjectionCacheIgnored | A .oxbin projection-cache section (id 6) is present but INVALID, so it is ignored and the read re-saturates from the event log. Refusal causes: the derivation commitment does not match the consumer’s own recomputation, the seed generation tuple does not match the pristine-seeded store, the section’s directory content hash does not verify against the body, the payload is undecodable or over the decode bounds (the 64 MiB / events-derived cap, or a declared-count/length bomb caught by the structural preflight), its payload_version / derivation semantic epoch is outside what this consumer reads, its fragment_v1 assertions disagree with the consumer’s own re-derivation (or the module is outside the v1 fragment), or a zone-aware program’s pinned tzdb does not equal the consumer’s bundle. An invalid cache is NEVER served — the arm fails closed to a full recompute, so answers stay correct; this warning exists so a corrupt, tampered, or stale cache is a visible event rather than a silent per-read performance cliff. An ABSENT section is the normal cold shape and falls through silently (the book’s ‘runtime re-saturates if absent’); only a present-but-refused, consumption-admitted section warns this code — a store-shape decline (a non-pristine or registry-bearing store, a non-default scope, or the user’s own --no-derived-cache) stays silent, and an untrusted FOREIGN artifact’s present section warns its own trust-refusal code instead, before any validity gate runs. Rebuild the artifact (ox build) to refresh the cache, or pass --no-derived-cache to disable cache consumption entirely. The emitter is the strict-read consumer arm (ensure_strict_cache); the CLI consumers (ox query / derive / run-scenario) render it once per session. |
| OW1209 | Warning | Live | ProjectionCacheForeignIgnored | A loaded .oxbin carries a projection-cache section (id 6), but the artifact is FOREIGN — not built by this invocation and not recorded in this user’s trusted-artifacts ledger — so the cache is ignored and the read recomputes from the event log. Answers stay correct; this warning makes the trust refusal a visible event rather than a silent per-read performance cliff. WHY the gate exists: 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 — so consumption defaults to self-built artifacts only, established mechanically (ox build appends the whole-file BLAKE3 hash of every artifact it writes to the per-user ledger: $ARGON_TRUSTED_ARTIFACTS, else $XDG_STATE_HOME/argon/trusted-artifacts, else $HOME/.local/state/argon/trusted-artifacts), never by path shape. To proceed deliberately: rebuild the artifact yourself (ox build), pass --derived-cache to trust it for one load, set trust-derived-cache = true under [cache] in the per-user config file to trust standing, or run ox verify-cache to recompute the model and diff it against the embedded cache before trusting. --no-derived-cache disables consumption silently by request. This code also names a VOIDED environment-directed trust grant: when the trusted-artifacts ledger or the per-user config file resolves INSIDE the current workspace root (repository-shipped env machinery — a direnv .envrc, a Makefile, a .cargo/config.toml [env] table — pointing ARGON_TRUSTED_ARTIFACTS / XDG_STATE_HOME / ARGON_CONFIG / XDG_CONFIG_HOME at repository-controlled files), the grant is void — a repository must not vouch for its own artifacts — the artifact classifies foreign (or the standing config grant is ignored, the rest of the config still applying), and the warning names the resolved path and the workspace root. The emitter is the strict-read consumer arm; the CLI consumers (ox query / derive / test / run-scenario) render it once per session. Its sibling warning fires when a TRUSTED cache is present but invalid (corrupt, tampered, stale); this one fires before any validity gate — an untrusted section’s bytes are never even decoded. |
| OW1240 | Warning | Live | UnusedManifestKey | An ox.toml section or key is not recognized by the compiler (Cargo-style “unused manifest key”). Recognized surface is [package]/[project]/[schema]/[dependencies]/[lattice]/[standpoints]/[store]/[placement]/[workspace]/[patch]/[registry]/[fmt]/[lints]/[temporal] and their known keys; anything else (a typo, a not-yet-supported section, an unknown [lattice].max_tier ladder name) is surfaced as a warning naming the section/key rather than silently dropped by deserialization. |
| OW1241 | Warning | Live | UnknownLint | A lint name in a #[allow/warn/deny(...)] attribute or the ox.toml [lints] table is not a registered lint. A lint name keys a level-controllable diagnostic (e.g. non-canonical-clause-order for OW0010); an unknown name is surfaced — with a did-you-mean suggestion when one is close — rather than silently ignored, because a typo’d name would leave the diagnostic at its default level with no signal. The level value itself (allow/warn/deny) is also checked: a [lints] entry whose value is not one of those words is reported here too. The configured lint set is OxcDiagnosticCode::ALL_LINTS. |
Runtime contract and rule-evaluation gates (13xx, §17)
ox build is fail-closed: it re-runs the runtime’s own rule compiler as an oracle over every rule and refuses to write an .oxbin if any rule will not evaluate. The aggregate/quantifier codes (OE1311–OE1315) are the gate’s refusals.
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1301 | Error | Reserved | UnsafeLogicTimeout | Query touching an unsafe logic { } rule exceeded the runtime’s wall-clock budget (§17.5). |
| OE1302 | Error | Live | FederationDisagreement | A federated query under #[federate(strict)] produced a row whose AFT info-join across contributing standpoints is Truth4::Both — at least one source asserts Is, at least one asserts Not. The K3 fail-closed projection at the query boundary rejects the disagreement. Switch to the default paraconsistent policy to receive the row tagged as Both, or reconcile the contributing standpoints’ axioms (§11.2). |
| OE1303 | Error | Live | RuleNotRangeRestricted | A reasoner rule is not range-restricted (unsafe): a variable in the head, inside a negated (NAF) atom, or as a comparison/compute operand is never bound by a positive body atom. Such a rule degrades silently — an unbound head variable projects Null, an unbound negated/compared variable mis-evaluates — so it is refused at compile. Bind the variable with a positive body atom (standard Datalog range-restriction / safety). The named offenders are genuine VARIABLES: every multi-segment path in a value position is resolved (enum constant / axis value / type reference / declared individual) or refused (OE0227 / OE0620) at elaboration, so a qualified path like rigidity::anti_rigid can no longer reach this error — an offender containing :: indicates an artifact built by a pre-S0 toolchain; rebuild from source. |
| OE1304 | Error | Live | NonComparisonOperator | A reasoner Comparison atom uses a non-comparison operator. Only == / != / < / <= / > / >= are meaningful as a rule-body filter; any other operator (e.g. +) makes the executor’s ordering test undefined and would silently drop every row, so it is refused at compile. |
| OE1305 | Error | Live | TierNotYetImplemented | Rule classified at a tier the current runtime executor does not yet support. |
| OE1306 | Error | Live | RuleExceedsExecutorTier | Rule’s classified tier exceeds the dispatched executor’s supported_tiers upper bound. |
| OE1307 | Error | Live | ComptimeNotStatic | Rule declared #[comptime] depends on runtime state (mutation, dynamic individual, or non-static relation). |
| OE1308 | Error | Live | ComptimeForbiddenOnMutate | #[comptime] is not admissible on mutate declarations. |
| OE1309 | Error | Live | StratificationNafCycle | NAF dependency cycle prevents stratified evaluation; use SLG (expressive tier) or rewrite. |
| OE1310 | Error | Live | SemiringMismatch | Provenance witnesses combined under incompatible semirings. |
| OE1311 | Error | Live | NestedAggregate | Nested aggregates (count { count { ... } > N }) require answer-subsumption (SLG-class) evaluation and are refused in v0.1. Flatten via an intermediate pub derive rule, or wait for the V1 SLG kernel. |
| OE1312 | Error | Live | AggregateKindNotImplemented | Collection/string aggregate kind not yet implemented. The folding kinds (count / count_distinct / sum / min / max / avg) evaluate at runtime today; collect / set_collect / string_join / percentile require the V1 SLG kernel (Expressive tier). The collection-kind subquery forms — collect { … } (List), set { … } (Set), one { … } (Option) — are refused under this code too: pre-refusal they silently lowered to count (a green-checked cardinality test instead of the documented collection semantics). Use count { … } / exists { … }, or an intermediate pub derive. |
| OE1313 | Error | Live | NafAggregateForbidden | Negation-as-failure (not) cannot wrap an aggregate subquery (count { … }, exists { … }) in v0.1 — the result is unsound (the inner aggregate was silently dropped by lowering, pre-γ.7). Rewrite via an intermediate pub derive rule that exposes the aggregate result, then negate over it: pub derive any() :- exists { p(x) }; pub derive none() :- not any();. Lift planned alongside the V1 SLG kernel. |
| OE1314 | Error | Live | EmptyAggregateBody | Empty aggregate body (count { }, exists { }) is refused — the resulting cardinality is the trivially-vacuous count of the surrounding binding (always 1), which is almost certainly not what the modeler intended. Provide at least one atom in the body, or use a constant predicate. |
| OE1315 | Error | Live | QuantifierNotYet | A quantifier rule atom is refused because the executor can’t yet evaluate this shape as a true quantifier (emitting anyway would silently over- or under-derive). The supported universal forall v: T where Body, Head (≥2 where-atoms — the last is the consequent, the rest the domain) DOES evaluate: it lowers to the count-equality count { v: Body, Head } == count { v: Body }. OE1315 fires for the shapes that don’t yet: an exists-FOL binder (exists v: T where …), the paren restriction form (forall(path, T)), or a forall whose where clause has fewer than two atoms (ambiguous domain/consequent split). Rephrase via explicit aggregates or an intermediate pub derive; the count { … } / exists { … } subquery forms are unaffected. |
| OE1316 | Error | Live | TemporalOpUnsupported | An unsupported temporal operation reached the evaluator and is refused loudly rather than silently mis-evaluated: arithmetic outside date±duration / date−date / duration±duration (e.g. Date + Date, scaling a temporal value, or mixing a temporal value with a number), a chronological comparison across mismatched temporal types (Date vs Duration, or a temporal value vs a non-temporal one), a calendar-relative duration (P1M/P1Y) or sub-day component (PT…), or today()/now() inside a rule. Surfaced identically in the reasoner and the mutate body. The PROVABLE subset is additionally refused at ox check / ox build under this same code: a rule/query-body arithmetic op whose BOTH operand types are statically known (a Date/Duration literal, or a field projection whose base is TypeTest-bound to a concept with a declared Date/Duration field) and fall outside the defined matrix — e.g. e.happened + 30 with a bare numeric literal (fix: a duration literal, 30.days) — refuses before the artifact is written instead of dying at the first evaluation; an operand the build cannot type stays runtime-guarded, never a false positive. Scalar day-granular date/duration arithmetic and chronological comparison are supported; interval and Allen-relation operations live in the std::time/std::allen library over this value layer. |
| OE1317 | Error | Live | RecursionThroughAggregation | A predicate’s derivation recurses through an aggregate over itself — an aggregate body (count { … }, exists { … }, or the forall count-equality encoding) references a predicate in the same recursive cycle, e.g. pub derive Fulfilled(c) :- …, forall x where x in c.parts, Fulfilled(x);. Stratified-aggregate semantics (Faber-Pfeifer-Leone 2010) require the aggregated predicate to live in a strictly-lower stratum, so recursion through aggregation has no well-defined fixpoint and is refused at ox check / ox build rather than crashing the runtime evaluator. If the aggregate is incidental to the cycle, break it with an intermediate pub derive that materializes the aggregated predicate in a lower stratum. If the aggregate is a universal over the predicate being defined (a forall / count-equality over child parts — recursion depth is data-dependent, so no intermediate derive can leave the cycle), rephrase it as negation-as-failure double negation: derive a counter-example helper with not P(child) (pub derive HasUnfulfilledChild(p) :- childOf(p, c), not Fulfilled(c);), then P(parent) :- …, not HasUnfulfilledChild(parent); — recursion through negation is evaluated under well-founded semantics. |
| OE1318 | Error | Live | UnsupportedMutationForm | A mutate body contains a statement form the runtime cannot yet execute (emit, …). The parser and elaborator admit the form but execution would abort at first invocation, so ox check / ox build refuse the artifact up front instead of shipping a mutation that dies at runtime. Rephrase the statement or wait for the executable form to land. match is executable in BOTH positions: value position (a let RHS, an update … set value, a return/tail value, a require guard, desugared to a Term::IfExpr chain) and statement position (arms running effects, desugared to an Operation::If chain). |
| OE1319 | Error | Live | MatchPatternNotYet | A match arm uses a pattern form that does not yet execute: a binding pattern (a bare identifier that does not resolve to a payloadless enum constant), a payload pattern (Some(x), Type { … }), a tuple pattern, an arm guard, a type-test arm, or an is-outcome arm. The executable subset is ordered first-match over CONSTANT patterns: payloadless enum constant paths (Status::Active), Int / String / Bool / Date literals, or-patterns of those (`A |
| OE1320 | Error | Live | ConditionalArmFieldNotTotal | A rule-body conditional expression (a value if, or a match desugared to one) projects a field inside a conditional ARM that is not total (required) on its holder — the field is declared optional (T?), or no declaration of it is visible. The relational lowering hoists every arm’s projection into an unconditional $field::<f> join, and $field::<f> has no row when an individual lacks the field — so a row missing an optional field would be SILENTLY dropped even when the arm projecting it is never selected. Refused at ox check / ox build rather than mis-evaluated. Workarounds: split the conditional into one rule per arm (each joining only the fields that arm needs), or declare the field required. Projections of required fields inside arms, and projections in always-evaluated positions (the rule body proper, the outermost condition), are unaffected. Lazy presence-guarded branch joins are the tracked long-term fix. |
| OE1321 | Error | Live | EffectInValuePosition | A block in VALUE position (a let RHS block, a value-if branch, a value-match arm) contains an effectful mutate statement (update, delete, forget, require, for, emit). Value lowering reduces a block to its tail expression — the statements would not execute, so the effect would silently vanish. Refused at ox check / ox build instead. Use a statement-position if / match (whose branches DO run effects), or hoist the effect out of the value expression. |
| OE1322 | Error | Live | StaticCheckInstanceVocabulary | A #[static] check reads instance vocabulary. #[static] asserts compile-time discharge intent, and discharge is staged by what the body reads: a check is catalog-level — evaluated finally at ox check / ox build — iff every head parameter and body variable is catalog-sorted: a reflective sort (TypeRef, TraitRef, or Metatype; declaration structure read through specializes, implements, the type column of iof / meta), or an axis-value type (the element type of a pub metaxis Set/Typed domain, read through the catalog-closed $setAxis / $axis relations with n in meta(t).axis). This check binds at least one variable over individuals (a concept/relation predicate atom, a $field:: projection, or the entity column of iof / meta), so its vocabulary classifies it instance-level and it would silently reclassify to runtime discharge — exactly the drift #[static] exists to catch. Either rephrase the body over catalog-sorted variables only, or drop #[static] to accept instance-level (runtime + declared-EDB-at-build) discharge. |
| OE1323 | Error | Live | MalformedCheckDiagnostic | A check rule’s => Diagnostic { … } payload is missing or malformed. Diagnostic is check-surface syntax interpreted by the compiler (like the #[default] defeat-plane directive), not a user type — no import declares it, and its shape is fixed: exactly the fields severity: (the path Severity::Error / Severity::Warning / Severity::Info — nominal, closed set), code: (a string literal), and message: (a string literal, or format!("…{}…", args) with positional {} placeholders matching the argument count one-to-one). code: and message: are always required; severity: is required UNLESS the check is a trait-member impl whose trait pins the member’s severity (check Member(Self) => Severity::…;), in which case omitting severity: inherits the pinned one (a divergent restatement is OE0676); unknown or duplicate fields are refused; at: is reserved for span attribution (the LSP consumes it when it lands) and is refused until then. Named ({name}) or spec’d ({:?}) interpolations are refused — only the empty positional {} form evaluates; a brace placeholder in a plain (non-format!) message literal is also refused, since nothing would interpolate it. Literal braces are written {{ / }} — the escapes work in plain messages and format! templates alike. #[observe] is refused on a non-Error check — Warning / Info checks never guard, so there is nothing to opt out of. |
| OE1324 | Error | Live | CheckCodeNamespace | A check’s code: string does not follow the user-code namespace discipline: the code must be ::-qualified (e.g. "Lease::E001" — a package/domain prefix, then the code), and the OE / OW prefixes are reserved for the compiler’s own catalog (this extends the compiler’s hygiene rule to user space — a user check masquerading as a compiler diagnostic would be indistinguishable in build output and tooling filters). Pick a namespace that names your package or standard ("OntoClean::C1", "HR::W014") and keep the compiler prefixes out of it. |
| OE1325 | Error | Live | CheckMessageArgUnbound | A format! argument in a check’s message: does not resolve against the check body’s bindings. Message arguments are evaluated per violation tuple, so every variable they mention (directly, or as the receiver of a field chain like c.cite) must be bound by the rule — a head parameter or a variable bound by a positive body atom — exactly the range-restriction discipline OE1303 enforces for rule heads. An unbound argument has no value at rendering time and would interpolate garbage (or drop the row) silently, so it is refused at ox check / ox build. Bind the variable with a positive body atom, or interpolate a bound one. |
| OE1326 | Error | Live | TraitMemberNotYet | A trait or impl body declares a form the compiler cannot elaborate. All five TRAIT member forms land end-to-end: rule-plane members (derive/check/query, slice 1) and invocation-plane members (fn/mutate, slice 3) elaborate, conformance-check, and execute. This gate remains for the genuinely unsupported shapes: trait-side DEFAULT member bodies (V1 bounded-generics territory — a default body cannot typecheck against Self’s fields without structural bounds; the OE0667 family), inherent (bare-impl) members (no trait contract, no dispatch story — a follow-on), and unrecognized member forms (associated const/type bodies, junk items). The member parses and is named in this diagnostic together with its declaring trait or impl; ox check / ox build refuse the program rather than shipping an artifact in which the member silently does not exist (v0.1 previously DISCARDED trait/impl body items without a diagnostic). Remove the member, move the default body into each impl, or declare inherent behavior through a trait impl. |
| OE1327 | Error | Live | TraitMemberUncovered | A bare trait-member atom is called over an argument whose static type (the calling rule’s head-parameter annotation) is not FULLY covered by the trait’s impls. Coverage is a workspace-closed catalog scan: every instantiable declared S ⊑ T must satisfy S ⊑ target for some impl target — instantiable means NOT abstract (the type’s own modifier, or its introducing metatype’s, read from the wire — never a user axis name; the pre-S4 sortality token scrape is retired). The default is instantiable: an unmodified declaration obligates coverage, and the exemption is the explicit abstract opt-in. Under naive clause union an uncovered type would make the atom silently FALSE — the silently-vacuous shape this project refuses. Fixes: add the missing impl(s), declare the never-directly-instantiated supertype abstract, or make the partial dispatch explicit with the conformance guard implements(meta(p), Trait) as a body conjunct — the guard’s $implements join then excludes uncovered individuals visibly. A static type with NO covered instantiable subtype stays refused even with the guard written (the guard would be vacuous; the help says so). |
| OE1328 | Error | Live | DuplicateCheckHead | Two check declarations in the same module scope share a head name. derive rules union same-head clauses natively (the §7.3.1 disjunction idiom), but that idiom canNOT apply to checks: a check’s head carries payload identity — the => Diagnostic { severity, code, message } report, the guard classification, and the violation relation all belong to the head — so two checks sharing one head would cross-talk (one check’s violations rendered under the other’s severity, code, and message template, with garbage interpolation when the templates differ). Refused at ox check / ox build. Rename one check, or express the disjunction in a single check body (e.g. over a shared derive predicate whose same-head clauses union, then one check reading it). Trait check members are unaffected: each impl’s monomorphized member rule is qualified by its impl target (Trait::Member@Target) and keeps a distinct head. |
| OE1329 | Error | Live | CheckHeadConsumed | A rule-body predicate atom resolves to a check head. Check heads are observer-only (§7.1: check never populates the IDB) — a check’s violation set is a report with payload identity (severity / code / message), not a derived relation other rules may join. Consuming it would make derived facts depend on diagnostic machinery (and on whether a check happens to be discharged), the inverse of the architecture. Refused at ox check / ox build for every consuming rule mode (derive, check, query, bridge bodies). Derive from the underlying body predicates instead: factor the check’s violation pattern into a pub derive head, reference THAT from both the check and the consuming rule. |
| OE1330 | Error | Live | TemporalQualifierOnWrite | A write statement in a mutate body carries a valid-time qualifier (insert iof(p, T) at <expr>, during <expr>, since <expr>). The parser and elaborator build the qualifier faithfully, but the runtime write path never consumes it — every DML arm destructured it away, so the qualified write applied as if unqualified (the bitemporal extent stamped vt_start = vt_end = None), silently producing atemporal data for a user writing temporal assertions per the book. The storage substrate carries bitemporal extents and as-of reads are exact, but threading user-supplied valid-time through the write path (evaluating the qualifier Term, mapping at/during/since onto vt_start/vt_end, and extending the shared emit_* storage signatures across both backends) is work that is not yet wired. Refused at ox check / ox build rather than dropping the qualifier silently, the same fail-closed discipline as OE1318. Drop the qualifier to write at the current valid time, or wait for the bitemporal mutation forms to land. |
| OE1331 | Error | Live | ValueAggregateBraceForm | A value aggregate (sum / min / max / avg) was written in brace form sum(expr) { atoms }. Only the cardinality aggregates count { … } / exists { … } take the brace form; a value aggregate uses the comprehension form sum(expr for x in Source, atom, …), whose comma-separated trailing atoms (relation joins, comparisons, type tests) refine the fold’s domain and whose grouping is the outer bound variables. Rewrite sum(e.amount) { posted(e, acct) } as sum(e.amount for e in Entry, posted(e, acct)). Refused at ox check / ox build rather than silently mis-folding — one spelling per aggregate role (the house single-spelling rule). |
| OE1332 | Error | Live | AggregateOverUndefined | An aggregate folds over a relation that carries well-founded-UNDEFINED atoms ($undefined::R is non-empty for an aggregated predicate). Aggregates evaluate over the definitely-true extent; folding when part of the input is neither true nor false would silently treat undefined as false (the undefined-as-false leak) — biasing sum / count low and min / max arbitrarily, with no diagnostic. Refused loudly at runtime instead, the same fail-closed discipline the WFS check boundary uses. The refusal is WHOLE-RELATION, not per-group: any non-empty $undefined::R companion trips it, even when the undefined tuples belong to a group key that the current binding would not fold — a conservative stopgap that never folds undefined-as-false. Three-valued aggregate intervals over the undefined set are the designed follow-up; for now, stratify the aggregated predicate so it has a definite extent (no recursion-through-negation feeding the fold), or aggregate over an intermediate pub derive that is itself definite. |
| OE1333 | Error | Live | RoundingBuiltinArity | A rounding builtin (round / round_half_even / trunc) was called with the wrong number of arguments. round(x) rounds to the nearest integer (ties away from zero) and round(x, n) to n decimal places; round_half_even(x, n) rounds to n places with ties to even (banker’s rounding — the money default); trunc(x, n) truncates toward zero at n places. All operate exactly over the numeric tower (Decimal stays Decimal, never via f64). Refused at compile rather than silently dropping the row. |
| OE1334 | Error | Live | BindingLhsNotSimpleVar | A body-level binding atom x = expr has a left-hand side that is not a fresh single-segment variable — a dotted projection (x.f = e) or a ::-qualified path (m::x = e). A binding introduces a NEW variable, so its left-hand side must be a bare identifier. = is the binding (assignment) operator in a rule body, distinct from the comparison ==; it is NOT a field-update operator (that is the mutate-body update … set { field = … } statement). Refused at ox check / ox build rather than falling through to a bare-path Boolean atom, which would silently drop the right-hand expression — the loud-or-working discipline (nothing silently dropped). |
| OE1335 | Error | Live | BindingLhsAlreadyBound | A body-level binding atom x = expr names a left-hand side x that is ALREADY bound — by a prior positive predicate atom, a head parameter, a projection, an aggregate result, or an earlier binding. A binding introduces a FRESH variable; reusing a bound name was silently degrading into an equality FILTER (x joined against the computed value) rather than a binding, so two readings of the same = (bind vs. compare) collapsed with no diagnostic. Refused at ox check / ox build: use == to compare x against the expression, or pick a fresh name for the binding. (== filters over already-bound operands; = binds a new one.) |
| OE1336 | Error | Live | BindingInComprehension | A binding atom x = expr (single =) appears as a trailing atom inside an aggregate comprehension (sum(… for v in S, …, x = expr)). Bindings are NOT a comprehension trailing-atom form: the comprehension body admits predicate joins, comparisons, type tests, and membership — filters that refine the fold’s domain — but a binding introduces a fresh variable whose safety the aggregate sub-scope does not check, so it is refused rather than admitted half-checked. Two rewrites cover every use: project the value directly into the fold (sum(u.v for u in T, rel(t, u)) instead of sum(w for u in T, rel(t, u), w = u.v)), or bind in the OUTER rule body and aggregate the bound variable. Refused at ox check / ox build with this directed hint rather than a generic parse error (== to compare is still accepted as a trailing filter). |
| OE1337 | Error | Live | CalendarRelativeDuration | A duration literal <int>.<unit> names a CALENDAR-RELATIVE unit (.months / .years) whose length in days depends on the anchor date — 1.months is 28–31 days, 1.years is 365 or 366 — so it has no fixed value in Argon’s day-granular Duration layer. The supported units are .days and .weeks (a fixed day count: <n>.days ⤳ n, <n>.weeks ⤳ 7n), which lower at rule-term resolution to a Duration literal that Date ± Duration arithmetic evaluates exactly. Admitting a calendar-relative unit would force a silent, anchor-dependent approximation — refused loudly instead. Use .days / .weeks, or compute the target date explicitly (e.g. an end-of-month date as data). |
| OE1338 | Error | Live | DateIntrinsicArity | A date intrinsic (today()) was called with arguments. The date intrinsics are NULLARY — today() reads the current valid-time of the evaluation (the reasoner’s evaluation clock, fixed for the whole fixpoint so a rule stays a pure function of its snapshot) and takes no parameters. A stray argument is refused at ox check / ox build rather than passed through as an un-evaluable application that would later surface as a generic term-shape failure. Write today(). |
| OE1339 | Error | Live | TemporalQualifierUnexecutable | A mutate-body write’s valid-time qualifier could not be executed. Two cases: (1) during <window> — window-valued valid-time needs an interval value the surface does not yet carry, so it is deferred; use at <date> (valid-from a civil day), which IS executable. (2) the at <date> / since <date> expression did not evaluate to a Date — e.g. a bare arithmetic value where a #YYYY-MM-DD# literal was meant (the #date# silent-Int landmine: a bare 2024-01-01 is integer subtraction, not a date). The valid-time qualifier must evaluate to a Date (a #date# literal or a Date-typed value). Raised at runtime before any event is persisted, so the mutation commits nothing. |
| OE1340 | Error | Live | BareDateArithmetic | A bare YYYY-MM-DD in a rule body parses as INTEGER SUBTRACTION (2024-01-01 ⤳ 2024 - 1 - 1 = 2022), not a calendar date, and was silently accepted — even against a Date-typed position — modeling a date off by two millennia with no diagnostic (the #date# silent-Int landmine). Argon’s calendar-date literal is HASH-DELIMITED: write #YYYY-MM-DD#. The check fires only on the exact date shape — three zero-padded integer literals (4 / 2 / 2 digit widths) whose fields form a valid civil date (year 1000–9999, month 01–12, day 01–31) — so a genuine subtraction of differently-shaped operands is never flagged. Refused at ox check / ox build with a directed hint toward the real literal. |
| OE1341 | Error | Live | RelationCardinalityExceeded | A mutate-body relation write (insert <rel>(…)) would exceed a relation endpoint’s declared finite max-cardinality (§5). The i-th bracket constrains how many DISTINCT values position i may take for a fixed combination of the OTHER positions — the standard UML association-end multiplicity. So in pub rel ParentOf(parent: Person, child: Person) [0..2] [*], the position-0 bracket [0..2] caps each fixed CHILD at two distinct parents; a third distinct parent for one child refuses atomically (the body commits nothing) under the closed-world default, counted over committed-plus-overlay tuples (read-your-writes). Before this gate the bracket parsed into a token-soup node no lowering read and the cap was silently ignored. Max-caps are the CWA-checkable half of a cardinality bracket; the min-card half is the deferred OW1342. Emitted by the runtime write path; an open upper bound ([lo..*] / [*]) imposes no cap. |
| OE1342 | Error | Live | BodilessDeriveUnboundArg | A bodiless pub derive P(...) — a derive head with no :- ... clause — is a GROUND FACT: it seeds P’s extent with the named tuple, and every head argument must therefore be a CONCRETE term (a declared individual in scope, an enum constant, an axis value, or a type reference). The named argument is none of these — it is a free variable, and a bodiless derive has no body to range-restrict it, so the tuple it would seed is undefined. (This is the bodiless-derive analogue of the range-restriction refusal OE1303 for rules WITH a body: there a head variable is bound by a positive body atom; here there is no body, so the argument must be a constant.) Declare the individual (pub fact T(name); classifies name into concept T), or add a :- ... body that binds the variable. To seed ground tuples on a derived predicate, this bodiless form over concrete arguments is the sanctioned idiom — it unions with P’s other derive clauses and rules over the same head; alongside it, declaring pub rel P(...) and asserting pub fact P(...) is equally valid. |
| OE1343 | Error | Live | MalformedCardinality | A relation endpoint cardinality bracket is malformed (§5): the lower bound exceeds the upper ([3..1]), a bound is non-numeric, or the bracket contains junk tokens. Before this the bracket parsed into a token-soup node that no lowering read, so a malformed cardinality was silently dropped along with a well-formed one; now the bracket is interpreted into a structured cardinality and a malformed one is refused at ox check / ox build. Use [n] (exactly n), [lo..hi], [lo..*] (at least lo), or [*] (any). |
| OE1344 | Error | Live | QueryCteNotYetImplemented | A query select-body opens a with <name> = select …; common-table-expression (CTE) clause (§7.4) that does not yet execute. The CTE surface is documented but the binding is not wired into the query planner, so accepting one silently would drop the named sub-result and evaluate the outer select against nothing. Refused by name at parse — distinct from a generic typo — until the planner materializes CTE bindings. For now, inline the sub-query’s filter into the outer select … from … where …, or declare an intermediate pub derive and select over it. |
| OE1345 | Error | Live | QueryGraphTraversalNotYetImplemented | A query uses a graph-/path-traversal form (§7.4) that does not yet execute: a select shortest path … / select all_shortest path … projection, a select path <name> … path-binding projection, or a `… as |
| OE1346 | Error | Live | QueryOptionalFromNotYetImplemented | A query select-body uses an optional from … clause (§7.4) — the left-outer-join form that admits rows whose optional pattern has no match (binding its variables to null) — which does not yet execute. The clause is documented but the planner has no outer-join operator, so accepting one silently would drop exactly the rows it is meant to keep. Refused by name at parse until the outer-join lands. Split into two queries, or model the optional fact with an explicit Option-typed field, for now. |
| OE1347 | Error | Live | QuerySetOpNotYetImplemented | A query combines two select-bodies with a result-table set operator — union, union all, intersect, or except (§7.4) — that does not yet execute. The operators lex and the surface is documented, but the planner has no set-combination stage, so accepting one silently would return only the first select’s rows. Refused by name at parse rather than a generic token error. Combine the branches with multiple pub derive clauses sharing one head (union), or filter explicitly (intersect/except), for now. |
| OE1348 | Error | Live | RuleRoleStepClosureNotYetImplemented | A rule body uses a role-step transitive-closure navigation — x.Role+(y: T) (one-or-more steps) or x.Role*(y: T) (zero-or-more) (§7.3.1) — that does not yet execute. The closure surface is documented (the headline ontology path-traversal form) but the reasoner has no closure operator on a role step, so the +/* after a navigation parsed as stray arithmetic and died generic. Refused by name at parse until role-step closure lands. Write the closure as an explicit recursive pub derive reaches(x, y) :- x.Role(y); reaches(x, y) :- x.Role(z), reaches(z, y); for now. |
| OE1349 | Error | Live | RuleOutcomeSuffixNotYetImplemented | A rule-body predicate atom carries a multi-valued reasoning-outcome suffix — is both(a, b), is ambiguous(a), or is timeout(a) (§7.3.1 / §14) — that does not yet execute: both is paraconsistent over-determination and ambiguous / timeout are resolution states, all of which need the richer §14 reasoning-outcome surface. Before this, is ambiguous(a) / is timeout(a) parsed and were SILENTLY DISCARDED (lowered to the bare predicate, dropping the outcome) and is both(a, b) died on a generic parse error — both now refuse by name. The distinct well-founded truth qualifier R(arguments) is unknown is executable for a predicate in a completed lower component; it does not implement these richer outcomes. |
| OE1350 | Error | Live | RuleEverShortcutNeedsDisjunction | The ever <atom> metric-temporal shortcut (§7.3.2) expands to a DISJUNCTION — diamond_minus[0, ∞](atom) ∨ diamond_plus[0, ∞](atom) (φ held at some past OR some future point) — which the rule-declaration lowering cannot emit, because a single surface rule lowers to a single conjunctive body (no rule-splitting at this layer). The other metric operators lower directly: the four prefix box/diamond forms and since/until are conjunctive atoms, and always <atom> expands to the CONJUNCTION box_minus[0, ∞](atom), box_plus[0, ∞](atom) in one body. Write the two diamond rules explicitly — one head :- diamond_minus[0, ∞](atom) and one :- diamond_plus[0, ∞](atom) — to express the disjunction. (Once rule-splitting lands, ever will desugar to those two rules automatically.) |
| OE1351 | Error | Live | UnsafeLogicBlockNotYetImplemented | An unsafe logic { … } block (§9.3) — the escape hatch that admits a full first-order-logic formula above the decidable tier ladder, opting out of the tier classifier’s termination guarantee — does not yet execute. The surface is documented for the fol tier but no FOL solver is wired in, so the block had no item production and died at module level with a generic parse error. Refused by name until the FOL escape hatch lands. Stay within the decidable tiers (structural…recursive) for now. |
| OE1352 | Error | Live | MutateUpsertNotYetImplemented | A mutate body uses an upsert statement (§8) — insert-or-update keyed on identity, with optional on insert { … } on update { … } arms — that does not yet execute. The keyword is reserved and the surface is documented, but the write path has no upsert effect, so upsert had no statement production and fell through to a generic expression parse error. Refused by name at parse until upsert lands. Branch explicitly on existence with require + a conditional insert / update for now. |
| OE1353 | Error | Live | MutateDetachDeleteNotYetImplemented | A mutate body uses a detach delete <rel>(…) statement (§8) — delete a relation tuple and the individuals it solely connects — that does not yet execute. The detach keyword is reserved and the surface is documented, but the write path has no detaching-delete effect (only a plain delete tuple/iof), so detach had no statement production and died on a generic parse error. Refused by name at parse until detach-delete lands. Use a plain delete <rel>(…) and delete any orphaned individuals explicitly for now. |
| OE1354 | Error | Live | AsyncFnNotYetImplemented | A function is declared async (or uses .await) (Appendix B) — asynchronous evaluation — which does not yet execute. The async / await keywords are reserved against a future effect system but there is no async runtime, so async fn had no declaration production and fell through to a generic module-level parse error. Refused by name at parse until async lands. Declare a plain pub fn for now. |
| OE1355 | Error | Live | ConstDeclNotYetImplemented | A top-level const <NAME>: <T> = <expr>; declaration (§19) does not yet execute. The form parsed but SILENTLY SWALLOWED its initializer — it emitted no event and bound no value, so a model could reference a constant that did not exist with no diagnostic (contradicting the no-hollow-features rule and STATUS’s every surface decl lowers to events). Refused by name at parse, rather than silently accepted, until const evaluation and the compile-time-constant plane land. Inline the literal at each use site, or model it as a field default, for now. |
| OE1356 | Error | Live | SinkDeclNotYetImplemented | A pub sink <name> { … } declaration carries a brace body, but a sink has no body: it declares a name and the type of value it accepts (pub sink <name>: <Type>;), nothing more. WHERE a sink’s values are delivered — log file, webhook, message queue, HITL ticket — is runtime configuration (the egress dual of read-side placement), never in-language source, so there is no body form to fill in (this is by design, not a pending feature). The braces used to be SILENTLY SWALLOWED via balanced-token skipping — no event, no validation, arbitrary garbage accepted with no diagnostic (a no-hollow-features violation); the declaration now refuses by name. The one sink form pub sink <name>: <Type>; is unaffected, and emit targeting a sink refuses with OE1318 until sink publication has a runtime. |
| OE1357 | Error | Live | RuleNegatedSomePayloadBindingUnsupported | A rule body negates an Option/T? payload binding — path is not Some(<ident>). The positive form is Some(a) binds a to the payload and the conjunction fails on None; under negation there is no payload to bind (the binder would range over nothing), so a bound is not Some(a) is incoherent. The intended meaning is the absence test, written path is None (true iff the field is absent) — refused by name rather than binding a meaningless variable. Use path is None for the negative test, or path is Some(a), not <p>(a) to negate a property of the payload. |
| OE1358 | Error | Live | RuleOptionTestNotFieldProjection | An Option/T? membership test is Some(a) / is None has a left side that is not a field projection. The test reads an OPTIONAL FIELD’s presence — a present field materializes one $field::<f>(holder, value) row, an absent one materializes none — so its left side must be a field access (p.age is Some(a)), not a bare variable or an expression with no relationalizable receiver. Refused by name rather than silently dropping the option test. Write <holder>.<field> is Some(a) / <holder>.<field> is None. |
| OE1359 | Error | Live | RelationIrreflexivityViolated | A relation declared #[irreflexive] (a generic relation-algebra property — no element relates to itself, OWL IrreflexiveObjectProperty) has a self-loop R(x, x) in its extent. #[irreflexive] synthesizes a check rule over the declared relation’s own extent that fires on any tuple whose two positions are equal; this is that check firing. Either the offending self-pair must not be asserted, or the relation is not actually irreflexive and the directive should be removed. (The directive enforces the relation’s OWN extent — propagation to relations classified-by an irreflexive metarel is a recorded follow-up, not yet enforced.) |
| OE1360 | Error | Live | RelationAsymmetryViolated | A relation declared #[asymmetric] (a generic relation-algebra property — R(x, y) forbids R(y, x), OWL AsymmetricObjectProperty) contains a mutual pair: both R(x, y) and R(y, x) are in its extent. #[asymmetric] synthesizes a check rule over the declared relation’s own extent that fires when a tuple and its converse both hold; this is that check firing. Either one direction of the mutual pair must not be asserted, or the relation is not actually asymmetric and the directive should be removed. Note asymmetry implies irreflexivity (a self-loop is its own converse), so #[asymmetric] alone already forbids R(x, x). |
| OE1361 | Error | Live | RelationFunctionalityViolated | A relation declared #[functional] (a generic relation-algebra property — each source maps to at most one target, OWL FunctionalObjectProperty) has a source related to two distinct targets. #[functional] now emits a blocking check — a definition-site __{rel}_functional head that fires on R(x, y), R(x, z), y != z — refusing a source mapped to two distinct targets. This is the same check-based enforcement as #[irreflexive] (OE1359) and #[asymmetric] (OE1360), and the same check the metarel arm has always emitted; both the rel and metarel arms of the procmacro paste it. This code names the functional-property origin of the refusal so the modeler sees that a #[functional] directive, not a hand-written constraint, is what fired. Either one of the conflicting targets must not be asserted, or the relation is not actually functional and the directive should be removed. |
| OE1362 | Error | Reserved | RelationPropertyNotImported | Retired — no longer emitted. #[functional] re-homed from a #[builtin] stub to a genuine procedural macro in std::rel (both its rel and metarel arms re-emit the decorated declaration and paste a guarding source-maps-two-targets check, OE1361). It is no longer a registered directive, so an unimported #[functional] — like its siblings #[transitive]/#[irreflexive]/#[asymmetric] — is now refused as OE0705 (unknown attribute macro). This code is retained for historical numbering. |
| OE1363 | Error | Live | MutationCallCycle | The mutation call graph contains a cycle — a mutate body invokes another mutation (directly, mutually, or itself) along a path that returns to its start (§7.5). A sub-mutation call is closed-nested: it runs the callee’s operations into the CALLER’s transactional buffer rather than evaluating to a fixpoint, so the whole transitive call tree is one atomic transaction that commits or aborts wholesale. A recursive cycle therefore has no terminating composed evaluation in v1 — it would unbound-recurse the runtime — so the call graph is required to be statically acyclic and the cycle is refused at ox check / ox build, naming the trail, rather than shipping a mutation that overflows the stack on first invocation (the same fail-closed discipline as OE1318). A trait-member callable edges to every impl body in its group (dispatch picks one at runtime, but any is statically reachable). Break the cycle; recursive mutation composition (fixpoint-to-quiescence) is a separately gated future feature. |
| OE1364 | Error | Live | UnknownTermInBody | A derive / query / check / fn body contains an expression the elaborator could not lower — it degraded to the <unknown> lowering sentinel (Term::Var { name: "<unknown>" }), the fallback an un-lowerable expression form falls back to during expr/atom lowering. The sentinel resolves to no binding at runtime and surfaces as a generic “unbound variable” the first time the body is evaluated, so a body reaching a built artifact carrying one is a silent landmine (this is the same bug-class OE1318 closes for mutate bodies — the self-in-expression-position case — extended to every rule-plane and compute body). The sentinel is refused at ox check / ox build, naming the offending declaration, rather than shipping a rule/query/check/fn that dies generically on its first evaluation. Rephrase the offending expression into a supported form. |
| OE1365 | Error | Live | NegatedWfsRelation | A derive rule negates (not R(..)) a relation R that is evaluated under well-founded semantics in a DIFFERENT stratum — a relation whose extent can carry well-founded-UNDEFINED atoms because recursion-through-negation left part of it neither true nor false. The well-founded SCC materializes 2-valued at its boundary: only the definitely-true extent lands in R; the undefined tuples live in the $undefined::R companion relation. Ordinary derive-rule NAF reads only R, so a higher stratum’s not R(x) for a genuinely-UNDEFINED R(x) would read it as definitely-FALSE and over-assert the head — an unsound over-derivation with no diagnostic. Refused LOUDLY at ox check / ox build rather than silently evaluating undefined-as-false, the same fail-closed discipline OE1332 uses for aggregates over an undefined relation (the check boundary already excludes $undefined::R correctly via its K3 firing rule; derive heads do not yet). This refuses ONLY the genuinely-unsound case — a NAF edge whose target SCC is well-founded and is NOT the negating rule’s own SCC; ordinary stratified NAF over a fully 2-valued relation (no recursion-through-negation) is unaffected, and a same-SCC NAF is the recursion-through-negation the well-founded evaluator handles internally. Collapse the negation into the well-founded SCC: instead of splitting into a lower R stratum and a higher H :- .., not R(..) stratum, route H’s recursion through its OWN negation so {H, R} form a single NAF-cyclic SCC the engine evaluates by well-founded semantics directly (the documented win-move idiom in examples/robot_plan_execution/robot.ar ~115-140). Three-valued propagation of UNDEFINED into derive heads (the AFT pair-encoding) is tracked separately. |
| OE1366 | Error | Live | CheckViolation | A user-authored check rule fired at Severity::Error (static discharge). The check’s body matched over the package’s catalog / declared EDB, so the obligation it encodes is violated. This is the typed-channel carrier the language server uses to surface a fired check in-editor: the violation’s own namespaced code (e.g. Lease::E001) and its rendered message lead the diagnostic message, since a user check’s identity is the author’s ::-qualified string, not a compiler catalog code. ox check / ox build report the same violation through the user-diagnostic channel and fail the command (the loud-gate); #[observe] opts an Error check out of blocking, and Warning / Info checks render and pass — neither is carried through this code. |
| OE1367 | Error | Live | NegatedOpenWorldDerived | A rule negates (not H(..)) a relation H whose extent can carry OPEN-WORLD-UNDEFINED atoms — H is (transitively) derived through a negation over an open-world (#[world(open)] / default_world = "open") relation, yet H itself is read closed-world. An open-world not C(x) over an absent C(x) is can (unknown), not a definite not, so H’s definitely-true extent omits those can rows; a 2-valued not H(x) then reads such a can row as definitely-FALSE and over-asserts the head — an unsound over-derivation with no diagnostic. Refused LOUDLY at ox check / ox build rather than silently evaluating open-world-undefined-as-false — the open-world analog of OE1365 (the well-founded case), the same fail-closed discipline. This refuses ONLY the genuinely-unsound case: a closed-world not H over a relation whose derivation depends on an open-world negation. To resolve: mark H’s concept #[world(open)] too (so not H reads open-world and tolerates the can), or restructure so the open-world negation does not flow into a closed-world-negated head. Full three-valued propagation of open-world-undefined into derive heads (the AFT pair-encoding) is tracked uniformly with the well-founded case. |
| OE1368 | Error | Live | EmitReservedDiagnosticsSink | An emit targets the reserved Diagnostics sink. Diagnostics is the language’s built-in diagnostic stream, and a check rule is its SOLE producer — it is fed only through the check => Diagnostic { severity:, code:, message: } consequence, never by a hand-written emit. A direct emit Diagnostics { … } (in a mutate/fn body) or a check => emit Diagnostics { … } consequence is refused by name: the diagnostic channel’s structured shape (severity, namespaced code, per-violation message, the K3 definite-firing discipline) is the compiler’s, not an arbitrary egress payload, so routing a raw value through it would bypass every diagnostic invariant. Emit to a sink you declare (pub sink <name>: <Type>;) for ordinary egress, or use a check’s => Diagnostic { … } consequence to contribute a diagnostic. |
| OE1369 | Error | Live | AggregateCardinalityOverflow | A count / count_distinct aggregate’s cardinality exceeds the Int value domain (it does not fit a signed 64-bit integer). The factorized aggregate path computes a count as the EXACT product of its independent factor groups’ sizes — a body of k independent two-row factors yields a count of 2^k — and the value domain’s Int is signed 64-bit, so a count of 2^63 or more has no faithful Int representation. Historically the count was accumulated in usize with a saturating multiply (capping a true ≥2^64 product at usize::MAX) and then cast as i64 (sign-wrapping a ≥2^63 count to a NEGATIVE Int), so the aggregate silently returned a wrong — even negative — count with no diagnostic. The count is now computed exactly and refused loudly when it overflows the value domain rather than emitting a corrupted result. Reduce the aggregated cross-product, or aggregate over a narrower body whose cardinality fits the Int domain; an arbitrary-precision count is a designed follow-up. |
| OE1370 | Error | Live | ScalarFromFieldNonFunctional | A scalar navigation-view field (f: T from Rel.endpoint, declared T rather than [T] / T?) is a function: it presupposes the holder backs at most one endpoint tuple, so it can yield a single value (the Lean FieldView rule — a navigation-view field is present iff a backing tuple exists, and stores no value of its own). A scalar from-view is admitted IFF the backing relation is BINARY with a FUNCTIONAL value endpoint (max-cardinality [0..1] / [1..1]). Two refusal reasons fall under this code. (1) BINARY, non-functional value endpoint: the endpoint may back several tuples for one holder, so the scalar view is ill-defined — at runtime it would discard a present tuple. (2) N-ARY relation (arity ≥ 3): a single [0..1] / [1..1] bracket on the value endpoint bounds the value per fixed combination of the OTHER endpoints, NOT per holder, so it is NOT a holder→value functional dependency — e.g. Employment(person, company, salary) [..] [..] [0..1] permits one person two salaries across two employments, which a scalar pay: Int from Employment.salary would silently collapse. (A holder→value functional-dependency analysis that would safely admit some n-ary cases is a documented follow-up, not yet implemented.) The parser admits the declaration but the projection is incoherent, so ox check / ox build refuse it up front. Either declare the field as a collection ([T]) to project the whole endpoint set, or model the dependency with a BINARY relation carrying a functional [0..1] / [1..1] value endpoint. |
| OE1371 | Error | Live | MutateQueryTermNotExecutable | A deductive-plane read — a pub query call (applicable(), applicable_for(0)) or a bare derived-predicate (derive) head (Applicable) — appears in a NON-iterand TERM / value position of a mutate body: a let right-hand side (let x = applicable();), a require guard, an update … set value, a constructor field value, or the body tail. A deductive read is SET-VALUED (mutate.md §body-semantics): its sole admissible use in a mutate body is as the iterand of a for x in <query> { … } loop, which binds each projected row in turn and lowers to a reasoner snapshot read over the body’s committed-state plane (§7.5). In a scalar term position it has no value, so it is refused here at ox check / ox build rather than passing the static gates and then dying at the first runtime call with unsupported function … in a term position — the silent-death hole this gate closes. To resolve: iterate the read with for r in applicable() { … } (the documented surface; parameterized reads for r in applicable_for(arg) are admitted and execute), binding each row, instead of reading the whole extent into one scalar slot. A pure compute over a single value belongs in a fn, not a query. The SAME gate refuses a mutate callee in a term position (let x = registerMike();): a mutation is INVOKED as a statement, never read as a value — it is not a term-position callable (nested mutation-as-expression is not a supported form), and before this arm it too passed the static gates and died at the first runtime call. Invoke the mutation as its own statement; a value to compute belongs in a fn. |
| OE1372 | Error | Live | MembershipRhsNotRelationalizable | A membership atom member in <rhs> / member not in <rhs> — or a comprehension where … in <rhs> — has a right-hand side the relationalizer cannot lower to membership semantics. The accepted RHS forms are a set/list LITERAL ([1, 2]), a COLLECTION or CONCEPT-EXTENT source (x in Person, x in p.records), and the reflection extents (extent(T), implementors(Tr)). Any other RHS — a bare scalar, an arithmetic expression, a projection that does not relationalize to a bound receiver — was previously dropped to a vacuous zero-arg predicate that derives NOTHING, so a positive in evaluated to the empty extent and a not in was vacuously true for every row, with no diagnostic. The atom is refused here at ox check / ox build so the membership LOWERS to a real relation join or REFUSES — never silently mis-derives. To resolve: write the RHS as a set/list literal, a collection/extent source, or a reflection extent; for a bounded numeric window use the two-sided comparison (r >= lo, r <= hi). (A lo..hi RANGE RHS refuses earlier at parse with OE0001 on ...) |
| OE1373 | Error | Live | IntegerLiteralOutOfRange | An integer literal — in a pub fact argument (pub fact amount(a, 9223372036854775808)), in a rule / check / refinement body (a comparison constant n == 9223372036854775808, a binding right-hand side, an in set element), or as an as_of <int> transaction-time pin on a query (declared or ad-hoc-submitted, where the pre-fix silent drop read as if no pin were declared) — exceeds the range the implementation’s integer carrier can hold, or is a malformed integer token. Argon’s Int is ARBITRARY-PRECISION at the language level (the exact-numeric tower Nat <: Int <: Real), so the literal is WELL-TYPED; the current compiler simply cannot represent it in the 64-bit (i64) carrier its build artifact and reasoner use — the supported range is -9223372036854775807..=9223372036854775807 (the lower bound is i64::MIN + 1, not the carrier’s true i64::MIN: unary - is a separate token, so -9223372036854775808 is parsed as the positive magnitude 9223372036854775808 first and overflows positive i64, so i64::MIN is not currently a reachable literal). Refused loudly at ox check / ox build rather than the prior silent coerce-to-0, which recorded / matched the WRONG value with no diagnostic: a too-large pub fact amount(a, N) stored 0, and a body comparison n == N aliased to n == 0, so a row stored as 0 spuriously matched a guard written for N. This is an honest implementation-limit refusal, not a claim that the literal is illegal — promoting the carrier to arbitrary-precision is a separate principled change. To resolve: use a value within the supported range, or model the quantity differently (e.g. scale the unit) until the wider carrier lands. |
| OE1374 | Error | Live | RoundingScaleOutOfRange | A rounding builtin (round / round_half_even / trunc) was given a decimal-place count n outside the supported scale domain. The scale is 10^-n, so the builtin forms `10^ |
| OE1375 | Error | Live | MutateTupleArityMismatch | A mutate-body insert R(args) / delete R(args) supplies the wrong number of arguments for relation R’s declared arity. The mutate-body analogue of OE0221 (pub fact) and OE0225 (rule atom), split by context per the OE0220→OE0225 precedent: a pub fact worksAt(alice) against rel worksAt(person, company) is refused (OE0221), so the symmetric insert worksAt(p) in a mutate body must be refused identically. Left ungated, the runtime would commit a malformed n-tuple into the relation’s extent — a 1-tuple where a 2-tuple was declared — and every read of the relation would surface the corrupt shape. Refused at ox check / ox build, before any artifact commits, exactly as the ground-fact gate refuses it. Supply the declared number of arguments for R. |
| OE1376 | Error | Live | NavViewHolderAmbiguous | A COLLECTION navigation-view field (field: [T] from Rel.endpoint) over an N-ARY (arity ≥ 3) relation names only the VALUE endpoint; its HOLDER — an individual of the declaring concept — must occupy a DIFFERENT endpoint, resolved by type. The relation must have EXACTLY ONE non-value endpoint whose declared concept is <:-comparable with the declaring concept. Two refusal reasons fall under this code. (1) NO holder endpoint: no non-value endpoint’s type is comparable with the declaring concept, so the holder can never occupy the relation and the view is statically empty — e.g. declaring staff: [Person] from R.person on Company where R(person, widget, n) has no Company-comparable endpoint. (2) AMBIGUOUS holder: two or more non-value endpoints are comparable with the declaring concept, so from Rel.endpoint cannot say which one is the holder — e.g. Pairing(buyer: Company, seller: Company, item: Item) with deals: [Item] from Pairing.item declared on Company (both buyer and seller could be the holder). A BINARY relation’s holder is the sole other endpoint, resolved POSITIONALLY at runtime with no type constraint, so this gate does not apply to it (a holder dual-classified under an unrelated endpoint concept is a legitimate projection); a SCALAR from-view over an n-ary relation is owned by OE1370. The parser admits the declaration but the projection holder is incoherent, so ox check / ox build refuse it up front rather than silently projecting the wrong column at runtime. To resolve: project from a binary relation whose sole other endpoint is the holder, or model the relationship so the holder endpoint is the unique one comparable with the declaring concept. |
| OE1377 | Error | Live | ValidTimeOutOfRange | A valid-time qualifier’s #date# / #…Z# instant — an at <#date#> / at <#…Z#> effective-dating stamp on a pub fact or derive rule, or an as_of <#date#> query clause — names a civil day / instant so far from the Unix epoch that its start-of-day nanosecond value overflows the implementation’s i64 valid-/transaction-time axis (which tops out near year 2262 and bottoms out near year 1677). The literal is a WELL-FORMED, in-calendar-range date/instant; the current compiler simply cannot represent its nanosecond position in the 64-bit axis the bitemporal engine and read points use. Refused loudly at ox check / ox build rather than the prior silent saturating_mul, which pinned a pathological far-future/-past date to i64::MAX / i64::MIN with no diagnostic — so a far-future #YYYY-MM-DD# valid-time aliased to the same axis point as any other overflowing date, and a read as_of such a date matched an unrelated point. This is an honest implementation-limit refusal, sibling to OE1373 (out-of-range integer literal), not a claim that the date is illegal; widening the axis carrier is a separate principled change. To resolve: use a valid-time within the representable range (roughly 1678-01-01 .. 2262-04-11). |
| OE1378 | Error | Live | RelationFunctionalCapConflict | A relation declared #[functional] (each source maps to at most one target) also declares an explicit cardinality on its TARGET (last) endpoint whose upper bound exceeds one ([0..2], [0..*], [1..*], [*]). The two constraints contradict: #[functional] requires at most one target per source, but the bracket permits more. Rather than silently drop the user’s cardinality or the functional constraint (either would be a silent-wrong), the declaration is refused, naming the relation and the conflicting bracket. An agreeing [0..1] on the target (or no explicit target bracket — the functional check then enforces the bound) is accepted. Remove the conflicting bracket or drop #[functional]. |
| OE1379 | Error | Live | RefinementPredicateNotBool | A where/iff refinement predicate — a concept refinement, or the value filter of a navigation-view from clause — has an expression whose inferred type is PROVABLY not Bool (e.g. a bare self.age projection of an Int field, an arithmetic result, a String literal). A refinement predicate decides membership, so its expression must evaluate to Bool; a non-Bool result is unevaluable as a predicate. Before this code such a predicate built cleanly and then failed LOUD at its first evaluation (RefinementUnevaluable at the first extent read, classification, or write-gate check) — a build-accepts/runtime-refuses drift; cause-determines-response puts the refusal at build time, where the fault is. Only a provable non-Bool is refused: a predicate whose type the checker cannot resolve (an Unknown-typed expression, an unresolved field or name) is NOT rejected here — no false positives — and remains guarded by the loud runtime error. Fix it by making the expression a boolean test (self.age >= 18, not self.age). |
| OE1380 | Error | Live | DuplicateDefinition | The same module defines the same name twice: a second fn with the SAME name and SAME arity as an earlier one, or a second rel re-declaring an earlier relation’s qualified path. Argon has no overloading-by-type: the two definitions are indistinguishable at every call/atom site, so before this code the program built green and the layers silently DISAGREED on the owner — the checker typed call sites against the FIRST definition while the runtime’s bare-name dispatch table (whose remove-on-second-claim dedup exists for CROSS-module collisions, where qualified calls disambiguate) dropped the shared key, so the first runtime call died with an unsupported-function error naming a function the checker had accepted. A duplicate is refused at build, naming both the duplicated path and its earlier definition. Same-name fns in DIFFERENT modules remain legal (a bare call is ambiguous and refused at the call site; qualified calls dispatch normally), and same-name fns with DIFFERENT arities remain the loud-ambiguity case bare-name dispatch already refuses. Rename one definition, or move it to its own module. |
| OE1381 | Error | Live | FirstOrderInstantiationTarget | A : T instantiation clause targets a type the same package PROVABLY uses at the individual tier (pub kind Dog : Animal while a query reads a: Animal, a pub fact Animal(x) asserts a member, or a mutate constructs/classifies under Animal). The : T clause declares type×type iof-instantiation: the DECLARING type becomes an INSTANCE of T, so T’s instances are TYPES — populating or reading the SAME T with INDIVIDUALS is tier-mixing, and with near certainty a mis-spelled SUBTYPE declaration. It does NOT subtype: the clause built green, lowered to a catalog-tier iof row, and the declaring type’s individuals silently never appeared in the target’s individual-tier extent (insert Dog {} never reached a query over Animal) — a silent no-op for the intent. Use <: for specialization (pub subkind Dog <: Animal: every dog IS an animal). The refusal is evidence-driven, never intent-guessing: a : T whose target shows NO individual-tier usage is the legit higher-order form (the target’s extent read at the TYPE tier via iof(t, T), a type-valued relation endpoint) and is never refused. |
| OE1383 | Error | Live | UnboundMutateValueName | A bare identifier in a mutate / test body value position — a relation-tuple argument, a constructor / update field value, a let initializer, a require guard, a return, a for / comprehension source — resolves to nothing: it is neither a parameter, a let / loop / match binder in scope, nor a declared pub fact individual. Such a name has no value at runtime — no identity is synthesized for an unbound name — so the mutation would build green and then refuse at its first invocation (unbound variable). It is refused at ox check / ox build instead, so no artifact ships that dies at dispatch (the loud-gate contract). Fix: bind it as a parameter or a let, declare it with pub fact, or correct the typo. A same-named parameter / let SHADOWS a declared individual (a bound local always wins). A name resolving to a pub query / derive head is a DIFFERENT refusal: a set-valued deductive read has no value in a term position and its sole admissible use is a for x in <head> iterand. A name spelling a DECLARED TYPE (concept / trait / metatype / metarel / relation) is not unbound and never reaches this refusal: unshadowed, it lowers to a type-as-value reference — the same lowering rule bodies apply — and a mis-sorted landing of that reference is OE1384, not OE1383. |
| OE1384 | Error | Live | MutateTypeValueEntityPosition | A declared type name used as a type-as-value in a mutate / test body write LANDS in a position whose declared sort holds entities/values, not type references — a relation-tuple argument, constructor field, update … set target, or insert … into element whose declared type is not headed by one of the reference sorts TypeRef / Metatype / TraitRef. A declared type name in a value position lowers to a type-as-value reference (the same lowering rule bodies apply), and the reference lands soundly only in a position declared to hold one: committed into an entity slot it would match no read path — equality dispatch keys type references to reference-sorted columns — so the write is refused at ox check / ox build rather than silently persisted. This is the imperative-body analogue of OE0262 (which refuses a type-as-value pinned into a variable-expecting rule-body slot). A relation whose declared signature the elaborator cannot see reads as all-entity-expecting (fail-closed, as OE0262); cross-module field-type chains and binder-laundered references defer to the same-coded runtime write-gate backstop. Fix: bind a value or individual in that position, or declare the position TypeRef (or TypeRef<C> / Metatype / TraitRef) if it should reference a type. |
| OE1385 | Error | Live | MutateIndividualReferencePosition | An INDIVIDUAL is written to a mutate / test body position DECLARED to hold a type-as-value reference — a relation-tuple argument, constructor field, update … set target, or insert … into element whose declared type is headed by one of the reference sorts TypeRef / Metatype / TraitRef. The reverse direction of OE1384: a reference-declared position holds references to DECLARED TYPES (equality dispatch keys such positions to the declared-symbol catalog), so an individual committed there matches no read path — the write is refused rather than silently persisted, and a tuple DELETE with an individual at a reference position is refused rather than silently matching nothing. Refused at ox check / ox build where the individual is statically known (a construct binding), and at the runtime write gate otherwise (a parameter-passed or binder-laundered individual). The wire boundary already refuses this shape (#i… entity references are rejected for TypeRef-sorted parameters at coercion), so the gate closes the in-language write path to match. A position declared Entity / Top admits individuals — this refusal never fires there; a type reference written to such a position remains governed by the forward landing gate (OE1384). The reflective sort names are not reserved: a USER-declared type named TypeRef (or Metatype / TraitRef) types ENTITY positions, decided by resolved identity, so this refusal never fires against a position typed by such a user declaration. Fix: bind a declared type name (or a TypeRef-valued expression) in that position, or declare the position with the entity sort it should hold. |
| OE1386 | Error | Live | RelationSelectorDomainUnproven | A variable is applied like a relation (r(p, c)) but nothing pins which relations it can stand for, so the compiler cannot list the relations to dispatch to. It refuses rather than guess a set or search every relation at runtime. Constrain the variable to a compile-time-known finite set of relations: equality with a relation name (r == Home), the specializes(r, Family) subsumption family, or a rel/arm/meta reflection atom — each proves the complete candidate set. |
| OE1387 | Error | Live | RelationSelectorNonRelation | A variable applied like a relation (r(p, c)) is constrained to a value that is provably not a relation (an Int, String, struct value, …), or its constraints combine relation and non-relation values. Constrain the variable to a relation (r == Home, specializes(r, Family)) or drop the constraint pinning it to a non-relation. |
| OE1388 | Error | Live | RelationApplicationArityMismatch | A variable applied like a relation is given a number of arguments that no relation it can stand for accepts — either the relations agree on an argument count that differs from the call, or they disagree among themselves. Apply the variable with a number of arguments every candidate relation accepts. |
| OE1389 | Error | Live | RelationApplicationEndpointMismatch | An argument passed to a variable applied like a relation has a type that cannot match the type declared for that position in at least one relation the variable can stand for (e.g. a text string where the declared type is Int). Pass a value whose type matches the declared position type. Only provable conflicts of primitive and category are refused; an unresolved nominal type that could still be classified compatibly is admitted. |
| OE1390 | Error | Live | RelationApplicationInaccessible | A variable applied like a relation can stand for one or more relations that are private to another module and not visible at the application site. The candidate set is built completely before visibility is checked, so silently dropping the private members would change which relations the rule reads. The refusal names the private relations. Make them public where they are declared, or apply the variable only inside the module that declares them. |
| OE1391 | Error | Live | RelationApplicationUnsafeNegation | A negated application of a relation-valued variable (not r(p, c)) is unsafe: negation here means “not derivable”, which is only well-defined over values the rule has already enumerated. Every argument and the variable itself must first be bound by a positive (non-negated) condition. Add a positive condition binding the variable and each argument before the not. |
| OE1392 | Error | Live | CallableTemplateUnavailable | A rule calls a function whose stored expandable body is missing or explicitly unavailable, so the call cannot be inlined (this happens when the function comes from a compiled artifact too old to carry the body, or the body was recorded as unavailable). The refusal names the function and the recorded reason. Recompile the module that declares the function with the current compiler, or give the function a usable body. |
| OE1393 | Error | Live | RelationApplicationMetadataUnavailable | A variable applied like a relation can stand for a relation whose compiled artifact is too old to carry the visibility and parameter-type information this needs. The refusal names the relation rather than guessing that information (a wrong guess would misevaluate). Recompile the module that declares the named relation with the current compiler. |
| OE1394 | Error | Live | MembershipReadInRecursion | A rule reads a membership through the reflection surface — a variable type position (iof(x, t)) or the metatype read (meta(x)) — from inside the recursive cycle that decides that membership: the rule’s head shares a strongly-connected component with a concept relation whose extent feeds those reflection relations. The reflection relations are extensional and are re-materialized once, at a stratum boundary above every rule that reads them; that boundary is what makes the read a settled two-valued fact set and keeps negation over it stratification-safe. Inside the cycle there is no such boundary — the read is part of the fixpoint that decides the answer — so the membership is not yet settled when the read evaluates and any answer served would be arbitrary. Serving it from the pre-rebuild snapshot answers empty for a rule-decided concept, and its negation admits every individual, with no diagnostic; that silent-wrong shape is why this is refused instead. The message names the offending rule(s) and the concept relation(s) they cycle with. Fix: ask the concept directly with a constant type position (C(x)), which lowers to the concept atom and evaluates inside the fixpoint, so it may recurse freely; or move the reflection read out of the cycle — derive the cycle’s conclusion first, then read the reflection surface in a rule that does not feed it back. |
| OE1395 | Error | Live | NegatedUndefinedMembershipRead | A rule negates a membership read that is served from the extensional reflection relations — a bound type position (iof(x, t)) or the metatype read (meta(x)) — while the membership it asks about is UNDEFINED under the well-founded semantics. A concept whose members are decided by a rule inherits that rule’s truth value, so when the deciding rule’s own definition loops through the negation of that membership, the individuals concerned are neither definitely members nor definitely non-members. The reflection relations carry that third value in a companion relation beside each of them, and a positive reader propagates it, but a negation reads only the definitely-true rows: it would report an undefined membership as a settled non-membership and over-assert its head. The message names the negating rule(s) and the concept(s) whose membership did not settle. Whether a membership settles is a property of the model rather than of the rule text, so this is decided over the converged model, and only the heads whose rows the negation manufactured are refused — the positive read of the same membership in the same program still answers. Fix: break the negative loop in the rule that decides the concept’s members so the membership settles, or drop the negation and read the membership positively, which propagates the third value instead of collapsing it. |
| OW1342 | Warning | Live | RelationMinCardinalityDeferred | A relation endpoint declares a non-zero MINIMUM cardinality (pub rel Married(a: Person, b: Person) [1..1] — every person must be married) that the closed-world write/check channel cannot soundly enforce in v0. Under CWA a build-time refusal would wrongly reject staged construction (relate the entity in a later mutation), and the field-access-time / evaluation-channel emitter that would actively check it is designed but not built in v0 (the same disposition as OE0207’s staged-construction note). The bracket is NOT silently accepted-as-enforced: it is parsed into a structured cardinality, PERSISTED on the wire (RelationDeclBody.cardinalities), the max-cap half IS enforced at the write path (OE1341), and this WARNING is emitted at ox check / ox build (by oxc-driver) naming the relation + position so the modeler knows the minimum is recorded-not-enforced. Full min-card-under-OWA enforcement is a recorded follow-on. |
| OW1369 | Warning | Live | EmitCheckBuildTimeNotDelivered | An => emit Sink { … } check FIRED over the package’s declared facts at ox check / ox build, but its egress is delivered only at the RUNTIME mutation boundary — driven by the violation delta (a newly-violating tuple emits Assert, a resolved one Retract), which has no build-time analogue (there is no receipt at build). So the build-time firing produces no emission; this WARNING surfaces it (naming the check and sink) rather than dropping it silently, the same no-silent-drop discipline a diagnostic check follows. It is informational — the check is sound, the emission simply belongs to the write path. To see the egress, exercise the check through a mutation that changes its violation set. |
| OW1382 | Warning | Live | InstantiationTargetUnproven | A : T instantiation clause targets a declared concept the package shows NO individual-tier usage of (pub kind Dog : Animal with no facts, direct membership reads, or writes over Animal anywhere). The clause declares type×type iof-instantiation — the declaring type becomes an INSTANCE of T, one order above — and does NOT subtype: if Dog : Animal was meant as specialization, Dog’s individuals will silently never appear in Animal’s extent, and a whole taxonomy authored this way flattens without a single error. With no usage evidence the build cannot PROVE the mistake (the same shape is the legit higher-order form whose target extent is read at the type tier), so this is a WARNING, not a refusal: check whether specialization (<: T) was meant. A target the package does use at the individual tier upgrades to the OE1381 refusal (provable tier-mixing). An iof-headed body read is evidence in NEITHER direction: the reflective call iof(a, T), the membership operator x in T, and for x in extent(T) all lower to one identical IR shape, so the build cannot tell a type-tier catalog read from an individual-tier membership read — a PROVING type-tier evidence channel needs the tier tagged at elaboration and is a separate, pending surface. The warning is level-controllable as the lint instantiation-target-unproven (#[allow] on the declaring concept, [lints] in ox.toml, -A/-W/-D, --quiet). |
Individual lifecycle and write-path stability gates (14xx)
Continuation of the runtime write-path gates past the exhausted 13xx range: the individual-retraction cascade’s transactional invariants and relation-end stability refusals allocate here.
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1400 | Error | Live | RetractedIndividualReintroduced | A mutation body re-introduces an individual after a RetractIndividuals operation in the same transaction retracted it — a later re-classification, relation-tuple endpoint, or property write names the retracted identity, so the committed state would hold live events introducing or referencing an individual the same transaction’s retraction gates treated as ended. The cascade released those bindings on the premise the individual was gone; re-introducing it in the same commit falsifies that premise after the fact, so the whole transaction refuses. Split the resurrection into its own transaction (where the immutable-fiber freeze gates it as a replacement assertion), or do not retract the individual. |
| OE1401 | Error | Live | RelationTargetUpdate | An update target: R set { … } annotates its target with a declared relation type. Relation ends are not assignable fields: a relation assertion is an immutable proposition whose endpoint values are never updated in place, and mut on an end authorizes value variation through tuple retract/assert history only. Replace the assignment with the tuple lifecycle — delete R(…) then insert R(…) — which the relation-end mutability gates validate; fires at ox check / ox build on the mutate body, and again at dispatch for a target only the runtime can classify. |
| OE1402 | Error | Live | ImmutableRelationEndRetraction | A delete R(…) names a live relation tuple with at least one immutable end. Relation ends are lifetime-bound unless declared mut: deleting the tuple would shrink an immutable end’s frozen value set while its dependent context (the individuals at every other position) still survives. Direct tuple deletion is admitted only when every end is mut. To remove an immutable-ended binding, retract its dependent-context individual — the incident tuple then retracts by cascade — or mark the end mut at its declaration if the binding is meant to vary. |
| OE1403 | Error | Live | ImmutableRelationEndInsertion | An insert R(…) introduces a NEW value at an immutable end whose fiber (its value set under the fixed complement of the other endpoints) is already initialized. An immutable end’s value set is frozen atomically by the first transaction that asserts the fiber and cannot grow while its dependent context survives; re-asserting an already-active value stays idempotent. Assert every value for the fiber in its initializing transaction, or mark the end mut at its declaration if the value set is meant to vary through assertion history. |
| OE1404 | Error | Live | ImmutableRelationEndStrandedContext | A RetractIndividuals (or capability-gated forget) would leave an immutable-ended relation binding whose dependent context still survives. For every tuple incident to a retracted individual and every immutable end, at least one individual in that end’s dependent context (the other endpoints) must be in the same retraction set; the check is conjunctive across all immutable ends. Retracting brain cannot leave its dependent person alive, and retracting one spouse cannot leave its marriage alive. Add the stranded dependent-context individuals to the retraction so the incident bindings retract by cascade, or mark the relevant ends mut. An incident shape whose end-mutability cannot be decided is refused here rather than admitted. |
| OE1405 | Error | Live | AmendWithoutCapability | A mutate body uses amend but the enclosing mutate declaration does not grant #[allow_amend]. amend corrects the belief-time record: it marks a recorded assertion false ab initio (retaining the audit history) and, in the composite amend R(old) => R(new) form, asserts the corrected tuple. Because it rewrites what was believed, the capability must be granted explicitly — annotate the mutate declaration with #[allow_amend], mirroring forget and #[allow_forget]. An ad-hoc body submitted at runtime is never granted the capability. |
| OE1406 | Error | Live | AmendmentTargetNotAsserted | An amend R(…) named a tuple that is live but has no directly-asserted or refuted event to correct — a purely rule-derived conclusion. amend supersedes a recorded belief-time assertion; a conclusion computed from premises has none. You amend premises, not conclusions: correct the asserted premises the tuple was derived from and let dependency maintenance recompute the conclusion. Amending the conclusion directly is refused rather than accepted as a no-op that recomputation would immediately undo. |
| OE1407 | Error | Live | AmendmentBelowMinimum | Withdrawing the amended assertion would leave an already-initialized immutable relation-end fiber non-empty but below its declared minimum cardinality. amend corrected an established fiber (one an earlier transaction froze), and the surviving corrected value set is smaller than the end’s declared lower bound. This is distinct from OE1398, which refuses an under-filled first initialization. The minimum is checked on the post-withdrawal fiber, before any corrected re-assert, so the composite amend R(old) => R(new) form refuses here too: the re-assert does not count toward the minimum this gate enforces. The remedies are to withdraw the whole fiber to empty (which is uninitialized, not under-filled), or to not amend below the declared minimum. |
| OE1408 | Error | Live | RetractTargetNotIndividual | A retract target must denote an individual — an entity with a retractable identity lifetime. The target given resolves to a collection, a type/concept name, or another non-individual value, which carries no individual identity to cease. retract x; ceases one individual and retract {a, b}; a set of them; to retract every individual of a collection iterate it (for x in xs { retract x; }), and to remove a derived or relational fact use delete. Give a target that denotes an individual value, or switch to the effect that matches the shape you have. |
| OE1409 | Error | Live | AmendCorrectionRelationMismatch | A composite amend R(old) => S(new) names a different relation in its correction than in its withdrawal. amend corrects a mis-recorded fact of a relation, withdrawing one tuple and re-asserting a corrected tuple of the SAME relation; it never moves a fact between relations. To change facts across two relations, delete one and insert the other. Give the withdrawal and its correction the same relation. |
| OE1410 | Error | Live | MappedSubsumptionNotYetEvaluable | A relation subsumption clause carries a mapping — a renamed or reordered end, a constant pin, or a subsort cast — against a parent whose declared ends are not visible in this file (a cross-module parent relation). Non-identity mappings are evaluated once the parent’s positions are known, but this build cannot resolve the fillers to the cross-module parent’s positions, so it cannot place them under the mapping and refuses rather than recording an under-resolved edge. Declare the parent in this module, or write the clause as a bare identity edge (<: Parent) whose ends need no translation. |
| OE1411 | Error | Live | MappedSubsumptionUnknownParentEnd | A relation subsumption filler is keyed by the parent end it fills (parentEnd = childEnd), but the named parent end is not one the parent relation declares. The mapping cannot be placed at any parent position, so the declaration is refused. Check the parent end name against the parent relation’s declared ends; every filler’s left-hand name must be a declared parent end. |
| OE1412 | Error | Live | MappedSubsumptionParentEndCoverage | A relation subsumption mapping must cover each parent end exactly once. This mapping either fills one parent end with two fillers or leaves a parent end with none, so it is not a total mapping onto the parent and is refused. Write exactly one parentEnd = childEnd filler for every declared parent end. |
| OE1413 | Error | Live | MappedSubsumptionNegativeDualUnsupported | A program declares a relation subsumption edge with a non-identity mapping (a renamed or reordered end, a constant pin, or a subsort cast) AND asserts a refutation (a negative fact) over the parent family, so the read path would have to flow that refutation DOWN from the parent to the mapped child. That downward direction needs the inverse of the mapping, which is not well defined: a constant pin collapses many child tuples onto one parent tuple, so a refuted parent tuple does not name a unique child tuple to refute. The forward direction (a child tuple contributes its mapped image to the parent, and withdrawing the child withdraws the image) is fully supported; only the negative downward dual through a non-identity mapping is refused, pending a decision on its semantics. Use an identity mapping on the refuted edge, or refute the child relation directly rather than the parent family. |
| OE1414 | Error | Live | FamilyQueryMemberFrameArity | A generalized specializes(k(memberArgs), Parent(parentArgs)) family query applies the member selector k in its own frame with a number of arguments that does not match the declared arity of a relation k can stand for. For an identity family the member frame and the parent frame have the same width; write the member frame with one argument per declared end of the member relations. Change k(memberArgs) to carry the arity every candidate member accepts, or use the Parent(args) via k sugar, which supplies the member frame for you. |
| OE1415 | Error | Live | FamilyQuerySelectorNotVariable | The member position of a generalized specializes(k(memberArgs), Parent(parentArgs)) family query must be a plain relation-valued variable applied to its own arguments — the witness k the query binds to the answering member. It was written as something else (a wildcard _, a literal, or a relation name in the member slot), so there is no variable to bind the answering member to. Name a fresh variable in the member slot — specializes(k(a, b), Parent(a, b)) — or drop to the Parent(args) via k sugar, which introduces the witness k for you. |
| OE1416 | Error | Live | FamilyQueryMappedMemberUnsupported | A generalized specializes(k(memberArgs), Parent(parentArgs)) family query — the form with an explicit member frame — reached a family member whose subsumption edge is an argument-mapped, non-identity edge (an end pinned to a constant, renamed, reordered, or cast). The explicit member frame is tied positionally to the member’s own ends, so a member whose mapping does not place its ends one-for-one onto the leading parent positions cannot be framed that way. The via sugar — Parent(args) via k — handles every member, mapped included: it presents the parent-frame row through the member’s mapping and binds k to the answering member. Use via for a mapped family, or query an identity family with the generalized form. |
| OE1417 | Error | Live | FamilyQueryNestedFrameTiesUnsupported | A generalized specializes(k(memberArgs), Parent(parentArgs)) family query (or its Parent(args) via k sugar) whose frame carries a filter — a parent-frame constant pin, or a member-frame rename, reorder, or pin — appears nested under negation or a modal / temporal operator. Such a query lowers to a dispatch-helper application plus one filter atom per tied column. A negation over that conjunction would distribute (De Morgan) into not helper and not filter… rather than the intended not (helper and filter…), and a modal / temporal operand holds only a single atom, so the filters would be dropped — either way the query would silently under- or mis-evaluate. The family-query surface therefore supports a pinned / renamed / reordered frame only as a top-level positive atom for now. Lift the family query to the rule-body top level, binding each column to a variable, then apply the negation or modal / temporal operator to those variables; or query an all-variable frame, which carries no filters and nests freely. The nested cases activate once the expansion can carry its filters into a negated or modal context. |
| OE1418 | Error | Live | FamilyQueryParentFrameArity | A generalized specializes(k(memberArgs), Parent(parentArgs)) family query (or its Parent(args) via k sugar) writes the parent frame Parent(…) with a number of arguments that does not match the declared arity of the parent relation. This is a fault in the parent position, distinct from a member-selector arity fault: the remedy is to match the parent relation’s declared ends, not to rewrite a member selector. Write the parent frame with one argument per declared end of the parent relation. |
| OE1419 | Error | Live | FamilyQuerySpecializesViaCombined | A via k clause trails an explicit specializes(...) (or iof(...)) reflection atom. The Parent(args) via k sugar is itself a specializes family query, so the two spellings cannot be combined. Before this refusal the via clause was silently dropped and the bare / explicit form — without the witness — was evaluated instead, a different query than written. Write either the explicit form specializes(k(args), Parent(args)) with a member frame, or the sugar Parent(args) via k, which supplies the member frame for you — not both. |
| OE1420 | Error | Live | FamilyQueryParentUnresolved | The parent position of a specializes family query (or its Parent(args) via k sugar) does not resolve to a single declared relation — an unknown name, or an ambiguous short name matching more than one accessible relation, which the resolver fails closed on. This is a fault in the parent position, distinct from the member-witness refusal: the remedy names the parent relation. Name the parent with a qualified path, or import it so the short name resolves uniquely. |
| OE1421 | Error | Live | MappedSubsumptionUnmappedChildEnd | A relation subsumption clause maps its child onto a parent but references only some of the child’s declared ends — at least one child end is named by no filler. Dropping a child end inside a subsumption edge is forbidden: two child tuples that differ only at the dropped end would merge into one parent row (the set-merge consequence — a relation’s extent is a set of true propositions, so facts agreeing on every kept end become one row and any aggregate over the parent counts them once), with no declared meaning for the collapse, and the withdrawal cascade relies on the child identity a dropped end would erase. Fix, in fixed order: (1) if the child genuinely belongs to the family, reference every declared child end in the filler list (a rename or a cast source; a child end may be referenced by more than one parent end — a diagonal — but never by none); (2) if a lossy projection is intended, express it as a separate derive rule — a projection view is not family membership. A dedicated explicit projection-marker on a subsumption edge is a reserved design under consideration (an open question in the mapped-relation-subsumption design record) and is not yet a surface form; until then, use a derive rule. |
| OE1422 | Error | Live | MappedSubsumptionPinSortMismatch | A relation subsumption clause pins a parent end to a constant individual whose sort does not inhabit that parent end’s declared sort. A constant pin freezes the named individual into the parent position for every mapped row, so the individual must be of the parent end’s sort — the same sort (from its concept-membership fact, e.g. pub fact Currency(USD)) or a subsort of it. An individual of an unrelated sort (a Person pinned into a Currency end) would produce parent rows that violate the parent relation’s own endpoint declaration, and is refused. The gate is fail-open: it fires only when the pinned individual’s sort is known in scope and is provably neither the parent end’s sort nor a subsort — a pin whose sort is not visible here is never falsely rejected. Fix: pin an individual of the parent end’s declared sort (assert its membership, pub fact <ParentSort>(<individual>)), or, if the pinned individual is correct, widen the parent end’s declared sort at the parent relation. |
| OE1423 | Error | Live | MappedSubsumptionCastSortMismatch | A relation subsumption clause casts a child end onto a parent end at a sort that does not match the parent relation’s declaration. A subsort cast (parentEnd = childEnd as Sort) presents the child end’s narrower-sorted values at the parent end, and is value-identity at runtime, so it is sound only when it names the declared sorts: the cast TARGET sort must equal the parent end’s declared sort, and the CHILD end’s declared sort must be a subsort of (or equal to) it. Casting to a third sort would frame the parent row at a sort the parent relation does not hold; a child end whose sort is unrelated to the parent’s is a type error, not a widening cast. Fix: cast to the parent end’s declared sort exactly, and ensure the child end’s sort is a subsort of it (declare the subsumption with specializes / <:); if the two sorts already agree, drop the cast and use a plain rename. Fail-open: the gate fires only when both sorts are known in scope. |
| OE1424 | Error | Live | MappedSubsumptionDivergentDiamond | A relation reaches the same ancestor through two subsumption paths whose composed argument mappings onto that ancestor DIFFER (a divergent-map diamond). Because the two compositions disagree, one child fact would translate to two different ancestor rows, and a family query over the ancestor follows only the first path in the subsumption-image closure — so it would silently report one framing and drop the other. Silent under-report is the worst failure shape, so the diamond is refused at declaration. A diamond whose paths compose to the SAME mapping is harmless and stays legal. The message names the child, the ancestor, and both paths with their composed mappings spelled out as parent-name-keyed fillers. Fix: reconcile the fillers so both paths compose to the same mapping onto the ancestor, or remove one subsumption edge. If both images are genuinely wanted — the child should occupy both ancestor rows — that is a feature request; the mapped-relation-subsumption design record keeps both-images support open as future work. |
| OE1425 | Error | Live | MutateEndpointValueInConceptSlot | A mutate body’s insert R(...) places a literal value in a relation endpoint whose declared type is a concept. A value can never inhabit a concept’s extent — an individual and a value are different sorts — so the tuple is provably wrong independently of any world assumption or of what is classified at run time. The same tuple written as a ground pub fact R(...) was already refused for exactly this reason; accepting it from a mutate body committed a tuple whose endpoint contradicted the relation’s own signature, and queries then served the raw value in a slot declared to hold an individual. The message names the relation, the endpoint position and its declared concept, and the offending literal. Fix: pass an individual of the declared concept in that position, or change the relation’s declared endpoint type to the primitive type the value actually has. Refused before the artifact is written, so no .oxbin carries the bad tuple; the whole mutate body is rejected, not silently narrowed. |
| OE1426 | Error | Live | MutateEndpointDisjointConcept | A mutate body’s insert R(...) places a value whose statically declared concept is disjoint from the endpoint’s declared concept, where the disjointness is asserted by a disjoint { ... } or partition ... { ... } group (lifted through subtyping: a subtype of a disjoint member is disjoint from the other member). Because the group declares the two concepts pairwise non-overlapping, no individual can ever be classified under both, so the tuple can never be satisfied in any reachable state — this is a schema-level contradiction, not a fact about the current state. Two concepts with NO declared disjointness are left accepted: multiple classification is legal, an individual under one may later also be classified under the other, and refusing that would be unsound in the other direction. The message names the relation, the endpoint position and its declared concept, the offending value’s declared concept, and the group that makes them disjoint. Fix: pass a value of the endpoint’s declared concept, widen the endpoint’s declared type to a common supertype outside the disjoint group, or drop the disjointness declaration if the two concepts were never meant to be exclusive. |
| OE1427 | Error | Live | RelationEndpointValueInConceptSlot | A write places a plain value in a relation endpoint whose declared type is a concept. An individual and a value are different sorts, so the value can never inhabit the concept’s extent and the tuple is provably wrong regardless of the concept’s world assumption or of anything classified at run time. This is distinct from the refusal that fires when an endpoint individual exists but is classified under nothing: that one turns on the world assumption, whereas this one needs no assumption at all — nothing can make a number an individual. The message names the relation, the endpoint position and its declared concept, and the offending value. Fix: write an individual of the declared concept in that position, or declare the endpoint with the primitive type the value actually has. Raised before the tuple is buffered, so the enclosing mutation body commits nothing. |
| OE1428 | Error | Live | RelationEndpointDisjointConcept | A write places an individual in a relation endpoint whose declared concept is disjoint from a concept the individual is already classified under, where the disjointness is asserted by a disjoint { ... } or partition ... { ... } group (lifted through subtyping on both sides: a subtype of a disjoint member is disjoint from the other member). The group declares its members pairwise non-overlapping, so classifying the individual under the endpoint’s concept would itself be refused as a group violation — the tuple can therefore never be satisfied in any reachable state. This does not depend on the endpoint concept’s world assumption: an open world permits unknown membership, but not membership that a declaration forbids. When the individual is classified only under concepts with NO declared disjointness from the endpoint’s concept, the write is ACCEPTED and this diagnostic does not fire — multiple classification is legal and the individual may later also be classified under the endpoint’s concept. The message names the relation, the endpoint position and its declared concept, the individual, and the conflicting concept it is classified under. Fix: classify the individual under the endpoint’s declared concept (which requires it not to be in a disjoint group with its current classification), pass a different individual, or reconsider the disjointness declaration. Raised before the tuple is buffered, so the enclosing mutation body commits nothing. |
| OE1429 | Error | Live | RelationSubsumptionUnknownParent | A relation declares <: Parent, but Parent resolves to no declared relation: it is not declared in the same file, and it does not name exactly one workspace-visible pub relation (by unique short name, or by unique exact/suffix match for a qualified path). Accepting the clause would persist a subsumption edge against a relation that does not exist, so the declared flow-up could never deliver a tuple anywhere — the declaration would type-check and change nothing, silently. The message names the child relation and the unresolved parent path. Fix: declare the parent relation, make the reference unambiguous (qualify the path), or remove the <: clause. An ambiguous short name — two visible relations sharing it — also lands here after the unique-resolution step fails; qualify the path to pick one. |
| OE1430 | Error | Live | InconclusiveMembershipBlocked | A blocking (Severity::Error) check cannot be discharged for a subject the write introduces: its firing verdict depends on membership in a computed concept that the well-founded semantics leaves UNDETERMINED at the write point (a Can verdict — a negation/defeat standoff, or an open-world absence of evidence), so the check is neither definitely violated nor definitely satisfied. Admitting the write would commit data whose governing constraint could not be decided; firing the check would be a false accusation of a violation that is only possible, not established. Neither is honest, so the write is refused (fail-closed). The message names the check, the subject, and the undetermined membership the verdict depends on. Remedies, in order: make the membership decidable — establish the classifying evidence for the subject, or break the negation/defeat standoff among the classifying rules so the concept settles to a definite verdict; OR, if the check should not block on an undetermined verdict, lower its severity to Warning/Info (or mark it #[observe]) so it reports at undefined grade instead of blocking. |
| OE1431 | Error | Live | DeterminednessTestOutsideCheckPlane | The determinedness test undetermined(...) / determined(...) reads whether a membership atom’s verdict is left UNDEFINED (Can) by the well-founded semantics. That read is sound only in a check body, which runs AFTER the fixpoint has converged and contributes nothing back to it — a post-fixpoint consumer reading a settled value. Used in a derive/query/bridge body the same read is truth-value reflection feeding the fixpoint: a rule that contributes to the model would let its own firing depend on the in-progress undefinedness of the model it is helping to compute, a non-monotone self-reference the well-founded semantics has no fixpoint for. So the operator is refused outside the check plane. The refusal also fires when the argument is not a membership atom of the check body (the operator guards the check’s own membership atoms, not an arbitrary atom). Remedies: move the determinedness test into a check body (the only plane where it is meaningful); or, if a derive must react to a standoff, restructure so the classifying rules settle to a definite verdict rather than reading their own undefinedness. |
| OE1432 | Error | Live | RuleAtomArgSortMismatch | An entity-typed argument position in a derive / check / bridge rule-body atom (or a bridge head atom) is given a logic variable whose statically DECLARED sort is <:-incomparable with the predicate’s declared parameter concept — neither a subtype nor a supertype of it on the declared <: lattice. A variable’s declared sorts are the ones the program itself asserts: its head-parameter annotation, any body sort guard (x : T), and any unary concept-membership atom (T(x)); types a variable merely picks up by occupying other predicates’ argument slots are classification evidence, not declarations, and never trigger this refusal. A subtype in a supertype slot passes (ordinary subsumption); a supertype in a subtype slot passes (the atom is a membership join, so it narrows — the same direction an x : T occurrence-typing test narrows); only the <:-incomparable pair is refused. A predicate declared in several clauses — one per admissible sort — licenses an argument comparable with ANY clause’s parameter concept at that position, and a clause leaving the position unannotated admits anything there. This is deliberately NOT a runtime-disjointness claim: Argon individuals are multiply classified, and two <:-incomparable concepts can share instances — the refusal is the static-sort discipline of the declared signatures (the OE0665 precedent), because an atom whose declared sorts are incomparable can only succeed through a multi-classification the program never states, and in practice it compiles to a silently-empty join surfacing as mass downstream wrong answers with no pointer to the ill-typed atom. To state the multi-classification deliberately, assert the second sort in the body (x : T, or the membership atom T(x)) — that assertion licenses the atom. The message names the variable, its declared sort(s), the predicate, and the parameter’s declared concept(s), at the argument’s span. |
| OE1433 | Error | Live | AggregateProjectionNotNumeric | A value aggregate (sum / avg / min / max) folds a projection whose type is statically known to be non-numeric — a temporal value (Date / DateTime / Time / Duration / Instant), a String, a Bool, or an entity/structured value — where the fold’s domain is the exact numeric tower (Int / Real / Decimal / Money / Nat). All four value folds are numeric in both evaluation planes: the rule-plane fold lifts every projected value into the exact-rational domain, and the fn/compute-plane min / max / sum / avg do the same — min / max do NOT order non-numeric values (an ordered comparison over them is its own gate), so no orderable-but-non-numeric projection is admissible either. Refused at ox check / ox build when the projection’s declared type proves the mismatch (naming the projection expression, its type, and the aggregate’s numeric domain); the same code is raised loudly by the runtime fold if a non-numeric value reaches it dynamically, in place of the historic internals-leaking internal reasoning engine error: … got Date(<epoch days>). Evidence-gated: a projection whose type is unknown or generic is left to the runtime gate. Project a numeric field or expression into the fold; to count rows or fold something else, use count, or model the value numerically. |
| OE1434 | Error | Live | MutateFieldValueDisjointConcept | A mutate body writes into a concept-typed field — an insert T { f: v } field initializer or an update ... set { f = v } assignment — a value whose statically declared concept is disjoint from the field’s declared concept, where the disjointness is asserted by a disjoint { ... } or partition ... { ... } group (lifted through subtyping: a subtype of a disjoint member is disjoint from the other member). Because the group declares the two concepts pairwise non-overlapping, no individual can ever be classified under both, so the value can never satisfy the field’s declared type in this state or any later one — this is a schema-level contradiction, not a fact about the current state. A concept-typed field is a typed slot exactly as a relation endpoint is, and this is the field-side counterpart of the endpoint refusal; the two write planes do not hold private rules for the same question. Two concepts with NO declared disjointness are left accepted: multiple classification is legal, an individual under one may later also be classified under the other, and refusing that would be unsound in the other direction. A value that already satisfies the field’s own concept (under that concept or a subtype of it) is likewise accepted — the field’s demand is met, and an overlap the value also carries with a declared-disjoint sibling is the group axiom’s own overlap refusal to make against the classification declarations, not this write’s to make against the field. The gate is per-file and abstains rather than refusing where it cannot establish agreement with the runtime write gate: a field whose declared concept type is not declared in this file, a value term whose sort is not statically in hand (a projection, a call result, a rebound binder), and an individual whose complete classification set this file cannot see all defer to the runtime gate, which reads the value’s real classification and is the total authority there. The message names the field and its declared concept, the offending value and its declared concept, and the group that makes the two disjoint. Fix: write an individual of the field’s declared concept, widen the field’s declared type to a common supertype outside the disjoint group, or drop the disjointness declaration if the two concepts were never meant to be exclusive. |
| OE1435 | Error | Live | FieldValueDisjointConcept | A write places an individual in a concept-typed field whose declared concept is disjoint from a concept the individual is already classified under, where the disjointness is asserted by a disjoint { ... } or partition ... { ... } group (lifted through subtyping on both sides: a subtype of a disjoint member is disjoint from the other member). The group declares its members pairwise non-overlapping, so classifying the individual under the field’s declared concept would itself be refused as a group violation — the field value can therefore never satisfy the declared type in any reachable state. A concept-typed field is a typed slot exactly as a relation endpoint is, and this is the field-side counterpart of the endpoint refusal; the two write planes do not hold private rules for the same question. This does not depend on the field concept’s world assumption: an open world permits unknown membership, but not membership that a declaration forbids. When the individual is classified only under concepts with NO declared disjointness from the field’s concept, the write is ACCEPTED and this diagnostic does not fire — multiple classification is legal and the individual may later also be classified under the field’s concept. An individual that already satisfies the field’s own concept (under that concept, a subtype of it, or a rule-derived membership of it) is likewise accepted: the field’s demand is met, and an overlap it also carries with a declared-disjoint sibling is what the group axiom’s own overlap check exists to refuse, against the classification that created it rather than against this write. Classification is read against the write’s own read-your-writes view, so classifying the individual earlier in the same mutation body satisfies the gate. The message names the field and its declared concept, the individual, the conflicting concept it is classified under, and the declared group members that prove the conflict. Fix: classify the individual under the field’s declared concept (which requires it not to be in a disjoint group with its current classification), write a different individual, or reconsider the disjointness declaration. Raised before the buffered write is applied, so the enclosing mutation body commits nothing. |
| OE1441 | Error | Live | RuleUnknownTruthQualifierInvalid | The executable truth qualifier is exactly R(arguments) is unknown: after R’s well-founded component converges, it reads R’s $undefined::R companion, containing tuples that are neither definitely true nor definitely false. The stratifier gives that read a strict dependency on R, so the consumer runs later. OE1441 refuses two classes that cannot use this meaning safely. First, is not unknown, is unknown(payload), and path-headed x is unknown do not select one predicate companion; silently treating them as an ordinary atom would return definite rows and answer a different question. Rewrite as an unnegated, payload-free predicate call; complements require an explicit finite domain and separate rules. Second, a consumer that feeds back into R’s own recursive component would inspect $undefined::R before R converges. Move that consumer to a separate higher-stratum derive that does not feed back into R. Concrete rewrite: remove any dependency from Voter or its recursive peers back to UnknownVoter; let that component finish, then add pub derive UnknownVoter(p) :- Person(p), Voter(p) is unknown;. ox derive target/root.oxbin Voter prints the component’s unknown tuples as Can rows; the new head turns those tuples into definite UnknownVoter rows. The multi-valued outcomes is both, is ambiguous, and is timeout remain the separate OE1349 feature. |
| OE1442 | Error | Live | ShapeClauseMemberNotEntitySorted | A concept whose membership is decided by a shape clause (pub type C iff Shape(self, …)) would classify VALUES, not entities: head parameter 0 of the shape derive — the parameter that receives each classified member — is value-sorted (a primitive such as Int or String, or a struct/enum-declared type). Concept membership ranges over entities: iof(x, T) takes an entity in its subject position and a concept’s extent is a set of entities, so a value can never be a member of a concept, and asking for the metatype or the extent of language-level data is a category error. Left to compile, the concept’s extent would contain plain values, the membership read would answer true for them, and the whole-domain reflection read would return them beside real individuals. Fix: give the shape derive an entity-typed member column — annotate parameter 0 with the concept whose individuals are being classified, and bind it in the derive body to a position that holds that individual (typically a membership atom T(m) or an entity end of a relation) — or, when the thing being classified really is a value, model it as a FIELD of an entity and classify the entity instead. Declaring the concept’s supertype (pub type C <: P iff Shape(self);) also bounds the member column, since the synthesized membership rule then conjoins membership in P. |
Cross-level instantiation bodies (19xx)
Field-coverage gates for cross-level instantiation bodies (§5.2). The MLT reasoning codes (categorization / partition / order / subordination / powertype) that once sat here are gone — MLT is an unprivileged package (packages/mlt), so those constraints are authored as the package’s own rules/checks over the neutral substrate, not as reserved compiler codes.
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1901 | Error | Reserved | InstantiationFieldNotInTarget | A cross-level instantiation body assigns = to a field absent from the target metatype (§5.2). |
| OE1902 | Error | Reserved | InstantiationFieldShadows | A cross-level instantiation body declares a : field that shadows an inherited field (§5.2). |
| OE1908 | Error | Reserved | IntrinsicPropertyMissing | A cross-level instantiation body fails to bind a #[intrinsic] field required by the target metatype (§8). |
Collection operators (24xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE2403 | Error | Live | NonIterableForSource | A for x in EXPR { … } loop in a mutate / fn body iterates a source whose type is provably not iterable — a scalar (Int, String, Date, …), a concept / struct, a tuple, or a function value. A for body ranges over a COLLECTION ([T] / List<T> / Set<T>) or an OPTIONAL (T? — the zero-or-one “insert-if-present” idiom: an absent optional runs the body zero times, a present one exactly once). Before this gate the checker inferred the iterand’s type but never constrained it, so for x in <scalar Int> passed ox check and then either died at runtime with `for` iterable must be a collection or — once a present optional is admitted as a one-element loop — silently iterated a genuine scalar exactly once. Refused at ox check / ox build instead, the same fail-closed discipline as OE0230 (insert … into a non-collection field) and aligning the Rust checker with the Lean forLoop semantics, which requires a list-shaped source. An iterand of no statically-known type (Unknown, a generic application such as Set<T>, a bound generic parameter, Top) carries no proof of non-iterability and stays silent — no cascade. Iterate a collection or an optional, or hoist the scalar into a one-element list ([x]). |
| OW2402 | Warning | Reserved | RefineSingletonAsOption | A refinement [T; <=1] is more naturally expressed as T? (Option<T>) (§5.3). |
Explanations
Codes with an extended explanation (also available offline via ox explain <CODE>):
OE0204 — UnknownField
Field access x.f where the receiver’s established type declares no field f
(counting inherited fields and any : T narrowing in scope). In a rule body the
error fires only on provable absence: an Argon individual can be classified
under several concepts, and a field on any established concept is admitted, so
f must be missing from every one of them. A receiver with no established
concept (an unbound variable, or a path to a declared type/enum) is skipped.
Fix: declare the field on the concept, narrow the receiver to a type that has it, or correct the field name.
use std::core::{type, rel};
pub type Person { name: String, age: Nat }
pub derive adult(p) :- p: Person, p.age >= 18; // ok: `age` is on Person
pub derive bad(p) :- p: Person, p.income > 0; // OE0204: no `income` on Person
OE0211 — IofInsertOnDefined
A concept defined with iff is defined, not asserted: its membership is
derived, so you cannot write membership into it manually — the substrate
classifies individuals automatically whenever the definition holds. There are
two defined forms:
- A braced-
iffconcept (iff { … }) derives membership from its refinement predicate over the individual’s fields. - A shape-defined concept (
iff <Shape>(self)) derives membership from a synthesized rule over relations/facts.
(A primitive concept declared with where is the opposite: membership is
asserted, and insert iof is exactly how you assert it.)
insert iof / delete iof into either defined form is refused — membership
is single-source, never asserted. Constructing an instance of a shape-defined
concept (insert C { … }), or of a subtype beneath one, is refused for the
same reason: construction buffers an asserted iof, and a shape concept’s
relational evidence can never be supplied by a construct. A braced-iff
concept is constructible — its predicate evaluates over the constructed
fields, so its extent stays honest.
Fix — write the underlying state the definition reads, and let classification follow:
use std::core::{type, rel};
pub type Adult <: Person iff { self.age >= 18 };
pub mutate seed() {
insert Person { name: "Mia", age: 19 }; // not `insert iof Adult`
// Mia classifies as Adult automatically (age >= 18).
}
For a shape-defined concept, construct the supertype and assert the relations/facts its shape derive reads.
The same applies through <:-ancestors: if any defined ancestor derives the
membership, the write is refused — asserting (or constructing) a descendant
would assert the single-source ancestor.
OE0213 — IofInsertIntoBot
Bot (⊥) is the empty concept — the lattice bottom. By definition it has no
instances: nothing is ever an instance of Bot. So insert iof(x, Bot) asserts
a contradiction (x is a member of a category that, by definition, is empty) and
is refused statically.
This is a sound, unconditional refusal — it follows from Bot’s definition
alone, not from any two-concept disjointness analysis (that stays reserved). The
dual, Top (⊤), is the opposite: every individual is an instance of it, so a
rule body Top(x) ranges over the whole domain.
If you meant to assert membership in a real concept, name that concept:
pub type Person;
pub mutate seed(p: Person) {
insert iof(p, Person); // not `insert iof(p, Bot)`
}
OE0605 — UnknownMetatype
Argon has no built-in ontology vocabulary. A declaration like pub kind Person
reads kind as a classifier, and classifiers are not keywords — the leading
introducer must resolve to a pub metatype that is in scope (§3.4). That is what
keeps the language ontology-neutral: kind, class, rigid_sortal and friends
ship in vocabulary packages (UFO, BFO, …), never in the compiler.
Even the no-commitment baseline type/rel are not ambient: they
live in std::core and must be imported.
Fix — bring the baseline into scope:
use std::core::{type, rel};
pub type Person; // ontology-uncommitted
or commit to a vocabulary you declare or import:
pub metatype kind = { }; // declares the `kind` classifier
pub kind Person; // now legal
To make the baseline available across a whole package, set
prelude = ["std::core::{type, rel}"] under [package] in ox.toml.
OE0668 — RefinementInvariantViolated
A primitive concept declared with where carries a necessary invariant: every
member must satisfy it. This write would place an individual in the concept while
the predicate evaluates to a definite false, so it is refused.
Three-valued by design: only a definite false blocks the write. If the
predicate is unknown (a field the data has not supplied yet), the write is
allowed — absence of information is not a violation.
use std::core::{type, rel};
pub type Adult <: Person where { self.age >= 18 };
pub mutate admit() {
insert Adult { name: "Lee", age: 15 }; // OE0668: 15 >= 18 is definitely false
insert Adult { name: "Ada", age: 21 }; // ok
// age unknown -> allowed (unknown, not false)
}
Fix: write a value that satisfies the invariant, model the looser set with a
weaker where, or — if membership should be derived rather than asserted — use
an iff definition instead.
OE0239 — FactReferencesDerivedPredicate
A predicate in Argon has a single origin: its extent is either EXTENSIONAL
(asserted as ground facts) or INTENSIONAL (computed by derive/query rules) —
never both. pub fact P(...) asserts into an extensional relation, but here P
is a rule head, so the seed fact would key a different relation node than the
rule reads and silently drop out of the fixpoint: no error, wrong answer. Argon
refuses at ox check / ox build rather than emit an artifact that mis-derives.
To give a derived predicate ground tuples, keep it single-origin: seed the head
with a bodiless derive clause, or read a base relation. Note that rule bodies
must be range-restricted — bind each head variable with a positive atom, not a
bare comparison (p.income > 0 alone leaves p unbound):
use std::core::{type, rel};
// a base relation the rule reads (seed it with `pub fact HighEarner(...)`),
// unioned with the rule clauses over the same head:
pub rel HighEarner(mut p: Person);
pub derive owes_tax(p) :- HighEarner(p);
pub derive owes_tax(p) :- p: Person, p.income > 0;
Axiom-ADT variant catalog
The canonical axiom_events table (The single-event-log architecture) carries one of 27 axiom-kind variants. Each variant has a stable string tag (the axiom_kind column value) and a canonical-CBOR body shape (the body BYTEA payload).
The catalog is ontology-neutral. UFO contributes zero variants; UFO-specific meta-properties (rigidity, sortality, identity-provision) are meta_property events whose body declares (axis, target, value). Other vocabulary packages (BFO, DOLCE, MLT) contribute zero variants on the same principle.
The variants mirror Argon’s CoreIR 1:1. The canonical source is the Lean inductive Argon/Storage/AxiomKind.lean (variant set and order) and Argon/Storage/AxiomBody.lean (per-variant body shapes); the Rust mirror is oxc-protocol::storage::AxiomKind, pinned against drift by oxc-protocol/tests/drift.rs. When CoreIR shakes during compiler development, the Lean drives and this catalog follows. For the exact CBOR field shape of any variant, read AxiomBody.lean — it is authoritative; the body shapes are not reproduced here.
The 27 variants
Tag (axiom_kind) | Group | Notes |
|---|---|---|
concept_decl | Declarations (11) | Ontology-neutral; UFO axes via meta_property |
relation_decl | is_iof / is_subsumption flags identify privileged relations | |
struct_decl | ADT-product type | |
enum_decl | ADT-sum type | |
metaxis_decl | A meta-axis exists; vocab packages ship their axes here | |
metatype_decl | Bundles axis-value commitments for a concept-introducer | |
metarel_decl | Structural mirror of metatype_decl, for relations | |
standpoint_decl | Multi-parent DAG; rigidity per node | |
module_decl | Carries the composition signature (Composition signature) | |
trait_decl | Trait surface | |
impl_decl | Conditional impls supported | |
iof_assertion | Assertions (7) | “individual is an instance of concept”; bitemporal-aware |
iof_refutation | Strong (classical) negation of an iof_assertion (RFD 0010); maps to Truth4::Not at federation; same body as iof_assertion | |
relation_tuple | n-ary relation tuple; literal codomains allowed | |
relation_tuple_refutation | Strong-negation counterpart of relation_tuple (RFD 0010); same body as relation_tuple | |
property_assertion | Literal-valued specialization of relation_tuple; declared-symbol carrier | |
individual_property_assertion | Per-instance mut-field set (RFD 0006); per-instance IndividualId carrier | |
meta_property | Vocab-specific axis values land here, not in catalog columns | |
subsumption_axiom | Axioms (2) | sub <: super edge; multi-parent via multiple events |
partition_axiom | Vocab-neutral cover; MLT’s partitions(T) lowers onto this | |
rule_decl | Rule-modes (4) | Unifies fn / derive / query / mutate / check via a rule_mode field |
query_decl | Distinct from rule_decl to index queries; query-specific attrs (group_by, order_by, limit, offset) | |
mutation_decl | Named pub mutate; operations include forget (capability-gated, Capability surface) | |
compute_decl | Inline-compute (tier: recursive by definition) | |
bridge_decl | Federation (1) | Bridge rules bridge rule — directional inference between standpoints; added in δ.2 |
test_decl | Tests (1) | testing / The test atom — ox test in-language unit test (RFD 0048); body is a mutate-body Vec<Operation> plus Operation::Assert, run by ox test. Inert at query/serve time — carries no rule-mode semantics |
relation_tuple_amendment | Markers (1) | Belief-time-amendment marker (amend, RFD 0076): names the relation tuple withdrawn as false ab initio. Asserts no fact — no read or materializer consults it — but the freeze fold subtracts a marked tuple from the freeze witness. Body shape mirrors relation_tuple |
Total: 27. The body encoding is canonical CBOR (deterministic per RFC 8949 §4.2.1); content_id = SHA-256(body), so identical bodies deduplicate. UUIDs encode as bytes(16). The per-variant body maps are specified in Argon/Storage/AxiomBody.lean.
The set is MVP-final but not closure-locked. Additive variant additions bump core_ir_version minor; breaking changes bump major. Semantics-ACTIVATION of a variant already in this table — a producer beginning to emit an event kind it never emitted, which a runtime gate then depends on — is itself a major event, not a minor one: a reader predating the activation already recognizes the kind, so it decodes the artifact cleanly and skips the event as it always did, executing with the new gate silently absent. The worked case and the version arithmetic are recorded at CURRENT_CORE_IR_VERSION in compiler/crates/oxc-oxbin/src/versioning.rs.
A .oxbin reader that encounters an unknown axiom_kind in the events section MUST report OE1220 UnknownAxiomKind(kind) and refuse the load — unless the artifact is lenient-validated (The OxbinRuntime trait surface), in which case the unknown events are dropped from the in-memory representation but kept on disk. Strict runtimes always fail.
Runtime error codes
Failures on the ox runtime serve /v1 HTTP surface (Serving surface) carry a stable
error.code string in the ARGON_RUNTIME_* family. This is the published
failure reference an integrator switches on. Every 4xx/5xx response —
including routing, method, and request-body parse failures — uses the same
envelope, so a client can key on error.code uniformly:
{
"error": { "code": "ARGON_RUNTIME_VALIDATION_FAILED", "message": "…", "details": { … } },
"requestId": "…",
"moduleHash": "…"
}
These are runtime codes — distinct from the compile-time OE#### catalog
of Appendix C, which are build-time
diagnostics. Runtime codes are an API contract surfaced at request time.
The set below is checked against the runtime’s canonical registry
(RUNTIME_ERROR_CODES in oxc-serve): a drift test in oxc-serve fails if a
code is emitted without a row here, or a row names a code the runtime does not
emit. Do not hand-add a row without the matching registry entry.
| Code | HTTP | Meaning |
|---|---|---|
ARGON_RUNTIME_VALIDATION_FAILED | 400 | Argument coercion, entity-reference validation, or a malformed / wrong-content-type request body failed. |
ARGON_RUNTIME_SIGNATURE_MISMATCH | 400 | Descriptor arguments do not match the declared pub callable’s parameters. |
ARGON_RUNTIME_MISSING_CONTEXT | 400 | A required context field (e.g. tenantId) was absent from headers and body. |
ARGON_RUNTIME_FORK_CONFLICT | 400 | The x-fork-id header and the request/path fork disagree. |
ARGON_RUNTIME_INVALID_CURSOR | 400 | A pagination cursor was not issued by this server — pass back nextCursor verbatim or omit it. |
ARGON_RUNTIME_UNSUPPORTED_QUERY_BODY | 400 | The declared query’s body uses a construct the served evaluator does not yet support. |
ARGON_RUNTIME_UNSUPPORTED_QUERY_RESULT | 400 | The query’s result shape cannot be rendered on the wire. |
ARGON_RUNTIME_UNSUPPORTED_MUTATION_RESULT | 400 | The mutation’s result shape cannot be rendered on the wire. |
ARGON_RUNTIME_DERIVE_FAILED | 400 | A derive / projection evaluation failed for the named rule. |
ARGON_RUNTIME_EXPLAIN_FAILED | 400 | RFD 0046 D3: reconstructing the per-fact proof tree failed for the named relation (e.g. the relation could not be resolved or the materialization erred). |
ARGON_RUNTIME_EXPLAIN_BAD_TUPLE | 400 | RFD 0046 D3: an element of the explain request’s tuple is not a valid wire value. |
ARGON_RUNTIME_EXPLAIN_UNRESOLVED_INDIVIDUAL | 400 | An explain tuple element names an individual (by declared name or @name) at a concept-typed position that is not a declared individual in the loaded module — returned instead of a false definite-Not. Reference a mutation-created individual by its #i<id>. |
ARGON_RUNTIME_CHECK_VIOLATION | 400 | An RFD 0025 delta-guard rejection: the mutation would create a blocking check violation. Atomic — nothing persisted; error.details.diagnostics lists the firings. |
ARGON_RUNTIME_ADHOC_TYPE_ERROR | 400 | An ad-hoc query/mutation body failed to type-check against the loaded module’s schema; error.details.diagnostics lists the firings. The body is never run. |
ARGON_RUNTIME_ADHOC_PREPARATION_FAILED | 400 | An ad-hoc query/mutation source could not be parsed/lowered to an executable form (malformed source, no query/derive/mutate item, or a lowering refusal). |
ARGON_RUNTIME_FORK_NOT_DERIVED | 400 | A fork operation needs a prior derive that has not run. |
ARGON_RUNTIME_METHOD_NOT_ALLOWED | 405 | The path exists but does not support the request method. |
ARGON_RUNTIME_CAPABILITY_DENIED | 403 | The request needs a runtime capability (e.g. fork, forget) the caller/server does not grant. |
ARGON_RUNTIME_ADHOC_DISABLED | 403 | The ad-hoc query/mutation surface is disabled on this deployment (RFD 0033): it is restricted to its declared invocables. |
ARGON_RUNTIME_UNKNOWN_ROUTE | 404 | No route matches the request path. |
ARGON_RUNTIME_UNKNOWN_QUERY | 404 | No declared pub query matches the descriptor’s qualified path. |
ARGON_RUNTIME_UNKNOWN_MUTATION | 404 | No declared pub mutate matches the descriptor’s qualified path. |
ARGON_RUNTIME_UNKNOWN_COMPUTE | 404 | No supported pure pub fn matches the compute descriptor’s qualified path. |
ARGON_RUNTIME_UNKNOWN_CHECK | 404 | The /v1/checks check filter names no check in the module. |
ARGON_RUNTIME_FORK_NOT_FOUND | 404 | The named fork does not exist in this scope. |
ARGON_RUNTIME_REQUEST_TIMEOUT | 408 | The request (or the synchronous reasoner) exceeded its wall-clock budget and was abandoned before persisting. |
ARGON_RUNTIME_FORK_CLOSED | 409 | The fork has been promoted or aborted and is no longer writable. |
ARGON_RUNTIME_FORK_EXISTS | 409 | A fork with this id already exists; creating it again is refused so an existing branch’s state is never overwritten (abort it first, or pick a fresh name). |
ARGON_RUNTIME_SCOPE_ID_COLLISION | 409 | Two distinct scopes hash to the same scope id (a configuration fault). |
ARGON_RUNTIME_SCHEMA_MISMATCH | 409 | An artifact’s schema identity does not match the scope’s recorded identity (R-B7); set ARGON_ACCEPT_SCHEMA_CHANGE=1 to override. |
ARGON_RUNTIME_MODULE_INCOMPATIBLE | 409 | A hot-reload candidate is incompatible with live ABox state (removed / changed declarations). |
ARGON_RUNTIME_ADHOC_MULTI_MODULE_UNSUPPORTED | 409 | An ad-hoc query/mutation was submitted against a composed/multi-module artifact (declarations span more than one module prefix); a bare body predicate has no single prefix to re-qualify against. A tracked follow-on (RFD 0033, open questions). |
ARGON_RUNTIME_RESULT_TOO_LARGE | 413 | An un-paged read exceeded the result-row cap; narrow the request or page it (page / ?limit). |
ARGON_RUNTIME_REQUEST_TOO_LARGE | 413 | The request body exceeded the server’s body-size limit (default 4 MiB). |
ARGON_RUNTIME_CHECK_EVAL_FAILED | 500 | A check could not be evaluated (a server / artifact fault, not a client error). |
ARGON_RUNTIME_QUERY_EVAL_FAILED | 500 | A query could not be evaluated against the artifact — a corrupt / version-skewed .oxbin (decode / layer-1 fault) or an unevaluable refinement predicate (a server / artifact fault, not a client error). |
ARGON_RUNTIME_COMPUTE_EVAL_FAILED | 500 | A compute (pub fn) body could not be evaluated against the artifact — a corrupt / version-skewed .oxbin (decode / layer-1 fault) or a stored-value corruption (a server / artifact fault, not a client error). |
ARGON_RUNTIME_NAV_FIELD_RESOLVE_FAILED | 500 | A relation-backed navigation field could not be resolved during entity materialization — e.g. a functional [0..1] endpoint backed by conflicting tuples, or an unevaluable refinement on a derived classifier (a server / artifact fault, not a client error). |
ARGON_RUNTIME_META_RESOLVE_FAILED | 500 | The $meta runtime-type discriminator could not be resolved during entity materialization — an unevaluable defined-concept (iff) predicate (a corrupt / version-skewed artifact). Surfaced loud rather than a silently-narrowed $meta (a server / artifact fault, not a client error). |
ARGON_RUNTIME_STORAGE_ERROR | 500 | The storage backend returned an error during the operation. |
ARGON_RUNTIME_INTERNAL_ERROR | 500 | An unexpected internal error — a handler or middleware panic, caught at the boundary and rendered as the structured envelope (so every 5xx carries the envelope by construction, not by audit). |
ARGON_RUNTIME_MODULE_NOT_LOADED | 503 | No module is currently loaded (a failed load left the prior module active or none present). |
ARGON_RUNTIME_STORAGE_UNREACHABLE | 503 | /readyz / /v1/health: the storage backend is not reachable. |
A batch-step failure (POST /v1/batch) rides the same envelope under the
step’s own code, with the failing step’s 0-based index added at
error.details.batchStep. A 429/load-shedding code is not part of the
current surface (the request-timeout and result-row caps are the back-pressure
mechanisms).
Mechanization provenance
This index maps each language feature to the Lean module(s) that mechanize it and the spec section it lives in. It is generated from the feature registry, and CI verifies that every cited module exists — a feature naming a vanished mechanization fails the build.
| Feature | § | Lean module(s) | RFD |
|---|---|---|---|
| Module extraction (⊥-locality) | §3 | Locality/Locality.leanLocality/ScopedConservativity.lean | — |
mod, use, visibility, multi-file build, ox.toml package | §3 | CoreIR/Path.lean | 0022 |
| The meta-calculus (metaxis / metatype / metarel) | §4 | Substrate/MetaCalculus.lean | — |
abstract / fixed declaration modifiers | §4 | Runtime/ModifierGates.lean | 0027 |
Concepts (type baseline; declared classifying vocabulary) | §5 | — | 0038 |
Facts pub fact / strong negation pub not_fact | §5 | Storage/AxiomEvent.lean | 0010 |
| First-class relations | §5 | Substrate/RelationSubsumption.lean | 0005 |
Relation subsumption R2 <: R1 | §5 | TypeSystem/RelationExtension.lean | 0005 |
struct / enum data declarations | §5 | Substrate/Construct.lean | — |
Field intents T / T? / Truth4Of<T> | §6 | Foundation/Truth4Of.lean | 0007 |
Generics, collections ([T] / Set / Map / Option), function types | §6 | Syntax/TypeExpr.lean | — |
Instance-of : clause (cross-level) | §6 | MetaCalculus/IsCanNot.lean | — |
Mutable fields mut + update | §6 | Runtime/MutationSemantics.lean | 0006 |
Numeric tower — Top/Bot, exact Decimal/Real, result widening | §6 | Runtime/AggregateExact.lean | 0016 |
Refinement: iff (defined) vs where (primitive) | §6 | TypeSystem/Realization.lean | 0017 |
Specialization <: (subtyping) and a concept hierarchy | §6 | TypeSystem/Subtyping.lean | — |
| World assumptions — CWA default + per-concept OWA opt-in (`#[world(open | closed)]`) | §6 | Schema/WorldAssumption.leanTypeSystem/Soundness/CwaOwa.lean |
Aggregates count / exists / sum / min / max / avg | §7 | Runtime/AggregateExact.lean | 0011 |
Defeasibility — honest heads + defeat directive (#[default] / #[defeats] / #[label]) | §7 | Reasoning/Defeasibility/Transform.leanTypeSystem/Soundness/Defeasibility.lean | 0028 |
Defeasibility — proof tags (+Δ / +∂) | §7 | Reasoning/Defeasibility/Transform.lean | 0028 |
Modal box / diamond (fixed-discharge) | §7 | Reasoning/Modal.lean | — |
| Recursion-through-negation (well-founded semantics) | §7 | Reasoning/Datalog/WellFounded.lean | — |
Removed defeasible strength triple (#[strict] / #[defeasible] / #[defeater] / #[priority]) | §7 | — | 0028 |
| Rule-atom forms: predicate, comparison, NAF, type-test, field projection | §7 | Reasoning/Rule.lean | — |
Sinks (pub sink, emit) | §7 | — | 0015 |
Stable models (#[brave]) | §7 | Reasoning/Stability.lean | 0003 |
Temporal rule atoms (since, until, ever, …) / FOL quantifiers | §7 | Reasoning/Temporal.leanDecidability/Temporal.lean | — |
Universal quantifier forall v: T where Body, Head | §7 | Reasoning/Rule.lean | — |
#[comptime] build-time lifting | §7 | Substrate/Comptime.lean | 0002 |
check (consistency observers) | §7 | Reasoning/Checks.leanReasoning/StaticDischarge.lean | 0025 |
derive (recursive head-population) | §7 | Reasoning/EvalProgram.leanReasoning/Stratification.lean | — |
fn (pure compute) | §7 | CoreIR/Lowering.lean | — |
mutate — Datalog-style body | §7 | Runtime/MutationSemantics.lean | — |
mutate — imperative body | §7 | Runtime/MutationSemantics.lean | 0015 |
query (read-only) | §7 | Runtime/EngineModuleStore.lean | — |
| MLT level theory as an unprivileged package | §8 | — | 0043 |
Reflective rule-body intrinsics (iof / meta / specializes / extent) | §8 | MetaCalculus/Reflect.lean | 0023 |
TypeRef reflective type-as-value | §8 | Storage/CatalogProjections.lean | 0023 |
| Decidability tier ladder (classifier + per-tier admittance) | §9 | Decidability/Tier.leanDecidability/Classifier.leanDecidability/Admittance.lean | — |
| Decidability — Expressive tier | §9 | Decidability/Expressive.lean | 0003 |
Decidability — Fol tier (unsafe logic { }) | §9 | Decidability/FOL.lean | — |
| Decidability — Metaorder tier | §9 | Decidability/MetaOrder.lean | — |
| Decidability — Modal tier | §9 | Decidability/Modal.lean | — |
| Decidability — Structural / Closure / Recursive tiers | §9 | Decidability/Structural.leanDecidability/Closure.leanDecidability/Recursive.lean | — |
| Polynomial bound on type-graph (D1) evaluation | §9 | Decidability/Complexity/Bounds.leanDecidability/Domain1/Eval.lean | — |
Bridge rules pub bridge | §11 | Standpoint/Federation.lean | — |
| Federation policies (paraconsistent / strict / weighted) | §11 | Standpoint/Consistency.lean | — |
| Sheaf semantics (categorical) | §11 | Locality/SheafEquivalence.lean | 0008 |
Standpoint declarations, lattice <:, standpoint-block scoping | §11 | Standpoint/Visibility.lean | — |
| Standpoints and four-valued federation (Truth4) | §11 | Standpoint/Federation.leanFoundation/Truth4.lean | — |
Truth4 (is / not / can / both) | §11 | Foundation/Truth4.lean | — |
Macros — declarative pub macro, procedural #[procmacro] | §12 | Substrate/Macro.lean | 0037 |
| Traits (behavioral contracts + dispatch) | §12 | TypeSystem/Conformance.leanSubstrate/Trait.lean | 0026 |
ox build → .oxbin (preamble, four version axes, validation, content hash) | §16 | BuildArtifact/Oxbin.leanBuildArtifact/Validation.lean | — |
Bitemporal stamps + time-travel (query … as_of) | §17 | Runtime/AsOf.lean | — |
Forks, hot reload, retention / forget capability | §17 | Runtime/Capability.lean | — |
In-language tests (the test declaration) | §17 | Storage/AxiomKind.lean | 0048 |
Runtime serving ox runtime serve (/v1) | §17 | Conformance/Envelope.lean | 0014 |
| Two-layer incremental cache (compile + runtime) | §17 | Runtime/IncrementalContract.lean | — |
Module / Store, execute_mutation, query_* | §17 | Runtime/EngineModuleStore.lean | — |
Environment switches
This appendix is the single operator-facing catalog of every environment
switch the toolchain reads: the rollback levers to reach for during an
incident, and the benchmark and test-harness knobs. It is kept in lockstep
with the in-code registry (oxc_diagnostics::env_registry) by a drift guard
that fails the build if this page and the registry name a different set of
switches.
An environment switch is a process-environment variable in the engineered
OXC_ / OX_ namespace that changes runtime behavior. Operational
configuration — install paths, service endpoints, credentials, and the
standard HOME / PATH / DATABASE_URL variables — is not a switch and is
documented with the deployment guide instead.
Every switch name — including test-only and bench knobs — carries the
engineered OXC_ / OX_ product prefix. A bare topic prefix (DEFEAT_, …)
is refused: such names are too generic for the process environment and
collision-prone with ambient variables. So a defeat-plane soak knob carries
the product prefix — OXC_DEFEAT_PLANE_SOAK — rather than a bare topic-only
spelling.
Truthiness
Boolean switches follow one convention. A switch is engaged when its
variable is set to a truthy value and disengaged when unset or set to a
falsy value. Falsy values are the empty string and, case-insensitively, 0,
false, off, and no (surrounding whitespace ignored); every other value
is truthy. So NAME=1 engages and NAME=0 disengages any boolean switch —
an A/B run flips one between =1 and =0 without per-switch parse rules.
Switches with a numeric or string value (case counts, seeds, iteration counts) take that value directly and state their own default below.
Classes
- rollback — changes which code path serves production answers. Flipping it can change results, latency, or the artifact a source elaborates to. These are the incident levers.
- bench/probe — a benchmark, perf-probe, or observability knob with zero product effect: it changes what a measurement run does or reports, never the answer served.
- test-only — a test-harness control (a case count, a random seed, or a golden-file regeneration toggle) with zero product effect.
Rollback levers
| Switch | Effect when set | Default (unset) | Subsystem | Introduced |
|---|---|---|---|---|
OXC_DISABLE_CHECK_CONE_SLICING | disable check-cone slicing on the write path, forcing a full recheck of every check per commit | off (slicing enabled) | oxc-runtime (write path) | #1745 |
OXC_DISABLE_INCREMENTAL_FREEZE_INDEX | rebuild the freeze-witness index from scratch each generation instead of folding the commit delta | off (incremental fold enabled) | oxc-runtime (write gate) | #1771 |
OX_DISABLE_NAIVE_CACHE | disable the per-pass naive-arrangement cache | off (cache enabled) | oxc-reasoning (executor) | #1710 |
OX_DISABLE_DECODE_CACHE | disable decode-once relation sharing across a pass | off (cache enabled) | oxc-reasoning (executor) | #1735 |
OX_DECODE_VIEWS_UNCAPTURED | skip capturing the module-sourced declaration views at each warm-store build, forcing every per-request view rebuild through the decode scan | off (views captured once per warm-store build) | oxc-runtime (indexed views) | #1857 |
OX_DECODE_BODIES_UNFROZEN | skip capturing the frozen committed-body snapshot at each warm-store build, so every per-request store clone re-decodes the committed ABox bodies | off (committed bodies frozen once per warm-store build) | oxc-runtime (decode cache) | #1865 |
OX_DISABLE_NAF_MEMO | disable the negation-as-failure verdict memo | off (memo enabled) | oxc-reasoning (executor) | #1710 |
OX_DISABLE_MODEL_RESYNC | disable the post-commit model-resync route, falling back to wholesale drop-and-rebuild of the derived model | off (resync route enabled) | oxc-runtime (maintenance router) | #1772 |
OXC_ENABLE_INCREMENTAL_CIRCUITS | prefer incremental circuit maintenance over the established per-commit recompute, falling back where circuits are unsupported | off (established recompute) | oxc-runtime (maintenance mode) | #1753 |
OXC_REQUIRE_INCREMENTAL_CIRCUITS | require incremental circuit maintenance — hard-error instead of falling back to the established recompute | off (fallback allowed) | oxc-runtime (maintenance mode) | #1753 |
OXC_ENABLE_UNAUDITED_PRODUCT_INCREMENTAL_CIRCUITS | prefer the unaudited Product well-founded circuit backend when circuits are enabled | off (qualified Product default) | oxc-runtime (maintenance mode) | #1863 |
OXC_ENABLE_AUDITED_PRODUCT_INCREMENTAL_CIRCUITS | prefer the audited Product well-founded circuit backend (the opt-in publication oracle) when circuits are enabled | off (qualified Product default) | oxc-runtime (maintenance mode) | #1863 |
OXC_REQUIRE_UNAUDITED_PRODUCT_INCREMENTAL_CIRCUITS | require the unaudited Product circuit backend — hard-error instead of selecting any other maintenance route | off (mode chosen by the other flags) | oxc-runtime (maintenance mode) | #1863 |
OXC_REQUIRE_AUDITED_PRODUCT_INCREMENTAL_CIRCUITS | require the audited Product circuit backend — hard-error instead of selecting any other maintenance route | off (mode chosen by the other flags) | oxc-runtime (maintenance mode) | #1863 |
OXC_USE_ESTABLISHED_WFS_INCREMENTAL_CIRCUITS | select the established WFS circuit backend instead of the qualified Product default when circuits are enabled — the operational escape hatch | off (qualified Product default) | oxc-runtime (maintenance mode) | #1880 |
Each rollback lever is read once per process (behind a OnceLock), so it must
be set before the process starts; changing it mid-run has no effect.
Bench and probe knobs
| Switch | Effect | Default (unset) | Subsystem | Introduced |
|---|---|---|---|---|
OX_TRACE_ARR | emit per-pass arrangement trace tallies and counters | off (no trace) | oxc-reasoning (executor) | #1690 |
OX_DECODE_COUNTERS | emit committed-body CBOR decode-execution counters to stderr | off (no counters) | oxc-runtime (decode probe) | #1657 |
OXC_PROOF_BENCH_AFFECTED | number of affected checks in the materialization-impact probe | 0 | oxc-runtime (materialization probe) | #1745 |
OXC_PROOF_BENCH_ITERS | iteration count for the materialization-impact probe | 100 | oxc-runtime (materialization probe) | #1745 |
OXC_PROOF_BENCH_OPAQUE | include opaque checks in the materialization-impact probe | off (opaque checks excluded) | oxc-runtime (materialization probe) | #1745 |
OXC_FREEZE_INDEX_BENCH | commit count for the freeze-index micro-benchmark; absent skips it | unset (skipped) | oxc-runtime (freeze-index bench) | #1771 |
OXC_FREEZE_INDEX_BENCH_RUNS | interleaved run count per cell for the freeze-index micro-benchmark | unset (bench default) | oxc-runtime (freeze-index bench) | #1771 |
OXC_PREINTERN_BENCH | visible-individual count for the enumeration-order pre-numbering pre-pass cold-seed micro-benchmark; absent skips it | unset (skipped) | oxc-runtime (pre-numbering bench) | #1851 |
OXC_SETTLE_STATS | emit process-cumulative settle route counters (eligible/counting/fallback) to stderr on every settle; per-settle deltas are the difference of successive lines | off (no counters) | oxc-reasoning (settle maintainer) | #1785 |
OXC_INCREMENTAL_SCALE | scale profile for the incremental-scale benchmark: million, ten-million, or steady-million | unset (default scales) | oxc-reasoning (incremental-scale bench) | #1753 |
OXC_CORRECTION_SAMPLES | sample count for the incremental recursive-corrections benchmark (floored at 10) | 10 | oxc-reasoning (recursive-corrections bench) | #1753 |
OXC_SAMPLED_COUNT | sample count per cell for the sampled incremental benchmark | 30 | oxc-reasoning (sampled bench) | #1753 |
OXC_SAMPLED_DELTA | run only the cells with this delta size in the sampled incremental benchmark | unset (all delta sizes) | oxc-reasoning (sampled bench) | #1753 |
OXC_SAMPLED_IMPLEMENTATION | run only this maintenance implementation in the sampled incremental benchmark: circuit, product, established, or full | unset (all implementations) | oxc-reasoning (sampled bench) | #1753 |
OXC_SAMPLED_PREPARATION_PHASES | record and report per-phase preparation timings in the sampled incremental benchmark | off (no phase report) | oxc-reasoning (sampled bench) | #1753 |
OXC_SAMPLED_PROFILE | scale profile for the sampled incremental benchmark: million or ten-million | unset (default scales) | oxc-reasoning (sampled bench) | #1753 |
OXC_SAMPLED_STATE | run only the cells with this state size in the sampled incremental benchmark | unset (all state sizes) | oxc-reasoning (sampled bench) | #1753 |
OXC_SAMPLED_WORKLOAD | run only the workload with this name in the sampled incremental benchmark | unset (all workloads) | oxc-reasoning (sampled bench) | #1753 |
OXC_WELL_FOUNDED_PREPARATION_PHASES | record and report per-phase preparation timings in the well-founded incremental memory benchmark | off (no phase report) | oxc-reasoning (well-founded memory bench) | #1876 |
OXC_AGGREGATE_PREPARATION_PHASES | record and report per-phase preparation timings in the well-founded aggregate incremental memory benchmark | off (no phase report) | oxc-reasoning (aggregate memory bench) | #1753 |
Test-harness knobs
| Switch | Effect | Default (unset) | Subsystem | Introduced |
|---|---|---|---|---|
OXC_FUZZ_CASES | per-target proptest case count for the fuzz harness | 256 | oxc-fuzz | #304 |
OXC_DEFEAT_PLANE_SOAK | extra random-seed count run past the fixed CI seeds | unset (CI seeds only) | oxc-runtime (defeat-plane oracle) | #1793 |
OXC_DEFEAT_PLANE_SEED | evaluate exactly one seed, for isolated reproduction | unset (single-seed repro skipped) | oxc-runtime (defeat-plane oracle) | #1793 |
OXC_DEFEAT_EMIT_BLESS | regenerate the defeat-emission golden vectors instead of asserting against them | unset (assert against committed goldens) | oxc-runtime (defeat-emission vectors) | #1800 |
OXC_SETTLE_CHECK | re-run each A/B stratum settle as a full recompute and hard-error on any head-extent divergence (always on under cfg(test)) | off in release (always on under test) | oxc-reasoning (settle maintainer) | #1785 |
OXC_INCREMENTAL_CONFORMANCE_CORPUS | root directory of an external incremental-conformance corpus to replay | unset (external-corpus run skipped) | oxc-driver (conformance replay) | #1753 |
OXC_INCREMENTAL_CHECKPOINT_CHILD | marks the checkpointed child process the incremental read-model test re-executes itself as | unset (parent process) | oxc-runtime (incremental read-model test) | #1753 |
OXC_COMPILED_DEFEAT_PRODUCT_CHILD | marks the Product-mutation child process the compiled-defeat test re-executes itself as | unset (parent process) | oxc-runtime (compiled-defeat test) | #1753 |
OXC_COMPILED_DEFEAT_LOCALITY_CHILD | marks the tuple-locality child process the compiled-defeat test re-executes itself as | unset (parent process) | oxc-runtime (compiled-defeat test) | #1753 |
OXC_COMPILED_DEFEAT_REINSTATEMENT_CHILD | marks the reinstatement child process the compiled-defeat test re-executes itself as | unset (parent process) | oxc-runtime (compiled-defeat test) | #1753 |
OXC_COMPILED_DEFEAT_CHECKPOINT_CHILD | marks the checkpointed child process the compiled-defeat test re-executes itself as | unset (parent process) | oxc-runtime (compiled-defeat test) | #1753 |
OXC_CROSS_MODULE_DEFEAT_PRODUCT_CHILD | marks the Product-mutation child process the cross-module-defeat test re-executes itself as | unset (parent process) | oxc-runtime (compiled-defeat test) | #1753 |
OXC_LOCKSTEP_REQUIRED_CIRCUITS_CHILD | marks the child process the lockstep-retraction test re-executes itself as with the incremental circuit engine required | unset (parent process) | oxc-runtime (lockstep-retraction test) | #1898 |
OXC_LOCKSTEP_REQUIRED_UNAUDITED_PRODUCT_CHILD | marks the child process the lockstep-retraction test re-executes itself as with the unaudited product-time well-founded backend required | unset (parent process) | oxc-runtime (lockstep-retraction test) | #1898 |
OXC_LOCKSTEP_REQUIRED_AUDITED_PRODUCT_CHILD | marks the child process the lockstep-retraction test re-executes itself as with the audited product-time well-founded backend required | unset (parent process) | oxc-runtime (lockstep-retraction test) | #1898 |
Keeping this page honest
The registry (oxc_diagnostics::env_registry) is the source of truth; this
page is its human-readable projection. A drift guard scans the workspace and
fails the build if a switch-shaped variable is read anywhere without a
registry row, if a registry row is never read, or if this page and the
registry name different sets of switches. A new switch therefore cannot ship
without a row here.
Glossary
This glossary is not yet written. The defining occurrences of Argon’s core terms live in their home chapters — the meta-calculus vocabulary in the meta-calculus atom, the construct vocabulary in the constructs atom, and the reasoning and world-assumption vocabulary in Part III. A consolidated glossary collecting those definitions is a deferred follow-up.
Walking example
//! Mini lease vocabulary. Exercises all five atoms.
use std::math::{Nat, Money, Date, DateTime};
use std::diag::{Diagnostic, Severity};
use std::format::format;
use std::time::{today, now};
/// A natural person.
pub type Person { name: String, age: Nat }
/// A rentable real-estate unit.
pub type Property { address: String, sqft: Nat }
/// Lease — relation with intrinsic property body.
pub rel Lease(tenant: Person, property: Property) [0..*] [0..*] {
monthly_rent: Money,
start: Date,
end: Date,
status: LeaseStatus,
}
pub enum LeaseStatus { Pending, Active, Expired, Terminated }
impl Person {
/// All active leases held by this person.
pub query active_leases(self) -> [Lease] {
select l from l: Lease, Lease(self, _) where is_active(l)
}
}
impl Lease {
pub fn daily_rent(self) -> Money = self.monthly_rent / 30;
}
pub derive is_active(l: Lease) :- l.start <= today(), l.end > today();
pub mutate sign_lease(t: Person, p: Property, rent: Money, term_days: Nat) -> Lease {
require { rent > 0, term_days > 0 }
let l = insert Lease(t, p) {
monthly_rent: rent,
start: today(),
end: today() + term_days.days,
status: Pending,
};
emit AuditLog { LeaseEvent::Signed { lease: l, at: now() } };
return l;
}
pub check no_overlapping_leases(t: Person, p: Property) :-
count { from l: Lease where l.tenant == t, l.property == p, is_active(l) } > 1
=> Diagnostic {
severity: Severity::Error,
code: "Lease::E001",
message: format!("Tenant {} has overlapping active leases on {}", t.name, p.address),
};
pub trait Renewable { fn renew(self, term_days: Nat) -> Self; }
impl Renewable for Lease {
fn renew(self, term_days: Nat) -> Lease = Lease {
..self,
start: self.end,
end: self.end + term_days.days,
status: Pending,
};
}
#[derive(Json, Hashable)]
pub struct Receipt { lease: Lease, paid: Money, at: Date }
#[consistency(strict)]
pub standpoint CaliforniaTenancyLaw <: BaseTenancyLaw {
pub check ab1482_rent_cap(l: Lease) :-
l.monthly_rent > l.property.market_rate * 1.10
=> Diagnostic {
severity: Severity::Error,
code: "CA::AB1482",
message: "Lease exceeds AB-1482 rent cap",
};
}
pub sink AuditLog: LeaseEvent;
pub enum LeaseEvent {
Signed { lease: Lease, at: DateTime },
Renewed { lease: Lease, at: DateTime },
Terminated { lease: Lease, at: DateTime },
}
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];
}