Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Foreword

There has long been a gap between the languages we use to think about a domain and the languages we use to run code over it. On one side, ontology engineers reach for OWL, OntoUML, or hand-rolled formalisms in the description-logic tradition: precise, mechanically reasonable, but awkward to compose, package, or evolve like real software. On the other side, programmers reach for Python, TypeScript, or Rust: ergonomic, well-tooled, but almost defenseless against a domain whose constraints really do live in the type system.

Argon is an attempt to close that gap from the programming-language side.

It is a programming language whose primary subjects are concepts, relations, and rules. It compiles. It has a package manager. It has an LSP, an editor extension, and a registry. It also has refinement types, a decidability tier ladder, bitemporal axes, standpoint-aware queries, and a mechanically-verified core. The same source can model a residential lease, a regulatory regime, and the events that move money between them — and the compiler can tell you, before you run anything, whether your rules are decidable, whether your refinement is satisfiable, and whether your invariants follow from your axioms.

This book is the path into that language. It moves atoms-to-molecules, the way The Rust Programming Language does for Rust, and it does so against a single running example — a residential-lease ontology that we build up piece by piece across the chapters. By the end of Part 3, the reader has a working, queryable, validated model of a non-trivial legal domain. Part 5 covers the formal foundations: the meta-property calculus, the seven-tier decidability ladder, defeasibility semantics, and the bitemporal + standpoint runtime. Where the chapters say “mechanically verified,” they state the result and the math behind it — termination, uniqueness, soundness, decidability — and let the reader inspect the engine the result is about.

Argon is not finished. The team is in the middle of a measured redesign: cleaning up vocabulary, unifying body shapes, collapsing a few item kinds into more general primitives. The book teaches today’s syntax and flags every place where the language is moving. Migration notes point forward, and Appendix D is the lever that will keep the book honest as the redesign rolls out.

The audience the book imagines first is the foundational-ontology research community: people who already know UFO, BFO, DOLCE, OntoUML, OWL — what an ontology is, what reasoning is, why decidability matters. The book skips that preamble and goes straight to what is distinctive: how Argon turns an ontology into a program you can ship.

If you arrive from the other side — Rust, TypeScript, Haskell, a working programmer who has never touched description logic — the book still works. The early chapters teach the formal vocabulary as it goes. You will not be asked to read a paper before the first compile.

— the Argon team, 2026

Introduction

Argon is a programming language for declarative ontology modeling with first-class reasoning. Its purpose is to let you describe a domain — its concepts, the relations between them, the rules that hold over them — in a single, typed, packageable language and then run things against that description: queries, mutations, simulations, regulatory checks, diagrams.

This chapter answers four questions: what Argon is, who it is for, what is distinctive about it, and how it relates to the tools you already know.

What Argon is

A working Argon program contains four kinds of items.

Concepts are the things in your domain. A Person, a Lease, a Property, a Mortgage. A concept declaration introduces a type and the fields that go with it. Concepts form hierarchies: a ResidentialLease is a Lease, a Tenant is a Person.

Relations connect concepts. They can be binary (HasResidence(person, residence)) or carry their own structure (a Lease mediates a Tenant, a Landlord, and a property — and has its own dates and rent).

Rules derive new facts from existing ones. derive ActiveLease(l) :- l.start_date <= today(), l.end_date > today() reads as “a Lease is ActiveLease when these conditions hold.” The reasoner picks an evaluator per rule type — structural saturation for the cheap fragment, Datalog for the recursive one, an event-driven engine for reactive rules — and the same evaluators run at compile time and at runtime. Every derivation carries provenance.

Operations do work over the model. compute items are pure functions. query items return bindings. mutation items change the world and emit typed events into a bitemporal log. Each kind picks the body shape that fits its job: = expr, :- body, or { stmts }.

A package collects these items, ships them through a registry, and lets other packages import them. The toolchain — oxup for installing, ox for building/testing/running, ox-lsp for editor integration — is shaped like Cargo’s, intentionally.

Why Argon

A few things that working ontology engineers have asked of their tools, and that Argon tries to answer.

Compose like software. Foundational ontologies (UFO, BFO, DOLCE) and domain ontologies live in different repositories, evolve at different rates, and need to compose. Argon packages do that. There is no monolithic schema; there are libraries, and a manifest, and a lockfile.

Reason at compile time when you can. Most rules are decidable. A few are not. Argon’s tier ladder makes the line explicit: rules at tiers 0–3 execute; rules at tiers 4–6 parse and elaborate but do not run, so the compiler tells you up front when you have written something the engine cannot evaluate. @[theorem] marks a derive as a theorem claim and preserves the marker through to the kernel for a future mechanical-verification pipeline (the pass itself is not yet active). Refinement types are a decidable fragment with a polynomial-time decision procedure.

Carry time with you. Every fact has a valid time (when it is true in the world) and a transaction time (when the system learned it). Mutations publish typed events into an append-only log. Queries can dial in a standpoint, a fork, or a historical timestamp. You do not bolt this on; it is built in.

Show your work. Every result carries why-provenance. Every diagram is generated from the same source the rest of the language reads. Tests live next to the model.

Who Argon is for

The book imagines two readers.

The first is a foundational-ontology researcher or a domain modeller who has been building in OntoUML, OWL, or hand-rolled DL formalisms. You know what a relator is, what a sortal is, why disjointness and completeness matter, why decidability is not a curiosity. What you have wanted is a language whose ergonomics, packaging, and tooling let you ship a model the way a Rust crate ships a library.

The second is a working programmer — Rust, TypeScript, Haskell, OCaml — who has been hand-rolling typed domains and finally wants to declare them once. You may not yet know the term relator; you have certainly written one many times by hand and watched the runtime checks pile up.

Either way, the book starts at the toolchain and walks you through to a compiled, tested, queryable, visualizable lease ontology. Read straight through and it should take a long afternoon.

What is distinctive

Several pieces of Argon, taken individually, exist elsewhere. The synthesis is what is unusual.

  • Vocabulary-neutral by construction. Argon’s language core ships zero foundational metatypes. kind, subkind, role, phase, relator, category — these are not keywords. They come from a vocabulary package (UFO, BFO, your own). The reserved-keyword set is small; almost everything composes from packages.
  • Three computational modes, one type system. compute for pure functions, derive/query for declarative reasoning, mutation for state-changing operations. Refinement types and the metatype calculus apply uniformly to all three.
  • Decidability tier ladder. Seven named tiers from tier:structural (polynomial, the cheapest) through tier:fol (full first-order logic) up to tier:mlt (multi-level theory). The compiler classifies every rule. Tier-cap enforcement at the module level keeps a package’s effective fragment honest.
  • Bitemporal + event-sourced + standpoint-aware. The runtime is built around an append-only axiom-event log with valid-time and transaction-time axes; standpoints discriminate views; forks branch the world.
  • Mechanically verified core. Refinement-fragment decidability (PTIME), occurrence-typing soundness, and meta-property-fixpoint termination + uniqueness are stated and proven in Part 5.

How Argon compares

A short, comparative sketch — useful if you have spent time with any of these.

ToolWhat it does wellWhere Argon differs
OWL / Description LogicsMechanically reasonable; standardised (Motik et al. 2012); well-studiedProgramming-language ergonomics; first-class packaging; bitemporal + event log; refinement types; per-rule decidability classification rather than fixed profiles
Datalog with stratified negationClean declarative semantics; PTIME data complexity; well-foundational (Abiteboul, Hull & Vianu 1995)Richer types (refinement, primitives, generics); mutations + events; modal + Allen operators; standpoints; bitemporal axes
PrologMaximally expressive; SLD resolutionDecidable subsets via the tier ladder; @[theorem] markers (mechanical-verification pipeline planned); package system; static types; non-defeasibility default
OntoUML / UFOSemantic richness; foundational-ontology grounding (Guizzardi 2005)Executable; ox diagram round-trips OntoUML; mutations + tests next to the model; UFO is a package, not a built-in; refinement types over the metatype calculus
Defeasible Logic ProgrammingArgument-based defeasibility (García & Simari 2004; Governatori et al.)Native @[default] / @[override] decorators; integrated with type system; project-level lex orderings ([defeat] order = [...])
TypeScript / Rust typesMainstream; tooled; refinement support via librariesReasoning, not just type-checking; ontology-shaped, not class-shaped; constraints in the type system, not in runtime checks; first-class temporal axes
RDF* / Property graphsQuad/edge-attribute representation; vendor-supportedStatic typing; mechanically-verified refinement-fragment decidability; first-class metatype calculus; tier-classified rules

A finer-grained feature matrix:

FeatureOWL 2 RLDatalog±NaFOntoUMLArgon
Subsumption reasoning✓ (encoded)✓ (via UFO axioms)✓ (Tier 0)
Default reasoning / defeasibility✓ (@[default] / @[override])
Refinement types✓ (PTIME-decidable fragment)
Bitemporal valid + tx
Standpoint discrimination✓ (project-level lattice)
Event-sourced mutations✓ (mutation items + append-only log)
Compile-time mechanical verificationplanned (@[theorem] markers preserved today; verification pass forthcoming)
Vocabulary-neutrality (no built-in metatypes)n/a✗ (UFO baked-in)✓ (UFO is a package)
Per-rule decidability classification✓ (seven-tier ladder)
First-class diagrams✓ (via tooling)✓ (diagram items in source)

You do not have to pick a side. An Argon project can import an OntoUML-flavored UFO package, emit OntoUML JSON for visual tools, target OWL where downstream consumers need it, and still keep a typed compile-and-test loop the rest of the time.

What this book covers

The book covers Parts 1 through 5: getting started, foundations (concepts, relations, rules, computations and mutations, the type system), composition (patterns, state machines, tests, diagrams), packages and tooling, and the advanced material — the metatype calculus, defeasibility, the tier ladder in detail, standpoints and bitemporal. Where the language is still moving, the chapters flag it inline; the full migration trajectory lives in Appendix D.

The shortest path to a working ontology is to read Chapter 1 end-to-end, then Chapter 2, then Chapter 3.4. That gets you to “concepts, relations, rules, and a diagram of what you have built.”

What’s in argon/examples/

The repository ships with a library of runnable example packages alongside this book. Each is a complete Argon workspace — ox.toml, src/prelude.ar, sometimes a README.md — that you can cd into and exercise with ox check. The book draws on them throughout; this is the canonical index.

Tutorial:

  • lease-story/ — the residential-lease ontology built up through Chapters 2–3. The book’s running example.

Foundational ontology ports (each is a minimal smoke test, useful when you want to see a vocabulary stand on its own):

  • bfo-smoke/ — Basic Formal Ontology. Declares temporal_status and dependence axes, four BFO metatypes, six BFO concepts, two BFO structural rules.
  • dolce-lite/ — DOLCE-Lite. A second independent foundational vocabulary, used in the book to demonstrate disjoint-non-interference composition with BFO.

Domain ontologies (each is a port of a published ontology, faithful to the source):

  • pizza/ — the Manchester pizza ontology. The canonical OWL classroom example, ported to Argon. Exercises five relation characteristics (@[transitive], @[symmetric], @[asymmetric], @[functional], @[inverse-functional]) across a class lattice with multiple inheritance.
  • time/ — OWL-Time. All 13 Allen relations as first-class pub rel declarations, ~25 datatype properties, DayOfWeek and MonthOfYear enums.
  • foaf/ — Friend Of A Friend. The social-graph ontology.
  • skos/ — Simple Knowledge Organisation System.
  • wine/ — the wine ontology (Hendler / McGuinness OWL teaching example).

Composition demos:

  • ontology-tour/ — a registry consumer that pulls pizza, foaf, and wine into a single project and declares a tour_role metaxis for type. Useful as a worked example of cross-package composition.

Beyond these, argon/examples/_catalog/ holds ~530 minimal mini-workspaces — one per (feature, variant) pair — that exercise each atomic feature of the language in isolation. The catalog is the reference companion to the book; see argon/examples/_catalog/README.md for the index.

Open a terminal. The next chapter installs the toolchain.

Getting Started

This part takes you from no toolchain to a running Argon program in one sitting. Three short chapters:

By the end of this part you have a working installation, you have run an Argon program, and you have the skeleton of the lease tutorial that threads through the rest of the book.

Installation

You install Argon by installing one binary: oxup. From there, oxup fetches the rest of the toolchain and keeps it up to date, the way rustup does for Rust.

What gets installed

A complete Argon toolchain bundles six things:

  • oxc — the compiler.
  • ox — the project manager: ox check, ox build, ox test, ox query, ox mutate, ox diagram, and the package commands.
  • ox-lsp — the language server, used by the editor extension.
  • std — the standard library bundle.
  • tree-sitter-argon — the grammar, compiled to a dynamic library.
  • shell-completion scripts.

A single oxup binary does the work of oxup, ox, oxc, and ox-lsp by inspecting argv[0]. The proxy binaries in ~/.argon/bin/ are symlinks back to the same oxup binary, so installing a new toolchain version is a single atomic flip of the active symlinks.

Install via Homebrew

$ brew tap sharpe-dev/tap
$ brew install argon
$ oxup init
$ oxup install stable

Four commands, in order:

  • brew tap sharpe-dev/tap — register the Argon Homebrew tap.
  • brew install argon — install the toolchain.
  • oxup init — first-run setup. Provisions ~/.argon/{bin,toolchains,extensions} and adds ~/.argon/bin to your shell rc inside a guard block.
  • oxup install stable — download the latest signed toolchain and make it the default.

The shell-rc edit is bounded — it lives between two clearly-marked comment lines that oxup controls. Running oxup uninstall later cleanly removes the edit along with everything else oxup manages.

Note: the tap is private to sharpe-dev while Argon is in pre-public-release. brew tap will prompt for GitHub authentication; oxup auth login configures the token oxup itself uses to fetch toolchain release assets.

Verifying the install

Open a fresh shell and check the four commands report a version:

$ oxup --version
$ ox --version
$ oxc --version
$ ox-lsp --version

If any of them fail, run oxup doctor. It checks that ~/.argon/ exists and is writable, that the proxy symlinks resolve, that the active toolchain manifest is intact, and that the shell-integration block is present in your rc-file.

$ oxup doctor

oxup doctor --json prints the same diagnostic in machine-readable form; oxup doctor --fix applies auto-fixable repairs.

Editor extension (VS Code / Cursor)

The editor experience is the canonical UX for Argon. Install the extension via oxup, which reads the bundled VSIX from the active toolchain and hands it to your editor’s CLI:

$ oxup extension install

Pass --editor vscode or --editor cursor to override auto-detection. oxup extension status reports the installed extension version. oxup extension uninstall removes it (with --editor all to uninstall from every detected editor at once). For dev-cut toolchains that ship no release asset, oxup extension install --path ./argon-dev.vsix installs a locally-built VSIX while still recording the install in oxup’s state.

The extension brings: LSP-driven hover, semantic highlighting, inlay hints, palette commands for ox query / ox mutate / ox diagram / ox test, an output channel for compiler messages, and the InfoView panel. Use it. The book’s later chapters reference InfoView output directly.

Updating

$ oxup update

Updates the default toolchain to the latest channel build. Pass a specific version to update just that one: oxup update 0.4.0.

To roll back to a previously-installed toolchain: oxup default <version>. To list installed toolchains: oxup toolchain list. To remove a specific version: oxup toolchain remove <version>. To garbage-collect older toolchains while keeping the active one: oxup toolchain gc [--keep N] [--keep-active] [--dry-run]. To check authentication status: oxup auth status. To update or uninstall oxup itself: oxup self update / oxup self uninstall. To uninstall everything oxup manages (including the shell-rc edit): oxup uninstall.

Channels

oxup resolves toolchains through four channel forms (oxup/src/channel.rs::Channel):

  • stable — the newest non-prerelease tag.
  • nightly — the newest nightly-YYYY-MM-DD tag.
  • nightly-YYYY-MM-DD — a specific dated nightly.
  • An exact version string — 0.4.0, 0.4.0-rc.1, etc.

Pin a channel:

$ oxup install nightly
$ oxup default nightly

Or pin to an exact version:

$ oxup install 0.4.0
$ oxup default 0.4.0

What’s next

Open a terminal in a fresh directory. The next chapter writes the smallest meaningful Argon program and runs it through the compiler.

Hello, Argon

Argon programs start the same way Rust programs do: with the smallest piece of code that compiles. We will write that piece first and then walk through what every line means.

The smallest program

Argon does not have a single-file mode. The smallest meaningful unit is a project — a directory with an ox.toml manifest and a src/ tree of .ar modules. Set one up:

$ mkdir -p hello-argon/src
$ cd hello-argon

Write a manifest at ox.toml:

[project]
name = "hello-argon"
version = "0.1.0"
edition = "2025"
entry = "root.ar"

[schema]
root = "src"

[package]
name = "hello-argon"
version = "0.1.0"
edition = "2025"

[dependencies]

Chapter 1.3 explains the manifest’s two-section structure ([project] for the perspectival-composition layer; [package] for the registry layer); for now treat it as boilerplate.

The program we are about to write declares its metatype machinery inline so you can see Argon’s substrate forms (pub metaxis, pub metatype) at work. In a production project you would normally write use ufo::prelude::* and pull in a vocabulary package’s catalog (kind, subkind, role, …) instead — kind is not a built-in keyword, but a metatype declared by the UFO package on top of the same substrate. Either way, the substrate is what the language actually ships; Chapter 2.1 covers it in full.

Then write src/root.ar:

use std::math::String

pub metaxis rigidity for type {
    rigid,
    anti_rigid
}

pub metaxis sortality for type {
    sortal,
    non_sortal
}

pub metatype kind = { rigid, sortal }

pub kind Person {
    name: String,
}

pub derive HasName(p: Person) :-
    p: Person,
    p.name != ""

pub query AllNamedPersons() -> [Person] :-
    ?p: Person,
    HasName(?p)
    => ?p

The use std::math::String line at the top is required: Argon ships no implicit prelude, so any primitive a module references — String, Nat, Int, Real, Bool, Date, DateTime, Duration, Decimal, Money — needs an explicit import path. Bare primitive names without a use produce OE0101 UnresolvedIdentifier with a hint at the right import.

Run the type-checker:

$ ox check
ox hello-argon v0.1.0 (./ox.toml)
ox 1 modules resolved from entry point
ox 1 standpoints: default
check passed

Clean. We have an Argon program.

Walking through the program

Eight things happen in twenty-odd lines. Take them in order.

pub metaxis rigidity { rigid, anti_rigid }

Argon ships no foundational metatypes in its language core. The keywords kind, subkind, role, phase, relator — none of them exist as built-in concept-keywords. They are defined by user packages.

A metaxis declares a dimension along which metatypes vary. rigidity is one such dimension; its values are the IDENT atoms rigid and anti_rigid. The mechanism is open-ended — domain modellers in finance, biology, or law can declare their own axes (sortality, criticality, regulability, …) without changing the language.

rigidity and sortality together are enough to define the metatypes Hello, Argon needs. The axes capture (loosely) Guizzardi’s UFO classification: rigidity distinguishes “instances belong to the type permanently” from “instances belong contingently”; sortality distinguishes “the type provides identity criteria” from “the type classifies but does not individuate.” Chapter 5.1 covers the calculus formally.

pub metatype kind = { rigid, sortal }

A metatype declaration introduces a concept-keyword and pins its position along the axes. kind is now a name the rest of this file can use to declare concepts: a kind is rigid (instances belong to it for as long as they exist) and sortal (it supplies its own identity criteria).

This is the line that makes kind exist. If you delete it, the next line stops parsing.

Note: in production code you would normally use ufo::prelude::* (or similar) and pull in a curated metatype catalog instead of declaring axes and metatypes inline. This chapter shows the mechanism in full so you can see what use would otherwise hide.

pub kind Person { name: String, }

A concept declaration. Person is the name; kind is the metatype (the IDENT before the name); the body lists fields with their types. The trailing comma is allowed — keep it on, since the next chapter’s version of Person will grow more fields.

String is one of Argon’s primitive types. The full set is Nat, Int, Real, Decimal, String, Bool, Date, DateTime, Duration, and Money. Each lives in std::math and reaches a module via explicit use.

pub derive HasName(p: Person) :- p: Person, p.name != ""

A rule. HasName is the head; (p: Person) is its argument list with a single typed binding; :- separates the head from the body; p: Person, p.name != "" is the body.

A few mechanics worth pinning down:

  • Head parameters bind bare names. p in the head is the binding; the body’s atoms reference it without a prefix. The ? prefix is reserved for body-fresh variables — those introduced by an aggregate’s iteration clause or a query’s body when no head parameter is in scope. Head parameters never carry ?.
  • Comma is the conjunction separator for rule bodies. There is no && and no and keyword inside a body — just commas.
  • Type tests bind. p: Person is the body’s first atom; it both tests p’s type and re-asserts p’s binding for the rest of the body. Subsequent atoms see p narrowed to Person.

Reading the rule in English: “for any Person p, HasName(p) holds when p.name is non-empty.”

pub query AllNamedPersons() -> [Person] :- ... => ?p

A query is a first-class item: declared, type-checked, callable from the runtime by name. AllNamedPersons takes no arguments and returns a list of Person. The body looks like a rule body. The optional => head_expr after the body says what the result row is.

Because the query head takes no parameters, the body introduces a fresh variable ?p. That is the one place the ? prefix appears: query bodies (and aggregate sub-comprehensions) introduce body-fresh variables explicitly.

Read in English: “return every Person ?p for whom HasName(?p) holds.”

The [Person] syntax is a collection type: a list of Person. We will see cardinality-constrained variants like [Tenant; >= 1] (at least one tenant) in Chapter 2.3.

Running the query

Type-check is ox check. To actually evaluate the query, we need a fact in the world. The runtime command is ox query:

$ ox query AllNamedPersons
   No facts asserted; result set is empty.

That is correct. We have declared a kind, a derive rule, and a query — but we have not yet asserted any Person facts. Once we have a package and a test scaffold (Chapter 3.3), asserting facts and seeing query results will be straightforward.

What you noticed

Three things, probably.

No semicolons, but commas matter. Field lists are comma-separated; rule bodies are comma-separated; argument lists are comma-separated. Trailing commas are allowed. Statements inside imperative blocks (we have none yet — they appear in mutation bodies) do not need separators.

Whitespace is friendly, not significant. You can split a long rule body across lines or keep it on one. The compiler does not care.

Every item is opt-in to export. The default visibility is module-internal — file-scoped. Adding pub makes an item reachable from another module. There is no third tier (no per-package or per-workspace visibility); just the two.

Edge cases worth knowing

  • Rule bodies want a typed binding for every variable. p.name != "" reads p’s name field, but the body needs at least one atom that pins p’s type — typically a leading type test like p: Person. The compiler rejects rules whose bodies reference identifiers no atom binds.
  • derive with no asserted instances will not fire. pub derive Always(p: Person) :- p: Person only derives Always(p) for asserted Person instances; declaring a kind does not by itself create instances.
  • Type errors land at ox check, not at runtime. Misspelling name as nme in the body of HasName raises an error at the field-access; the query never reaches the runtime.

Putting it in the running example

The next chapter promotes this code into the lease tutorial — the running example the rest of the book extends. The metatypes we declared inline here will move into a small metatypes.ar module you write once and never re-think.

Hello, ox.toml

A single .ar file is fine for hello-world. Anything beyond that wants to be a package: a directory tree with a manifest, a stable public surface, and a lockfile so the same code resolves to the same dependencies tomorrow.

This chapter promotes the Hello-Argon program into a proper package — lease-tutorial — which the rest of the book extends.

We continue to declare metatypes inline (this time in a dedicated metatypes.ar module, shown below). A real project would replace that module with use ufo::prelude::* and pull in UFO’s catalog of kind, subkind, role, phase, relator, etc. Whichever path you take, kind is a metatype declared via Argon’s substrate, not a language built-in; Chapter 2.1 covers the substrate end-to-end.

Setting up a package by hand

The toolchain doesn’t yet ship a cargo new-style scaffolder; you create the two files yourself. From a fresh directory:

$ mkdir -p lease-tutorial/src
$ cd lease-tutorial

The package needs a manifest at the root (ox.toml) and a schema-root directory (src/) holding the .ar source files. The next two sections build them.

The manifest

A working ox.toml for the tutorial:

[project]
name = "lease-tutorial"
version = "0.1.0"
edition = "2025"
entry = "root.ar"
default-world = "open"

[schema]
root = "src"

[package]
name = "lease-tutorial"
version = "0.1.0"
edition = "2025"
description = "Running example for The Argon Programming Language"
license = "CC-BY-4.0"
max_tier = 3

[dependencies]

Four sections. They will accumulate as the package grows. Two of them — [project] and [package] — repeat the package’s identity; that is intentional, and the next subsections explain why.

[project] — the perspectival-composition layer

[project] is what ox check, ox build, ox query, and ox mutate read when they need to know how to compose your modules into a world the reasoner can execute over. Required fields:

  • name and version — the same name and version the package reports.
  • edition = "2025" — the language edition this source is written in. Editions are parse-time only; no runtime split. "2025" is current.
  • entry = "root.ar" — the entry-point module under src/. Idiomatic packages name it root.ar; the file lists use … ::* for every submodule the project should see.
  • default-world = "open" | "closed" — the world assumption applied to every module that does not override it. Open-world is the default for ontology work.

Optional siblings include [modules], [standpoints], [defeat], and [tree-shaking] — covered in Chapter 5.4 and Chapter 4.1 respectively.

[schema] — where the source lives

[schema]
root = "src"

Every path under [project] (notably entry) is resolved relative to [schema] root. The default is "." (the manifest’s directory); the convention is "src".

[package] — the registry / dependency layer

[package] is what ox install, ox publish, ox audit, ox tree, and ox why read. It is the Cargo-shaped registry surface:

  • name, version — must match [project] for hybrid manifests.
  • edition = "2025" — the registry’s view of the edition. Inherited from [workspace.package] via edition.workspace = true when the package belongs to a workspace.
  • description, license, authors, repository, homepage, documentation, readme, keywords, categories — registry-facing metadata.
  • max_tier = N — caps the decidability tier the compiler will let your rules sit at. Tier 3 (full Datalog with stratified negation) is the typical cap; lower it to be conservative, raise it if you have invariants in higher tiers that you intend to verify with @[theorem] rather than execute. Chapter 5.3 covers the full ladder.
  • default_world — the default world assumption for items declared in this package’s modules (overridden by [modules].default-world per-module).

Package names match ^[a-z][a-z0-9_-]*$; hyphens and underscores are both legal at the manifest level and canonicalize to underscores in use paths (so lease-tutorial becomes lease_tutorial).

[dependencies]

Empty for now. We will fill this in Chapter 4.1 when we start pulling in foundational vocabulary. A few shapes of dependency entry:

[dependencies]
ufo = "0.2.1"                              # registry, semver
cofris.workspace = true                    # inherit from workspace
my-local = { path = "../my-local" }        # local path

The source tree

Lay down two files under src/. First, the project entry — a root.ar that pulls every submodule into the module graph:

$ tree src
src
├── prelude.ar
└── root.ar
// lease_tutorial::root
//
// Project entry — pulls every submodule into the module graph so
// `ox check`, `ox test`, `ox build`, and `ox diagram` see the full
// project. Add `use foo::*` here when you create `src/foo.ar`.

And a curated prelude.ar — the library surface consumers will reach for:

// lease_tutorial::prelude
//
// Re-export the package's public surface here. Use `pub use foo::*`
// to thread items through. Internal modules left out of the prelude
// stay private — refactor without breaking downstream.

Both are empty for now. We will fill them in shortly.

Every .ar file under src/ is a module. The path under src/ becomes the qualified module path: src/party.ar is lease_tutorial::party, src/prelude.ar is lease_tutorial::prelude, and so on.

Adding a module

Promote our hello-world into a real module. Create src/metatypes.ar:

pub metaxis rigidity for type {
    rigid,
    anti_rigid
}

pub metaxis sortality for type {
    sortal,
    non_sortal
}

pub metatype kind = { rigid, sortal }

Then src/party.ar:

use metatypes::*

pub kind Person {
    id: String,
    name: String,
}

Wire both into the project. Edit src/root.ar to pull them in:

use metatypes::*
use party::*

And expose them through the prelude as the package’s public surface. Edit src/prelude.ar:

pub use metatypes::*
pub use party::*

pub use re-exports — items pass through prelude to consumers as if prelude had defined them itself. This is the canonical pattern: root.ar makes everything reachable for compilation; prelude.ar curates what consumers see.

The directory now looks like this:

src
├── metatypes.ar
├── party.ar
├── prelude.ar
└── root.ar

Type-check it:

$ ox check
ox lease-tutorial v0.1.0 (./ox.toml)
ox 3 modules resolved from entry point
ox 1 standpoints: default
check passed

The package compiles. We have a working skeleton.

What ox.toml is doing for you

Three things, mainly.

Resolution. ox install reads [dependencies], walks the dependency graph against the registry, picks compatible versions, and writes ox.lock. The lockfile is bivalent: it records both a content hash (byte-identical fetches) and a Merkle root over construct signatures (semantic identity — what the package actually contains). Chapter 4.2 covers the lockfile in detail.

Visibility. Two-tier: pub exports across module boundaries; the default is module-internal — file-scoped. There is no per-package or per-workspace third tier. The pub use re-export pattern in prelude.ar is how you make a package’s surface intentional rather than accidental.

Direct-dependencies rule. A package can use only items reachable through its own [dependencies], not items reachable transitively through some dependency’s dependencies. If you want to use a transitive package, add it explicitly to your manifest.

Workspace flavor

A workspace is a root ox.toml whose [workspace] section names member packages:

[workspace]
members = ["packages/lease-tutorial", "packages/lease-tests"]
resolver = "1"

[workspace.package]
edition = "2025"
license = "CC-BY-4.0"

[workspace.dependencies]
ufo = "0.2.1"

Workspace members inherit version pins via ufo.workspace = true and shared metadata via edition.workspace = true. The lockfile lives at the workspace root and is shared across members. We will use a workspace once the model has grown enough to want a separate test package.

Running things

Three commands you will use constantly inside a package:

$ ox check                    # Type-check + meta-property + package constraints
$ ox build                    # Compile to kernel types + Datalog
$ ox test                     # Run test blocks (Chapter 3.3)

Two more once there are queries and mutations to run:

$ ox query <name> [--explain]
$ ox mutate <name> --principal <id> [--dry-run] [--explain]

ox check is the fastest of these — it skips reasoning by default. Pass --reason to invoke the saturation engine; you will need it when an invariant depends on the reasoner having classified the TBox.

Edge cases worth knowing

  • root.ar versus prelude.ar. root.ar is the project entry declared in [project] entry; the toolchain starts module discovery from it. prelude.ar is the library surface — what consumers see when they use lease_tutorial::prelude::*. The convention is for root.ar to use foo::* every internal submodule, and for prelude.ar to pub use foo::* only the curated public surface.
  • Module names canonicalize. lease-tutorial becomes lease_tutorial for use paths. If you start with hyphens in your package name, expect underscores everywhere else.
  • Editions are strings. Edition "2025" is current. Editions are parse-time only; no runtime split.

Putting it in the running example

What you have now is the skeleton the rest of the book extends:

lease-tutorial/
├── ox.toml
└── src/
    ├── metatypes.ar       # the small set of metatypes we need
    ├── party.ar           # Person (and soon Tenant, Landlord)
    ├── prelude.ar         # curated re-exports — public surface
    └── root.ar            # project entry — pulls submodules in

Chapter 2.2 extends party.ar and adds lease.ar. By the end of Part 2 the package has rules, queries, computes, mutations, and refinement types. By the end of Part 3 it has a state machine, tests, and diagrams. The complete running version lives in examples/lease-tutorial/ alongside this book.

Summary

A working Argon package is ox.toml plus a src/ tree of .ar files. The manifest names the package, pins dependencies, and configures reasoning. Modules are files; the prelude curates the public surface; pub use re-exports propagate. ox check keeps you honest. The next part teaches what to put into those modules.

Foundations

This part is the atoms. By the end of it you have written enough Argon to model a domain end-to-end: the substrate that lets a vocabulary package declare metatypes in the first place, then concepts, relations, rules, computations, and a type system over them.

We start with the substrate because every other chapter in this part uses metatypes — kind, role, phase, relator — that come from a vocabulary package (UFO, in our case) built on the substrate. Knowing what those declarations actually are makes the rest of Part 2 about modeling rather than memorizing keywords.

Chapter by chapter:

By the end of Part 2 the reader has a working, queryable, validated lease model with derived state, mutations for signing and expiry, and refinement types that catch bad data at compile time — and an understanding of which keywords are language primitives and which are package declarations.

The Argon Substrate

Argon’s compiler ships with zero foundational ontology content. It does not know what a kind is, what rigidity means, or what a relator does. The keywords kind, subkind, role, phase, relator, category, mixin — all the vocabulary the introduction promised was not built in — are exactly that. Identifiers, declared by user packages, brought into scope by use.

What the compiler does ship is the substrate that lets a vocabulary package declare them. This chapter is that substrate: the four declaration forms — pub metaxis, pub metatype, pub metarel, pub decorator — that turn a Markdown-flat universe of names into a typed metaontology where the compiler can check Person : Kind, route a transitive decorator to a recognized fast-path, or refuse pub kind X : Role because rigid cannot specialize anti-rigid.

The rest of Part 2 uses UFO’s metatypes (kind, role, …) as if they were primitives, because UFO is the most established vocabulary and the chapters need something to model with. This chapter is what makes that legitimate. UFO is one user package built on the substrate. BFO is another. So is whatever your team needs that doesn’t yet exist.

Why a substrate

Foundational ontologies do not agree.

UFO carves the world along rigidity (does the type apply necessarily, or contingently?) and sortality (does it carry an identity criterion?). BFO carves along temporal status (continuant vs. occurrent) and dependence (does the entity require a specific bearer?). DOLCE has its own axes, GFO its own. They overlap in places, contradict in others. None is wrong; each is a different commitment about what categories are foundational.

A language that bakes one of these in cannot host the others. Make rigidity a built-in keyword and BFO no longer fits — BFO does not commit to rigidity at the upper level. Make temporal_status built-in and you have privileged BFO over UFO.

Argon’s solution is to ship the machinery — axes, metatypes, metarels, decorators — and let the foundational ontology supply the content. UFO declares rigidity; BFO declares temporal_status; both packages run on the same compiler. A project picks one (or several, if their axes do not collide) by writing use.

The four declaration forms

pub metaxis, pub metatype, pub metarel, pub decorator. They are language primitives — the compiler parses each as a top-level declaration form, like fn in Rust or data in Haskell. Everything else in an ontology package — the kinds, the roles, the transitive decorator — is built from these four.

pub metaxis — declare an axis

A metaxis (short for meta-axis) is a dimension along which metatypes vary. Rigidity is one. Sortality is another. Temporal-status is another. A metaxis declaration names the axis, scopes it to the kind of declaration it qualifies (type, rel, or dec), and supplies its value space.

The simplest form is a finite enumeration:

pub metaxis rigidity for type {
    rigid,
    anti_rigid,
    semi_rigid
}

rigidity qualifies type declarations. Its value set is {rigid, anti_rigid, semi_rigid}. The values are atoms — bare identifiers carving the axis into points.

A metaxis can also be a typed domain. Instead of an enumeration, it admits values from a primitive type, optionally narrowed by a refinement predicate:

use std::math::Nat;

pub metaxis cardinality_floor : Nat for rel where { _ > 0 }

The values are natural numbers; the refinement requires positivity. A metarel that binds cardinality_floor = 0 triggers a refinement violation at elaboration time. Typed-domain axes admit Nat, Int, Real, Bool, and String, with promotion Nat → Int → Real.

A package may declare any number of axes. The constraint is one of acyclicity: the meta-property calculus that runs at elaboration time builds a dependency DAG over axes (axis B reads axis A), and a cycle is rejected at vocabulary-load time. Within that constraint, axes are free.

pub metatype — declare a metatype

A metatype is a bundle of axis values. Where a metaxis says “here is an axis,” a metatype says “here is a point in axis space.” kind lives at { rigid, sortal, provides_identity }. role lives at { anti_rigid, sortal, relational }. The declaration form is a record literal over in-scope axes:

pub metatype kind = {
    rigidity = rigid,
    sortality = sortal,
    identity_provision = provides
}

(In practice, packages often elide the axis names when the values themselves are unambiguous: pub metatype kind = { rigid, sortal, provides }. Either form is accepted; the compiler resolves each value against in-scope axes.)

A metatype’s name is the surface keyword. After

pub metatype kind = { rigid, sortal, provides }

writing

pub kind Person { ... }

is a concept declaration whose metatype is kind. The grammar position where a metatype name appears — between pub and the concept’s identifier — accepts any in-scope pub metatype. There is nothing privileged about kind in the parser.

If the resolver cannot find a pub metatype matching that name, the compiler emits OE0605 UnknownMetatype with a four-variant hint: the name might be unimported, ambiguous across packages, declared as something other than a metatype, or genuinely undeclared.

pub metarel — declare a relation metatype

Concept declarations describe individuals. Relations describe how individuals connect: mediates, componentOf, inheresIn. Just as concepts have metatypes that classify them (a Person is a kind), relations have metarels that classify them (a mediates is a material_relator_relation). A metarel declaration ties a relation kind to its endpoint constraints, default cardinalities, and any decorators it implies:

pub metarel mediates = {
    source: relator,
    target: kind,
    cardinality_default: { source: [0..1], target: [1..*] },
    properties: { irreflexive, asymmetric }
}

mediates is a relation whose source must be a relator and whose target must be a kind; by default a relator mediates one or more kinds. The properties set lists structural-property names that apply to every relation tagged with this metarel. The body is record-shaped (= { ... }) — the same shape pub metatype and pub decorator use.

When user code writes pub rel teaches(t: Course, s: Student) :: mediates, the compiler validates the endpoints (Course must be a relator-typed concept, Student a kind-typed concept) and applies the cardinality defaults if the rel does not specify its own. Endpoint mismatches emit OE0226 MetarelEndpointMismatch; an unknown metarel name is OE0225 UnknownMetarel.

pub decorator — declare a decorator

Decorators are reusable annotations that lower to compiler behavior. @[transitive] on a relation tells the reasoner to compute the relation’s transitive closure. @[theorem] on a derive rule marks the rule as a theorem claim — at the current cut, the marker is preserved through emission and surfaces in the kernel’s runtime so a future verification pipeline can target it; the compile-time pass does not yet attempt mechanical verification. The decorators themselves are user-declared:

pub decorator transitive() on rel = {
    semantics: r(x),
    lowers_to: transitive_closure
}

A decorator declaration carries:

  • a parameter list (transitive is zero-arg; inverse(of: rel) takes one);
  • a target (on rel, on type, on dec, on field, on individual, or on frame);
  • a semantics body — a Datalog atom that anchors the decorator’s compile-time intent. The body’s logical content rides on the lowers_to: recognized shape, not on the surface atom itself; conventionally semantics: is a stub atom and the recognizer table picks up the shape;
  • an optional lowers_to: <shape> hint that pre-tags the decorator with a recognized canonical shape (more on this below).

When user code writes @[transitive] on a relation, the compiler binds transitive against in-scope decorator declarations, validates arity / target / argument types, and lowers the application. A mismatch emits one of OE0229 UnknownDecorator, OE0230 ArityMismatch, OE0231 TargetMismatch, OE0234 ArgumentTypeMismatch.

The lowers_to hint is the substrate’s bridge to the recognizer table — a set of canonical FOL shapes (transitive, symmetric, functional, disjoint-classes, qualified-cardinality, …) that the compiler can route to a fast-path implementation in the reasoner. A user decorator whose body matches a recognized shape gets the fast-path treatment for free; one that does not falls through to generic Datalog. Chapter 5’s Metatype Calculus covers the recognizer in detail.

How user packages compose this

UFO declares its core axes and metatypes using these four forms — nothing else. Here is the start of UFO’s prelude.ar, which ships with the package:

pub metaxis rigidity for type {
    rigid,
    anti_rigid,
    semi_rigid,
    order: anti_rigid < semi_rigid < rigid
}

pub metaxis sortality for type {
    sortal,
    non_sortal
}

pub metaxis identity_provision for type {
    provides,
    inherits,
    condition: rigidity == rigid and sortality == sortal
}

pub metatype kind     = { rigid, sortal, provides }
pub metatype subkind  = { rigid, sortal, inherits }
pub metatype role     = { anti_rigid, sortal, relational }
pub metatype phase    = { anti_rigid, sortal, intrinsic }
pub metatype relator  = { rigid, sortal, provides }
pub metatype category = { rigid, non_sortal }

That is the entire mechanism. UFO publishes this file (and a much larger one for relations, decorators, and structural-check rules); user code writes use ufo::prelude::*; from that point kind, role, phase are in scope as identifiers the parser can dispatch.

BFO is a peer package that publishes a different prelude:

pub metaxis temporal_status for type {
    temporal,
    atemporal
}

pub metaxis dependence for type {
    independent,
    specifically_dependent,
    generically_dependent
}

pub metatype continuant                       = { temporal }
pub metatype occurrent                        = { atemporal }
pub metatype independent_continuant           = { temporal, independent }
pub metatype specifically_dependent_continuant = { temporal, specifically_dependent }

BFO declares temporal_status and dependence; UFO declares rigidity, sortality, identity_provision. Zero shared source. A project that imports use bfo::prelude::* writes pub independent_continuant Object {} instead of pub kind Person {} — the same compiler, the same substrate, a different vocabulary on top.

A project can also import both, provided the axes do not collide. The meta-property calculus treats two non-overlapping vocabulary packages as independent — one’s rules do not see the other’s facts. A shared axis (both packages declare rigidity) requires a confluence check, which the calculus runs at vocabulary-load time.

Two runnable example packages exercise the substrate from opposite ends. argon/examples/bfo-smoke/ is the minimal BFO smoke test — declares two BFO axes (temporal_status, dependence), four BFO metatypes (continuant, occurrent, independent_continuant, specifically_dependent_continuant), six BFO concepts, and two BFO disjointness rules. Zero shared source with the UFO package; both compile against the same oxc. argon/examples/dolce-lite/ does the same for DOLCE-Lite.

For the four substrate declaration forms tested in isolation, the catalog ships:

// _catalog/metarel-keyword/composition/ — exercises pub metarel
// with concept-reference endpoints (RFD-0031). Three rels use the
// same metarel; the compiler validates each endpoint against the
// concept-typed source/target via the four-arm endpoint resolver.

pub metaxis dependence_axis for type { independent, dependent }
pub metatype kind = { independent }
pub metatype mode = { dependent }

pub concept Aspect <: ConcreteIndividual

pub metarel characterization = {
    source: Aspect,           // concept reference (RFD-0031 Arm 3)
    target: ConcreteIndividual,
}

pub rel inheres_in(s: IntrinsicMode, t: Quality) :: characterization

Variants: _catalog/metarel-keyword/{minimal,composition}/ and the dedicated decorator workspaces under _catalog/decorator-{theorem,monotone,cache,chain,defeat,scope,complexity,unproven}/.

What you can do with this

The practical takeaway: you are not limited to the metatypes UFO ships.

A regulated-systems project that wants to distinguish Regulation from Norm from Procedure — three concepts UFO does not separate — declares its own axis and metatypes:

pub metaxis enforceability for type {
    enforceable,
    advisory,
    informational
}

pub metatype regulation = { rigid, sortal, enforceable }
pub metatype norm       = { rigid, sortal, advisory }
pub metatype procedure  = { rigid, non_sortal, informational }

These metatypes can layer on top of UFO’s (the rigid and sortal values come from UFO’s axes) or stand alone (declare your own foundational axes; ship them in your own package). User code in the regulated-systems project then writes:

use std::math::{String, Date};

pub regulation TaxRegulation12 {
    statute_id: String,
    effective_date: Date,
    text: String
}

The compiler treats regulation exactly the way it treats kind: it resolves the metatype, propagates its axis profile to TaxRegulation12, runs the meta-property calculus, fires whatever structural rules the package declared, classifies the concept against the inferred profile, and admits the declaration if everything is consistent. There is no compiler change. There is no special case. The substrate is the mechanism; the package is the content.

The same pattern applies at the relation and decorator level. A medical project might declare a treats(source: treatment, target: disease) metarel; a legal project might declare a @[binding] decorator that lowers to a custom Datalog rule. The substrate handles both because both are built from the same four forms.

Where we go next

This chapter taught the surface of the substrate — what the four declaration forms look like, how they compose, what OE0605 and OE0226 and OE0229 are checking for. The rest of Part 2 uses metatypes from UFO to teach concepts (2.2), relations (2.3), rules (2.4), computations (2.5), and the type system over them (2.6). When those chapters write pub kind, pub role, pub relator, you now know where the keywords come from.

The deeper machinery — the IS / CAN / NOT three-valued semantics, the stratified fixpoint with its termination and uniqueness results, cross-package metatype composition, the recognizer table’s shape-dispatch rules, and the structural-check error class — lives in Chapter 5.1: The Metatype Calculus. That chapter is the place to go when you start declaring your own axes and want to understand exactly what the engine does with them.

Concepts and Hierarchies

A concept is the unit of meaning in an Argon ontology. Concepts are the things in your domain — Person, Lease, Property. They have names, fields, and supertypes. Most everything else in the language hangs off them.

This chapter teaches concept declarations from one-line forms up to multi-level hierarchies, and starts the lease tutorial in earnest. The metatype keywords used below — kind, subkind, role, phase, relator — come from the UFO package, declared via the substrate forms covered in Chapter 2.1; they are not language built-ins.

The smallest concept

pub kind Person {
    id: String,
    name: String,
}

Three things to notice.

First, kind is not a keyword — it’s an identifier that resolves to an imported metatype declaration. We met that mechanism in Chapter 1.2. For the rest of this chapter we will assume metatypes::* is in scope, which is the case in our running tutorial because prelude.ar re-exports it.

Second, the body is a comma-separated list of name: Type field declarations. Trailing comma is allowed; use it.

Third, pub is opt-in. Without it, Person is visible only inside the file. With it, Person is part of the package’s exported surface (assuming prelude.ar re-exports the module, as ours does).

Subtypes

A concept declares a supertype with <: (the specializes operator), or : as a documented sugar:

pub subkind ResidentialLease <: Lease {
    bedrooms: Int,
}

Read in English: ResidentialLease specializes Lease. It inherits all of Lease’s fields and adds one more, bedrooms. The typeset alias is also accepted (pub subkind ResidentialLease ⊑ Lease) — useful when rendering ontology source for academic publication.

Sugar: : in supertype slots. Argon also accepts : here:

pub subkind ResidentialLease : Lease { bedrooms: Int }

The two forms produce identical compilation. Why both? Argon’s two primitive relations are instantiates (: at the atom level — alice : Tenant) and specializes (<: at the atom level — Tenant <: Person). In a supertype slot the only coherent reading is specializes (you cannot instantiate a type from a type at the same metalevel), so : is admitted as a sugar for the <: of that position. New code is encouraged to use <: for clarity; existing :-form code remains valid indefinitely.

The metatype you use carries semantic weight:

  • A kind is rigid (instances belong to it for as long as they exist) and sortal (it supplies its own identity criteria). Person is a kind.
  • A subkind is a proper subtype within a hierarchy that still supplies identity. ResidentialLease is a subkind of Lease.
  • A role is anti-rigid — a thing plays it temporarily, not permanently. Tenant is a role someone plays for a while.
  • A phase partitions the lifecycle of its parent kind. We meet phases in Chapter 3.2.
  • A relator is a concept whose whole purpose is to mediate other concepts. Lease is a relator — it connects a Tenant, a Landlord, and a Property.
  • A category is rigid but non-sortal — a cross-cutting umbrella. LegalEntity might be one.

The metatype of a concept is its kind in the type-theoretic sense: it tells you what kinds of statements about identity, persistence, and instantiation hold for instances. We get to the formal calculus in Chapter 5.1; here we use the metatypes idiomatically.

Migration note: the team is exploring moving sibling-disjointness — the rule that says distinct subkind siblings cannot share an instance — from explicit partition axioms onto the subkind metatype itself, via axioms { siblings_disjoint } blocks in metatype declarations. Today’s code does not write partition axioms in this book’s tutorial because we do not need them; the constraint will become implicit when the redesign lands. See Appendix D.

Multi-level hierarchies

Concepts compose through inheritance. The lease model has two levels:

pub kind Person {
    id: String,
    name: String,
}

pub role Tenant : Person {
    contact_email: String,
}

pub role Landlord : Person {
    payout_account: String,
}

A Tenant is a Person who plays the tenant role. A Landlord is a Person who plays the landlord role. Both inherit id and name; each adds its own role-specific fields.

In a richer foundational ontology you might add a layer in between — say, LegalSubject : Person and have Tenant : LegalSubject — but the tutorial keeps this flat. The tradeoff is the usual one: more layers, more semantic precision, more to maintain.

The relator pattern

Some concepts exist precisely to connect other concepts. The lease itself is the canonical example:

pub kind Property {
    id: String,
    address: String,
}

pub relator Lease {
    id: String,
    tenant: Tenant,
    landlord: Landlord,
    property: Property,
    start_date: Date,
    end_date: Date,
    monthly_rent: Money,
}

A Lease does not exist independently of the parties it connects. Its identity comes from the Tenant, Landlord, and Property it relates, plus the time interval. The relator metatype marks this kind of “this-is-a-connection” concept distinctly from kind, which marks an independent existent.

You will recognise the pattern from foundational ontologies (UFO calls these relators; OntoUML uses the same name). Argon does not bake UFO into the language, but the pattern is naturally expressible because we have the metatype mechanism.

Note: the Money field type lives in std::math alongside the other primitives. A use std::math::Money at the top of the module brings it into scope; bare Money without an import produces OE0101.

Specializing the relator

Just as Tenant : Person specializes Person, leases specialize:

pub subkind ResidentialLease : Lease {
    bedrooms: Int,
}

pub subkind CommercialLease : Lease {
    permitted_use: String,
}

A ResidentialLease is a Lease (so it carries all the lease fields plus its own). The two siblings — ResidentialLease and CommercialLease — are intended to be disjoint: a single lease is residential or commercial, not both. Today that disjointness is not enforced by the metatype itself, so a partition assertion would express it explicitly. The redesign captured in Appendix D folds the disjointness into subkind’s sibling-axiom, which is what the rest of the book treats as the default reading.

Variations

A few common shapes worth knowing.

Concept without a body

The body is optional. A concept with no fields declares a name and a metatype:

pub kind LegalEntity { }

Often you do this when the concept is a parent type whose instances always pick a more specific subtype.

Concept with multiple supertypes

: admits a comma-separated list:

pub kind LawyerTenant : Tenant, Lawyer { }

Use this when a concept is genuinely both — a thing that plays the tenant role and the lawyer role. The compiler builds the closure over the supertype set.

Concept body extension

You can add fields, derives, and asserts to an existing concept from a different module via extend:

extend Person {
    email: String,

    derive HasEmail(?p: Person) :- ?p.email != ""
}

extend blocks admit only fields, derives, and asserts — not mutations, queries, or computes. The intent is structural extension (data shape, derived facts, invariants), not behavioural extension. We will use extend sparingly in the tutorial; it is mostly useful when adding a small amount of structure to a concept defined in a dependency.

Cross-package extend is a common shape: a downstream package adds optional fields to an upstream concept without forking it. The _catalog/concept-extend/cross-package/ workspace ships a two-package example — base-ontology exports Person; contact-extension extends it with phone-number fields and a derive rule classifying which contacts are reachable. Variants: _catalog/concept-extend/{minimal,composition,cross-package,idiom,anti-pattern,negative-orphan,negative-bad-member}/.

The cross-package variant looks like this in source:

// In base-ontology/src/prelude.ar:
pub kind Person {
    id: String,
    name: String,
}

// In contact-extension/src/prelude.ar:
use base_ontology::*

extend Person {
    primary_phone: String,
    secondary_phones: [String; >= 0],

    derive Reachable(?p: Person) :- ?p.primary_phone != ""
}

A downstream consumer that uses both packages sees a single Person concept with the union of declared fields and rules. The negative-orphan/ variant attempts to extend a Person the package does not have in scope — OE0805 OrphanExtend fires.

Edge cases worth knowing

  • Trailing comma in field lists. Allowed everywhere a comma-separated list lives. Use it; it makes diffs cleaner.
  • pub is per-item, not per-module. Each concept declares its own visibility.
  • No anonymous concepts. Every concept has a name. Inline structural types like { name: String, age: Int } are not concept declarations — those are structural records, used only in narrow positions.
  • Field types can be other concepts. tenant: Tenant (above) types the field as a Tenant instance. Argon handles the reference automatically; you do not write &Tenant or Box<Tenant>. There is no notion of borrowing in the language.

Putting it in the running example

Two new files in the lease tutorial:

src/party.ar:

use metatypes::*

pub kind Person {
    id: String,
    name: String,
}

pub role Tenant : Person {
    contact_email: String,
}

pub role Landlord : Person {
    payout_account: String,
}

src/lease.ar:

use metatypes::*
use party::*

pub kind Property {
    id: String,
    address: String,
}

pub relator Lease {
    id: String,
    tenant: Tenant,
    landlord: Landlord,
    property: Property,
    start_date: Date,
    end_date: Date,
    monthly_rent: Money,
}

pub subkind ResidentialLease : Lease {
    bedrooms: Int,
}

pub subkind CommercialLease : Lease {
    permitted_use: String,
}

Update src/prelude.ar to re-export both:

pub use metatypes::*
pub use party::*
pub use lease::*

Then:

$ ox check
   Checking lease-tutorial v0.1.0
    Finished in 0.10s

The hierarchy compiles. We have a structural skeleton — concepts, supertype relations, fields. What we do not yet have is constraints over those structures (ranges, multiplicities, refinement predicates) or rules that derive new facts. Those come in the next two chapters.

Summary

Concepts are the primary unit of meaning in Argon. They have a name, a metatype, an optional supertype list, and a field block. Hierarchies form via the supertype relation. Different metatypes — kind, subkind, role, phase, relator, category — mark different ontological commitments. The lease tutorial now has a Person/Tenant/Landlord hierarchy and a Lease relator with two subtypes. Next we attach properties and relations to it.

Properties and Relations

Concepts on their own describe a vocabulary. To say what holds between things in that vocabulary, you need properties (data attached to a concept) and relations (links between concepts).

In Argon, properties are concept fields — we have already seen them. Relations are a separate item kind, with two surface forms. This chapter teaches both, plus multi-valued fields, the relator pattern, and refinement-typed fields. The relator pattern uses UFO’s relator metatype, declared in UFO’s prelude with the substrate forms from Chapter 2.1.

Single-valued fields

We have written these. The shape is familiar:

pub kind Person {
    id: String,
    name: String,
}

Each field declares a name and a type. Types can be primitives (Nat, Int, Real, Decimal, String, Bool, Date, DateTime, Duration, Money — all in std::math) or other concepts (Tenant, Lease, Property).

A field of concept type holds a reference to an instance — Argon handles the indirection automatically. There is no notion of borrowing or ownership; fields read like values.

Multi-valued fields

Sometimes a concept needs to hold more than one of something — a property with several rooms, a lease with several tenants. Use the collection-type syntax [T]:

pub kind Property {
    id: String,
    address: String,
    rooms: [Room],
}

rooms: [Room] reads “an unordered collection of Room.” Argon does not commit you to a list-versus-set semantic at the surface; the runtime stores collections without duplicates by identity.

Cardinality constraints

A bare [T] admits any count, including zero. To pin the count, add a cardinality clause:

pub relator Lease {
    tenants: [Tenant; >= 1],
    landlord: Landlord,
    property: Property,
    start_date: Date,
    end_date: Date,
    monthly_rent: Money,
}

The >= 1 constraint says “at least one tenant.” Argon admits three operators in this position:

OperatorMeaning
>= NAt least N
== NExactly N
<= NAt most N

There is no < or >. If you find yourself wanting strict inequality, use >= N + 1 or <= N - 1 and pre-compute the bound, or model the constraint as a refinement type instead (see Chapter 2.6).

Tip: put the cardinality constraint on the field, not in a separate assert. The compiler can use the field-level cardinality during type-checking and reasoning; an assert that re-states the same constraint is redundant and will be flagged as such.

Examples

pub kind Square {
    sides: [LineSegment; == 4],
}

pub kind Committee {
    chair: Person,
    members: [Person; >= 3],
}

pub kind OptionalAddress {
    line_one: String,
    line_two: [String; <= 1],
}

Relations

Argon has a dedicated item kind, rel, for binary relations whose presence at the call site you want to be explicit. There are two shapes.

Binary form

pub rel HasResidence: Person -> Residence

A binary relation between two concepts. The arrow is ->. The relation name reads as a predicate at rule positions:

pub derive ActiveResident(p: Person, r: Residence) :-
    HasResidence(p, r),
    r.is_occupied

Binary relations can also carry attributes via a body:

pub rel Owns: Person -> Asset {
    acquired_on: Date,
    purchase_price: Money,
}

Treat the attached attributes as fields on the relation instance.

A worked example with decorator characteristics, from argon/examples/pizza/src/prelude.ar (the Manchester pizza ontology, ported faithfully):

// Transitive ingredient inclusion: every Margherita is a Pizza,
// every Pizza is a Food, every Food has ingredients, transitively.
@[transitive]
pub rel hasIngredient: Food -> Food

// Functional + asymmetric base: every Pizza has exactly one PizzaBase,
// and a base never "has" a pizza.
@[functional]
@[asymmetric]
pub rel hasBase: Pizza -> PizzaBase

// Functional spiciness: a topping has at most one Spiciness value.
@[functional]
pub rel hasSpiciness: PizzaTopping -> Spiciness

The pizza workspace exercises five relation characteristics — @[transitive], @[symmetric], @[asymmetric], @[functional], @[inverse-functional] — across class lattices with multiple inheritance. Read it end-to-end as a 200-line tour of relation-shape modeling without a foundational-ontology dependency. The argon/examples/wine/, argon/examples/skos/, and argon/examples/foaf/ workspaces show the same relation patterns at different domains.

Reified form

When a relation is rich enough that “binary plus attributes” feels strained — for instance, when the relation has its own subtypes, or participates in further relations — promote it to a concept of its own:

pub relator ResidentialLease : Lease {
    bedrooms: Int,
}

This is precisely the relator pattern from Chapter 2.2. The relator is a concept; its participants are fields on it; its identity is the tuple of its participants over a time interval.

The choice between rel and relator is mostly pragmatic: use rel when the relation is genuinely binary and lightweight; use a relator concept when it has substantial structure of its own. The lease in our running example is a relator because it carries dates, rent, subtypes, a lifecycle, and so on.

Composition

Binary relations can declare a composition chain over other roles:

pub rel composes_to: Agent -> Resource = role1.role2

The = a.b clause says composes_to is the composition of a and b — a derived relation, not a primitive one. We use this rarely in the tutorial; it is most useful when integrating with a foundational ontology that ships role compositions.

Inverses

If you want both directions of a relation explicitly, declare them both:

pub rel TenantOf: Tenant -> Lease
pub rel HasTenant: Lease -> Tenant

A derive rule can keep them synchronized:

pub derive HasTenant(l: Lease, t: Tenant) :- TenantOf(t, l)

Argon does not have a built-in inverse keyword. The pattern above is the idiom; it is explicit, which means the type-checker and reasoner both see the bidirectional structure.

Refinement types and field invariants

Argon admits refinement types — sub-types carved by a predicate over the metatype calculus. They live on a concept-decl’s where { ... } clause and are used at field positions like any other type:

pub subkind RigidLease : Lease where { A.rigidity == rigid }

pub kind LeaseAccount {
    lease: RigidLease,
    balance: Money,
}

The body of a refinement type is a conjunction of axis-predicates — A.rigidity == rigid, A.sortality == sortal, etc. — over the meta-property calculus, not instance-level field comparisons. The compiler proves this fragment decidable; Chapter 2.6 covers the surface in full.

Instance-level invariants — “monthly rent must be positive,” “grace days must be between zero and fourteen” — live on assert items in Chapter 2.4, not on field declarations. The two mechanisms compose: a refinement type narrows by metatype; an assert narrows by instance value.

Note: the refinement fragment is decidable in PTIME with respect to the size of the concept hierarchy and axis count. Membership uses Kleene’s strong three-valued semantics () under the open-world assumption with the Pietz-Rivieccio “Exactly True” designation: unknown does not satisfy refinement membership; only true does. We will revisit the OWA implications in Chapter 2.6.

The Top type

Sometimes a field can hold “anything.” Argon’s top type is (the Unicode character; the ASCII alias Top parses too):

pub kind Annotation {
    target: ⊤,
    note: String,
}

A field of type accepts any concept instance. The trade-off is the obvious one: refinement, dispatch, and reasoning on the field are limited because the compiler does not know what to assume about it. Use sparingly — for genuinely heterogeneous data like comment annotations or metadata blobs.

Putting it in the running example

Update src/lease.ar to use cardinality and refinement:

use metatypes::*
use party::*

pub kind Property {
    id: String,
    address: String,
}

pub relator Lease {
    id: String,
    tenant: Tenant,
    landlord: Landlord,
    property: Property,
    start_date: Date,
    end_date: Date,
    monthly_rent: Money,
}

pub subkind ResidentialLease : Lease {
    bedrooms: Int,
}

pub subkind CommercialLease : Lease {
    permitted_use: String,
}

For the tutorial we keep the lease single-tenanted (one Tenant, not [Tenant; >= 1]); a richer model would relax that. The refinement on monthly_rent will land in Chapter 2.6, where we introduce the dedicated refinement-type item.

$ ox check
   Checking lease-tutorial v0.1.0
    Finished in 0.10s

The model still compiles. We have a small but well-typed structural skeleton. Next chapter: rules.

Edge cases worth knowing

  • Cardinality versus comparison. The >= / == / <= inside [T; …] are cardinality operators, separate from value-comparison operators of the same spelling. The compiler tracks them in different enums precisely so this distinction is visible. You will not accidentally cross them.
  • Underscore separators in counts. [String; >= 1_000_000] works; underscores are stripped before the integer parses.
  • Multi-supertype fields. A field whose type is LawyerTenant (a concept with multiple supertypes) carries the closure of all its supertypes’ fields. The compiler enforces field-name uniqueness across the closure; if two parents declare a field with the same name and different types, you get a type error and must either rename or override.
  • Cyclic field types are fine. Person { friends: [Person] } works. The compiler builds a graph, not a tree.
  • No anonymous concepts in field positions. A field of type { name: String } (an inline record) is not admitted; declare a named concept instead.

Summary

Fields attach data to concepts; multi-valued fields use [T] with optional cardinality clauses; relations come in binary (rel name: A -> B) and reified (relator) shapes; refinement-typed fields sharpen the field’s type with a boolean predicate. The lease tutorial now has properties and relations enough to model a typical residential agreement. What it lacks is rules: the engine that turns this static structure into derived facts. That is the next chapter.

Rules and Reasoning

Rules are where Argon stops looking like Rust and starts looking like its own thing. A rule says “whenever this body is true, this head holds.” The compiler turns rules into reasoning — given some facts, it derives more facts mechanically, with provenance for every conclusion.

This chapter teaches the rule grammar, the assertion variant, the three-engine reasoning pipeline, and a first look at the decidability tier ladder (Chapter 5.3 covers it in depth). References to kind / role / phase throughout the chapter are UFO metatypes — user-declared on top of Argon’s substrate per Chapter 2.1, not built-in keywords.

A first rule

pub derive ActiveLease(l: Lease) :-
    l: Lease,
    l.start_date <= today(),
    l.end_date > today()

Read in English: “for any Lease l, ActiveLease(l) holds when l’s start date has passed and its end date has not.” Whenever the engine has a Lease instance, it checks the body; if every atom holds, it derives ActiveLease(l) as a new fact.

Five things to notice:

  1. derive introduces a rule item. The head is ActiveLease(l: Lease). The body follows :-.
  2. The body is a conjunction. Each atom must hold for the rule to fire. The separator between atoms is , — there is no && and no and keyword inside a body.
  3. Head parameters bind bare names. l in the head and body refers to the same binding. The first body atom (l: Lease) gives l its type; subsequent atoms reuse the binding. The ? prefix is reserved for body-fresh variables — those introduced by an aggregate’s iteration clause or a query’s body when no head parameter is in scope. Head parameters never carry ?.
  4. <= and > are comparison operators, not subtype operators. We will meet specializes later for subtype tests.
  5. today() is a context-supplied date function intended to return the date for the current evaluation context. It is referentially transparent for any given query — calling the same query at the same wall-clock instant should produce the same result. Status: the call parses and elaborates today, but a registered runtime evaluator is on the roadmap; in the current toolchain expressions involving today() are accepted by ox check and ox build but evaluation against a real wall clock is not yet wired. Use a Date literal (#2026-06-15#) when you need a concrete value to test against today.

Type-check it:

$ ox check
ox lease-tutorial v0.1.0 (./ox.toml)
ox 11 modules resolved from entry point
OI0804 ... derive rule `ActiveLease` classified at Tier 3 (value predicates)
check passed

The OI0804 info diagnostic is the tier classifier reporting where this rule landed on the seven-tier ladder. Three is the highest of the executable tiers — comparisons over value-typed fields put us in QF-LIA territory.

Body atoms — the small vocabulary

A rule body is a comma-separated list of rule atoms. Argon admits roughly a dozen atom shapes; in practice, most rules use four or five.

Type test

l: Lease

l is a Lease.” Narrows l to instances of Lease (or a subtype). When you want to narrow further, use a more specific type:

l: ResidentialLease

The narrowing is sound — the compiler tracks that subsequent atoms see l at the narrower type. This is occurrence typing; the soundness result is stated in Chapter 5.1.

Comparison

l.end_date > today()
p.age >= 18
n == 0

The comparison operators are ==, !=, <, <=, >, >=. They lift to rule-atom position naturally; both sides can be expressions over bound variables and constants.

Status note: comparison operand-type rule. Today’s elaborator does not type-check comparison operands against each other — p.age < "twenty" where p.age: Int and the right side is a String literal is accepted silently. The operand-type rule for comparison atoms is part of the upcoming Appendix B operator audit (“Reserved for the second edition”). Tracked at sharpe-dev/orca-mvp#412.

Predicate call

HasName(p)
ActiveLease(l)

Calls a derive head as an atom. If the call’s arguments unify with a derived fact, the atom holds. This is how rules compose: one rule’s head is another rule’s body.

Negation

not Expired(l)

“There is no derivation of Expired(l).” Argon uses negation as failure under stratified semantics (Apt, Blair & Walker, Towards a Theory of Declarative Knowledge, 1988); the engine refuses to compile cyclic negation (OE0501).

Aggregate

count(t for t in lease.tenants where t: ActiveTenant) >= 1
sum(p.amount for p in payments where p.month == #2026-01-01#)

Five aggregate functions: sum, count, min, max, avg. The for ... in path clause binds the iteration variable; where atom filters per row. The result is an expression you can compare against a constant.

Status note: aggregate carrier-type rule. The carrier-type rule for sum, min, max, avg (count is intrinsically type-agnostic — it counts witnesses regardless of type) is part of the upcoming Appendix B operator audit. Today’s elaborator does not reject sum(e.note for e in entries) where e.note: String; tracked at sharpe-dev/orca-mvp#412.

The atom families above cover the everyday surface. Five more atom shapes round out the rule-body grammar; each gets its full treatment in Chapter 5, but a short example here makes them concrete. Every example below has a matching workspace under argon/examples/_catalog/ you can cd into and run.

Advanced atoms — a brief tour

Modal — box(...) necessity, diamond(...) possibility. box(<atom>) reads “in every accessible Kripke world, this atom holds”; diamond(<atom>) is its possibility dual. Both wrap any rule-body atom (type tests, comparisons, predicate calls, even other modal atoms). Today’s elaborator accepts both and preserves the wrapper in the IR; the planned tier:modal semantics evaluates them per-world. See Chapter 5.4.

pub derive BoxedPerson(p: Person) :-
    box(p: Person)

Variants: _catalog/rule-modal-box/{minimal,composition,idiom}/, _catalog/rule-modal-diamond/{minimal,composition,idiom}/.

Allen interval relations. Argon admits the thirteen Allen relations (Allen 1983) as binary infix operators between interval-shaped values: before, after, meets, met_by, overlaps, overlapped_by, during, contains, starts, started_by, finishes, finished_by, equals. Each operator classifies the rule at tier:temporal (Tier 4); execution backed by DatalogMTL is on the roadmap.

pub derive Precedes(x: E, y: E) :-
    x: E, y: E,
    x.interval before y.interval

Variants: _catalog/rule-allen-relations/ — the multi-allen variant exercises all thirteen operators in one workspace; the idiom variant shows the canonical “no-double-booking” temporal-invariant pattern.

Quantifiers — forall and exists binding forms. Binding-form quantifiers introduce a fresh-bound variable and lift the rule to tier:fol. They live inside unsafe logic { ... } blocks and require a @[budget] annotation for fairness once tier-6 execution lands.

unsafe logic {
    @[budget(heartbeats: 1000)]
    pub derive SomeLeaseHasEmptyId() :-
        exists l: Lease where l.id == ""
}

Description-Logic-style bounded restrictions (exists(path, Type), forall(path, Type)) classify at lower tiers and don’t need unsafe logic; see Chapter 5.3. Variants: _catalog/rule-quant-{exists,forall}/.

Hypothetical reasoning — hypothetical { ... } blocks. A hypothetical { ... } block declares a counterfactual scenario: a forked knowledge state in which axioms get overridden, the reasoner re-saturates, and ox check --reason reports the diff against the baseline.

let alice: Person = { id: "alice", salary: 100000 }

pub hypothetical alice_double_salary {
    let alice.salary: Int = 200000
}

Plain ox check only confirms the block parses and elaborates; the reasoner-side fork lands at ox check --reason time. Variants: _catalog/hypothetical-block/{minimal,composition-with-derive,idiom}/.

Reflection — meta() and the is-family. meta(X) returns X’s metatype as a value; the intrinsic lowers to a CoreRuleAtom::Meta and surfaces one of the IS-facts the meta-property calculus’s Stratum 0 propagates from each declaration’s metatype profile (Chapter 5.1).

pub derive person_is_kind(p: Person) :-
    p: Person,
    meta(p) == kind

The Person :: kind form is sugar for meta(Person) == kind. The companion is-family of pattern shapes (is unknown, is ambiguous(x), is timeout(x)) handles Argon’s three-valued query outcomes at match-arm level — Chapter 3.1. Variants: _catalog/rule-meta-coloncolon/, _catalog/rule-is-reasoning-outcome/.

A worked derivation

Before running the engine on the lease tutorial, walk through what the saturator does step by step. Suppose the knowledge base contains one Lease fact:

Lease(id="lease-001", start_date=2026-01-01, end_date=2027-01-01,
      tenant=alice, landlord=bob, property=p1, monthly_rent=9500)

and the rule

pub derive ActiveLease(l: Lease) :-
    l: Lease,
    l.start_date <= today(),
    l.end_date > today()

Today’s date is 2026-06-15. The engine performs forward-chaining saturation:

IterNew derivations
0Lease(lease-001, …) (input fact)
1Check rule body for l = lease-001. Atom 1: l: Lease — yes. Atom 2: l.start_date <= today(), i.e. 2026-01-01 ≤ 2026-06-15 — yes. Atom 3: l.end_date > today(), i.e. 2027-01-01 > 2026-06-15 — yes. Body holds → derive ActiveLease(lease-001).
2No new rules whose bodies fire on ActiveLease. Quiescence.

The engine stops at iteration 2: nothing new is derivable. The set of derived facts is the least fixed point of the rule’s immediate-consequence operator over the input facts. This is the standard fixed-point construction from Datalog; the result is unique and independent of the order in which the engine considered candidate bindings.

Why-provenance traces the result back: ActiveLease(lease-001) was derived by the rule ActiveLease/1 from the input fact Lease(lease-001, …) at the body’s three satisfied atoms. ox query active_leases --explain prints this tree.

Multiple bodies, one head

Two rules with the same head form a disjunction: the head holds if any of the bodies fires.

pub derive ExpiredLease(l: Lease) :-
    l: Lease,
    l.end_date <= today()

pub derive ExpiredLease(l: Lease) :-
    l: Lease,
    Terminated(l)

A lease is ExpiredLease if its end date has passed or if it has been explicitly terminated. The compiler treats the two rules as a single Datalog disjunction at the engine level. The provenance you get back when querying tells you which body fired.

Asserts — invariants

A rule with derive produces facts. An assert declares an invariant: a body that, if it ever fires, signals a violation.

assert positive_rent(l: Lease) :-
    l: Lease,
    l.monthly_rent <= 0
    => error("monthly_rent must be positive")

Three differences from derive:

  1. The body is the violation condition. The body fires exactly when something is wrong. Read the rule above as “if a Lease exists with non-positive monthly_rent, that is the bad case.”
  2. The consequence is mandatory. => error("…") — no optional consequence. The string is the error message.
  3. No pub. Asserts are module-scope invariants, not exported.

Migration note: the team is exploring unifying assert and derive under a single rule shape with explicit event consequences (=> Error(...), => Warn(...), => FactDerived(...)). Today’s assert keyword stays the idiomatic form; the unified shape will land additively. See Appendix D.

Optional consequences on derive

A derive rule can carry a non-default consequence:

pub derive borderline_case(c: Case) :-
    c: Case,
    c.confidence < 0.7
    => flag_for_review("low confidence", c.id)

The escalate consequence sends a structured event to a human reviewer rather than tagging the case for asynchronous review:

pub derive low_confidence_decision(d: Decision) :-
    d: Decision,
    d.confidence < 0.5,
    not d.override_recorded
    => escalate("low-confidence decision", d.id, d.confidence)

Variants: _catalog/derive-escalate-consequence/ (minimal, composition-with-assert, idiom) and _catalog/derive-flag-for-review-consequence/ (same shapes).

The four consequence forms today:

FormUse
Implicit (no =>)Default. The head is asserted as a derived fact.
=> atomsAssertion form: derive multiple facts as the body holds.
=> escalate("msg", args)Send a structured escalation to a human reviewer.
=> flag_for_review("msg", args)Tag the case for review without escalating.

Most rules use the implicit form. The explicit forms are for when the head’s purpose is communication or fallback, not just fact derivation.

Migration note: the consequence forms above are being unified with the assert mechanism into a single event-typed => Event(...) system, so that escalate, flag_for_review, error, and warn become user-defined event types rather than keyword forms. The semantics stay; the surface tightens. See Appendix D.

Running the reasoner

ox check does not invoke the reasoner by default. It type-checks and runs the meta-property calculus, but full saturation is opt-in:

$ ox check --reason
ox lease-tutorial v0.1.0 (./ox.toml)
   Reasoning over 4 rules + 2 asserts...
   2 derivations
check passed

The trade-off is speed: type-check is sub-100ms; reasoning takes longer because it actually saturates the rule set. You typically run ox check in editor-loop mode and ox check --reason before commits or releases.

To actually evaluate a query against a populated knowledge base:

$ ox query active_leases
   1 result.
   Lease(id="lease-001", tenant=Tenant(id="t-1"), …)

ox query --explain adds the why-tree — the chain of facts and rule firings that produced each result.

The three-engine pipeline

Argon’s reasoner is not a single algorithm. Three engines tune themselves to different rule types:

  • Nous+ saturation — for tier:structural classification: subsumption, role hierarchies, partition. The Baader-style structural saturator (Baader et al., Pushing the EL Envelope, IJCAI 2005) lives in crates/nous. One implementation; two drivers — oxc::reason runs it at compile time, the Kernel runs it at runtime against the same evaluator.
  • DataFrog — for tier:closure through tier:recursive: derive rules, asserts, stratified negation, disjunction (Apt, Blair & Walker 1988). One implementation in crates/datalog; two drivers — oxc::reasoning::datalog_bridge at compile time, the Kernel at runtime.
  • DomainRuleEngine — for event-driven reactive rules and computations. Lives in the Kernel; runtime-only today (a compile-time driver is a development gap).

Above tier:recursive, rules parse and elaborate but do not execute. tier:fol is gated behind unsafe logic { … }; tier:modal and tier:mlt are syntax-only today.

The dispatch is automatic — you almost never have to think about which engine owns a rule. The classification surfaces through OI0804 if you want to see it.

The saturator follows a monotone-inflationary pattern: every derive rule’s body defines a monotone function on the fact set; the iterator applies the composition until quiescence. The fixpoint is finite (the lattice has finite ascending chains over a finite concept hierarchy) and unique (any two evaluation orders that respect stratification produce the same final state). Chapter 5.1 states the termination, uniqueness, and indeterminacy results in full.

The decidability tier ladder

Argon’s most distinctive design choice is making decidability a graded property, not a binary one. Every rule is classified at compile time into one of seven tiers:

TierFragmentExecutes?
0MetaPropertyOnly — pure axis lookupsYes — Stratum 0/1 of the calculus
1HierarchyTraversal — TC + countingYes — nous saturation
2CountingAndPaths — bounded pathsYes — Datalog
3ValuePredicates — QF-LIAYes — Datalog with arithmetic
4Temporal — Allen intervals (DatalogMTL)Parses; not yet executable
5BoundedFOL — Kodkod-style finite-model findingParses; not yet executable
6FullFOL — semi-decidableParses; gated by unsafe logic { ... }

The key thing: tiers 4–6 still parse and elaborate. The compiler checks them syntactically and runs the type-system; @[theorem] annotations are preserved through to the kernel as theorem markers for a future verification pipeline. What it does not do is run them at query time, because the engines for those fragments are either not yet shipped (tiers 4 and 5) or undecidable (tier 6); mechanical theorem verification itself is not yet active.

A package-wide cap lives in ox.toml’s [package] section:

[package]
name = "lease-tutorial"
version = "0.1.0"
max_tier = 3

The compiler refuses to elaborate any rule that classifies above this tier (OE1405). It is the package’s most general statement of “I will not surprise you with a query that takes forever.” The lease tutorial sits comfortably at tier 3.

Chapter 5.3 covers the full ladder, the formal classification function, @[theorem], unsafe logic, and the #[unproven] / #[assumed] test markers.

Edge cases worth knowing

  • Variables must be bound. Every variable in a body needs at least one atom that binds it (a type test, a relation atom, a path traversal). Query heads with unbound variables emit OE1409; derive bodies with unbound identifiers fail name resolution.
  • Order does not matter inside a rule body. Rule bodies are declarative — l: Lease, l.end_date > today() and l.end_date > today(), l: Lease mean the same thing. The engine picks an evaluation order that works.
  • Rule heads cannot share a name with computes. pub derive ActiveLease(...) and pub compute ActiveLease(...) would collide. The compiler tells you up front (OE0206).
  • Asserts run in ox check --reason and in tests. They do not run on every ox check (which skips reasoning for speed). Either run --reason or write a test that invokes the relevant assertion.
  • today() versus --as-of-valid. Inside a query, today() resolves to the valid time at which the query is asked. To time-travel, pass --as-of-valid <RFC-3339> to the runtime. The same rule body will fire or not depending on the temporal frame (Chapter 5.4).

Putting it in the running example

Add src/rules.ar:

use lease::*

pub derive ActiveLease(l: Lease) :-
    l: Lease,
    l.start_date <= today(),
    l.end_date > today()

pub derive ExpiredLease(l: Lease) :-
    l: Lease,
    l.end_date <= today()

assert valid_dates(l: Lease) :-
    l: Lease,
    l.start_date >= l.end_date
    => error("lease start_date must be before end_date")

assert positive_rent(l: Lease) :-
    l: Lease,
    l.monthly_rent <= 0
    => error("monthly_rent must be positive")

And src/queries.ar:

use lease::*
use rules::*

pub query active_leases() -> [Lease] :-
    ?l: Lease,
    ActiveLease(?l)
    => ?l

pub query expired_leases() -> [Lease] :-
    ?l: Lease,
    ExpiredLease(?l)
    => ?l

pub query active_residential_leases() -> [ResidentialLease] :-
    ?l: ResidentialLease,
    ActiveLease(?l)
    => ?l

Notice: queries’ bodies use ?l because the head is empty (active_leases()) — ? introduces the body’s fresh variable. Derives’ heads carry the binding name explicitly (ActiveLease(l: Lease)), so the body reuses l without ?.

Update prelude.ar:

pub use metatypes::*
pub use party::*
pub use lease::*
pub use rules::*
pub use queries::*

Then:

$ ox check --reason
   Reasoning over 2 rules + 2 asserts...
check passed

The model now derives lease state and enforces lease invariants. We have the engine running over our model.

Summary

Rules are declarative: a head holds when its body’s atoms all hold. derive produces facts; assert declares invariants. Bodies use a small vocabulary of atoms — type tests, comparisons, predicate calls, negation, aggregates — and a few advanced shapes for cases that need them. Head parameters bind bare names; ? prefixes are reserved for body-fresh variables. The reasoner is a three-engine pipeline; the tier ladder makes decidability a graded property; --reason opts in at compile time, and --explain shows why-trees at runtime. The lease tutorial now has rules, asserts, and queries. Next we add operations.

Computations and Mutations

derive and query give you reasoning. Most real ontologies need two more things: pure functions over the data (a lease’s days-until-expiry, the total rent owed) and operations that change the world (signing the lease, expiring it). Argon has dedicated item kinds for both.

compute is the function form: input → output, no side effects, fully evaluable as an expression. mutation is the state-changing form: imperative body, audit-trailed events, with bitemporal valid-time and transaction-time semantics modeled at the language surface and at the kernel API level. (ox mutate accepts --principal, --standpoint, --valid-at, and --idempotency-key; the in-process runtime applies them against the current knowledge store, while a fully durable bitemporal log lives in the kernel API tier — see Part 4.)

This chapter teaches both. The lease-tutorial examples below model parties as UFO kind / role concepts — those metatypes are user-declared via the substrate of Chapter 2.1, not built into the language; computations and mutations themselves are vocabulary-neutral.

compute — pure functions

The simplest compute is one expression:

pub compute annual_rent(l: Lease) -> Money =
    l.monthly_rent * 12

Read in English: “the function annual_rent, given a Lease, returns twelve times its monthly rent.”

A few mechanics:

  • compute is a hard-reserved keyword. Like derive and mutation, the name introduces a kind of item, not a type.
  • -> Money is the return type. Argon will not infer it; computes always declare their return type explicitly. This is intentional — the return type is part of the function’s contract.
  • = expr is the body. A single expression. No statements, no early returns, no side effects.

The body uses ordinary arithmetic over the primitive types (Int, Decimal, Money) and the operators in Appendix B. Date arithmetic — durations between dates, .days-style accessors, calendar-aware arithmetic — lives in domain packages today; the language core ships only the primitive types.

Three forms

compute admits three body shapes, each useful in different situations.

Form 1 — single expression

pub compute total_rent(l: Lease) -> Money =
    l.monthly_rent * months_between(l.start_date, l.end_date)

The most common form. Reads like a definition; lifts trivially into rule bodies; participates in the meta-property calculus and the tier classifier.

Form 2 — block with a body expression

When the computation needs intermediate bindings:

pub compute deposit_after_deductions(d: SecurityDeposit) -> Money {
    body {
        let allowed = sum(x.amount for x in d.deductions where x.is_allowed)
        let disallowed = sum(x.amount for x in d.deductions where not x.is_allowed)
        d.held_amount - allowed
    }
}

The body { let ... ; let ... ; <expr> } block carries let-statements (intermediate bindings, never side-effects) followed by a trailing expression that is the computed value. Form 2 looks imperative but evaluates as a pure expression — the engine inlines the lets and reads the trailing expression as the result.

Use Form 2 when an intermediate name makes the computation readable. When you find yourself writing five lets in a row, think about whether the intermediate values deserve to be their own computes — small composable functions read better than big block bodies.

Form 3 — Rust FFI

Some functions cannot be expressed in Argon — cryptographic primitives, regex matching, OS clock access. For those:

pub compute hash_id(input: String) -> String {
    impl rust("crypto_helpers::sha256_hex")
}

The impl rust("path::to::function") clause delegates to a Rust function bundled with your toolchain. Form-3 computes are opaque to the reasoner — the type-checker still validates the signature, but the body is dispatched through the runtime’s FFI surface. Use them sparingly; every Form-3 binding is a place the reasoner cannot see.

Tip: if a Form-3 compute crops up in a hot rule body, look for a way to refactor: either lift the rust call to a precomputed field (an asserted fact rather than a derived one), or rewrite the rule to keep the FFI off the inner loop.

Compositional bodies

Compute bodies admit the full expression vocabulary: arithmetic, if/else, match, aggregates, list literals, function calls. Tier-classification composes by taking the maximum tier across components, so if and match do not introduce new tiers — they propagate the tier of their branches.

Status note: if-condition type rule. The type rule for if-conditions (that they must be Bool-typed) is part of the upcoming Appendix B operator audit. Today’s elaborator accepts non-Bool conditions like if s.rank { … } where s.rank: Nat; tracked at sharpe-dev/orca-mvp#412.

pub compute lease_status(l: Lease) -> String =
    if l.end_date <= today() {
        "expired"
    } else if l.start_date > today() {
        "pending"
    } else {
        "active"
    }
pub compute lease_summary(l: Lease) -> String =
    match l {
        ResidentialLease(r) => "Residential lease, " + r.bedrooms + " bedroom",
        CommercialLease(c) => "Commercial lease, use: " + c.permitted_use,
        _ => "General lease",
    }

Aggregates lift naturally:

pub compute total_collected(l: Lease) -> Money =
    sum(p.amount for p in l.payments where p.cleared)

The for var in path where atom clause filters the iteration. The filter atom is the same predicate vocabulary as rule bodies.

mutation — operations that change the world

Where compute produces a value, mutation changes the model. A mutation can update fields, retract derived facts, emit typed events into the bitemporal log, and return a value.

pub mutation sign_lease(l: Lease, signed_at: Date) -> Lease {
    require {
        l.start_date < l.end_date
    }
    do { }
    emit LeaseSigned(l, signed_at)
    return l
}

Five clauses, all but do optional:

ClauseCardinalityPurpose
require { body }optional, oncePrecondition — body must hold for the mutation to fire.
retract { stmts }optional, onceFacts to remove from the knowledge base.
do { stmts }mandatory, onceField updates and intermediate lets.
emit <expr>repeatsEvents to publish into the axiom-event log.
return <expr>optional, onceReturn value (matches the declared -> T).

The recommended ordering is the table above. The parser is order-permissive — you can write emit before do — but the convention reads better in order.

A worked example using all five clauses. The retract clause removes facts from the knowledge base before the new state asserts; emit publishes the supersession event into the bitemporal log:

pub mutation correct_rent(l: Lease, new_rent: Money) -> Lease {
    require {
        new_rent > 0,
        l.monthly_rent != new_rent
    }
    retract {
        MonthlyRent(l, l.monthly_rent)
    }
    do {
        l.monthly_rent = new_rent
    }
    emit RentCorrected(l, new_rent, today())
    return l
}

Variants: _catalog/mutation-retract-clause/{minimal,composition,bitemporal,idiom}/. The catalog’s idiom/ variant pairs retract with a follow-up emit of a supersession axiom — the canonical pattern for keeping the bitemporal log audit-honest when a value is corrected.

Migration note: the team is exploring removing the do keyword: a mutation body would just be raw statements, with lets and field updates appearing directly. The emit clauses stay where they are. Today’s do { } block continues to compile; the migration will land additively. See Appendix D.

Field updates

The do { } block is where state actually changes:

pub mutation pay_rent(l: Lease, amount: Money) {
    require {
        amount > 0,
        amount <= l.monthly_rent
    }
    do {
        let new_balance = l.balance + amount
        l.balance = new_balance
    }
    emit PaymentReceived(l, amount, today())
}

Two statement forms inside do { }:

  • let name [: Type] = expr — bind an intermediate value (not a state change).
  • <field-path> = expr — set a field value (state change).

There is no compound assignment (+= or similar) yet. The Phase-B agenda has compound-assignment as an open ergonomics item; for now, write the equivalent: l.balance = l.balance + amount.

Events

emit Expr publishes the value of Expr into the bitemporal axiom-event log. The engine wraps the emission with three pieces of metadata: a valid time (when the event becomes true in the world), a transaction time (when the system learned of it), and a principal (which agent caused the emission, supplied via the --principal flag on ox mutate).

Events are first-class typed values. A user-defined event:

pub kind PaymentReceived {
    lease: Lease,
    amount: Money,
    received_at: Date,
}

Then emit PaymentReceived(l, amount, today()) constructs an instance and publishes it. The runtime’s bitemporal storage treats it as an append-only fact.

Migration note: events are moving toward being a first-class language primitive — a dedicated event item kind that ties consequences in rules and mutations through the same declared event types. Today, declare the event as a kind with the relevant fields; the migration will introduce event declarations as a richer alternative. See Appendix D.

Bitemporal axes — what the runtime gives you

Every event in the log carries two times. Concretely:

  • Valid time — the time at which the event is true in the world. By default, today() at the moment of emission, but you can override with ox mutate --valid-at #2026-01-01#.
  • Transaction time — the time at which the system learned of the event. Always the wall clock at emission; never overridable.

Queries can dial in either axis:

$ ox query active_leases --as-of-valid #2026-04-01#
   What was active on April 1?

$ ox query active_leases --as-of-tx #2026-04-15#
   What did we know was active, as of the database state on April 15?

--as-of-valid is “go back in time in the world.” --as-of-tx is “go back in time in the database.” They are independent: you can ask “as of April 15 in the database, what was active on April 1 in the world?” by combining them.

This is built in. You do not write any of it.

Forks

Sometimes you want to reason in a what-if world without mutating the canonical one. Argon’s runtime gives you forks:

$ ox mutate sign_lease --fork experimental --principal user-1 --arg l=lease-001
   Mutation applied to fork=experimental.

$ ox query active_leases --fork experimental
   ...sees the signed lease in the experimental fork only.

A fork is a structural-shared branch of the world. Mutations against a fork do not flow back to MAIN unless you explicitly promote them. We will see more of this in Chapter 5.4; here the point is just that mutations are fork-aware.

Reading the consequences

ox mutate --explain shows the mutation’s effects:

$ ox mutate sign_lease --principal user-1 --arg l=lease-001 --arg signed_at=#2026-01-15# --explain
   Preconditions:
     ✓ l.start_date < l.end_date

   Field updates:
     (none)

   Events emitted:
     LeaseSigned(lease-001, #2026-01-15#)
       valid_time = #2026-01-15#
       tx_time    = #2026-04-24T10:32:11Z#
       principal  = user-1

   Returned: lease-001

--dry-run runs the mutation without committing it — useful for testing whether preconditions hold and what events would land.

Edge cases worth knowing

  • compute Form-2 cannot emit events. That is the line between compute and mutation: computes are referentially transparent.
  • require is a precondition, not a guard. If require’s body does not hold, the mutation does not fire and an error is reported. It is not “skip silently.”
  • mutation items must be pub to be runtime-dispatchable. The CLI dispatches by qualified name; non-pub mutations are file-scoped and unreachable from outside.
  • --principal is mandatory on ox mutate. The runtime refuses to apply a mutation without an attribution. Set it; pass it; track it. The audit trail is the point.
  • Event types should be declared as concepts. Inline emit of an ad-hoc tuple loses you typed-querying later. Declare a kind for every event you emit; it pays back the keystrokes immediately.

Putting it in the running example

Add src/computes.ar:

use lease::*

pub compute annual_rent(l: Lease) -> Money =
    l.monthly_rent * 12

Add src/mutations.ar:

use lease::*

pub kind LeaseSigned {
    lease: Lease,
    signed_at: Date,
}

pub kind LeaseExpired {
    lease: Lease,
    effective_at: Date,
}

pub mutation sign_lease(l: Lease, signed_at: Date) -> Lease {
    require {
        l.start_date < l.end_date
    }
    do { }
    emit LeaseSigned(l, signed_at)
    return l
}

pub mutation expire_lease(l: Lease, effective_at: Date) -> Lease {
    do { }
    emit LeaseExpired(l, effective_at)
    return l
}

Update prelude.ar:

pub use metatypes::*
pub use party::*
pub use lease::*
pub use rules::*
pub use queries::*
pub use computes::*
pub use mutations::*

Then:

$ ox check
   Checking lease-tutorial v0.1.0
    Finished in 0.12s

The model now derives, queries, computes, and mutates. We have all three computational modes wired up.

Summary

compute is for pure functions; mutation is for state changes. compute has three body forms — single expression, block, and Rust FFI — picked by what the function does. mutation has five clauses — require, retract, do, emit, return — and operates against a bitemporal, fork-aware, audit-trailed runtime. The lease tutorial now signs and expires leases. Next we tighten the type system around the model.

The Type System

Argon’s type system is unusual on three dimensions. It is ontology-aware: the metatype calculus runs alongside type-checking, so the compiler tracks not just whether a value is a Lease but whether Lease is a rigid sortal relator. It is refinement-equipped: types can be sharpened by predicate sub-types, and the compiler proves the refinement fragment decidable. It is tier-classified: the type-checker knows which decidability tier each rule sits in and refuses to sit you above the package’s cap.

The metatype machinery the type-checker reads is itself user-declarable. UFO’s metatypes — kind, subkind, role, phase, relator — are built from the substrate forms covered in Chapter 2.1; the type-checker has no special-case knowledge of them.

Most readers will get value from the surface — primitives, concept types, generics, refinements, subtype reasoning, and the type — without reaching for the calculus underneath. This chapter sticks to the surface.

Primitive types

Ten primitives live in std::math and reach a module via explicit use:

PrimitiveWhat it carries
NatUnsigned 64-bit integer
IntSigned 64-bit integer
RealArbitrary-precision decimal with rounding semantics
DecimalArbitrary-precision decimal, text-preserved
StringUnicode text
Booltrue / false
DateISO-8601 date
DateTimeDate + clock time
DurationCalendar-aware time interval
MoneyCurrency-denominated decimal

Primitives appear as field types, parameter types, return types, and atom positions inside expressions:

use std::math::{Nat, Int, Real, String, Bool, Date, Money};

pub kind Person {
    id: String,
    age: Nat,
    height_cm: Real,
    is_active: Bool,
    born_on: Date,
}

pub compute total_owed(principal: Money, monthly_payment: Money, months: Nat) -> Money =
    principal - (monthly_payment * months)

There is no implicit prelude. A bare primitive name without a corresponding use produces OE0101 UnresolvedIdentifier with a hint pointing at the right use path.

Literals are written naturally: 42 is a Nat (or promotes to Int / Real per context), 3.14 is a Real, "hello" is a String, true and false are Bool, #2026-01-15# is a Date. Numeric promotion follows the chain Nat → Int → Real. Money literals are written as plain numerics; the compiler dispatches to the Money type when the context demands.

Concept types

Every pub kind X { … } (or pub subkind, pub role, pub relator, …) declaration introduces both a concept and a type. You can write a Lease-typed parameter, a Tenant-typed field, a [ResidentialLease]-typed return value.

pub compute primary_tenant(l: Lease) -> Tenant =
    l.tenant

The subtype graph the metatype declarations + concept hierarchy define is exactly the subtype graph the type-checker uses. So you can pass a ResidentialLease where a Lease is expected, but not the other way around without an explicit narrowing.

Collection types

Multi-valued field types we met in Chapter 2.3:

[Tenant]                 // any number, including zero
[Tenant; >= 1]           // at least one
[Field; == 4]            // exactly four
[Modifier; <= 3]         // at most three

The cardinality clause is a structural constraint on the collection’s count, not a value-comparison. The same operators (>=, ==, <=) appear elsewhere with value-comparison semantics; the type-checker keeps the two uses separated.

The Top type

When a value is genuinely heterogeneous — a comment annotation that can apply to anything, a metadata blob — type it as :

pub kind Annotation {
    target: ⊤,
    note: String,
}

The Unicode is the canonical spelling; ASCII Top parses as an alias.

A value of type accepts anything, but the compiler knows nothing about it: dispatch, refinement, and field-access on -typed values is restricted because there is no static structure to reach into. Use sparingly; it is genuinely the right tool only for heterogeneous metadata.

Refinement types

A refinement type is a sub-type of an existing type whose instances satisfy a predicate over the metatype calculus. The dedicated item:

pub subkind RigidLease : Lease where {
    A.rigidity == rigid
}

Read in English: “RigidLease is the subtype of Lease whose metatype profile carries rigidity == rigid.” Because Lease is declared as a relator and relator carries { rigid, sortal } in its profile, every Lease is a RigidLease — the refinement is near-tautological for this base. The pattern becomes useful when the base type’s metatype profile is not pinned by the declaration alone.

The where { … } block is a comma-separated conjunction of refinement predicates. Today’s surface admits predicates over the meta-property calculus only — atoms of the form A.<axis> == <value>, with not available for negation:

Refinement bodies do not admit instance-level field comparisons (e.g., start_date < end_date) or arbitrary expressions. Instance-level invariants — “this lease’s start date precedes its end date” — ride on the assert items in Chapter 2.4, not on refinement types. The two mechanisms compose: a refinement narrows by metatype (A.rigidity == rigid); an assert narrows by instance value.

Three things make refinements useful:

  1. The refinement fragment is decidable in PTIME. With respect to the size of the concept hierarchy and axis count, deciding membership in a refinement subtype is a polynomial-time procedure. ox check mechanically determines TBox-level refinement membership — given a concept declared pub subkind T : U where { … }, the compiler decides whether the refined type is satisfiable and how it sits in the subtype lattice — without invoking the reasoner.

    Status note: ABox-level refinement classification. Determining whether a specific individual (let l: Lease = { … }) actually inhabits a refinement subkind at evaluation time — i.e. assert l: ValidLease or assert ValidLease(l) against an l declared at the base type — is on the roadmap. Today the runtime classifies individuals by their declared concept and the saturation graph, but it does not yet evaluate refinement-predicate membership against an asserted individual; expect refinement-membership tests to mark #[unproven] until the classifier extends to ABox.

  2. You can layer refinements. A refinement of a refinement composes by conjunction of axis-predicates.

  3. Three-valued semantics (). Membership uses Kleene’s strong three-valued logic (, Kleene 1952) under the open-world assumption with the Pietz-Rivieccio “Exactly True” designation (Pietz & Rivieccio 2013): only on every conjunct admits membership; leaves membership indeterminate; rejects. The “Exactly True” framing — only counts as success — is the formal anchor for the fail-closed convention. Under closed-world assumption, collapses to . That $\mathsf{CAN}$ at fixpoint completion really is genuine indeterminacy (no monotone extension of the input pins it to or ) is one of the principal results stated in Chapter 5.1. Cross-standpoint federated queries lift to the four-valued extension (Belnap 1977 / Dunn 1976) where source-level disagreement surfaces as ; per-standpoint refinement membership stays in . The full algebraic substrate is Approximation Fixpoint Theory (Denecker-Marek-Truszczynski 2000).

Variants: _catalog/concept-where-clause/{minimal,composition-with-derive,boolean-conjunction,tier-aware,negative-unsatisfiable}/. The boolean-conjunction/ workspace shows the multi-axis conjunction shape (A.rigidity == rigid, A.sortality == sortal); the tier-aware/ workspace caps the refinement to tier:closure via max_tier; the negative-unsatisfiable/ workspace declares a refinement whose body conjoins mutually-exclusive axis values — a tripwire for the post-elaboration refinement pass once the ABox classifier lands (Status note above).

A worked excerpt from _catalog/concept-where-clause/boolean-conjunction/src/prelude.ar:

// A rigid, sortal concept that also provides its own identity.
// The refinement reads three axes from the meta-property calculus
// and demands IS for each.
pub subkind IdentityProvider <: Concept where {
    A.rigidity == rigid,
    A.sortality == sortal,
    A.identity_provision == provides
}

// A field typed by this refinement only admits instances whose
// elaborated meta-property profile satisfies all three axes.
pub kind Registry {
    primary: IdentityProvider,
    aliases: [IdentityProvider; >= 0],
}

The body is a conjunction over the meta-property calculus axes from Chapter 5.1, not over instance fields. The compiler proves the fragment decidable in PTIME with respect to the concept hierarchy and axis count; the tier-aware/ variant tightens this further with max_tier: closure.

Layered refinements

pub subkind SortalLease : Lease where { A.sortality == sortal }
pub subkind RigidSortalLease : SortalLease where { A.rigidity == rigid }

RigidSortalLease is a refinement of a refinement. The compiler composes the predicates: a RigidSortalLease is a Lease whose meta-property profile satisfies both axis predicates.

Why metatype-axis-only

A refinement-type body that admitted arbitrary expressions over instance fields would be undecidable in general — equality on String and inequality on Decimal quickly take you out of any tractable fragment. Argon admits only the meta-property calculus’s three-valued state in the body precisely so the decidability proof goes through. Future cuts of the language may extend the fragment with carefully-chosen value predicates (A.rigidity == rigid plus f.size < 100, where f is a finite-domain field) — see Appendix D for the trajectory.

Narrowing and occurrence typing

Once a value is narrowed to a refinement type, subsequent uses see the narrower type. Argon’s occurrence typing is sound: a narrowing introduced at one point in a rule body remains valid for the rest of the body, and the narrowing survives composition with monotone rule application.

The CWA/OWA interaction is well-behaved as well: a narrowing established in a CWA context survives transition into an OWA context, because the narrowing is monotone in the information ordering, so adding open-world facts only strengthens it.

Subtype reasoning

The compiler’s subtype lattice is the closure of the supertype declarations across all visible packages. A few mechanics:

  • A : B makes A a direct subtype of B. All of B’s fields are inherited; subtype-typed values are usable wherever B-typed values are expected.
  • Multiple supertypes are admitted. A : B, C makes A a direct subtype of both. The compiler enforces field-name uniqueness across the closure and reports a diagnostic if B and C share a field name with different types.
  • Refinements participate. pub subkind A : B where { p } makes A a refinement subtype of B; an A value can be used where a B is expected, but the reverse requires a refinement check.

The subtype operator is a primitive relation in the language core. The notation <: is the primary form; specializes is its keyword spelling; is accepted as a typeset alternate:

pub derive AnyLease(l: ⊤) :-
    l <: Lease

: (instantiates) and <: (specializes) are first-class atoms in rule bodies, queries, and assertions. They are not user-declared pub rels — the type system builds on them.

Type-checking the running example

The lease tutorial accumulates refinement at this point. Add src/types.ar:

Refinements narrow by metatype, not by instance value — that constraint is what makes the membership decision tractable. Instance-level invariants (“this lease’s start date precedes its end date”) ride on assert items per Chapter 2.4. Here we use both: a refinement type that narrows by a meta-property axis, and an assert that gates instance-level admission.

use lease::*

// Refinement narrows by axis-predicate. The compiler composes the
// `where {...}` body against each candidate type's meta-property
// profile and decides membership without invoking the reasoner.
pub subkind RigidLease : Lease where { A.rigidity == rigid }

// `ValidLease` narrows by metatype as well — it's a `RigidLease` (so
// rigid) that's additionally sortal. Both predicates compose by
// conjunction over the axes declared in `metatypes.ar`.
pub subkind ValidLease : RigidLease where { A.sortality == sortal }

// Instance-level invariants live on `assert`, not on refinement bodies.
// The gate that checks `start_date < end_date` for each instance is an
// `assert` rule, decoupled from the type-system narrowing above.
assert valid_lease_dates(l: ValidLease) :- l.start_date < l.end_date
assert valid_lease_rent(l: ValidLease) :- l.monthly_rent > 0

And update prelude.ar:

pub use metatypes::*
pub use party::*
pub use lease::*
pub use rules::*
pub use queries::*
pub use computes::*
pub use mutations::*
pub use types::*

Then:

$ ox check
   Checking lease-tutorial v0.1.0
    Finished in 0.13s

ValidLease is now a type other items can reach for. A query that returns ValidLeases is well-typed:

pub query valid_active_leases() -> [ValidLease] :-
    ?l: ValidLease,
    ActiveLease(?l)
    => ?l

The compiler verifies at type-check time that every result row satisfies the ValidLease predicate. Bad data does not survive ox check.

Typed-domain metaxis values

A metaxis declaration may bind to a typed value space rather than a finite enum:

pub metaxis order : Nat for type
pub metaxis weight : Real for type where { _ > 0 }

metaxis <Name> : <Type> for <kind> admits values of the named primitive type. The optional where { _ <pred> } clause refines the admitted value-domain; the underscore _ is the candidate-value placeholder. A literal that fails type-check against the axis’s declared value_type emits OE0603 MetaxisValueTypeMismatch; a literal that satisfies the type but fails the refinement predicate emits OE0606 MetaxisRefinementViolation.

Typed-axis values flow into metatype declarations:

pub metatype kind = { rigid, sortal, provides, order = 1 }
pub metatype higher_order_type = { rigid, sortal, provides, order = 2 }

This is the foundation for multi-level theory: kind Person carries order = 1; higher_order_type Species carries order = 2; vocabulary-defined constraint rules over order enforce MLT axioms (specialization preserves order; instantiation crosses order by exactly one).

Reflection: meta() and std::meta

The compiler exposes two reflection surfaces over the type lattice. Both are usable in any rule body, query, assertion, or constraint. Both are pure: they evaluate at elaboration time against the resolved concept graph, no kernel call required.

The meta() intrinsic

meta(X) returns X’s metatype as a value. It is a compiler intrinsic, not a std::meta predicate. The argument is either a concept name or a bound variable.

pub kind Person {}
pub kind Employee : Person {}

// Test against a known metatype keyword
pub derive is_a_kind(c: Person) :- meta(c) == kind

// Explicit subject — the concept name itself
pub derive person_is_kind() :- meta(Person) == kind

meta() is part of the language substrate; no use declaration is required to call it. (use std::meta::* is for the seven reflection predicates covered below — a separate surface.)

The argument must be ground at rule-evaluation time. A free variable that no preceding atom binds emits OE0212 MetaArgumentNotGround:

// Compile error: ?z is unbound when meta() runs.
pub derive r(c: Person) :- meta(?z) == kind

Negation composes: not meta(c) == role is well-formed. The :: sugar form (Person :: kind to test meta(Person) == kind) lowers to the same atom. Tests Chapter 3.3 covers the testing-time idioms.

std::meta — universal reflection predicates

std::meta::* exposes seven typed-domain reflection predicates over the concept graph. They are universal: ontology-neutral, available without importing any vocabulary package, and structurally classified at tier:structural so they cost nothing in the decidability budget.

use std::meta::*
PredicateSignatureWhat it binds
is_type(X)unaryUniversal — every concept satisfies it. Useful as a guard.
metatype(X, M)binaryM as the metatype keyword string ("kind", "role", "phase", …).
supers(X, S)binaryS ranges over X’s direct supertypes (the : parents).
ancestors(X, A)binaryA ranges over X’s transitive ancestors (reflexive-transitive closure of supers).
fields(X, F)binaryF ranges over X’s declared field names.
axioms(X, A)binaryA ranges over X’s declared axioms.
module(X, M)binaryM is the owning module’s qualified path.

Examples that compile against std::meta::* alone:

use std::meta::*
use std::math::*

pub kind Person { name: String, age: Nat }
pub kind Employee : Person {}
pub kind Manager : Employee {}

// Walk the supertype lattice.
pub derive direct_super(c: Person, s: Person) :- supers(c, s)
pub derive any_ancestor(c: Person, a: Person) :- ancestors(c, a)

// Surface field names and the owning module.
pub derive has_field(c: Person, f: String) :- fields(c, f)
pub derive in_module(c: Person, m: String) :- module(c, m)

// Bind the metatype keyword as a string.
pub derive metatype_of(c: Person, m: String) :- metatype(c, m)

When to use which

meta() is for asking “what metatype is this?” — a single test against a keyword. std::meta::* is for walking the structure: every supertype, every field, every ancestor of every concept. Both compose with the rest of the rule grammar; neither carries foundational-ontology opinions. Vocabulary packages layer their own classifications on top — is_kind, is_rigid, is_relator — by composing these primitives.

std::meta and multi-level theory

std::meta::ancestors plus the order : Nat for type typed-axis from the previous section give you the substrate for multi-level theory enforcement:

pub metaxis order : Nat for type
pub metatype kind = { rigid, sortal, provides, order = 1 }
pub metatype higher_order_type = { rigid, sortal, provides, order = 2 }

// MLT axiom (one form): specialization preserves order.
// A vocabulary package writes the structural rule against std::meta:
//   ancestors(X, A), meta(X) == M_X, meta(A) == M_A
//   => order(M_X) == order(M_A)

The compiler does not bake this rule in; it is a vocabulary-level constraint that consumes the universal reflection surface. That is the design intent — Argon ships the substrate, vocabulary packages contribute the discipline.

Decorators and the recognizer

Decorators are user-defined annotations whose semantics lower to logical formulas. A vocabulary declares a decorator with an explicit body:

pub decorator transitive(r: rel) on rel = {
    semantics: forall x y z. r(x, y) and r(y, z) -> r(x, z)
}

The compiler pattern-matches lowered decorator bodies against a closed set of recognized shapes (Transitive, Symmetric, Asymmetric, Reflexive, Irreflexive, Functional, InverseFunctional, QualifiedCardinality, DisjointClasses, CoveringClasses). A recognized rule gets fast-path treatment in the reasoner — enable_transitive_closure_for(r) for Transitive, and so on — and inhabits the structural tier even when the body’s syntactic form is FOL. Six normalization passes (alpha-rename, flatten-associative, push-negations-inward, implications-to-disjunctions, canonicalize-equality, sort-conjuncts) ensure equivalent formulas with different syntactic surfaces match the same canonical shape.

Decorator authors may pre-tag a body with lowers_to: <shape> for explicit fast-path declaration; the recognizer verifies the body matches the named shape and emits OE0232 RecognizedShapeMismatch on divergence. Bodies that don’t match any recognized shape fall through to generic Datalog evaluation — this is a normal outcome, not an error.

Tier classification (a brief look)

Every type-related construct is also classified at one of seven decidability tiers, the same ladder we met in Chapter 2.4. Refinement predicates that compose tier:structural atoms classify at tier:structural; refinements that include aggregates classify higher. A #dec(<tier>) directive at module / block / declaration scope binds an ambient ceiling; a refinement whose effective tier exceeds the ceiling produces OE0604 TierViolation.

A rule’s effective tier is min(syntactic_tier, recognized_tier). A forall x y z. r(x,y) ∧ r(y,z) → r(x,z) body is syntactically tier:fol, but the recognizer matches it as Transitive and the effective tier collapses to tier:closure — the rule passes a stricter ceiling that a non-recognized FOL body would not.

You will rarely see the tier classification surface unless you reach above tier:recursive. When you do, the compiler tells you, by name, which atom pushed the classification up. The InfoView panel in the editor extension (Chapter 4.3) shows the tier of any symbol on hover.

Edge cases worth knowing

  • Refinements cannot be cyclic. pub subkind A : B where { p }, pub subkind B : A where { q } — the compiler rejects this at elaboration.
  • does not absorb subtypes. Lease is a subtype of , but a -typed binding does not implicitly narrow to Lease when used; you need an explicit type test.
  • Primitive types are not concept types. You cannot declare pub kind Customer : Int { ... }. Concepts subtype other concepts; primitives are leaves of the type lattice.
  • Generics have a future. Argon has the syntactic surface for generic parameters (<T: ordered>), but most of the running example does not need them. They land in detail in Part 5.

Putting it together

The type system gives you:

  • A small set of primitives.
  • A subtype lattice generated from concept declarations, refined per-package by visibility.
  • Cardinality-constrained collection types.
  • Predicate-subtypes via refinement, with mechanical decidability.
  • A graded decidability classification that applies uniformly to types and rules.

The lease tutorial uses primitives, concept types, refinement types, and subtype reasoning. It does not yet use generics or modal-typed bindings; those wait for Part 5.

Summary

Primitives, concept types, collections, refinements, the Top type, and subtype reasoning. The refinement fragment is decidable; the subtype lattice is the closure of declarations; tier classification runs alongside type-checking. The lease tutorial now has refinement types that catch bad data at ox check time. Part 2 is complete.

Enums

An enum declaration introduces a type whose values are a finite, named set of tags. Use it when you need a type-level enumeration of mutually-exclusive cases — a status, a mode, a severity, a response category — that are not themselves concepts in your ontology and don’t carry their own data.

Enums sit alongside the other type-introducing forms in Chapter 2.6: like primitives and concepts, an enum is a thing a field can hold. Unlike concepts, enums do not participate in the metatype calculus, do not specialise other types, and do not anchor relations. They are deliberately small.

Declaring an enum

Top-level form:

enum Severity {
    Info,
    Warning,
    Error,
}

Variants are bare identifiers separated by commas. A trailing comma is permitted. A pub keyword exports the enum from its module:

pub enum LeaseStatus {
    Pending,
    Active,
    Terminated,
}

Variants may carry nested sub-variants using a brace-enclosed body. Nesting expresses a tagged hierarchy on top of the outer enumeration:

pub enum HttpResponse {
    Ok,
    Error {
        NotFound,
        ServerError,
        Timeout,
    },
}

Argon enums do not carry payload data on their variants. A variant is a name; if you need a name plus associated structure, the right form is a concept hierarchy (covered below).

Doc comments attach to the enum and to each variant:

/// The lifecycle of a residential lease.
pub enum LeaseStatus {
    /// The contract has been signed but the term has not begun.
    Pending,
    /// The term is in effect.
    Active,
    /// The term has ended; deposits and reconciliations remain.
    Terminated,
}

Using an enum value

An enum is a type. It can appear anywhere a type can — as a field type on a concept, as a parameter type on a compute, as a filler in a relation’s range:

use std::math::String

pub enum LeaseStatus { Pending, Active, Terminated }

pub kind Lease {
    id: String,
    state: LeaseStatus,
}

Variants are referenced by name. When the enum is unambiguously in scope, the bare name resolves:

test "newly-signed lease starts pending" {
    let l: Lease = {
        id: "lease-001",
        state: Pending,
    }
}

When two enums in scope expose a same-named variant, qualify with the enum’s name and a dot:

use std::math::String

pub enum LeaseStatus { Active, Inactive }
pub enum AccountStatus { Active, Frozen }

pub kind Account {
    id: String,
    lease_state: LeaseStatus,
    acct_state: AccountStatus,
}

test "fully qualified variants" {
    let a: Account = {
        id: "a-001",
        lease_state: LeaseStatus.Active,
        acct_state: AccountStatus.Active,
    }
}

The qualified form is <EnumName>.<VariantName>. Nested variants extend the path: a HttpResponse.Error.NotFound expression names the deepest tag.

When to reach for an enum

Argon offers three forms that all enumerate something. They serve distinct purposes; mixing them up produces models that are harder to reason about than they need to be.

FormUse when
enum E { A, B, C }The cases are tags. They don’t have their own roles, fields, or relations. You’ll branch on them in a compute or read them out of a field.
Concept hierarchy (subtypes of a parent metatype)Each case is a concept that lives in the lattice. It can specialise further, anchor relations, refine its members, fire rules, and participate in structural reasoning. (Covered in Chapter 2.2.)
Typed-domain metaxisThe values feed the metatype calculus — they’re values of a meta-axis bound to a primitive type, not values of a domain field. (Covered in Chapter 2.6.)

A useful test: can a variant own a relation? If yes, it’s a concept and you want a hierarchy of subtypes. If no, it’s a tag and you want an enum.

// Tag: the response category doesn't have its own structure.
pub enum HttpStatus { Ok, Created, NotFound, ServerError }

// Concept: each lease phase carries its own roles, rules,
// and may refine further — each phase is a concept in the lattice.
pub kind Lease {}
pub phase SignedPending : Lease {}
pub phase Active : Lease {}
pub phase Terminating : Lease {}
pub phase Settled : Lease {}

Edge cases worth knowing

Empty enums are accepted. pub enum Marker {} elaborates today. There is no diagnostic for it. Empty enums are rarely useful and a future cut may flag them; don’t rely on the silence as endorsement.

Variant-validity at assignment time is not yet checked. A field declared as state: LeaseStatus will currently accept any identifier in its position — including identifiers that aren’t variants of LeaseStatus. The check is queued for a future cut; today the elaborator records the variant reference but does not yet emit a diagnostic when the reference doesn’t resolve to one of the enum’s declared variants. Treat the type annotation as documentation until the check lands.

Duplicate variant names within one enum elaborate today. A future cut will reject them; in the meantime keep variants unique by hand.

Cross-package referencing. A pub enum in package A is reachable from package B via the standard use path:

use lease_legal::LeaseStatus

Bare-name resolution applies once the use is in scope; otherwise qualify with the package-prefixed path.

Enums and the metatype calculus. Enum types are not metatypes. They cannot appear in metaxis declarations as the carrier of an axis (use a typed metaxis : <Primitive> for that), and they do not specialise via <:. They are leaves in the type system, not nodes in the lattice.

Putting it in the running example

The running lease model in Chapter 2.2 treats each lifecycle phase — SignedPending, Active, Terminating, Settled — as a concept in its own right, declared as phase subtypes of Lease. That is the right call when each phase carries its own roles, refinements, and per-phase rules. Phases are concepts.

A small enum sits alongside the phase hierarchy for an orthogonal concern — the disposition of an emitted notice:

use std::math::String

pub enum NoticeDisposition {
    Pending,
    Acknowledged,
    Disputed,
}

pub kind RentNotice {
    notice_id: String,
    disposition: NoticeDisposition,
}

The disposition is a tag attached to the notice. It doesn’t itself anchor roles or rules — those live on RentNotice — and a Disputed notice doesn’t structurally specialise into a sub-concept the lattice cares about. Enum is the lighter form, and it’s the right one here.

Worked examples in the codebase

Two of the foundational example workspaces use enums idiomatically:

  • argon/examples/pizza/src/prelude.ar declares pub enum Spiciness { Hot, Medium, Mild } and attaches it to PizzaTopping via hasSpiciness: PizzaTopping -> Spiciness. The variants are tags — a Hot topping does not own its own relations, fields, or further sub-spicinesses; the enum is the right form.
  • argon/examples/time/src/prelude.ar declares pub enum DayOfWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } and pub enum MonthOfYear { January, …, December }. These are textbook enums: the values are bare tags drawn from a finite, ordered set, and OWL-Time uses them in dayOfWeek and monthOfYear datatype properties without anchoring relations on the variants.

If you want a contrast — a case where what looks like an enum should actually be a concept hierarchy — read argon/examples/pizza/src/prelude.ar’s PizzaTopping lattice. There are 30+ tagged topping concepts (Mozzarella, Pepperoni, JalapenoPepperTopping, …), each capable of further specialisation, each anchoring hasSpiciness and hasCountryOfOrigin relations. Concept hierarchy is right; enum would be wrong.

Summary

  • [pub] enum Name { Variant, ... } introduces a type whose values are a finite set of named tags.
  • Variants are bare identifiers; nested variants extend the path (Outer.Inner.Leaf).
  • Doc comments attach to the enum and to each variant.
  • An enum is a type — fields and parameters can hold its values; variants are referenced bare or EnumName.Variant.
  • Reach for enum when the cases are tags. Reach for a concept hierarchy when the cases are themselves ontology objects with structure. Reach for a typed metaxis when the values drive the metatype calculus.

Collections

Most real models eventually need to talk about many things at once. A building has units; a unit has tenants; a lease has parents on the title; a tax filing has dependents. Argon’s collection surface — sets, lists, maps, optionals, and ranges — gives you the shapes you reach for, the operations that go with them, and an expression-level surface (method calls, indexing, slicing, comprehensions, membership tests) that reads like the languages you already know.

This chapter sticks to the collection substrate that ships with the language. Everything in it lives under std::collection (with Range under std::math); none of it is a user-declarable concept, and none of it comes from a foundational-ontology package. The reason that matters is the next section.

Why collections live in the substrate

Argon does not admit user-declared parametric concepts. A modeler cannot write pub kind Container<T> and have the type-checker treat Container[Person] as a distinct type from Container[Organization]. That position is deliberate: parametric concepts would commit the language to a particular reading of how foundational ontologies handle type families, which would couple Argon to a specific foundation. Argon stays foundation-neutral on that question; user code consumes a small closed set of parametric type constructors, and the substrate ships them.

The collection surface is therefore a fixed inventory:

ConstructorModule pathReading
Set[T]std::collection::SetUnordered, distinct, no positional access.
List[T]std::collection::ListOrdered, allows duplicates, indexable.
Map[K, V]std::collection::MapKey-value; K must be orderable.
Optional[T]std::collection::OptionalZero or one value (T? is sugar).
Range[T]std::math::RangeAn ordered interval over an orderable primitive.

Range lives under std::math, not std::collection, because it is parametric over any orderable primitive (a numeric range, a date range, a money range) rather than collection-shaped. The four under std::collection carry an operation surface that the language treats uniformly; Range carries a small operation surface of its own.

The substrate cannot ship a new constructor without a compiler change. That is the cost of foundation-neutrality on this point: the door to user-declared parametric concepts stays shut, and the closed inventory is the consolation. The everyday modeling cases fit.

The five type constructors

A collection-typed field, parameter, or return looks like any other type position:

use std::math::{Nat, Text, Money, Date}
use std::collection::{Set, List, Map, Optional}
use std::math::Range

pub kind Building {
    id: Text,
    units: List[Unit],
    tenants: Set[Person],
    primary_contact: Optional[Person],
    rent_band: Optional[Range[Money]],
}

pub kind Unit {
    number: Text,
    occupants: Map[Date, Person],
}

The brackets Set[T] are mandatory. Argon’s expression grammar reserves < and > for comparison; angle-bracket generic syntax would fight with a < b and force lookahead. Brackets make the grammar unambiguous.

T? is sugar for Optional[T]. The two spellings lower to the same type at elaboration, and either is well-typed:

pub kind Lease {
    cosigner:           Optional[Person],
    promo_code:         Text?,
}

T? is the idiomatic surface for the common 0-or-1 case. Reach for the unsugared Optional[T] when you want the optionality to read prominently in the field shape — typically when the inner type itself is a collection (Optional[List[Person]] reads more clearly than List[Person]?).

A Map[K, V] requires K to be orderable. Concept-typed keys are admitted because every kernel id orders; primitive keys order naturally. The map is built on the same deterministic ordering the kernel uses elsewhere — never insertion order.

A Range[T] is parametric over any orderable primitive. The most common instances are Range[Nat], Range[Money], and Range[Date].

For Agents: every collection type constructor uses brackets, not angle brackets. Writing Set<T> is a parse error today. The token < always means “less than” in this grammar.

Field cardinality vs List[T]

Concept declarations have used multi-valued field syntax since Chapter 2.3:

[Tenant]                 // any number, including zero
[Tenant; >= 1]           // at least one
[Modifier; <= 3]         // at most three
[Field; == 4]            // exactly four

That form is for relation-shaped fields: the cardinality bound is a structural constraint on the underlying binary relation, not a collection object. It lowers to Set[T] semantics by default. The cardinality clause survives elaboration as a constraint over the relation’s count.

List[T], Set[T], Map[K, V], and Optional[T] are collection-typed fields: the field’s value is a single collection object that carries its members. Cardinality on these is expressed by predicates over .size() in a where clause, not by an inline bound.

When do you reach for which?

  • The field is conceptually a relation between the owning concept and others (a building’s tenants; an organisation’s employees): write [T; <bound>]. Order does not matter; duplicates are not meaningful.
  • The field’s value is conceptually a collection object the modeler reaches into with methods, comprehensions, or indices: write Set[T] / List[T] / Map[K, V] / Optional[T].
  • The field is ordered: write List[T], or (if you want to keep the cardinality-bound surface) [T; <bound>, ordered].

The two forms share storage shape — both lower to a collection value at the data layer — but the expression-level operations differ. Method calls (xs.size(), xs.append(x)) are available on the collection-typed forms; relation-shaped fields participate in rule bodies through the relation’s atoms.

One special case carries a quickfix hint:

pub kind Lease {
    cosigner: [Person; <= 1],   // OW2402 fires here
}

[T; <= 1] is a singleton-bounded set. The semantics are identical to Optional[T], but the reading is different: a <= 1 cardinality reads like “at most one tenant”, which is fine; Optional[T] (T?) reads like “a value that may or may not be present”, which is what the surface usually means. The compiler emits OW2402 suggesting the rewrite. Take the suggestion when the intent is “value that may be absent”; keep [T; <= 1] when the intent is “this relation, capped at one”.

Method-call surface

Every collection operation is invocable by method-call syntax against a typed receiver:

pub compute count_units(b: Building) -> Nat = b.units.size()
pub compute knows_tenant(b: Building, p: Person) -> Bool = b.tenants.contains(p)

The xs.m(a, b) form desugars at elaboration time to <TypeOf(xs)>::m(xs, a, b) — a qualified call into the receiver’s submodule. There are no traits; there is no runtime method-resolution table; the receiver’s type at the call site determines the submodule, and the submodule lookup succeeds or fails at elaboration.

b.units.size()
// elaborates to
std::collection::List::size(b.units)

The desugar is mechanical and total. Unknown method names produce OE2402 UnknownCollectionMethod with a “did you mean” hint listing the valid methods on the receiver’s submodule. Wrong-typed receivers — calling .size() on something that isn’t a collection — produce OE0101 UnresolvedIdentifier at the method position.

Chained calls compose left-to-right:

pub compute active_unit_count(b: Building) -> Nat =
    b.units.filter(unit_is_active).size()

The chain is read as (b.units).filter(unit_is_active).size() and elaborates two qualified calls. Each intermediate value carries its own type; the chain is well-formed exactly when each step’s receiver matches the next step’s expected receiver.

Higher-order arguments accept two forms:

  • Compute references — the bare name of a pub compute in an argument position passes that compute as a callable value. The elaborator validates the compute’s signature against the operation’s expected closure shape; mismatches fire OE2407.
  • Comprehensions — inline transformations that don’t need a named function. See the comprehensions section below.

First-class lambdas (|x| x + 1) and explicit function-type syntax (T -> U) are deferred. The two forms above cover the common cases without committing the language to a function-type vocabulary.

Comprehensions

A comprehension produces a List[U] (or, when the source is a Set, a Set[U]) by walking a source collection, optionally filtering, and projecting each surviving element:

pub compute active_unit_numbers(b: Building) -> List[Text] =
    [u.number for u in b.units where unit_is_active(u)]

The shape is [<projection> for <binder> in <source> where <predicate>]. The where clause is optional. The binder is a fresh name local to the comprehension body; the projection and the predicate both see it.

Comprehensions reuse the same binding subgrammar that aggregates use in rule bodies — sum(r for s in path where pred). The semantics are the same: walk the source, retain elements satisfying the predicate, project the surviving expression.

Multiple binders are not yet admitted in the v1 surface — one source, one binder, optional where. Multi-source comprehensions, when they land, will read as [expr for x in xs, y in ys where pred] and desugar to nested filter-maps.

A comprehension desugars to a filter-map chain:

[u.number for u in b.units where unit_is_active(u)]
// elaborates to
b.units.filter(unit_is_active).map(<closure projecting u.number>)

The projection’s closure has the same elaboration discipline as a named compute: the binder is in scope, and the body must type-check against the operation’s expected closure shape.

Shadow-with-warning. If the comprehension’s binder name collides with an in-scope let binding or parameter, the comprehension shadows the outer binding within its body and the compiler emits OW2403 ComprehensionBinderShadowsOuter. The warning matches what Datalog rule-body conventions already do — fresh binders per rule body — and keeps modelers from accidentally believing the outer name is in scope. Rename the binder to silence the warning.

pub compute weird(units: List[Unit], unit: Unit) -> List[Text] =
    [unit.number for unit in units]   // OW2403 — `unit` shadowed

Indexing, slicing, membership

Three postfix forms extend the method-call surface:

Indexing. xs[i] desugars to <TypeOf(xs)>::at(xs, i) returning Optional[T]. The index type must be Nat for a List; for a Map, the index must match the declared key type and the operation desugars to Map::get. Set[T] rejects indexing with OE2406 — set elements have no positional identity, so s[0] has no meaning.

pub compute first_unit(b: Building) -> Optional[Unit] = b.units[0]
pub compute tenant_at(b: Building, d: Date) -> Optional[Person] =
    b.units[0].occupants[d]

Slicing. xs[i..j] desugars to <TypeOf(xs)>::slice(xs, Range::new(i, j)), returning List[T]. Half-open i..j, open-ended i.. and ..j are admitted. Slice bounds are checked at elaboration time when both bounds are literals (xs[5..2] fires OE2405); dynamic bounds defer the check to runtime.

pub compute first_three(b: Building) -> List[Unit] = b.units[0..3]
pub compute tail(b: Building) -> List[Unit] = b.units[3..]

Range literals. i..j constructs a half-open Range[T]; i..=j constructs an inclusive range. The range never materializes its element sequence in the v1 surface; Range::contains(r, x) answers the membership question without iterating.

pub compute rent_in_band(l: Lease, lo: Money, hi: Money) -> Bool =
    Range::new(lo, hi).contains(l.monthly_rent)

Membership. x in xs desugars to <TypeOf(xs)>::contains(xs, x). x not in xs desugars to the negation. The form is admitted at expression position and at rule-atom position:

pub derive premium_unit(U) :-
    Unit(U),
    monthly_rent(U, R),
    R in Range::new(2500, 5000)

pub compute is_listed(b: Building, p: Person) -> Bool = p in b.tenants

Operation catalog

The v1 surface ships a fixed set of operations per type constructor. The table records the operation’s signature, its return shape, its decidability tier (the per-context admission cell uses this — see the next section), and a one-line example.

The signature notation reads op(receiver, args...) -> Return. Where the return depends on the receiver’s element type or on a closure argument’s return type, that is called out.

Set[T]

OpSignatureReturnTierExample
ofvariadicSet[T]closureSet::of(1, 2, 3)
emptynullarySet[T]closureSet::empty()
size(Set[T]) -> NatNatclosures.size()
contains(Set[T], T) -> BoolBoolclosures.contains(x)
union(Set[T], Set[T]) -> Set[T]receiverclosures.union(t)
filter(Set[T], pred) -> Set[T]receiverexpressives.filter(p)

List[T]

OpSignatureReturnTierExample
ofvariadicList[T]closureList::of(1, 2, 3)
size(List[T]) -> NatNatclosurexs.size()
contains(List[T], T) -> BoolBoolclosurexs.contains(x)
at(List[T], Nat) -> Optional[T]Optional[T]closurexs.at(0)
append(List[T], T) -> List[T]receiverclosurexs.append(x)
slice(List[T], Range[Nat]) -> List[T]List[T]closurexs.slice(0..3)
map(List[T], f: T -> U) -> List[U]derivedexpressivexs.map(f)
filter(List[T], pred) -> List[T]receiverexpressivexs.filter(p)
fold(List[T], U, f: (U, T) -> U) -> Uaccumulatorrecursivexs.fold(0, f)

Map[K, V]

OpSignatureReturnTierExample
ofvariadic key-value pairsMap[K, V]closureMap::of((k, v), ...)
get(Map[K, V], K) -> Optional[V]Optional[V]closurem.get(k)

Optional[T]

OpSignatureReturnTierExample
Some(T) -> Optional[T]Optional[T]closureSome(x)
None() -> Optional[T]Optional[T]closureNone()
is_some(Optional[T]) -> BoolBoolclosureo.is_some()
is_none(Optional[T]) -> BoolBoolclosureo.is_none()
unwrap_or(Optional[T], T) -> Telementclosureo.unwrap_or(default)
map(Optional[T], f: T -> U) -> Optional[U]derivedexpressiveo.map(f)

Range[T]

OpSignatureReturnTierExample
new(T, T) -> Range[T]Range[T]closureRange::new(0, 10)
contains(Range[T], T) -> BoolBoolclosurer.contains(x)

The v1 surface is intentionally narrow. Set algebra (intersection, difference, symmetric difference), additional list ops (prepend, insert_at, sort, take_while, …), additional optional ops (flat_map, and, or), and Range::collect (which would materialize a range’s elements) are tracked as follow-up work. Until they land, modelers compose the v1 ops via comprehensions and fold.

Tier dispatch matrix

Every operation carries a decidability tier (the rightmost column of each table above). The tier interacts with the surrounding evaluation context: different bodies admit different sets of operations.

Contextclosure-tierexpressive-tierrecursive-tier
pub compute bodyyesyesyes
pub mutation do { }yesyesyes
pub derive bodyyesreject (OE2408)reject (OE2408)
query bodyyesreject (OE2408)reject (OE2408)
Refinement where { }reject (OE2408)rejectreject
test blockyesyesyes

The reading is:

  • pub compute / pub mutation / test admit the full operation surface. These contexts are where transformations live; they are tier-tolerant by design.
  • pub derive / query admit only closure-tier operations. Rule bodies in these contexts feed the kernel’s saturation; higher-tier operations risk pushing the rule above the closure ceiling that derivation depends on for tractable evaluation.
  • Refinement where { } rejects every collection operation. Refinement bodies admit predicates over the metatype calculus only — see Chapter 2.6 — and collection ops are out of fragment.

The closure-tier operations (size, contains, union, at, append, slice, get, Some, None, is_some, is_none, unwrap_or, Range::new, Range::contains) are the ones a rule body or query body can call freely. The expressive- and recursive-tier ops (map, filter, fold) move to a pub compute body if you need them inside a rule’s logic.

A rule-body violation produces OE2408 with a hint suggesting the move:

error[OE2408]: higher-tier collection op in restricted context
  --> rules.ar:14:18
   |
14 |     adult_count = b.tenants.filter(is_adult).size(),
   |                            ^^^^^^^ filter is `tier:expressive`
   |
   = note: `pub derive` bodies admit only `tier:closure` ops
   = help: lift the transformation into a `pub compute` and call it
           from the rule body

Functional semantics and the rebuild-and-assign idiom

Every collection operation is pure: it returns a new collection and leaves the receiver unchanged. xs.append(x) does not modify xs; it returns a new List[T] whose elements are xs’s followed by x.

In a pub mutation do { } body, the idiomatic way to “modify” a collection-valued field is to rebuild it and assign:

pub mutation add_parent(l: Lease, p: Person) {
    do {
        l.parents = l.parents.append(p);
    }
    return l;
}

The right-hand side computes a new List[Person]; the assignment rebinds the field. The kernel records the change as a new GroundAssertion in the append-only event log — every collection-valued property change is a single event, not a partial-update event. The discipline matches the storage model byte for byte.

In-place mutators (l.parents.insert!(p), l.parents.push!(p)) are deferred. The rebuild-and-assign form is the only mutation idiom in v1.

For optional fields, the same pattern applies:

pub mutation set_primary(b: Building, p: Person) {
    do {
        b.primary_contact = Some(p);
    }
    return b;
}

pub mutation clear_primary(b: Building) {
    do {
        b.primary_contact = None();
    }
    return b;
}

Optional under the open-world assumption

Optional[T] interacts with Argon’s open-world semantics in a way that matters for two ops: is_some and unwrap_or.

is_some(o) is classical on presence. If o is Some(_) it returns true; if o is None() it returns false. The truth value of the inner element does not enter the determination — is_some(Some(undefined)) returns true because the option has a value, even if that value’s truth status is itself Undefined.

unwrap_or(o, default) returns the inner value when o is Some(_), returning default only when o is None(). Critically, unwrap_or(Some(Undefined), default) returns Undefined, not default. The fallback applies to absence, not to the inner element’s truth status.

The reason: K3 (the three-valued logic the language uses for refinement under OWA — see Chapter 2.6) distinguishes “no value here” (a structural absence) from “value present but its truth is unknown” (an epistemic gap on the value itself). unwrap_or collapsing both cases would lose the distinction; the truth-value semantics rely on the distinction surviving.

When the intent is “if the inner value is undefined, substitute a default,” chain a separate operation that operates on the inner value:

pub compute rent_or_zero(l: Lease) -> Money =
    l.monthly_rent.unwrap_or(0)

// If `l.monthly_rent` is `Some(undefined)`, the result is undefined.
// To force a fallback on undefined values, use a refinement
// or `match` over the truth state of the inner value.

The Argon truth-value substrate (RFD-0037) covers the bilattice mechanics. Optional[T]’s semantics are downstream of that substrate: presence is classical; truth-of-the-inner-value follows whichever lattice context the surrounding standpoint declares.

Worked example

A small but real exercise. A residential building owns units; each unit has occupants over time; each occupant has a contact; the building reports a roster.

use std::math::{Nat, Text, Money, Date}
use std::collection::{Set, List, Map, Optional}
use std::math::Range

pub kind Person {
    id: Text,
    full_name: Text,
    contact_email: Optional[Text],
}

pub kind Unit {
    number: Text,
    occupants: List[Person],
    rent: Money,
}

pub kind Building {
    id: Text,
    units: List[Unit],
    rent_band: Optional[Range[Money]],
}

// Closure-tier predicates — usable in rule bodies, query bodies,
// compute bodies, mutation bodies.
pub compute has_occupants(u: Unit) -> Bool = u.occupants.size() > 0

pub compute rent_in_band(u: Unit, band: Range[Money]) -> Bool =
    band.contains(u.rent)

// Expressive-tier transformations — compute / mutation / test only.
pub compute occupied_units(b: Building) -> List[Unit] =
    b.units.filter(has_occupants)

pub compute unit_numbers(b: Building) -> List[Text] =
    [u.number for u in b.units]

pub compute roster(b: Building) -> List[Text] =
    [p.full_name for u in b.units where has_occupants(u)
                 for p in u.occupants]
    // Multi-source comprehensions are deferred; today this would
    // chain two computes or compose two filter-map calls. Shown
    // here as the form the v2 surface will admit.

// Optional unwrap with a fallback.
pub compute contact_or_placeholder(p: Person) -> Text =
    p.contact_email.unwrap_or("(no contact)")

// Membership at rule-atom position — closure-tier, admitted in
// derive bodies.
pub derive premium_unit(U) :-
    Unit(U),
    rent(U, R),
    R in Range::new(2500, 5000)

// Mutation — rebuild and assign.
pub mutation add_occupant(u: Unit, p: Person) {
    do {
        u.occupants = u.occupants.append(p);
    }
    return u;
}

// Setting an optional field.
pub mutation set_rent_band(b: Building, lo: Money, hi: Money) {
    do {
        b.rent_band = Some(Range::new(lo, hi));
    }
    return b;
}

Running ox check against this module exercises every operator the chapter introduced: bracket type construction, the ? sugar at none of the field sites (intentional — every optional field here uses the unsugared Optional[T] so the optionality reads loud), method-call dispatch, comprehension, indexing through at, range construction and membership, the in operator at rule-atom position, and the rebuild-and-assign mutation idiom.

The compute roster references multi-source comprehensions; the v1 elaborator does not yet admit them (OE2402 fires today with a hint pointing at the tracking issue). The single-source forms are shipping; the multi-source form lands with the v2 surface.

For Agents

Four idioms cover the bulk of collection work:

  • Comprehension over method chain. Prefer [u.number for u in b.units where unit_is_active(u)] over b.units.filter(unit_is_active).map(unit_to_number) when the projection is a small expression. The comprehension reads closer to the intent. Reach for the chain when each step has a name worth preserving.
  • Optional unwrap with fallback. o.unwrap_or(default) covers presence-based defaulting. Remember that the fallback applies to absence only; if the inner value is Undefined, the result is Undefined. When you want “either way, substitute a default”, chain a separate operation that operates on the inner value’s truth state.
  • Rebuild-and-assign for mutation. do { l.parents = l.parents.append(p) } is the only way to “modify” a collection field in v1. The functional semantics are deliberate; the kernel’s event log depends on every collection-valued property change being a single GroundAssertion.
  • [T; <=1] should become T?. The compiler emits OW2402 on the singleton-bounded form. Take the suggestion when the field’s intent is “value that may be absent”; the Optional form unlocks the full optional op surface (is_some, unwrap_or, map). Keep [T; <= 1] only when the field genuinely reads as “this relation, capped at one occurrence”.

A fifth, less frequent: the tier matrix is the gate. A map / filter / fold call in a derive rule body fires OE2408; lift the transformation into a pub compute and call the compute from the rule. The tier ceiling is what keeps rule-body evaluation tractable.

Summary

  • Set[T], List[T], Map[K, V], Optional[T] live in std::collection. Range[T] lives in std::math. Brackets are mandatory; T? is sugar for Optional[T].
  • Field cardinality [T; bound] is relation-shaped; List[T] / Set[T] are collection-typed; [T; <= 1] triggers a rewrite hint to T?.
  • Method-call syntax desugars to a qualified UFCS call at elaboration time. No traits; no runtime dispatch.
  • Comprehensions reuse the aggregate-binding grammar; binders shadow with a warning.
  • xs[i] / xs[i..j] / x in xs desugar through the catalog. Sets reject indexing.
  • The operation catalog is the closed v1 surface. Each op carries a tier; the tier-dispatch matrix decides admission per context.
  • Mutation is rebuild-and-assign. Functional semantics align with the kernel’s append-only event log.
  • Optional under OWA: is_some is classical on presence; unwrap_or applies only to absence and preserves inner-value truth status.

Composition

Part 2 taught the atoms — concepts, properties, rules, computations, mutations, the type system. This part teaches what to do with them.

Two threads run through this part. The first is composition in the obvious sense: putting the atoms together to model real lifecycles, real rules, real interactions. The second is verification: pinning the meaning of the model down with tests and diagrams that double as living documentation.

By the end of Part 3 the lease tutorial has a state machine for Lease, a test suite that exercises it, and a set of diagrams you can hand to a non-technical stakeholder.

Patterns and Pattern-Matching

A match expression dispatches on a value’s shape. In Argon you reach for it in two settings: when destructuring data (a lease that might be residential, commercial, or general), and when handling reasoning outcomes (a query that might be unknown, ambiguous, or have timed out).

This chapter teaches both.

A first match

pub compute lease_summary(l: Lease) -> String =
    match l {
        ResidentialLease(r) => "Residential, " + r.bedrooms + " bedrooms",
        CommercialLease(c) => "Commercial: " + c.permitted_use,
        _ => "General lease",
    }

Three things to notice.

The scrutinee is the expression being matched. Here, l. Match arms are tried in source order; the first matching pattern fires.

Each arm is pattern => body. The body is any expression — a string, a method call, another match. The arrow => is the same => used elsewhere in the language; the parser disambiguates by context.

Trailing comma is allowed. Use it; rebasing match arms is friendlier when the last arm has a comma too.

Six pattern forms

Pattern formExampleUse
Wildcard_Match anything; bind nothing
Literal42, "active", #2026-01-01#Match a specific value
VariantResidentialLease(r)Match a subtype, bind the value
Named-with-guardr: ResidentialLease where r.bedrooms > 4Match + filter
Type testv: TenantMatch by type, bind the value
Reasoning outcomeis unknown, is ambiguous(x), is timeout(x)Match a reasoner’s answer

The first three are the bread-and-butter. The last three are situational.

Wildcard

match status {
    "active" => 1,
    "pending" => 0,
    _ => -1,
}

_ matches anything that did not match earlier arms. It is the conventional default.

Literal

match phase_name {
    "Pending" => initial_state(),
    "Active" => active_state(),
    _ => unknown_state(),
}

Literal patterns match the exact value. Int, Decimal, String, Bool, and Date literals are all admitted.

Variant

When the scrutinee’s static type is a parent concept and you want to dispatch on which subtype it actually is:

pub compute permitted_use_or_bedrooms(l: Lease) -> String =
    match l {
        ResidentialLease(r) => r.bedrooms.to_string(),
        CommercialLease(c) => c.permitted_use,
        _ => "unspecified",
    }

ResidentialLease(r) reads “if l is a ResidentialLease, bind it to r and use that binding in the body.” The variable r is typed as ResidentialLease inside the arm body.

Type test

match v {
    p: Person => p.name,
    o: Organization => o.legal_name,
    _ => "unknown",
}

p: Person is shorthand for the variant pattern when you want the binding’s type to drive the matching. The semantics are the same; the spelling is the one most programmers reach for first.

Named-with-guard

A pattern can carry an inline filter:

match lease {
    l: ResidentialLease where l.bedrooms > 4 => "large residential",
    l: ResidentialLease => "residential",
    l: CommercialLease => "commercial",
    _ => "other",
}

The where clause is a single rule-atom — comparisons, type tests, predicate calls. Use guards when the same type maps to multiple semantic categories.

Reasoning outcomes

Argon’s reasoner returns three-valued results. A query under the open-world assumption can return:

  • A concrete result.
  • unknown — the reasoner cannot decide.
  • ambiguous(x) — multiple inconsistent derivations.
  • timeout(x) — the reasoner gave up before deciding.

A match over a query result handles each:

match ActiveLease(l) {
    is unknown => Pending,
    is ambiguous(_) => undecided_state(),
    is timeout(_) => undecided_state(),
    _ => Active,
}

This is the surface for handling reasoning-outcome cases inside compute bodies. We use it sparingly in the running tutorial; most rules sit at tier 0–3 where outcomes are decided.

Each of the six pattern forms ships with a runnable workspace under argon/examples/_catalog/match-pattern-{literal,variant,wildcard,guard}/ and _catalog/match-expression/. The match-expression directory in particular has variants for each match-in-context shape: against-literal/, against-variant/, with-wildcard/, with-guard/, composition-rule-body/. Each is a self-contained workspace — cd and ox check.

A worked example from _catalog/match-pattern-guard/with-cross-axis-guard/src/prelude.ar — a pattern arm with a multi-axis guard:

pub compute classify_payment_state(l: Lease) -> PaymentState =
    match l {
        ActiveLease(a) if a.is_current and a.balance == zero => Current,
        ActiveLease(a) if a.balance > zero => InArrears,
        ActiveLease(_) => Active,
        TerminatedLease(_) => Settled,
        _ => Unknown,
    }

The guard’s body uses the rule-body sublanguage from Chapter 2.4; it can reference fields, call computes, and probe the metatype calculus. The compiler enforces exhaustiveness after guard analysis, so an unreached arm produces OE0203 not silent fallthrough.

Exhaustiveness

The compiler checks that every match has a way of reaching a body. If a match has no wildcard and no guardless variant covering every possible subtype, the compiler reports OE0203 — non-exhaustive match:

match l {
    ResidentialLease(r) => r.bedrooms.to_string(),
    CommercialLease(c) => c.permitted_use,
    // OE0203 — what about a bare `Lease`?
}

Adding _ => "general" as the last arm fixes this. Adding l: Lease => "general" (a guardless type test for the parent type) also fixes it.

The exhaustiveness check is static — it runs at ox check. You do not pay for it at runtime.

match in different contexts

match is a single expression form, but it shows up in three places:

Compute bodies

The most common — a single-expression compute returns the result of a match:

pub compute lease_summary(l: Lease) -> String =
    match l { ... }

Mutation bodies

Inside a do { } block, a let can bind the result of a match:

do {
    let category = match l {
        r: ResidentialLease => "residential",
        c: CommercialLease => "commercial",
        _ => "general",
    }
    l.category_label = category
}

Rule bodies

A whole rule body can be a single match expression — useful when the body splits cleanly along subtype lines:

pub derive lease_category(l: Lease, cat: String) :-
    match l {
        r: ResidentialLease => cat = "residential",
        c: CommercialLease => cat = "commercial",
        _ => cat = "general",
    }

The match is desugared into a disjunction of derive rules with subtype guards.

Edge cases worth knowing

  • Match arms are tried in source order. A more general pattern earlier in the list will short-circuit later, more specific arms.
  • Bindings are scoped to the arm body. r in ResidentialLease(r) => is not visible after the arrow leaves.
  • Type tests narrow. Inside r: ResidentialLease =>, r.bedrooms is well-typed because the compiler knows r is a ResidentialLease. This is occurrence typing (occurrence narrowing); the compiler tracks per-arm refinements automatically.
  • is unknown and friends are different from _ where outcome == unknown. The is-family is a dedicated pattern shape because reasoning outcomes are not ordinary values; they live in a sum-type at the engine level that the regular comparison operators do not reach into.

Putting it in the running example

We do not add a dedicated patterns file — match shows up where it fits. Our running tutorial threads match into existing computes and mutations. For example, computes.ar could grow:

use lease::*

pub compute annual_rent(l: Lease) -> Money =
    l.monthly_rent * 12

pub compute lease_summary(l: Lease) -> String =
    match l {
        r: ResidentialLease => "Residential, " + r.bedrooms + " bedrooms",
        c: CommercialLease => "Commercial, use: " + c.permitted_use,
        _ => "General lease",
    }
$ ox check
   Checking lease-tutorial v0.1.0
    Finished in 0.13s

Summary

match dispatches on a value’s shape. Six pattern forms cover the everyday cases — wildcard, literal, variant, type test, named-with-guard, and reasoning outcome. The compiler enforces exhaustiveness at ox check; occurrence typing narrows bindings inside arms. match shows up in compute bodies, mutation bodies, and rule bodies; the surface is the same in all three.

State Machines

A great many domain concepts have a lifecycle: a Lease is Pending, then Active, then Expired or Terminated. An Order moves through Placed, Paid, Shipped, Delivered. A Patient cycles through Admitted, InTreatment, Discharged.

Argon has a dedicated construct for these — a lifecycle { … } block inside a concept body — that turns “this concept moves through phases” into a first-class part of the type system. The phases become subtypes of the parent concept. The transitions become rules. The reasoner can answer “what phase is this lease in right now” and “could this lease ever reach Terminated from where it is.”

This chapter teaches today’s lifecycle surface and shows where it is going.

A first lifecycle

Inside the Lease declaration:

pub relator Lease {
    id: String,
    tenant: Tenant,
    landlord: Landlord,
    property: Property,
    start_date: Date,
    end_date: Date,
    monthly_rent: Money,

    lifecycle {
        initial Pending,
        Active,
        Expired,
        terminal Terminated,

        Pending -> Active { on: signed }
        Active -> Expired { on: term_end }
        Active -> Terminated { on: termination_notice }
        Expired -> Terminated { on: settled }
    }
}

Five things happen here.

Phases

Inside lifecycle { }, the first members are phases. Each phase is a subtype of the parent concept (Lease) — Pending, Active, Expired, and Terminated are all subtypes of Lease, picked out by which lifecycle phase a particular lease is in at a given valid time.

A phase can be marked initial (the entry state) or terminal (an exit state with no outgoing transitions). A phase can carry its own fields — Active { occupied_since: Date } says any active lease has an occupied_since field that the others do not.

The phase metatype (declared in our package’s metatypes.ar) carries the semantic weight: phase is anti-rigid (instances move through phases) and sortal. The compiler treats sibling phases as disjoint — a lease cannot be both Active and Expired at the same valid time.

Transitions

After phases come transitions: Source -> Target { clauses }. Each transition declares one allowed move from one phase to another.

A transition admits three inner clauses, of which only one — on: — is fully wired today:

ClauseUseStatus
on: <event>The event that triggers the transition.Wired today.
when { <body> }A guard predicate that gates the transition.Reserved; today’s surface uses a constraint sublanguage with a restricted grammar that does not yet accept the rule-body forms (path access, value comparisons). Use a require clause on the mutation that fires the trigger event for now.
brings_about { <events> }Events to publish when the transition fires.Reserved; landing alongside the unified consequence syntax (Appendix D). Use mutations to emit events for now.

The lease’s Pending -> Active { on: signed } reads: when the signed event arrives, transition to Active. The compiler turns this into a derive rule plus an event consequence.

Migration note (the big one for this chapter): the team is exploring a redesign that collapses lifecycle { … } into a top-level statemachine sugar, desugaring to an enum, a set of derive rules, and Transition events. The current lifecycle keyword stays through a migration window; the new shape sits next to it. The two forms are close enough that switching is mechanical:

Today:

pub relator Lease {
    ...
    lifecycle {
        initial Pending,
        Active,
        terminal Terminated,

        Pending -> Active { on: signed }
    }
}

Trajectory:

pub statemachine LeaseState {
    states { Pending, Active, Terminated }
    initial Pending
    terminal Terminated

    transitions {
        Pending -> Active :- signed(self), self.start_date <= today()
    }
}

The redesign’s body shape (:- pred-body => Transition(Pending, Active)) is more uniform with the rest of the language; today’s form is a curried sugar. Both produce the same runtime behavior. Do not rewrite working code today; expect the new form to be alias-accepted when it lands. See Appendix D.

What the lifecycle gives you

A lifecycle block is not just notation. The compiler does several things with it.

Disjointness for free

A Lease is in exactly one phase at a given valid time. The phase metatype encodes the disjointness; the compiler enforces it. You do not write a partition axiom over the phases. (This is one of the places the redesign tightens — see Appendix D.)

Phases as subtypes

pub query active_leases() -> [Active] :-
    ?l: Active
    => ?l

Active is a type. You can return phase-typed values; you can pattern-match on phases; you can dispatch behavior based on phase. The compiler tracks the subtype relationship so that an Active is also a Lease for any context that expects one.

Reachability checks

The compiler computes reachability from initial phases to terminal phases. If you declare a phase that is not reachable from any initial phase — say, you forget the transition into it — the compiler tells you. If you declare a terminal phase that has outgoing transitions, you get an error.

Bitemporal tracking

The runtime treats phase changes as events on the bitemporal log. A query against --as-of-valid #2026-04-15# answers “what phase was the lease in on April 15?” The phase history is queryable.

A worked time-travel pattern: the bitemporal/ variant in _catalog/lifecycle-block/ shows a lease declaring its lifecycle, transitioning through Pending → Active, and a query filtered by valid time:

pub relator Lease {
    id: String,
    start_date: Date,
    end_date: Date,

    lifecycle {
        initial Pending,
        Active,
        terminal Terminated,

        Pending -> Active { on: signed }
        Active -> Terminated { on: term_end }
    }
}

pub derive ActiveLease(l: Lease) :-
    l: Lease, Active(l)

// At the runtime, `ox query ActiveLease --as-of-valid #2026-04-15#`
// returns the leases that were in the Active phase on April 15,
// even if they have since transitioned to Terminated.

Variants: _catalog/lifecycle-{block,phase,transition}/{minimal,bitemporal,composition-with-derive,multi-transition}/.

Variations

A few common shapes worth recognizing.

Phase with fields

A phase can carry its own data:

Active { occupied_since: Date, last_payment: Date },

The intent: occupied_since exists on Active-typed values but not on Pending or Expired. Phase fields are reserved for data that only makes sense within a phase.

Note: today’s compiler accepts the syntax but emits OW1808 (“lifecycle phase data fields not yet elaborated”) because phase-field resolution is incomplete. Until that lands, keep phases bare names — Active, rather than Active { occupied_since: Date }, — and put per-phase data on the parent concept or on a related kind keyed by phase.

Guards and effects today

Until the unified consequence syntax lands, gate a transition by putting the precondition on the mutation that fires its trigger event, and emit downstream events from the same mutation. For the lease tutorial, the sign_lease mutation precondition checks the start date is valid, then emits signed; the lifecycle’s Pending -> Active { on: signed } does the rest.

pub mutation sign_lease(l: Lease, signed_at: Date) -> Lease {
    require {
        l.start_date < l.end_date
    }
    do { }
    emit LeaseSigned(l, signed_at)
    return l
}

The runtime publishes LeaseSigned into the bitemporal log; the lifecycle’s transition fires; the phase moves from Pending to Active. The event is queryable, audit-trailable, and contributes to provenance.

Migration note: when { … } and brings_about { … } clauses inside transitions become first-class once the unified consequence syntax lands — the redesign reads transitions as Pending -> Active :- signed(self), self.start_date <= today() => Transition(Pending, Active) and LeaseSigned(self). Same semantics; the rule-body grammar replaces the constraint sublanguage. See Appendix D.

Multi-source transitions

Two transitions can share a target:

Active -> Terminated { on: termination_notice }
Expired -> Terminated { on: settled }

The Datalog the compiler produces fans these out as separate rules with the same head; the disjunction is automatic.

Standalone state-machines

Sometimes the state machine is the whole point and the parent concept is just a peg to hang it on. Declare a minimal kind whose only purpose is to host the lifecycle:

pub kind ApplicationStatus {
    application_id: String,

    lifecycle {
        initial Submitted,
        UnderReview,
        Approved,
        terminal Rejected,

        Submitted -> UnderReview { on: review_started }
        UnderReview -> Approved { on: approved }
        UnderReview -> Rejected { on: rejected }
    }
}

Once the redesign lands, this becomes a top-level statemachine ApplicationStatus { … } declaration without the parent kind, but the semantics are the same.

Edge cases worth knowing

  • lifecycle is inside a concept body, not an item-start construct. You cannot write lifecycle X { … } at module level today.
  • Phases cannot be pub independently of their parent. Visibility flows from the host concept.
  • A concept can have at most one lifecycle. If you find yourself wanting two state machines on the same concept, either model the second as a separate concept or use a phase-of-phase nesting (which the parser does not directly support today; see Phase-B’s open thread on hierarchical state machines).
  • Phase names cannot collide with the host’s field names. The compiler reports a collision error.
  • initial and terminal are contextual modifiers, not keywords. They lex as IDENTs and the parser dispatches on context.

Putting it in the running example

Update src/lease.ar to add the lifecycle:

use metatypes::*
use party::*

pub kind Property {
    id: String,
    address: String,
}

pub relator Lease {
    id: String,
    tenant: Tenant,
    landlord: Landlord,
    property: Property,
    start_date: Date,
    end_date: Date,
    monthly_rent: Money,

    lifecycle {
        initial Pending,
        Active,
        Expired,
        terminal Terminated,

        Pending -> Active { on: signed }
        Active -> Expired { on: term_end }
        Active -> Terminated { on: termination_notice }
        Expired -> Terminated { on: settled }
    }
}

pub subkind ResidentialLease : Lease {
    bedrooms: Int,
}

pub subkind CommercialLease : Lease {
    permitted_use: String,
}

Then:

$ ox check
   Checking lease-tutorial v0.1.0
    Finished in 0.13s

The lease now has a lifecycle. We can ask ox query active_leases and the runtime knows how to filter by phase. We can mutate from Pending to Active via the existing sign_lease mutation, and the event flows through the lifecycle’s transition guard.

Summary

A lifecycle { … } block declares phases as subtypes and transitions as rule-events between them. The compiler gives you sibling-disjointness, subtype-typed phases, reachability checks, and bitemporal phase tracking. The lease tutorial now has a complete lifecycle. The redesign trajectory collapses the surface into statemachine sugar; today’s syntax stays through the migration. Next we test the model.

Tests and Verification

A test is a small named world the runtime sets up, makes claims about, and tears down. Argon has a dedicated test item kind, plus @[theorem]-tagged rules that the compiler preserves as theorem markers for a future mechanical-verification pipeline.

This chapter covers both.

A first test

test "a freshly-signed residential lease is active" {
    let alice: Tenant = {
        id: "alice-001",
        name: "Alice",
        contact_email: "alice@example.com",
    }
    let bob: Landlord = {
        id: "bob-001",
        name: "Bob",
        payout_account: "acct-bob",
    }
    let house: Property = {
        id: "p-1",
        address: "1200 University Ave",
    }
    let lease: ResidentialLease = {
        id: "lease-1",
        tenant: alice,
        landlord: bob,
        property: house,
        start_date: #2026-01-01#,
        end_date: #2027-01-01#,
        monthly_rent: 9500,
        bedrooms: 3,
    }

    assert ResidentialLease(lease)
    assert ActiveLease(lease)
}

Run it:

$ ox test
   Compiling lease-tutorial v0.1.0
   Running 1 test
test scene "a freshly-signed residential lease is active" ... ok

test result: ok. 1 passed; 0 failed; 0 unproven; 0 assumed; 0 ignored

Three things to notice.

test "string" not test name

Tests are named with string literals — they are descriptive narratives, not identifiers. This is intentional: a test description is what shows up in ox test output, in CI logs, and in failure reports. Make it readable. “a freshly-signed residential lease is active” beats signed_lease_active.

let for setup

The body’s let name : Type = { fields } form constructs an instance directly. Field-block syntax lets you supply each field by name; the compiler validates against the declared type. The instance is bound to the local name; you can refer to it in assert statements.

This is a different let from the one inside compute Form-2 bodies: that let binds an intermediate value during evaluation; this let constructs a concept instance for the test’s local world.

assert <atom> for claims

Inside a test, assert introduces a single rule-atom claim — a type test, a derive-head call, a comparison, an aggregate inequality, or any other shape the rule-body grammar admits. The runtime evaluates the atom against the test’s world; if it does not hold, the test fails.

assert here is post-reasoning: the runtime saturates the model first (running all asserts and derives), then checks each test claim against the saturated knowledge base. So assert ActiveLease(lease) works because the ActiveLease derive rule has fired during reasoning.

You can also write assert not <atom>:

test "a lease with start >= end is rejected by the validity refinement" {
    let bad: Lease = {
        id: "lease-bad",
        ...
        start_date: #2027-01-01#,
        end_date: #2026-01-01#,
        ...
    }

    assert not bad: ValidLease
}

not negates the atom — the test passes when the atom fails. Useful for exclusion claims like “this bad input does not pass our refinement.”

Status note: instantiates-form assertions in tests. The runtime’s assertion evaluator currently supports concept-as-predicate atoms (assert ResidentialLease(lease), assert ActiveLease(lease)) but does not yet evaluate the instantiates-form assert <ind>: <Concept> (and its negation) — running such a test reports atom shape not yet supported: Instantiates. The shape is parsed and elaborated and passes ox check / ox build; it just isn’t dispatched by ox test yet. Until that lands, prefer assert <Concept>(<ind>) for membership claims; refinement-membership tests will roll in alongside the runtime extension.

Sequenced statements: mutate and cleanup

Tests aren’t only declarative. A test can also exercise a pub mutation against the test ABox, observe its effects, and run ordered teardown. The test body admits four statement kinds in source order: let, assert, mutate, and cleanup.

test "rent payment passes timeliness check" {
    let p: RentPayment = {
        paid_on: 2025-03-03,
        amount: 9500,
        period_label: "2025-03",
        is_timely: true,
    }

    mutate record_rent_payment(p)

    assert RentPayment(p)
    assert tenant_balance(p.tenant) == 0

    cleanup {
        mutate retract_test_payment(p)
        assert not RentPayment(p)
    }
}

mutate <name>(<args>) invokes a pub mutation. The runner:

  1. Resolves the mutation by name through the same import surface as any other cross-package symbol.
  2. Evaluates each require { } precondition against the current post-saturation ABox. On the first false, the runner records a structured MutationPreconditionFailure and skips the mutation’s do / retract / emit clauses; subsequent statements continue against the unchanged ABox so you see all assertion drift in one run.
  3. Otherwise applies retract, do (field updates and locally-bound let individuals), and emit (events become individuals visible to subsequent saturation).
  4. Re-saturates. The next assert evaluates against the post-mutation state.

cleanup { stmts } is a single, trailing teardown block. Cleanup admits the same four statement kinds and runs regardless of whether main-body assertions failed. Failures inside cleanup are tagged with a cleanup: true flag on the runner output so you can distinguish “the operation failed” from “the teardown failed.”

Well-formedness rules:

  • A test admits at most one cleanup block (OE0240 MultipleCleanupBlocks if more).
  • cleanup must be the last statement (OE0241 CleanupNotAtEnd if anything follows it).
  • Cleanup blocks don’t nest (OE0242 NestedCleanup).

What v1 catches

Before this surface existed, the only way to “test” a mutation was the scene-shape convention: let-bind the mutation’s emitted events as if it had run, then assert the post-state shape. That convention proved input shapes were constructible but never evaluated require, never exercised do, never verified that emit clauses fired.

mutate <name>(<args>) closes those gaps for the clauses v1 fully evaluates:

  • Existing-precondition violations. A test that hands record_rent_payment an is_timely: false payment fails with MutationPreconditionFailure rather than silently passing.
  • emit clauses (constructor case). emit Event { ... } evaluates the expression and mints a fresh event individual with the literal field values. An assert Event(...) after the mutate statement is meaningful coverage of the emit clause.
  • Multi-mutation flows. mutate A(...); mutate B(...); assert <post-state> proves the chained effect with re-saturation between steps so B’s require sees A’s derived facts.
  • Cleanup ordering. A cleanup { } block runs regardless of mid-test assertion failures, so a teardown mutation’s post-state assertion is exercised even when an earlier assertion drifted.

What v1 does NOT catch

The runner is honest about which clauses it does not yet evaluate against the test ABox — those clauses surface as MutationUnsupported test failures so the modeler sees the gap rather than a silent pass:

  • do { x.field = expr } field-updates. v1 emits a MutationUnsupported failure per field-update. A test like mutate update_balance(acct, 100); assert acct.balance == 100 will see the runner flag the gap explicitly. v2 scope.
  • retract { } clauses. v1 emits a MutationUnsupported failure per retract. mutate teardown(x); assert not Concept(x) reports the unsupported flag rather than silently passing or failing the assertion. v2 scope.
  • do { let } value-expression bindings. The let’s type assertion lands and the binding is visible to subsequent statements as a typed individual, but the field assignments inside the let’s value expression do not yet bind. v2 scope.
  • emit p path-reference (no-op). When the elaborator lowers emit p (where p is a do { let }-bound name) the resulting CoreEmit carries empty args. v1 treats this as a no-op since p is already in the ABox via the let. Constructor-form emits are the meaningful coverage path; path-reference assertions test the let-binding shape, not the emit clause itself.
  • Dropped preconditions. A record_rent_payment whose require { p.is_timely } line was deleted from source still appears to pass — there’s no atom left to evaluate, so no MutationPreconditionFailure ever fires. Detecting “this mutation should have a precondition but doesn’t” requires a separate mutate_fails <name>(<args>) form that proves a precondition fails. v1 catches violations of preconditions the modeler kept; v2 will catch dropped-precondition regressions.

Multi-mutate failure propagation

When mutation A’s require fails, A’s do / retract / emit are skipped and the ABox stays unchanged. The runner does not halt the statement loop — it continues to the next statement. A subsequent mutate B(...) runs with B’s require evaluating against the pre-A state; if B’s preconditions hold against that state, B applies normally.

This means a chained-mutation test where B depends on A’s effects will see B fail or behave unexpectedly when A is skipped — but the source ordering of failures (A first, then B’s drift) makes the dependency obvious. The alternative — halting after A’s failure — would hide downstream drift.

Ordering

Tests with only let and assert statements observe today’s behavior unchanged: lets materialize, the ABox saturates, assertions evaluate. Tests that introduce mutate get per-statement saturation — re-saturation runs between mutate statements so each mutation’s require sees the previous mutation’s derived facts. A test author who introduces a mutate and observes that an assertion that previously passed now fails should investigate whether the mutation legitimately changes the asserted state — the change is signal, not noise.

Failed assertions don’t halt the loop. The runner records every assertion’s outcome and continues, so you see all failures in one run.

Compute equality

A specifically-supported test-only form: assert that a compute call equals an expected value.

test "deposit return arithmetic" {
    let r: DepositReturn = {
        deposit_held_amount: 9500,
        allowed_deduction_total: 1400,
    }

    assert deposit_after_deductions(r) == 8100
}

The compute is called against the test’s world; the result is compared with the expected value. This is how you validate that arithmetic-heavy computes do what you mean.

Multiple tests in a module

A test block is an item, like any other. Several can live in the same file:

use lease::*
use party::*
use rules::*
use types::*

test "active lease is queryable" { ... }
test "expired lease is queryable" { ... }
test "validity refinement catches bad dates" { ... }
test "validity refinement catches non-positive rent" { ... }

Convention: put tests in their own module — typically src/tests.ar for a small package. The book’s running tutorial follows this convention.

Frames and fixtures

When several tests share setup — the same Tenant, the same Property, the same baseline rules — pull that setup into a frame. A frame is a named, composable bundle of TBox + rules + ABox that any test can opt into.

frame canonical_party {
    let alice: Tenant = {
        id: "alice-001",
        name: "Alice",
        contact_email: "alice@example.com",
    }
    let bob: Landlord = {
        id: "bob-001",
        name: "Bob",
        payout_account: "acct-bob",
    }
    let house: Property = {
        id: "p-1",
        address: "1200 University Ave",
    }
}

A frame’s body admits any top-level item form — concept declarations, rules, asserts, lets. pub frame exports across packages; bare frame is package-local.

Tests compose frames with using:

test "a freshly-signed lease over the canonical party is active" using canonical_party {
    let lease: ResidentialLease = {
        id: "lease-1",
        tenant: alice,
        landlord: bob,
        property: house,
        start_date: #2026-01-01#,
        end_date: #2027-01-01#,
        monthly_rent: 9500,
        bedrooms: 3,
    }

    assert ResidentialLease(lease)
    assert ActiveLease(lease)
}

using pulls every binding, rule, and assertion the frame defines into the test’s world. Multiple frames compose: using canonical_party, baseline_rules brings both. The same name declared in two composed frames is a conflict (OE0215); a using reference that does not resolve to a known frame is OE0214; an inline fixture that redeclares a frame member is OE0216; circular frame includes are OE0218.

A worked multi-frame example:

frame canonical_party {
    let alice: Tenant = { id: "alice-001", name: "Alice", contact_email: "alice@example.com" }
    let house: Property = { id: "p-1", address: "1200 University Ave" }
}

frame baseline_rates {
    let standard_rent: Money = 9500
    let standard_deposit: Money = 19000
}

test "standard-rate lease matches baseline" using canonical_party, baseline_rates {
    let lease: ResidentialLease = {
        id: "lease-1",
        tenant: alice,
        property: house,
        monthly_rent: standard_rent,
        start_date: #2026-01-01#,
        end_date: #2027-01-01#,
        bedrooms: 3,
    }

    assert lease.monthly_rent == standard_rent
}

Variants: _catalog/test-frame-declaration/{minimal,multi-frame,frame-with-fixtures}/ and _catalog/test-using-clause/{multi-using,composition-with-expect}/.

Status note: OE0216 not yet emitted. OE0216 is registered in the diagnostic catalog and committed by the rule above, but the emit-site at the inline-fixture-vs-frame-composition junction is not yet wired in the elaborator. Tests that exercise the collision (an inline fixture redeclaring a name a using-pulled frame already contributes) currently compile clean. Sibling diagnostics OE0214 / OE0215 / OE0218 are wired. Tracked at sharpe-dev/orca-mvp#411.

Inside a test, fixture { … } declares a test-local mini-module — items declared in the fixture are visible in the surrounding test but never escape to the parent module:

test "deposit return arithmetic with a custom property" {
    fixture {
        let custom: Property = {
            id: "custom-1",
            address: "42 Galaxy Way",
        }
    }

    let r: DepositReturn = {
        deposit_held_amount: 9500,
        allowed_deduction_total: 1400,
    }

    assert deposit_after_deductions(r) == 8100
}

Diagnostics that arise inside a fixture block are captured — they are visible to the test’s expect block (below) but never propagate to the parent module’s diagnostic stream. This lets a test deliberately exercise a malformed fixture and assert the right error fires, without the malformation breaking unrelated tests in the same package.

Diagnostic expectations

Tests can verify the compiler’s diagnostics, not just post-saturation fact-claims. The mechanism is the expect { … } block paired with //~ <label> source markers:

test "a sortal without a Kind ancestor is flagged" {
    fixture {
        pub kind Person { name: String }
        //~ orphan_role
        pub role Orphan { }
    }

    expect {
        diagnostic OW0207 at orphan_role
    }
}

//~ <label> anchors a labelled marker to the next source line — the convention follows Rust’s compiletest. Inside expect { … }, the bare form diagnostic <code> requires that exactly one diagnostic with that code fires somewhere in the test’s scope; at <marker> constrains the match to the labelled span; severity:<level> (one of error, warning, info) further constrains. Severity defaults to error when omitted.

expect {
    diagnostic OE0211 at line_a
    diagnostic OE0212 severity:warning
    diagnostic OE0605
}

The test passes when every claim in the expect block matches. An unmatched claim, an unexpected diagnostic above the configured severity floor, or a diagnostic anchored at the wrong span is a test failure; the runner surfaces the diff between expected and actual.

expect { … } and assert <atom> compose. A single test can verify both that a fixture produces the right diagnostic and that the rest of the model still satisfies its post-saturation claims.

test "bad lease shape flags + good lease still classifies" {
    fixture {
        //~ bad_lease
        let bad: Lease = { id: "x", start_date: #2027-01-01#, end_date: #2026-01-01# }
    }

    let good: Lease = {
        id: "y",
        start_date: #2026-01-01#,
        end_date: #2027-01-01#,
        monthly_rent: 9500,
    }

    expect {
        diagnostic OE0606 at bad_lease    // refinement violation on bad
    }

    assert ValidLease(good)               // good still satisfies the refinement
}

Variants: _catalog/test-expect-block/{minimal-expect,multi-arm-expect,with-fixture,expect-with-query}/.

meta() reflection and :: classification

Two related forms surface the metatype of a concept inside a rule body, query, or assertion. meta(X) returns X’s metatype as a value:

pub derive is_kind(x) :- meta(x) == kind

Person :: Kind is sugar for the same query — read “Person is a Kind”. The two lower to the same internal atom; pick whichever reads more naturally in context.

pub derive equiv(x) :- x :: Kind

meta() and :: classify types, not instances. A bound variable or named concept fits. A free unbound variable does not — meta(?z) with no binding fires OE0212 MetaArgumentNotGround at elaboration. Negation composes: not meta(x) == role is well-formed.

Tests use these forms to assert metatype-shape directly:

test "Tenant is a role" {
    assert Tenant :: Role
}

@[theorem] — mechanical verification

Some claims are not test-shaped. They are statements about the rule system itself: “this property holds for every possible lease, not just the three I happen to construct in a test.” For those, mark a derive rule with @[theorem]:

@[theorem]
pub derive transitive_subtype(x: ⊤, y: ⊤, z: ⊤) :-
    x specializes y,
    y specializes z
    => x specializes z

@[theorem] marks the rule as a theorem claim. At the current cut, the marker is preserved through compilation and surfaces in the kernel’s runtime so a future verification pipeline can target it; the compile-time mechanical-verification pass (the saturator + meta-property calculus attempting to prove that the rule’s body always implies its head) is not yet active. Today the value is signalling intent — the body of @[theorem]-tagged rules is the inventory the verification pass will close over when it lands.

@[theorem] is restricted to derive items. Placing it on a compute, mutation, query, or anything else yields OW0821 (“@[theorem] applied to a non-derive item”); declaring it twice on the same derive yields OW0822.

Two related attributes come from the same family:

  • #[unproven] — applied to a test block. Marks a theorem-shaped claim that the test asserts as true without mechanical verification. The test runs as usual; the runner reports it under its own status category so the build’s body of unproven claims is visible at a glance.
  • #[assumed] — applied to a test block. The body’s claim is injected as a postulate within the test’s scope. Used when a real-world axiom (e.g., “every SSN is unique”) is needed for downstream reasoning but cannot be derived from the model. Reported under its own status category for the same reason.
#[unproven]
test "subtyping is transitive across the entire lattice" {
    /* claim — not yet mechanically verified */
}

#[assumed]
test "every SSN is unique" {
    assert forall ?p1, ?p2: Person where ?p1.ssn == ?p2.ssn => ?p1 == ?p2
}

@[…] AttrList and @<ident> / #<ident> Decorator forms are accepted as alternates to #[…] for both attributes. Pick the form that matches the package’s prevailing style.

What ox test runs

ox test runs every test block in the project. @[theorem] markers are surfaced at ox check / ox build time (today as preserved markers; the verification pass is forthcoming), not under ox test — so a (future) verified theorem will be a property of the build, not of an individual test invocation.

The runner reports five status categories: Pass (every claim held), Fail (a claim did not hold), Unproven (a claim marked #[unproven] ran without proof), Assumed (a claim marked #[assumed] was injected as a postulate), Ignored (the test was excluded from the run). The categories are surfaced as separate counts in the test result: line so a build’s body of unproven and assumed claims stays visible.

$ ox test
   Compiling lease-tutorial v0.1.0
   Running 5 tests

test result: ok. 3 passed; 0 failed; 1 unproven; 1 assumed; 0 ignored

Pass -p <package> to scope to a single package within a workspace. Pass --filter <pattern> to run only tests whose names match. Pass -v / --verbose to show every assertion, not just failures.

Edge cases worth knowing

  • let : Type = { fields } is the canonical creation form inside tests. It runs the same field-validation the type-checker runs on parameter passing; bad field types fail at ox check, not at ox test.
  • Tests run with full reasoning. Unlike the default ox check, tests trigger the reasoner so that derives and asserts have fired before tests’ assert claims are evaluated.
  • Assertions in tests are not the same as assert items at module scope. A module-scope assert declares an invariant over the package; a test’s assert is a single rule-atom check inside the test’s world.

Putting it in the running example

Add src/tests.ar. Bundle the shared party-and-property setup into a frame so each test’s body stays focused on what it actually exercises:

use lease::*
use party::*
use rules::*
use types::*

frame canonical_party {
    let alice: Tenant = {
        id: "alice-001",
        name: "Alice",
        contact_email: "alice@example.com",
    }
    let bob: Landlord = {
        id: "bob-001",
        name: "Bob",
        payout_account: "acct-bob",
    }
    let house: Property = {
        id: "p-1",
        address: "1200 University Ave",
    }
}

test "a freshly-signed residential lease is active" using canonical_party {
    let lease: ResidentialLease = {
        id: "lease-1",
        tenant: alice,
        landlord: bob,
        property: house,
        start_date: #2026-01-01#,
        end_date: #2027-01-01#,
        monthly_rent: 9500,
        bedrooms: 3,
    }

    assert ResidentialLease(lease)
    assert ActiveLease(lease)
    assert lease: ValidLease
}

test "a lease with start >= end is rejected by the validity refinement" using canonical_party {
    let bad: Lease = {
        id: "lease-bad",
        tenant: alice,
        landlord: bob,
        property: house,
        start_date: #2027-01-01#,
        end_date: #2026-01-01#,
        monthly_rent: 9500,
    }

    assert not bad: ValidLease
}

Then:

$ ox test
   Compiling lease-tutorial v0.1.0
   Running 2 tests
test "a freshly-signed residential lease is active" ... ok
test "a lease with start >= end is rejected by the validity refinement" ... ok

test result: ok. 2 passed; 0 failed; 0 unproven; 0 assumed; 0 ignored

The model is now self-validating. Bad inputs do not survive the test pass.

Summary

Tests are first-class — test "string-name" { let setup; assert claims }. Frames bundle setup so tests share it via using. Fixtures scope test-local items and capture their diagnostics. expect { diagnostic … } blocks paired with //~ <label> markers verify the compiler’s output, not just post-saturation facts. meta() and :: surface metatype-shape directly. @[theorem], #[unproven], and #[assumed] provide a graded set of verification claims, from informally-tested through mechanically-verified through axiomatic, and the runner reports them as five distinct status categories. The lease tutorial now has tests covering the happy path and the failure modes that the refinement type catches. Next we render the model.

Diagrams

In Argon, the source is the truth and visuals are lenses onto it. A diagram is not a separate document you maintain alongside the model; it is a block of source code in the same file as the rest of the model, evaluated by the compiler, rendered to SVG or OntoUML JSON by ox diagram. (PNG / PDF are produced by passing the SVG through an external rasterizer such as rsvg-convert or cairosvg.)

When the model changes, the diagram changes with it. When you want to look at a different facet of the same model, you write a different diagram block — not a different ontology.

This chapter teaches the diagram surface and ends the running example with a set of pictures of the lease model.

A first diagram

diagram "lease participants" {
    include {
        Property,
        Person,
        Tenant,
        Landlord,
        Lease,
        ResidentialLease,
        CommercialLease,
    }
}

Render it:

$ ox diagram "lease participants"

ox diagram writes one SVG per diagram into the output directory (diagrams/ by default; override with -o <dir>). The file for the example above lands at diagrams/lease participants.svg — an SVG showing the seven concepts and the supertype + role + relator edges between them. No layout work, no manual node placement — the renderer’s layout engine handles that.

Anatomy of a diagram block

Three slots:

diagram "<name>" [from "<base>"] {
    <statements>
}
  • "<name>" is a string literal — diagram names admit spaces and Unicode so they read well in figure captions.
  • from "<base>" is optional inheritance: a diagram can derive from another by adding/removing/overriding statements. We use it sparingly; it is occasionally useful when you have a base diagram for an audience and want a tightened version for a different one.
  • <statements> are the eight families of diagram statements — include/exclude, set/show, color/label/highlight, group/layer/collapse, layout/direction/align, ensure/encourage, title/description, format/size.

Most diagrams need only a couple of statement families. We will mostly use include/exclude.

Source forms

The include statement takes a source. Argon admits seven source forms:

FormExampleUse
Explicit listinclude { Person, Lease }Enumerate the concepts you want
Wildcardinclude *Every concept in scope
Module scopeinclude mod ufo::aEvery concept in a module
Module globinclude mod ufo::a::*Every concept in a module subtree
Predicate filterinclude where metatype == kindFilter by a structural predicate
Neighborhoodinclude 2 from LeaseConcepts within N hops of an anchor
Relation traversalinclude connected_by mediates to LeaseConcepts reachable via a specific relation

The same forms work for exclude (which removes concepts from the diagram). exclude runs after include, so the typical pattern is “include broad, exclude narrow.”

Pipe chains

A trailing pipe-chain refines the source via predicate filters:

include * | where metatype == kind | where rigidity == rigid

Each | where ... filters the running selection. The grammar inside where is the same predicate vocabulary you use in rule-atom positions: comparisons, type tests, and the ontology-aware predicates that come from the metatype calculus (metatype, rigidity, sortality, …).

Use pipes when you want to show a facet of the model rather than every concept — “all rigid kinds,” “every concept with a non-empty lifecycle,” and so on.

Styling and layout

A handful of statement families let you reach into the visual:

diagram "lease participants" {
    include { Person, Tenant, Landlord, Property, Lease }

    color by metatype {
        kind: blue,
        role: green,
        relator: orange,
    }

    layout sugiyama(spread: 1.5)
    direction left-to-right
    format svg, png
}
  • color by metatype { ... } — colour-code nodes by metatype. Other axes (color by rigidity, color by sortality) work too.
  • layout <algo>(...) — pick a layout algorithm. sugiyama is the default; alternatives include force, tree, circular. The (...) arguments are layout-specific.
  • directionleft-to-right, right-to-left, top-to-bottom, bottom-to-top.
  • format svg, pdf, png — declare output formats at the diagram level. The CLI flag --format overrides.

You can usually leave layout alone for small diagrams; the defaults work well up to about 30 nodes.

OntoUML round-trip

For audiences who use OntoUML tooling — Visual Paradigm, Menthor Editor, the ontouml-vp-plugin — Argon can emit an OntoUML JSON document instead of (or in addition to) a visual format:

$ ox diagram "lease participants" --format ontouml

The OntoUML format writes a single <package-name>.ontouml.json whose diagrams array carries every (filtered) diagram as an OntoUML 2.0 diagram-section entry — landing in the same output directory (diagrams/ by default; override with -o <dir>).

Round-trip is layout-stable: the visual layout decisions an OntoUML editor adds when a human curates the model can be preserved through a @[layout] attribute and round-tripped on re-export. The mechanism is in flight; treat it as the way to keep visual edits when collaborating with OntoUML-native colleagues.

Inheriting a diagram

Use from "<base>" to extend a base diagram with refinements:

diagram "lease participants" {
    include { Person, Tenant, Landlord, Property, Lease }
}

diagram "lease lifecycle" from "lease participants" {
    include connected_by lifecycle to Lease
        | where metatype == phase

    color by phase {
        Pending: gray,
        Active: green,
        Expired: yellow,
        Terminated: red,
    }
}

"lease lifecycle" carries everything from "lease participants" and adds the lifecycle phases plus a colour scheme. Inheritance composes — you can derive again, and again — though in practice two layers is plenty.

Status note: lifecycle-as-relation projection. Today’s diagram parser does not yet recognise a lifecycle { … } block as a binary-relation source for connected_by, so the connected_by lifecycle form above currently produces OE1102 unknown relation 'lifecycle'. The form shown is the trajectory; the tutorial therefore ships its participants and ontology-overview diagrams and defers the lifecycle projection to a follow-up. If you need a phase view in the meantime, render the lifecycle states by including the phase concepts directly: include { Pending, Active, Expired, Terminated }.

Variations

A handful of common diagram shapes worth remembering.

Module map

The shape of a package’s vocabulary, seen at one glance:

diagram "lease-tutorial overview" {
    include mod lease_tutorial::*
    color by metatype {
        kind: blue,
        role: green,
        relator: orange,
        phase: gray,
    }
}

Neighborhood around a focal concept

What does Lease connect to, two hops out?

diagram "lease neighborhood" {
    include 2 from Lease
}

Filtered facet

Just the rigid sortals — the things with their own permanent identity:

diagram "rigid sortals" {
    include * | where metatype == kind
}

Relator and its mediates

The lease and only the concepts it mediates:

diagram "lease-mediated" {
    include { Lease }
    include connected_by mediates to Lease
}

Edge cases worth knowing

  • Diagrams are evaluated, not stored. ox diagram re-renders from source every time. There is no cached SVG that drifts from the model.
  • include accumulates; exclude subtracts. Order between an include and a later exclude matters; order between two includes does not.
  • Anchor concepts must exist. include 2 from Lease fails to compile if Lease is not in scope.
  • Diagram names are visible in ox show. ox show diagrams enumerates every diagram in the package.

Putting it in the running example

Add src/diagrams.ar:

use lease::*

diagram "lease participants" {
    include {
        Property,
        Person,
        Tenant,
        Landlord,
        Lease,
        ResidentialLease,
        CommercialLease,
    }
}

diagram "lease ontology overview" {
    include 2 from Lease
}

(The "lease lifecycle" projection shown earlier in this chapter is on the roadmap — see the Status note above. The tutorial ships the two diagrams that compile cleanly today and will pick up the lifecycle projection when the diagram parser learns to treat lifecycle as a structural source.)

We do not need to re-export diagrams through prelude.ar; they are runtime artifacts, not items consumers use.

Render them:

$ ox diagram --all
   Rendering lease-participants.svg
   Rendering lease-ontology-overview.svg
✓ 2 diagrams.

You now have two SVGs of the model. The first shows the structural skeleton around the lease’s participants; the second gives a two-hop overview around Lease.

Diagrams in the example workspaces

Two example packages ship diagrams in source form:

  • argon/examples/pizza/src/diagrams.ar (when present) renders the Manchester pizza ontology’s class lattice with color by metatype to distinguish kind (the abstract Food, Pizza) from subkind (specific pizza varieties) from concrete phase examples. The diagram is the same data as the source — change the supertypes in prelude.ar, re-render, the picture updates.
  • argon/examples/ontology-tour/ is itself a “diagram-driven tour”: the package re-exports concepts from pizza, foaf, and wine, and the tour diagram uses include 2 from tour_role (a pub metaxis tour_role for type { event, venue }) to show the cross-package connections.

Both packages render with ox diagram --all from inside the workspace; the SVGs land in diagrams/.

Summary

A diagram block is source code that compiles to a picture. The body uses include/exclude statements with seven source forms and pipe-chain filters; styling and layout are first-class statements; from "<base>" lets diagrams inherit from one another. ox diagram renders to SVG or OntoUML JSON; raster formats (PNG / PDF) are produced by piping the SVG through an external rasterizer. The lease tutorial ships two diagrams today (structural participants + ontology overview), generated from the same model the rest of the book has been building; the lifecycle projection lands when the diagram parser picks up lifecycle-as-source. When the model changes, the diagrams catch up automatically.

Packages and Tooling

The model is built. This last part covers what it takes to ship it: how packages are organized, how dependencies and workspaces work, and what the compiler and LSP do for you in daily editing.

This part is shorter than the others. The ergonomics are largely Cargo-shaped, and most readers will find the surface familiar. We cover what is the same so you can rely on intuition, and what is different so you do not get caught.

Modules and Packages

We have used mod, use, pub, and prelude.ar informally throughout the book. This chapter pulls them together: how a package is laid out, what the visibility tiers actually mean, and how the conventions compose.

Files are modules

Every .ar file under src/ is a module. The file path under src/, with / replaced by ::, is the module’s qualified path:

src/prelude.ar              →  lease_tutorial::prelude
src/party.ar                →  lease_tutorial::party
src/lease.ar                →  lease_tutorial::lease
src/legal/california.ar     →  lease_tutorial::legal::california
src/legal/mod.ar            →  lease_tutorial::legal

Two file-name conventions carry weight:

  • prelude.ar is the package’s library entry. It is also aliased as pkg::prelude, so consumers use lease_tutorial::prelude::* to bring in the whole curated surface in one line.
  • mod.ar is a directory module’s primary file. When you have src/legal/california.ar and want to add additional content to the legal module itself (rather than legal::california), put it in src/legal/mod.ar. Without mod.ar, src/legal/ is a namespace only — you can use lease_tutorial::legal::california but lease_tutorial::legal itself has no items.

A third file role: root.ar is the project entry — declared in [project] entry, it is the file the toolchain starts module discovery from. Both library packages (which expose a curated surface through prelude.ar) and application packages have a root.ar; the file’s job is to use foo::* every internal submodule so the project’s full module graph is reachable from one anchor.

Inline mod

A module can also be declared inline:

mod legal {
    pub kind Statute { ... }

    mod federal {
        pub kind FederalLaw : Statute { ... }
    }
}

Inline modules nest. The path for FederalLaw is lease_tutorial::legal::federal::FederalLaw. Use inline mod when the sub-module is small enough that splitting into its own file would be overkill. Use a separate file when the sub-module has substantial content.

Variants: _catalog/mod-inline/{minimal,nested-mod,mod-with-pub-items,cross-package,negative-bad-shape}/ — the cross-package/ variant exercises inline-mod items re-exported across a workspace boundary; the nested-mod/ variant demonstrates the qualified-path resolution shown above.

A worked excerpt from _catalog/mod-inline/nested-mod/src/prelude.ar:

use metatypes::*

mod jurisdiction {
    pub kind Statute {
        cite: String,
        text: String,
    }

    mod federal {
        pub kind FederalLaw <: Statute {
            uscode_section: String,
        }
    }

    mod state {
        pub kind StateLaw <: Statute {
            state_code: String,
        }
    }
}

// Qualified-path resolution works across inline mods:
use jurisdiction::federal::FederalLaw
use jurisdiction::state::StateLaw

Inline modules nest as a tree; each level adds one segment to the qualified path. The compiler treats jurisdiction::federal exactly as it would a src/jurisdiction/federal.ar file.

use

Three import shapes:

use ufo::prelude::*                          // glob — every exported item from the path
use ufo::a::endurants::{Object, Person}      // named — specific items
use ufo::a::endurants::{Object as Thing}     // named with rename
use lease                                    // module — the module itself, not its items

The as rename form is the canonical fix when two upstream packages export the same name and you need both in scope. Variants: _catalog/use-rename/{minimal,cross-package-rename,composition-with-re-export,negative-name-collision,idiom}/.

// continuing the import patterns above
use produce::Apple
use tech::{Apple as Computer}   // disambiguates the collision

Choose by intent. Glob imports are convenient for prelude-style modules whose items you almost always want; named imports are explicit about what you depend on; module imports let you write lease::Lease when you want the qualifying segment to appear at the call site.

pub use re-exports

pub use is the canonical pattern for curating a public surface:

// in src/prelude.ar
pub use metatypes::*
pub use party::*
pub use lease::*

Each pub use ::* brings the named module’s exported items into prelude and re-exports them. A consumer who writes use lease_tutorial::prelude::* sees everything, without knowing or caring which internal module declared what.

The pattern decouples your public API from your internal organization. You can split lease.ar into three files later — core.ar, subtypes.ar, lifecycle.ar — and update prelude.ar to re-export from them; no consumer code changes.

Two-tier visibility

There are exactly two visibility levels:

VisibilityReachDefault?
pubCross-module (and cross-package, when re-exported)No — opt-in
Module-internalFile-scoped onlyYes

There is no third tier. There is no per-package or per-workspace pub(crate) equivalent. The two-tier discipline is the design choice — when you want a wider surface, re-export through prelude.ar; when you want a narrower one, leave items unmarked.

The default direction matters: an Argon item is file-scoped until you export it. This is the reverse of OOP-conventional public-by-default. Opting into export is explicit.

The direct-dependencies rule

A package can use only items reachable through its own [dependencies], not items reachable transitively through some dependency’s dependencies.

If your package imports cofris and cofris itself imports coex, your package cannot use coex::Foo unless you also list coex in your own [dependencies]. The compiler rejects the transitive import; the rule is not a warning.

The intent is clarity: a package’s manifest is a contract over what it depends on. Transitive use creates accidental coupling — when an upstream removes a dep, downstreams break unexpectedly. The direct-dependencies rule eliminates the surprise.

Putting the conventions together

A typical small package:

my-domain/
├── ox.toml                # package manifest
├── ox.lock                # lockfile (generated)
└── src/
    ├── prelude.ar         # pub use re-exports — the curated surface
    ├── core.ar            # central concept declarations
    ├── rules.ar           # derives + asserts
    ├── queries.ar         # named queries
    ├── computes.ar        # pure functions
    ├── mutations.ar       # state-changing operations
    └── tests.ar           # test blocks

prelude.ar re-exports from each file. Internal modules stay private-by-default; only items they pub slip through prelude’s globs.

A larger package adds directories:

my-large-domain/
├── ox.toml
├── ox.lock
└── src/
    ├── prelude.ar
    ├── party/
    │   ├── mod.ar
    │   ├── person.ar
    │   ├── org.ar
    │   └── relationship.ar
    └── lease/
        ├── mod.ar
        ├── lease.ar
        ├── lifecycle.ar
        └── subtypes.ar

Each directory’s mod.ar re-exports from its siblings. The package’s prelude.ar re-exports from each top-level directory. Consumers use my_large_domain::prelude::* and the whole curated surface lands.

Edge cases worth knowing

  • Module names canonicalize. my-package becomes my_package for use paths. Hyphens in package names are friendly for the registry; underscores are the canonical form in code.
  • No item renames at the pub use site. You can rename at the use site (use foo::{Bar as Baz}); you cannot at the pub use site. Rename via a wrapper module if you need this.
  • Glob re-exports do not propagate beyond direct re-export. If pkg-A’s prelude is pub use pkg-B::* and pkg-B’s prelude is pub use pkg-C::*, then pkg-A’s prelude exposes everything from pkg-B’s prelude including its re-exports of pkg-C. The fixpoint is computed at elaboration; chain re-exports work as expected.
  • pub does not mean “registered globally.” Visibility is per-package; the registry is a separate concern.

Summary

Files are modules; mod declares inline modules; use imports; pub opts items into the cross-module surface; pub use re-exports through the prelude. The default visibility is module-internal; the two-tier system is the discipline. The direct-dependencies rule keeps imports honest. The lease tutorial uses these conventions throughout.

Workspaces and Dependencies

A workspace groups several related packages under one root with a shared lockfile and a shared dependency-version pool. Use one when you have packages that need to evolve together — a domain ontology and the test suite that exercises it, for example, or a foundational vocabulary and the domain packages that consume it.

This chapter covers workspaces and the lockfile + registry mechanics that hang off them.

A workspace manifest

The workspace root has its own ox.toml, with a [workspace] section in place of (or alongside) [package]:

[workspace]
members = ["packages/lease-tutorial", "packages/lease-tests"]
resolver = "1"

[workspace.package]
edition = "2025"
license = "CC-BY-4.0"
authors = ["Lease Team <leases@example.com>"]

[workspace.dependencies]
ufo = "0.2.1"
cofris = "0.2.1"

Three sections:

  • [workspace] — declares the workspace and lists members. members is a list of relative paths to package directories (glob patterns allowed); each must contain its own ox.toml.
  • [workspace.package] — defaults that members can inherit. Common fields: edition, license, authors, repository, description.
  • [workspace.dependencies] — version pinnings that members can inherit via <dep>.workspace = true.

Each member’s ox.toml carries both the [project] perspectival-composition layer and the [package] registry layer (see Chapter 1.3 for why both):

[project]
name = "lease-tutorial"
version = "0.1.0"
edition = "2025"
entry = "root.ar"

[schema]
root = "src"

[package]
name = "lease-tutorial"
version = "0.1.0"
edition.workspace = true
license.workspace = true

[dependencies]
ufo.workspace = true
cofris.workspace = true

workspace = true says “use the workspace root’s value.” This keeps version pins consistent across members without the rote of repeating them per file.

How resolution works

When you run ox install at the workspace root:

  1. The compiler walks each member’s [dependencies], expanding workspace = true to the workspace root’s pin.
  2. It builds the unified dependency graph across all members.
  3. External deps that appear in multiple members resolve to the same version — Cargo’s pattern. If two members declare ufo with incompatible version constraints, the resolution fails up front.
  4. Workspace members always resolve to the local sibling, never to a registry version. So if lease-tests depends on lease-tutorial and lease-tutorial is a workspace member, lease-tests sees the local source, not a published version.
  5. The lockfile lands at the workspace root (ox.lock) and is shared across members.

The lockfile

ox.lock records the exact resolution. It is bivalent: each entry carries two identifiers:

  • A content hash — the byte hash of the package’s tarball as published. Reproducible-byte verification at install.
  • A Merkle root over construct signatures — a hash over the package’s exported items’ shapes (concept names, field types, rule signatures). Semantic identity: two packages with the same Merkle root expose the same surface, even if their byte hashes differ.

The bivalence matters because Argon packages can be republished with cosmetic changes (a comment fix, a doc-string rewrite) that change the content hash but not the Merkle root. Tooling that wants “did the API change” looks at the Merkle root; tooling that wants “did the artifact change” looks at the content hash.

The lockfile is checked into version control — it is the source of truth for “what versions did this build use.” Re-running ox install against an existing lockfile is deterministic.

Updating dependencies

Two commands:

$ ox update                       # update all packages within their semver constraints
$ ox update <package>             # update a specific package

Both write a new ox.lock. Both report a compatibility diff: which package versions changed, and (via the Merkle root) which API surfaces changed. A surface-changing update is a yellow flag worth reviewing before committing.

The registry

ox publish ships a package to the registry:

$ ox publish
   Building canonical tarball
   Computing content hash + Merkle root
   Authenticating against registry
   Uploading tarball
✓ Published lease-tutorial v0.1.0

The registry’s URL and authentication are configured in ~/.argon/settings.toml; for the Sharpe-internal argon-packages registry, authentication uses GitHub tokens.

ox audit re-verifies the lockfile’s hashes against the cached packages:

$ ox audit
   ✓ All package content hashes match the lockfile.
   ✓ All Merkle roots match the lockfile.

The verification surfaces tampering or registry corruption. Run it in CI if you need a strong guarantee that the build’s inputs have not been substituted.

Local-path dependencies

For dependencies under active development, point at a local directory:

[dependencies]
local-pkg = { path = "../local-pkg" }

The compiler treats local-path deps as workspace members for resolution — they resolve to the source on disk, not a registry version. Use this during development; switch to a version pin before release.

Edge cases worth knowing

  • Single-package workspace is fine. A package with [package] only is a workspace of one. You do not need a separate [workspace] table to use a workspace pattern; many small packages live this way.
  • Members cannot have [workspace]. A member’s ox.toml carries [project] + [package] (and optionally [dependencies]); the workspace declaration belongs at the root.
  • The lockfile lives at the workspace root, not per member. Members do not have their own ox.lock.
  • target/ is per-workspace. Build artifacts and test caches share a directory at the workspace root, so members do not duplicate work.
  • Git deps are policy-controlled. The Sharpe-internal default disallows git-source dependencies (only registry + path are admitted). The mechanism exists in the manifest grammar for future use.

Putting it in the running example

We have built lease-tutorial as a single-package workspace so far. To grow into a multi-member layout, restructure:

lease-workspace/
├── ox.toml
├── ox.lock
└── packages/
    ├── lease-tutorial/
    │   ├── ox.toml
    │   └── src/
    │       ├── root.ar
    │       ├── prelude.ar
    │       ├── metatypes.ar
    │       └── ...
    └── lease-extras/
        ├── ox.toml
        └── src/
            └── ...

Workspace ox.toml:

[workspace]
members = ["packages/lease-tutorial", "packages/lease-extras"]

[workspace.package]
edition = "2025"
license = "CC-BY-4.0"

[workspace.dependencies]
# none yet

Each member’s ox.toml:

[project]
name = "lease-tutorial"
version = "0.1.0"
edition = "2025"
entry = "root.ar"

[schema]
root = "src"

[package]
name = "lease-tutorial"
version = "0.1.0"
edition.workspace = true
license.workspace = true

[dependencies]

Type-check from the workspace root:

$ ox check
ox lease-tutorial v0.1.0 (./packages/lease-tutorial/ox.toml)
ox lease-extras v0.1.0 (./packages/lease-extras/ox.toml)
check passed

The workspace pattern scales — a real ontology project may have ten or twenty members, with foundational packages (UFO, BFO), domain packages (lease, payments, regulatory), and a test workspace member coordinating them.

Manifest patterns in the catalog

The _catalog/manifest-*/ workspace family exercises each manifest section in isolation:

  • _catalog/manifest-catalog/{minimal,multi-version,registry-override,negative-missing-dep,idiom}/[catalog] declarations across single- and multi-version slot configurations, plus a registry-override variant that points one dep at a private registry.
  • _catalog/manifest-tree-shaking/{minimal,with-conditional-exports,with-feature-flags,cross-package,negative-orphan-import}/ — the [tree-shaking] directives that prune unused items at canonicalization time.
  • _catalog/manifest-defeat/{minimal,prioritized-ordering,plugin-strategy,cross-package,negative-missing-strategy}/[defeat] section configurations driving the defeasible-rule ordering (foundation: Chapter 5.2).
  • _catalog/manifest-modules/{minimal,nested-lattice,cross-package,mixed-world,negative-cycle}/ — the [modules] + [standpoints] machinery covered in Chapter 5.4.

Each variant ships a runnable ox check (and, for negative variants, the expected diagnostic code). Read them as a reference when wiring up a new workspace.

Summary

A workspace groups packages under one root with a shared lockfile and shared version pool. Members inherit defaults from [workspace.package]; they pin versions through [workspace.dependencies]. The lockfile is bivalent — content hash plus Merkle root — so tooling can distinguish byte and semantic identity. The registry handles publish/fetch/audit. Local-path deps support development. Single-package workspaces are fine; the pattern scales when you need it.

The Compiler and LSP

A working day with Argon spends most of its time in two surfaces: the ox CLI in a terminal and the language server in your editor. This chapter is a tour of both — what they do, what flags matter, and what to reach for when something is wrong.

The ox CLI in one page

Eight subcommand families. Most are familiar.

CommandPurposeNotable flags
ox checkType-check + meta-property + package constraints--reason (opt-in reasoning)
ox buildCompile to kernel types + Datalog
ox testRun test blocks--filter <pat>, --verbose, --timings
ox query <name>Run a named query--explain, --standpoint, --as-of-valid, --as-of-tx, --fork, --limit, --format, --arg KEY=VALUE
ox mutate <name>Run a named mutation--principal, --dry-run, --explain, --valid-at, --idempotency-key, --standpoint, --fork, --arg KEY=VALUE
ox diagram [<name>]Render diagrams to SVG / OntoUML--format svg|ontouml, -o <dir> (default diagrams/)
ox install / update / publish / auditPackage managementper-command
ox show <kind>Schema introspection--filter, --sort, --lattice, --limit, --format

A few habits that pay off:

ox check for the editor loop, ox check --reason before commits. The reasoning step is slower; not every save needs it. Save reasoning for moments where you want the invariants verified.

--explain whenever a query result surprises you. It produces a why-tree showing every rule firing that contributed to the result. Most “why is this happening” questions evaporate after one --explain.

--dry-run for any mutation you have not seen run before. It exercises the preconditions and shows you which events would be emitted, without committing. Combine with --explain to see the reasoning chain.

Diagnostic codes

Argon’s diagnostics use a structured naming scheme. Every diagnostic has a code of the form OE/OW/OI + four digits:

  • OE — Errors. The build fails.
  • OW — Warnings. The build proceeds; the issue is flagged.
  • OI — Info. Hints; nothing is wrong.

A few codes you will see often:

CodeMeaning
OE0101Unresolved name
OE0202Property type mismatch
OE0203Non-exhaustive match
OE0207Mixed strict/defeasible strength on the same head

Run ox explain <code> for a longer explanation:

$ ox explain OE0203
   OE0203 — Non-exhaustive match expression.

   A match has no wildcard arm and no guardless arm covering every
   possible subtype of the scrutinee. Every input must reach a body.

   Fix: add a `_ => …` wildcard arm, or add an arm that covers the
   missing subtypes.

The codes are designed to be lookuppable: knowing the code is enough to reach the explanation, the relevant chapter, and (eventually) the relevant decision rationale.

Note: an ox doc command — Argon’s analogue of cargo doc — is on the roadmap. Until it lands, ox explain <code> is the canonical surface for diagnostic-code lookup.

The LSP and editor extension

The language server (ox-lsp) and the VS Code / Cursor extension together provide the editor surface. Open an .ar file in your editor; the extension launches ox-lsp automatically.

Capabilities you will lean on:

  • Hover — type, decidability tier, governing decision, links to documentation. Hover over any identifier to see its full classification.
  • Inlay hints — inferred types, computed metatype values, implicit consequences on rules. Useful for reading rule code; the implicit => FactDerived(H) shows up as a ghosted suffix.
  • Go-to definition / Find references — works across packages.
  • Rename — refactor across the workspace.
  • Code actions — quick fixes for the common diagnostics. OE0203 (non-exhaustive match) inserts a wildcard arm; OE0101 (unresolved name) suggests an use import; OW0813 (deprecated .ol extension) suggests the .ar rename.
  • Diagnostics in real timeOE/OW/OI surfacing as you type; the editor shows them in the gutter and the problems panel.
  • Semantic tokenskind / subkind / role / phase tagged distinctly so syntax highlighting is metatype-aware.
  • Palette commandsArgon: Run query, Argon: Run mutation, Argon: Render diagram, Argon: Run tests. The full ox surface, accessible without leaving the editor.

The InfoView panel

A live-updating side panel that surfaces what the compiler knows about the file under the cursor. Open it from the palette (Argon: Open InfoView) and dock it next to your editor.

Three modes:

  • Module Dashboard — a bird’s-eye view of the active file: every concept, every rule, every test, with their decidability tiers and meta-property classifications. A good starting place when reading an unfamiliar package.
  • Symbol — focuses on whatever symbol your cursor is on. Shows the type, the metatype, the subtype lineage, the rules that mention it, and the tests that exercise it.
  • Metatype — shows the meta-property calculus result for a given metatype. Which axes? Which axiom obligations? Which axes are settled, which are open?

You will find yourself in the Module Dashboard when you arrive in a new file, then drill into Symbol when something specific puzzles you, then jump to Metatype when you want to understand why an item classifies the way it does.

Salsa-incremental everything

Behind the scenes, the compiler is built on Salsa — an incremental computation framework. When you change a file, only the things that depend on what changed are recomputed. In practice this means:

  • The editor loop is fast. Most ox check invocations through the LSP are sub-100ms.
  • ox check --reason reasons over only the rules whose dependencies changed; previously-saturated rules cache.
  • The InfoView updates incrementally. Hover after a change shows the new classification.

You do not configure any of this. It is the default mode of the compiler.

When something is wrong

A small triage tree:

The editor reports a diagnostic you do not understand. ox explain <code>. If the explanation does not name the issue, the diagnostic might be missing — file an issue against the project tracker.

ox check is slow. Run with OX_LOG=trace to see what stages take the time. Most slowdowns are reasoning-heavy rules; consider whether @[budget(N)] on the offending rule helps.

A mutation does not fire. --dry-run --explain. The output shows preconditions and event sequencing; one of them will be off.

A query returns wrong results. --explain. The why-tree shows the rules that fired (and the rules that did not fire when you expected them to).

The LSP stops responding. The output channel under Argon in the editor’s output view shows the LSP’s logs. A restart from the palette (Argon: Restart LSP) usually works.

Exercising the CLI in isolation

The _catalog/cli-ox-*/ workspace family ships one minimal workspace per CLI subcommand, so you can run a single command against a focused fixture and watch the output:

  • cli-ox-check/{passes,fails-OE0101,fails-OE0203,fails-OE0207,fails-OE0805}/ — pinned regressions for the four most common type-check errors.
  • cli-ox-test/{passes,test-with-expect,test-with-assert,multi-frame,negative-no-tests}/ — runs ox test with each test surface in isolation.
  • cli-ox-query/{simple,with-explain,with-standpoint,with-as-of-valid,with-fork}/ — exercises each query flag against a small fixture.
  • cli-ox-mutate/{simple,with-explain,with-dry-run,with-principal,with-idempotency-key}/ — same for mutations.
  • cli-ox-diagram/{single,all,format-ontouml,with-from,negative-bad-source}/ — diagram render paths.

For the LSP surface, _catalog/lsp-{hover,inlay,goto-def,find-refs,rename,code-action}/{minimal,…}/ workspaces ship reproducer source under src/prelude.ar and an EXPECTED.md describing the LSP response the user should see when the file is opened in an ox-lsp-aware editor.

Summary

The CLI is the compile/run surface; the LSP is the editing surface; the InfoView panel is the inspection surface. Diagnostic codes are structured (OE/OW/OI + four digits) and ox explain produces longer explanations. Salsa makes the editor loop incremental. The book’s discipline — ox check in the loop, ox check --reason before commits, --explain whenever something surprises you — produces a fast, debuggable workflow.

A Tooling Cookbook

ox check, ox build, ox install, and ox test are the day-to-day commands the previous chapters lean on. The toolchain has more — for moving models between Argon and OWL/OntoUML, for pulling concepts out of curated catalogs, for inspecting the dependency graph, and for browsing what a package actually exports. This chapter is the reference for those.

Each section is a short recipe: what the command does, what it expects, what it emits, and a 1–2-line example you can run against the lease tutorial (or, where the recipe needs a fixture from outside the tutorial, an explicit fixture path).

ox import — bringing a foreign model in

Argon does not assume your starting point is a green-field package. ox import ingests an OntoUML JSON (or OWL-family) artifact and emits a new Argon package with the corresponding concepts, relations, and rules already wired:

$ ox import model.ontouml.json --into ./my_package

The format is auto-detected from the extension and a content-sniff; pass --from ontouml to force it. The emitted directory contains:

  • ox.toml — dual-shape manifest (legacy [project] for ox check / ox test, modern [package] for ox install).
  • README.md — generic scaffold describing layout / build / round-trip, with attribution to the source project’s name.
  • src/main.ar — root-level declarations plus use ufo::* (and any other foundation packages auto-detected against the registry).
  • src/<slug>/mod.ar — one module per OntoUML source package, when the source has nested packages. Each module declares its own classes/relations/generalization-set blocks and use-imports peer modules so cross-module references resolve without fully-qualified paths.

Round-trip identity is preserved via @ontouml-id: doc-comment markers on every emitted declaration and @ontouml-package-id: markers on each mod.ar; no sidecar file is needed.

Two flags are useful in practice:

  • --into <DIR> — target directory. Defaults to a sibling directory named after the foreign artifact’s project name (slugified).
  • --package-name <NAME> — override the emitted package name when the foreign metadata isn’t what you want. Names must match ^[a-z][a-z0-9_-]*$.

The importer prints a one-line summary at the end (wrote package "X" to <dir> (N files, M warnings)) plus a per-code breakdown of any non-fatal XW084x warnings — unmapped stereotypes, name disambiguations, restrictedTo natures preserved as metadata, generalization-set lattice points lowered to partition / disjoint / complete, and so on. ox explain XW0841 (or any code) gives the full reasoning.

Foundation-package detection

If the OntoUML source references packages that the Argon registry already publishes (UFO, COFRIS, CCF, COEX), ox import resolves matching classes against the registry instead of inlining them. The mapping is built-in (UFO → ufo, COFRIS → cofris, etc.); the bridge introspects the local cache (~/.argon/packages/) for each mapped package, drops local declarations whose names appear in the registry’s public surface, and threads use <pkg>::* plus a [dependencies] entry through the emitted package. Cache-miss is a hard error — run ox install <pkg>@<version> first, or pass --no-registry-detect to opt out and inline everything.

Note: OntoUML JSON is the production-tested format today. OWL-family formats (Turtle, RDF/XML, JSON-LD) ride the same translation pipeline (ox-translate) and will be enabled as the surface stabilizes; until then, convert via OntoUML or hand-translate the parts you care about. The Argon roadmap treats round-trippable OWL as a first-class deliverable.

ox export — emitting a foreign model out

The dual of ox import. Run it inside a project directory to translate the project to a foreign format:

$ ox export --format ontouml -o my-model.ontouml.json

--format defaults to ontouml; --output defaults to <package-name>.ontouml.json next to the manifest.

Alongside the emitted file, ox export writes a .drop.toml sidecar listing constructs that have no analog in the target format — refinement types, defeasible rules, standpoints, the bitemporal axes — so a downstream tool that consumes the export can decide whether the loss is acceptable. A round-trip through ox importox export preserves structural identity (class / relation / generalization-set ids on every emitted declaration round-trip via @ontouml-id: markers); whitespace and key order in the emitted JSON may differ from the source, and constructs listed in .drop.toml are dropped by definition.

ox migrate.ol.ar

The legacy .ol source extension is being retired (OW0813 warns at every .ol it parses). ox migrate walks the project tree, renames files, edits the manifest, and rewrites .gitignore / .gitattributes patterns:

$ ox migrate --from ol --dry-run     # preview the plan, mutate nothing
$ ox migrate --from ol               # apply the rename
$ ox migrate --from ol --rewrite-docs  # also rewrite `.ol` mentions inside doc comments

The migration is git-aware: inside a git working tree it uses git mv so history follows the rename; outside, it falls back to std::fs::rename. It is atomic — if any step fails, no files are renamed. It is idempotent — running it on an already-migrated tree prints “nothing to migrate” and exits.

Exit codes are stable: 0 success, 1 I/O / manifest / git error, 2 conflict (a .ar file would clobber an existing .ar). CI scripts can branch on the code without parsing the human output.

ox catalog — pinned ontology catalogs

A catalog is a curated collection of ontology entries — pins of UFO, BFO, OntoUML libraries, internal vocabulary packages — that downstream projects can browse and selectively pull from. The pin lives in ox.toml under [catalog]:

[catalog]
url = "git@github.com:sharpe-dev/argon-catalog.git"
sha = "a8f3e2c1b4..."

The pinned SHA is authoritative; every catalog operation reads from it. ox catalog exposes four subcommands:

$ ox catalog list                    # every entry in the catalog
$ ox catalog show <entry>            # full manifest of one entry
$ ox catalog update                  # fetch catalog HEAD; rewrite [catalog].sha
$ ox catalog sources                 # print the pinned URL + SHA

--format human (the default) prints box-drawing tables; --format json emits machine-readable output for scripts.

Exit codes for ox catalog: 0 success, 2 entry not found, 3 version mismatch, 4 dependency cycle, 5 source-parse error, 6 auth failed, 7 lock mismatch.

ox yank — pulling concepts from a catalog into your source

Catalogs store complete ontologies. Often you want one piece — Contract from legal-kb, the Person shape from ufo. ox yank materializes named concepts into src/yanked/:

$ ox yank legal-kb::Contract                          # one concept + its parents
$ ox yank legal-kb::{Contract, Party, Obligation}     # several at once
$ ox yank legal-kb::Contract --no-deps                # just the concept; supply parents yourself
$ ox yank legal-kb::Contract --include-all            # the entry's entire translation
$ ox yank --verify                                    # re-yank everything and diff against disk

The yanked source lands at src/yanked/<entry>_<ConceptName>.ar (or a multi-concept variant). Each yank also appends a row to ox.yank.toml — the yank ledger — recording the catalog URL, SHA, entry, version, source IRIs, timestamp, and a Blake3 content hash. The ledger is what makes yanks reproducible.

--verify re-runs every recorded yank against the pinned SHA and reports any divergence. Run it in CI to catch upstream drift you have not yet pulled.

The default mode includes the transitive parent closure: if you yank Contract, you also get every concept Contract : ParentX : ParentY : ... references along its supertype chain. --no-deps opts out — useful when those parents already live in your project. --include-all opts way in — useful when you want a faithful copy of the upstream entry.

ox tree and ox why — inspecting the dependency graph

Cargo-shaped twin commands for understanding what your project depends on, and why.

$ ox tree
lease-tutorial v0.1.0 (./)

$ ox tree --depth 2
lease-tutorial v0.1.0 (./)
├── ufo v0.2.1
│   └── coex v0.2.1
└── cofris v0.2.1
    └── ccf v0.2.1

ox tree reads ox.lock (no network) and prints a DFS box-tree of transitive dependencies. --depth N truncates; --depth 0 is just the root.

ox why answers “why is this package in my graph?”:

$ ox why coex
coex v0.2.1
  ← ufo v0.2.1
  ← lease-tutorial v0.1.0 (./)

It walks reverse edges from the named package back to the project root, printing every distinct path. If the package is orphaned (lockfile stale; cleaned up by ox install), the output says so.

Both commands are read-only. They will not refresh the lockfile, fetch from the registry, or hit the network.

ox show — schema introspection

ox show prints the schema contents the elaborator built up. Useful for confirming what a package actually exports, or for piping into a script:

$ ox show concepts                                   # every concept the package elaborates
$ ox show concepts --filter rigidity=rigid           # filter by metatype-axis values
$ ox show concepts --filter qname=lease --limit 10   # substring match + cap rows
$ ox show standpoints --lattice                      # standpoint DAG, not the table
$ ox show queries --format json                      # machine-readable

Eight kinds are valid: concepts, relations, properties, standpoints, queries, mutations, computes. --filter KEY=VALUE is repeatable (multiple filters are AND’d; substring match on the column). --sort COL lexicographically sorts ASC by the named column. --limit N truncates after filtering.

--lattice is special: with standpoints, it emits the parent-child DAG instead of the table. With any other kind, it errors.

Both human (default) and json output formats are supported; the JSON shape is stable across patch releases.

ox stats — what’s in the project

A one-shot summary, useful for post-elaboration diffs and for talking-points in design reviews:

$ ox stats
project: lease-tutorial
modules: 11
total lines: 287
concepts: 6
roles: 2
properties: 19
rules: 2
constraints: 2
enums: 0
individuals: 0
queries: 3
computations: 1
standpoints:
  default

Output goes to stderr (so it composes with redirects); the format is intentionally human-shaped — there is no --format json because ox show already covers the machine-readable case.

A typical session

Pulling these together, a realistic UFO-team workflow:

# import the OntoUML model the team has been editing
$ ox import lease-domain.ontouml.json --into ./lease

$ cd lease
$ ox check
$ ox stats
$ ox show concepts --filter rigidity=rigid

# pull supplementary concepts from the curated catalog
$ ox catalog list
$ ox yank ufo::Relator
$ ox yank legal-kb::{Contract, Obligation}
$ ox check

# write a derive rule, run a query, mutate
$ ox query active_leases
$ ox mutate sign_lease --principal alice-001

# emit a snapshot for a downstream tool
$ ox export --format ontouml -o lease-derived.ontouml.json

# inspect the dependency graph before publishing
$ ox tree
$ ox why coex

Every command in that session is read-only against the model, except mutate (which appends to the bitemporal log) and import/yank (which write to the source tree).

Reference workspaces in the catalog

Each ox subcommand in this chapter has a dedicated workspace family in _catalog/:

  • _catalog/cli-ox-import/{ontouml-json,ontouml-with-package-name,xw0841,xw0843,negative-missing-file}/ — the ox import recipes plus the two most common XW084x warnings.
  • _catalog/cli-ox-export/{ontouml,with-drop-sidecar,format-json,negative-no-package,round-trip-stable}/ox export including the round-trip-stability variant exercising structural-identity preservation.
  • _catalog/cli-ox-migrate/{dot-ol,dot-ol-dry-run,with-rewrite-docs,already-migrated,exit-code-2}/ox migrate across the relevant exit-code branches.
  • _catalog/cli-ox-catalog/{list,show,update,sources,negative-missing-sha}/ox catalog subcommands.
  • _catalog/cli-ox-yank/{single,multi,no-deps,include-all,verify-drift}/ox yank recipes plus the CI-friendly --verify mode.
  • _catalog/cli-ox-tree/{minimal,depth-2,negative-stale-lock,via-workspace,via-publish-graph}/ and cli-ox-why/{direct,transitive,orphan,negative-not-installed,via-workspace}/.
  • _catalog/cli-ox-show/{concepts,relations,standpoints-lattice,with-filter,with-limit}/.
  • _catalog/cli-ox-stats/{minimal,multi-module,with-tests,with-mutations,via-workspace}/.

Read these as worked test fixtures — every command in this chapter has a reproducer in version control.

What’s elsewhere

Chapter 4.3 covers ox check, ox build, ox explain, the LSP, and the editor extension — the day-to-day surface. Chapter 4.2 covers ox install, ox update, ox audit, and ox publish — the lockfile and registry side. The commands in this chapter sit alongside both: model-shaped operations that don’t fit cleanly in either bucket but that you’ll use as the project grows.

Beyond — Advanced

This part covers the parts of Argon a working programmer can use without studying the calculus underneath, but that ontology engineers, language designers, and verification-minded readers will want the formal grounding for.

The four chapters in this part:

  • 5.1 The Metatype Calculus — the substrate’s calculus in detail: the IS / CAN / NOT three-valued state, the axis dependency DAG, the three-stratum fixpoint, cross-package composition, recognizer dispatch over canonical FOL shapes, and sibling-axiom propagation through user-declared structural rules. The chapter states the principal termination, uniqueness, and composition results.
  • 5.2 Defeasibility and Multi-Stratum Reasoning — the @[default] / @[override] / @[defeat] decorators, Governatori three-stratum compilation, NaF stratification, rule-conflict resolution. Builds on the foundational defeasibility-logic work of Reiter, García & Simari, and Governatori et al.
  • 5.3 Decidability Tiers — the seven-tier ladder in detail (tier:structural through tier:mlt), tier-cap enforcement at the module level, classification as a structural recursion, @[theorem] for compile-time mechanical verification, unsafe logic { ... } blocks, #[unproven] / #[assumed] test markers.
  • 5.4 Standpoints and Bitemporal Reasoning — standpoint declarations and the standpoint lattice, standpoint-discriminated queries, bitemporal axes (valid-time × tx-time) deep-dive, time-travel queries (--as-of-valid, --as-of-tx), forks and structural sharing.

Mathematical preliminaries

A few notational conventions this part uses, drawn from the foundational-ontology and formal-methods literature.

Three-valued state (). Every (concept, axis, value) triple in the meta-property calculus carries one of three values: (positively held), (positively excluded), (genuinely indeterminate). The information ordering is and , with and incomparable. This is canonical — Kleene’s strong three-valued logic (Kleene, Introduction to Metamathematics, 1952), realized as the consistent fragment of the four-valued bilattice (Belnap 1977 / Dunn 1976) under the Pietz-Rivieccio “Exactly True” designation. The / / naming follows the engine’s internal vocabulary.

Stratified evaluation. A stratum is an evaluation phase that reads only from earlier strata’s completed states. Stratified semantics for negation goes back to Apt, Blair & Walker (Towards a Theory of Declarative Knowledge, 1988); Argon uses stratification both for negation-as-failure inside rule bodies and for the three-stratum meta-property calculus.

Decidability tiers. Each rule lands at a tier classified by the compiler. Tiers 0–3 are executable (the saturator produces results); tiers 4–6 are syntax-only (parsed and type-checked but not evaluated by today’s runtime). The classification is a structural recursion over the rule body, taking the maximum of intrinsic-tier per atom.

Bitemporal axes. Every fact is timestamped with a valid-time (when the fact is true in the world) and a transaction-time (when the system learned it). A query against the log filters by both. Retraction is a new event with later ; the original fact is preserved. The model follows Snodgrass & Ahn (Temporal Databases, 1986) and Jensen, Soo & Snodgrass (Unifying temporal data models via a conceptual model, 1994).

Standpoints. A standpoint is a named position in a partial-order lattice; facts and rules are tagged with their standpoint and queries can be discriminated to a particular vantage. Standpoints discriminate, they do not partition — a fact in default is also visible in any descendant unless explicitly retracted.

Principal results

Each chapter in this part states the math the implementation rides on. Three results carry the substrate:

  • Meta-property fixpoint — termination and uniqueness of the stratified fixpoint over an acyclic axis-dependency graph; package composition (disjoint non-interference, shared-axis confluence); at completion is genuine indeterminacy. Covered in Chapter 5.1.
  • Flow-typing soundness — occurrence-typing narrowing persists across body atoms and survives monotone rule composition; CWA→OWA narrowing preservation. Covered in Chapter 5.1’s stratification discussion and applied throughout Chapter 2.
  • Refinement-fragment decidability — the refinement type-checking fragment is decidable in PTIME, with staging correctness for the multi-package case. Covered in Chapter 2.6 and Chapter 5.3.

Reading order

If you have read Parts 1–4, the chapters below can be read in any order. A natural sequence:

  1. Ch5.1 Metatypes — explains the calculus that powers Tier 0 reasoning across the language.
  2. Ch5.3 Decidability Tiers — shows the ladder the compiler classifies every rule against.
  3. Ch5.2 Defeasibility — the layer that runs on top of the tier ladder, organising rule conflicts.
  4. Ch5.4 Standpoints + Bitemporal — the runtime story, orthogonal to (and composing with) all of the above.

Or skip directly to the chapter that answers a specific question. Each is self-contained.

The Metatype Calculus

Chapter 2.1 introduced the four declaration forms — pub metaxis, pub metatype, pub metarel, pub decorator — that user packages compose to declare an ontology. This chapter is the engine underneath: the meta-property calculus the compiler runs over those declarations.

The calculus is a stratified Datalog-like fixpoint over a three-valued lattice. Given a concept hierarchy and a vocabulary’s metatype declarations, it computes for every concept its full meta-property profile across every axis the vocabulary declares; classifies declared metatypes against the inferred profile; runs the structural-rule constraints the package ships; and dispatches user decorators that match canonical FOL shapes onto fast-path implementations in the reasoner.

This chapter goes deeper than the surface needs. The audience is ontology engineers and language designers — readers who want the formal grounding behind ch02-01’s declaration forms, the cross-package composition rules, and the recognizer table that makes user-declared decorators run as fast as built-in ones. Working programmers can skim or skip; the surface in Part 2 is enough.

The chapter draws directly on Guizzardi’s UFO axiomatization (rigidity / sortality / identity-provision originated in Guizzardi 2005, refined in Guizzardi & Wagner 2010 and Guizzardi et al. 2018). The principal results below — termination, uniqueness, and necessity of acyclicity — are stated as theorems and accompany the engine that evaluates them.

The four foundational axes (UFO as worked example)

UFO declares four axes that the rest of its foundational vocabulary builds from. We use UFO here because it is the most-developed worked example; BFO, GFO, DOLCE each declare their own axes via the same substrate.

Rigidity

A concept is rigid if every instance falls under it in every world the instance exists in. A Person is rigid: an entity that is a person here is a person in every counterfactual world where that entity exists at all. A Student is anti-rigid: an entity may be a student in this world and not in another. A Mixin is semi-rigid: rigid for some classifications, anti-rigid for others.

Rigidity is the foundational axis in UFO and is operationalized as A.rigidity ∈ { rigid, anti_rigid, semi_rigid } in the meta-property state.

Sortality

A concept is sortal if it carries an identity criterion — a principle for telling when two instances are the same. A Person is sortal (we have a notion of which person is which). A Red Thing is non-sortal (red is a classifier, not an identity-provider).

In UFO, sortals must trace through their supertype chain to a Kind — the metatype that provides identity. OW0207 (“sortal missing Kind ancestor”) is the structural-rule check that surfaces a violation.

Identity-provision

A concept provides identity if instances classified by it gain an identity criterion from it. Kind provides identity. Role does not (it inherits identity from the role-playing concept’s Kind). Subkind does not (it inherits from the parent Kind).

This axis is what makes kind and subkind semantically distinct even though both have the same { rigid, sortal } profile on the rigidity-and-sortality axes alone. The third metatype dimension provides_identity ∈ { yes, no } separates them.

Provides-mediation

A concept provides mediation if it exists to connect other concepts. Relator provides mediation; that is the whole point of the metatype. Kind does not.

Lease is a relator: it has no business existing on its own; it exists to connect a Tenant, a Landlord, and a property. The structure traces back to Hohfeld (1913), who analyzed legal relations as bundles of right-duty / power-liability pairs that bind multiple parties — a tenant’s right to occupancy is paired with a landlord’s duty not to interfere, a landlord’s power to terminate is paired with a tenant’s liability to vacate, and so on. Each pair lives in the relator, not in the parties. UFO’s relator (Guizzardi 2005, §6.4) is the foundational-ontology generalization: a category of entities whose existence consists in mediating other entities.

OW0211 (“role has no mediation path to relator”) is the structural-rule check that fires when a role concept has not been wired to any concept that provides mediation.

The axis dependency DAG

A pub metaxis declaration may depend on other axes through a condition clause. UFO’s identity_provision axis declares:

pub metaxis identity_provision for type {
    provides,
    inherits,
    condition: rigidity == rigid and sortality == sortal
}

The condition says: identity_provision only ranges over concepts that have already been classified as rigid and sortal. The calculus reads this as a dependency edge in the axis DAG: rigidity → identity_provision and sortality → identity_provision. The fixpoint iterates axes in topological order so that, by the time it computes identity_provision, the predecessor axes have completed.

Two constraints follow:

  1. Acyclicity. A vocabulary whose dependency edges form a cycle is rejected at vocabulary-load time. Without acyclicity the fixpoint admits two evaluation orders that disagree at termination — the result depends on the order, not the input. Argon refuses such vocabularies; a package author must structure axis dependencies as a DAG.
  2. Topological evaluation. With acyclicity, the calculus runs the axes in topological order. Within an axis, the calculus runs three strata (below); across axes, the strata of axis B may read the completed state of axis A. This makes condition clauses load-bearing: they let one axis express its applicability in terms of another.

The DAG is per-vocabulary. Two unrelated vocabularies (UFO with rigidity/sortality, BFO with temporal-status/dependence) compose to two disjoint DAGs that evaluate independently. A vocabulary that extends another (a domain package declaring enforceability whose condition depends on UFO’s rigidity) extends the DAG with new edges to UFO’s existing axes; the result must remain acyclic, and the combined fixpoint over multi-package axis sets is well-defined under that condition.

The three strata

The engine runs each fixpoint pass in three strata, in this order. Strata are stratified in the technical sense: each stratum reads from the previous strata’s completed state, never from an in-progress one.

Formally, let be the set of concepts, the set of axes, and the value set for each axis . The meta-property state is a function where . The lattice on values is the information ordering and ; and are incomparable.

Stratum 0 — IS propagation

Stratum 0 is a set of monotone rules of the form

Each rule reads only IS premises and produces an IS conclusion. The set of rules is run to fixpoint: where is the immediate-consequence operator over the rule set. Because is monotone in the information ordering and the lattice has finite ascending chains, the iteration terminates.

If a concept is declared as a kind, the metatype’s profile materializes as starting IS facts: , . Subtype propagation rules give from when in the type hierarchy.

The composition of Stratum 0’s rules is monotone in the information ordering, and the lattice has finite ascending chains, so the iteration’s least fixed point exists, is reached in finitely many steps, and is unique.

Stratum 1 — NaF (negation-as-failure)

Reads the completed IS-state and adds NOT facts where the absent-condition holds:

This is classical Clark NaF semantics, restricted to a single direction (negative) so the calculus stays stratified. Stratum 1 reads completed Stratum 0 state, so the negation cannot circle back.

Stratum 2 — constraint checks

Reads both IS and NOT states. Emits diagnostics: structural rules (OW0207 through OW0214 plus more), rigidity / sortality consistency checks, sibling-axiom partition completeness checks. Stratum 2 never modifies the meta-property state — it is a one-way producer of diagnostics. A violation is a warning or error; the calculus’s IS / NOT state is unchanged.

The three-valued state

Each (concept, axis, value) triple lands in one of four states:

  • IS — Stratum 0 placed it there. Rigorously inferred.
  • NOT — Stratum 1 placed it there. The concept is not at this value.
  • CAN — neither IS nor NOT applies. The state is genuinely indeterminate.
  • (absent) — the axis is not in scope for this concept.

The CAN state is the load-bearing one. Under open-world assumption — which is Argon’s default for ontology work — the absence of evidence is not evidence of absence. CAN says: “the model has not committed to a value; further axioms may pin it.” The fourth theorem below pins this down formally: CAN at fixpoint completion really is genuine indeterminacy — not “we forgot to check.”

This three-valued state is essential for the refinement-type fragment (Chapter 2.6) and the standpoint world-assumption interaction (Chapter 5.4).

Sibling-axiom propagation and structural rules

The calculus is the engine. The structural rules — UFO’s R01–R37 (Barcelos et al. 2023) — are the content: constraint rules a vocabulary package declares to enforce well-formed modeling. They are the second-and-third strata’s main customers.

They land in the source as pub strict error or pub warning declarations:

pub strict error rigid_specializes_anti_rigid(A, B) :-
    A specializes B,
    A.rigidity == rigid,
    B.rigidity == anti_rigid

This is UFO’s R02 in source: “a rigid type cannot specialize an anti-rigid type.” The body reads the meta-property state populated in Strata 0 and 1; the head emits a diagnostic at Stratum 2. The rule does not mutate state — it only signals.

The full UFO ruleset (R01–R37) covers hierarchy constraints (rigidity-lattice descent), identity constraints (every sortal traces to a Kind ancestor; no two independent identity providers), mixin heterogeneity (semi-rigid mixins must aggregate at least one rigid and one anti-rigid descendant), phase constraints (every phase is in a partition), role constraints (every role has a mediation path to a relator), and anti-pattern detections (non-sortal aggregating sortals from a single Kind — likely a Subkind in disguise).

A different vocabulary ships a different ruleset. BFO’s structural rules carve continuant-vs-occurrent disjointness, independent-vs-specifically-dependent disjointness; cofris carves financial-quantity invariants; ccf carves accounting-equation invariants. The machinery is the same: rules read the calculus’s IS/NOT state, emit diagnostics at Stratum 2.

The reason structural rules are first-class — not decorative documentation — is that the calculus runs them at every ox check. UFO’s R02 is not a comment in a paper; it is a rule the elaborator applies, and a violation is a compile-time event. The structural-rule diagnostic codes (OE0204 for hard errors, OW0207OW0214 for warnings) are documented in Appendix C; each fires on a specific axiom and ox explain long-forms the underlying rationale.

Recognizer dispatch — fast-path for canonical shapes

A user package declares decorators like @[transitive], @[symmetric], @[functional] on relations. The decorator’s semantics: body is FOL — a formula the reasoner could in principle evaluate via Datalog with negation. In practice that is slow: a transitive-closure rule fires O(n^2) times in the worst case under naïve Datalog, and a Functional cardinality check naïvely materializes all pairs.

The compiler avoids both costs through the recognizer table: ten canonical FOL shapes that the reasoner has fast-path implementations for. When a pub decorator body matches one of these shapes, the compiler routes the application to the fast-path; when it does not, the body falls through to generic Datalog evaluation.

The ten shapes:

Transitive { rel }
Symmetric { rel }
Asymmetric { rel }
Reflexive { rel }
Irreflexive { rel }
Functional { rel }
InverseFunctional { rel }
QualifiedCardinality { rel, on_class, min, max }
DisjointClasses { class_a, class_b }
CoveringClasses { parent, children }

Recognition runs against the FOL body after six normalization passes — alpha_rename → flatten_associative → push_negations_inward → implications_to_disjunctions → canonicalize_equality → sort_conjuncts. Per-rule overhead stays under 5ms in a 100-rule package; the matching is exact, not heuristic. A body that recognizes routes the relation through a built-in fast-path (enable_transitive_closure_for, enforce_functional, …); a body that does not lowers to Datalog atoms and runs through the generic evaluator.

A user decorator can pre-tag its expected shape:

pub decorator transitive() on rel = {
    semantics: forall x y z. r(x, y) and r(y, z) -> r(x, z),
    lowers_to: Transitive
}

The lowers_to: Transitive annotation tells the compiler “I expect this body to recognize as a Transitive shape.” If the body does not match the declared shape, the compiler emits OE0232 RecognizedShapeMismatch. Without the annotation, the compiler runs the recognizer and uses whatever shape matches; with the annotation, it cross-checks. Pre-tagging is documentation that the compiler enforces.

The recognizer also feeds into tier classification. A rule whose body recognizes as Transitive collapses from the syntactic tier:fol to the structural tier:closure — its effective tier is min(syntactic, recognized). A user who writes a transitive-closure body inside unsafe logic { ... } (which is always tier:fol ambient) can still benefit from the fast-path and the lower effective tier, provided the body recognizes.

Chapter 5.3 covers tier classification end-to-end; the recognizer table is the substrate’s bridge to the reasoner’s fast-path layer.

Cross-package metatype composition

The substrate composes across packages. UFO declares rigidity, sortality, identity_provision. cofris (the financial-resources foundational ontology) declares its own axes — say, liquidity and convertibility. A domain package can declare a metatype that depends on both:

use ufo::prelude::*;
use cofris::prelude::*;

pub metatype MarketableAsset = {
    rigid,
    sortal,
    provides,
    liquid,
    convertible
}

The compiler resolves each axis name against the union of in-scope pub metaxis declarations from both packages and validates that every axis the metatype binds is satisfied. The dependency DAG extends naturally: UFO’s edges, cofris’s edges, and any new edges the metatype’s axes induce. The combined DAG must remain acyclic.

Three composition results matter here:

  • Disjoint non-interference. If two vocabulary packages share no axes, their rule sets evaluate independently. UFO’s structural rules cannot affect cofris’s classification, and vice versa. This is the common case for orthogonal foundational ontologies.
  • Shared-axis confluence. If two packages share an axis (both declare rigidity, say), the calculus runs the union of their rule sets and the result is well-defined provided the rule sets are jointly stratified — that is, the axis’s rules from one package do not cyclically depend on the axis’s rules from the other.
  • Combined-fixpoint convergence. The combined fixpoint over multi-package axis sets terminates and is unique under acyclicity.

A practical consequence: a project can mix foundational ontologies (UFO + cofris + ccf, the canonical Argon stack) without the compiler’s behavior depending on import order. The semantics is fixed by the union of declarations, not the iteration order through them.

Bidirectional reasoning

The engine exposes three operations:

DirectionFunctionUse
Forwardevaluate(hierarchy)concept hierarchy → meta-property state
Reversesynthesize_metatype(properties)meta-property profile → matching metatype names
Verificationcheck_metatype(concept, declared)declared metatype matches inferred profile

Forward is the obvious direction: given the modules, fill in every concept’s IS / NOT / CAN triples.

Reverse synthesis is the more unusual one. Given a meta-property profile (say { rigid, sortal, provides_identity: yes }), the engine returns the list of metatype names whose definition matches that profile. It is the answer to “given how this concept actually behaves, what metatypes should we consider for it?” — useful when porting concepts from one foundational ontology to another, or when the modeller knows a concept’s behavior but is uncertain about the right metatype label.

Verification is the consistency check: the user declared pub kind Person and the engine infers Person’s profile. If the inferred profile contradicts kind’s declared profile, the engine emits a diagnostic. The check is the foundation for the structural-rule warnings and the four-variant OE0605 UnknownMetatype hint.

Termination, uniqueness, and the indeterminacy guarantee

The fixpoint terminates because the axis-dependency graph is acyclic. With no cycles, the fixpoint runs the strata in topological order and finishes in time per pass, where is the rule count.

Four results carry the calculus.

Termination. For any stratified rule set over a finite concept hierarchy, the fixpoint iteration of the strata reaches a fixed point in finitely many steps.

Uniqueness. For any stratified rule set , the fixpoint is unique: any two evaluation orders that respect the strata produce the same final state.

Necessity of acyclicity. Acyclicity of the axis-dependency graph is necessary for fixpoint uniqueness — a cyclic dependency graph admits two evaluation orders that disagree at fixpoint.

Genuine indeterminacy of CAN. If at fixpoint completion, then no monotone extension of the input determines whether should be IS or NOT. CAN at completion is genuine indeterminacy — not “we forgot to check.”

The fourth result is the load-bearing one for refinement-type semantics: when Chapter 2.6 admits a refinement-type predicate only when its meta-property atoms are IS, the CAN cases left out are formally indeterminate, not arbitrarily excluded.

When a metatype declaration goes wrong

Six classes of error worth knowing:

  • OE0603 MetaxisValueTypeMismatch — a pub metatype binds an axis to a literal whose type does not match the axis’s declared value_type. Writing pub metatype kind = { rigidity = "rigid" } against pub metaxis rigidity { rigid, anti_rigid } triggers this — the axis values are atoms, not strings.
  • OE0605 UnknownMetatype — a pub <name> X { ... } concept declaration whose <name> does not resolve to an in-scope metatype. The diagnostic carries a four-variant hint: the name might resolve to something other than a metatype (NotMetatype); a matching metatype might exist in an unimported package (AvailableNotImported); multiple matching metatypes might exist across packages (MultipleAvailable); or no matching declaration exists anywhere (NotDeclared).
  • OE0606 MetaxisRefinementViolation — a value supplied to a typed-domain metaxis fails the axis’s where { _ <pred> } refinement. A metarel that binds cardinality_floor = 0 against a metaxis declared Nat for rel where { _ > 0 } triggers this.
  • OE0225 UnknownMetarel / OE0226 MetarelEndpointMismatch — a relation declared :: foo whose foo does not resolve to an in-scope metarel, or whose source/target endpoints do not satisfy the metarel’s declared metatype constraints.
  • OE0229OE0234 (decorator family) — unresolved name, arity mismatch, target mismatch, tier-clause divergence, argument-type mismatch on a decorator application, plus OE0232 RecognizedShapeMismatch when a body’s lowers_to annotation diverges from what the recognizer matches.
  • OE0204 and the OW0207OW0214 family — UFO’s structural rules (rigid specializing anti-rigid, sortal missing Kind ancestor, role missing mediation path, etc.) firing as either errors or warnings depending on the rule’s declared severity. These are vocabulary-side — UFO authors them, the compiler runs them. A different vocabulary ships a different family.

The first five classes are diagnostics from the substrate itself: they fire when a vocabulary package’s declarations are malformed. The last family is diagnostics from the vocabulary’s content: they fire when user code writes concepts that violate the vocabulary’s structural constraints. Both are run by the same calculus at Stratum 2.

A worked verification

Take the lease tutorial’s metatypes:

use ufo::prelude::*;

pub kind Person { ... }
pub role Tenant : Person { ... }
pub relator Lease { tenant: Tenant, ... }

The engine, given this input:

  • Stratum 0 propagates IS(Person, rigidity, rigid), IS(Person, sortality, sortal), IS(Person, identity_provision, provides) from kind’s profile. Subtype propagation gives IS(Tenant, sortality, sortal) and IS(Tenant, rigidity, anti_rigid) from role’s profile (overriding what would have inherited from Person’s rigidity, because the role declaration pins the value). Same for Leaserelator’s profile carries rigid, sortal, provides_identity.
  • Stratum 1 completes the negative facts: NOT(Tenant, rigidity, rigid), NOT(Person, rigidity, anti_rigid), NOT(Lease, sortality, non_sortal), etc. Every (concept, axis, value) triple where Stratum 0 placed nothing and no other value of the same axis was placed for the concept lands as NOT.
  • Stratum 2 runs the structural checks. Tenant: anti-rigid + sortal → must trace to a Kind ancestor → does, via PersonOW0207 does not fire. Tenant: relationally dependent → must have a mediation path to a relator → does, via the tenant field on LeaseOW0211 does not fire. Lease: provides_identity → checks the partition rule (no two independent identity providers in the ancestor chain) → passes.

All clean. The lease tutorial’s ox check reports zero structural-rule warnings, which means the metatype graph the modeller wrote is internally consistent against UFO’s structural axioms.

For a worked example of Stratum-0 IS propagation across an independent foundational vocabulary — i.e. one with axes that have nothing to do with UFO’s rigidity/sortality — see argon/examples/bfo-smoke/. The smoke test declares BFO’s temporal_status and dependence axes, four BFO metatypes (continuant, occurrent, independent_continuant, specifically_dependent_continuant), six BFO concepts (MaterialEntity, Process, Quality, …), and two BFO structural rules (continuant-occurrent disjointness, independent-dependent disjointness). Running ox check exercises every stratum of the calculus end-to-end against BFO; running it alongside argon/examples/dolce-lite/ exercises the disjoint-non-interference composition result. The dedicated _catalog/rule-meta-coloncolon/ workspaces additionally exercise the four-arm endpoint resolver from RFD-0031 — relations declared :: <metarel> whose endpoints reference concepts, metatypes, or wildcards.

If the user had instead written pub kind Tenant : Person { ... } (asserting Tenant is rigid), Stratum 0 would propagate IS(Tenant, rigidity, rigid). Verification — the third bidirectional operation — would then report that Tenant’s declared metatype kind carries the profile {rigid, sortal, provides}, while the inferred profile from the surrounding ontology (Tenant playing a role on Lease) says it should be {anti_rigid, sortal, relational}. The engine emits a diagnostic explaining the disagreement.

Why this matters

Four things, taken together, make the meta-property calculus more than just an implementation detail:

It makes vocabulary substitution real. Switching the lease tutorial from UFO to BFO is not a rewrite — it is a different use line and (potentially) a different set of axis declarations. The concepts, relations, and rules in lease.ar and rules.ar are the same. The metatype labels and structural rules are different, because that is what changes between foundational ontologies.

It makes structural rules first-class. UFO’s structural axioms (R01 through R37 in the foundational papers) are not documentation — they are constraint rules in the engine, with diagnostic codes and ox explain long-forms. A structural violation is a compile-time event.

It makes the recognizer a public surface. A user who declares @[transitive] knows that the body recognizes as a Transitive shape and routes to the reasoner’s fast-path. A user who declares a custom decorator with a body that recognizes as Functional gets the same fast-path treatment — without convincing the compiler maintainers to special-case their decorator. The substrate’s recognizer is a finite menu of canonical FOL shapes; users dial decorators against that menu.

The metatheory is closed. Termination is not “we hope so,” uniqueness is not “in practice, yes.” The four results above are theorems about the calculus, not observations about the implementation; that is a stronger statement than “the implementation seems right.”

What’s next

Chapter 5.2 covers the defeasibility layer that builds on top of the calculus — @[default], @[override], @[defeat], the Governatori three-stratum compilation. Chapter 5.4 covers standpoints, which interact with the meta-property state through per-(module, predicate) world assumptions. The tier ladder treats meta-property lookups as tier:structural reasoning — the cheapest possible derivation, polynomial in the size of the calculus, and the floor onto which recognized-shape decorators collapse from the FOL body language.

Defeasibility and Multi-Stratum Reasoning

A defeasible rule is one that can be overridden. The classical example, going back to Reiter’s A Logic for Default Reasoning (1980): birds fly; penguins are birds; penguins do not fly. The rule “birds fly” is not wrong — it is defeasible, defeated by the more-specific rule “penguins do not fly.” A reasoning system that cannot represent this kind of layered exception either over-asserts or under-asserts, and either failure mode shows up as soon as a real-world domain enters the picture.

Argon supports defeasibility natively, following Governatori et al.’s formalization in Defeasible Logic: Agency, Intention and Obligation (DEON 2004) and the broader Defeasible Logic Programming tradition (García & Simari, Defeasible logic programming, TPLP 2004). The three layers — strict rules, defeasible rules with a superiority relation, and the resulting stratified evaluation — give unambiguous semantics independent of evaluation order.

This chapter covers the three decorators that mark and order defeasible rules, the Governatori three-stratum compilation that gives them their semantics, and the diagnostics that surface when the defeasibility graph contradicts itself.

The mechanism lives in oxc::stratify. Defeasibility composes with the three-engine derive pipeline (Chapter 2.4), the project-level [defeat] configuration (later in this chapter), and the multi-head disjunction stratification covered below.

Strict versus defeasible

A rule with no decorator is strict: when the body fires, the head holds, full stop. No other rule can defeat it.

pub derive ResidentialLeaseHasOneTenant(l: ResidentialLease) :-
    l: ResidentialLease,
    count(t for t in l.tenant) == 1

A rule marked @[default] is defeasible: it fires when nothing more specific overrides it.

@[default]
pub derive standard_security_deposit_applies(l: Lease) :-
    l: Lease

The default says: every lease falls under the standard security-deposit regime. Unless something more specific says otherwise. (The actual deposit value is computed by a compute keyed off whichever regime the lease classifies under.)

@[override(rule)] — pointing at a specific defeater

Mark a rule as overriding another by name:

@[override(standard_security_deposit_applies)]
pub derive rent_controlled_security_deposit_applies(l: Lease) :-
    l: Lease,
    rent_controlled(l)

When the body of rent_controlled_security_deposit_applies fires, it defeats standard_security_deposit_applies for that lease. The engine treats the two rules as in a superiority relation: the override is more specific.

@[override(rule)] is the most-explicit form of defeasibility. The override target is named explicitly; the dependency is local; the resolver can build the superiority graph statically at compile time.

@[defeat(rule)] — defeasibility as an effect of the body

A different shape: a rule whose body asserts the defeat of another rule, without contributing a positive head of its own.

@[defeat(standard_security_deposit_applies)]
pub derive prohibit_standard_deposit_regime(l: Lease) :-
    l: Lease,
    deposit_prohibited_jurisdiction(l)

@[defeat(rule)] is for the cases where the body’s effect is “this other rule does not apply” rather than a positive consequence. The body fires; the named rule is defeated for the bindings that satisfied the body. The defeat is the rule’s contribution to reasoning.

A companion decorator @[chain(rule)] declares that a defeasible rule chains through another — the chain link’s body assumes the chained rule’s head and the resulting derivation carries both rules’ provenance into the why-tree. This is the canonical pattern for layered regulatory reasoning where one rule’s conclusion is another rule’s premise:

@[default]
pub derive eligible_for_subsidy(t: Tenant) :-
    t: Tenant,
    t.household_income < poverty_line

@[chain(eligible_for_subsidy)]
pub derive senior_subsidy_amount(t: Tenant) -> Money :-
    t: Tenant,
    t.age >= 65
    => t.household_income * 0.4

Variants: _catalog/decorator-chain/{minimal,composition-with-defeasibility,multi-chain-link,idiom}/ and _catalog/decorator-defeat/{minimal,composition-with-default,negative-bad-args}/. The manifest-defeat/cross-package/ workspace shows the project-level [defeat] order strategy resolving conflicts across packages.

Governatori three-stratum compilation

The semantics of a defeasible-rule program is given by a three-stratum compilation, after Governatori (and the broader defeasible-logic-programming tradition).

Formally, let partition the rule set into strict rules and defeasible rules , and let be the superiority relation induced by the @[override] and @[defeat] annotations plus the configured [defeat] order strategies. The semantics of is the fixed-point of the three strata:

  1. Stratum 1 — strict closure. , where is the immediate-consequence operator over strict rules. These derivations are unconditional. Strict rules cannot be defeated; if a strict rule and a defeasible rule disagree on the same head, the strict rule wins by construction.
  2. Stratum 2 — defeasible closure with superiority resolution. Starting from , add a defeasible derivation producing from facts in if and only if no rule produces from the same facts. Repeat to fixpoint . When two rules at incomparable priority disagree, the conflict is ambiguous (OE0401).
  3. Stratum 3 — multi-head disjunction stratification. Multiple same-named defeasible rules form a Datalog disjunction: a fact holds if any of the rules fires. The disjunction’s contribution to the negation-as-failure layer is resolved by stratification, giving the final closure .

Each stratum reads from the completed state of the previous one. Stratum 2 cannot read its own in-progress state; that is what makes the compilation deterministic and the result independent of evaluation order.

The output is a triple together with, for each defeasible-rule application in , a record of which superiority-graph edge made the application win. Why-provenance traces back through the graph; a regulator can ask “which override produced this result?” and get a precise answer.

The superiority graph

The set of @[override] and @[defeat] annotations across a project induces a superiority graph: an edge A → B means rule A is more specific than rule B. The compiler:

  • builds the graph from all annotations, across modules;
  • checks for cycles (OE0403) — a cycle is an unrecoverable inconsistency in the modeller’s defeat-ordering;
  • topologically sorts the graph to give Stratum 2 its priority order.

Cycle detection is binding. A cycle means two rules each claim to defeat the other; there is no consistent way to compute a fixed-point. The build fails.

OE0402 (“unreachable defeater”) fires when an @[override] or @[defeat] references a rule whose body could never fire under any interpretation — a static check that catches the kind of dead-code defeat-annotation that would otherwise pass silently.

Project-level [defeat] strategies

Some defeasibility orderings are systemic — not per-rule but per-project. Lex specialis (more specific wins), lex posterior (later wins), and lex superior (higher-status source wins) are the three classical orderings. Argon admits them as ordered project-level fallbacks:

[defeat]
order = ["explicit", "lex-specialis", "lex-posterior", "lex-superior"]

explicit is the per-rule annotations (@[override], @[defeat]). When two rules contradict and neither is explicitly annotated relative to the other, the engine consults the next strategy: lex specialis (the rule with the more-specific body wins). Then lex posterior (the rule defined later in source order wins). Then lex superior (the rule from the higher-priority package wins).

The four known strategies are explicit, lex-specialis, lex-posterior, lex-superior. Unknown strategies are OE1002. Empty order is allowed — it means “fall through to ambiguity diagnostics if no @[override] resolves.”

Mixed-strength rules and OE0207

A rule cannot be strict in some applications and defeasible in others. The compiler enforces this with OE0207 (“mixed-strength rules on same head”): if two rules with the same head differ in strength (one strict, one @[default]), the build fails. The fix is to make both defeasible or both strict — the modeller’s intent must be uniform.

This is a less-obvious constraint than it first appears. It catches the typical mistake of “I added an override but forgot to mark the original as defeasible,” which would otherwise silently produce contradictions.

Stratification and circular negation

NaF (negation as failure) interacts with defeasibility because both depend on a stratified evaluation order. A circular dependency through a NaF edge is OE0501 (“circular negation”); a program that cannot be stratified at all is OE0502 (“unstratifiable program”). Both are static checks; both fail the build.

The standard pattern that triggers OE0501:

pub derive A(x) :- not B(x)        // can A even be evaluated?
pub derive B(x) :- not A(x)        // ... we'd need to know B

The two rules cycle through a NaF edge. The compiler refuses to compile until the modeller breaks the cycle, typically by inserting a strict rule that grounds one of the predicates.

Multi-head disjunction

Multiple same-named @[default] derive rules form a disjunction: a fact holds if any of the rules fires. The pattern is canonical for genuine disjunctive reasoning where the head is the same predicate but the supporting bodies are independent:

@[default]
pub derive eligible_for_review(l: Lease) :-
    l: Lease,
    l.monthly_rent >= rent_review_threshold

@[default]
pub derive eligible_for_review(l: Lease) :-
    l: Lease,
    rent_controlled(l)

@[default]
pub derive eligible_for_review(l: Lease) :-
    l: Lease,
    l.tenant.is_senior

A lease is eligible for review if any one of the three conditions holds; the why-provenance tags whichever rule fired so a consumer can ask “which branch matched?” and get a precise answer. Mixing strict and defeasible rules under the same head is OE0207 for the same reason single-head mixed-strength is rejected.

Refinement-type interaction

The refinement-type fragment (Chapter 2.6) interacts with defeasibility through the three-valued semantics. A refinement-type predicate where { ... } is decided per (Kleene’s strong three-valued logic) with the Pietz-Rivieccio “Exactly True” designation: only IS admits membership, CAN does not. When a defeasible rule conditions on the refinement-type narrowing, the rule fires only if the narrowing is IS — the narrowing itself can be defeated by an override rule that asserts the converse. The corner cases are rare; the semantics is well-defined when they do come up.

Worked example — regulatory-regime classification

The lease tutorial does not yet exercise defeasibility, but the pattern is straightforward to add. Imagine rules.ar extended with:

@[default]
pub derive lease_governance_standard(l: Lease) :-
    l: Lease

@[override(lease_governance_standard)]
pub derive lease_governance_rent_controlled(l: Lease) :-
    l: Lease,
    rent_controlled(l)

@[override(lease_governance_rent_controlled)]
pub derive lease_governance_senior_rent_controlled(l: Lease) :-
    l: Lease,
    rent_controlled(l),
    l.tenant.is_senior

Three classification rules, each more specific than the last. The superiority graph is a chain: senior_rent_controlled → rent_controlled → standard. For a senior tenant in a rent-controlled lease, all three bodies fire; the engine applies them in topological order; lease_governance_senior_rent_controlled wins; the lease classifies under the senior-rent-controlled regime.

For a non-senior tenant in a rent-controlled lease, only the first two bodies fire; lease_governance_rent_controlled wins; the lease classifies under the rent-controlled regime.

For a market-rate lease, only the first body fires; lease_governance_standard wins; the lease classifies under the standard regime.

A separate compute keyed off the regime predicate produces the actual security-deposit cap, monthly-rent ceiling, or whatever the regime determines. Defeasibility classifies; the compute calculates. The why-provenance for each classification tags which override won, giving the regulator a precise audit trail.

Diagnostics worth knowing

CodeMeaning
OE0207Mixed-strength rules on same head
OE0401Ambiguous defeat (no superiority-graph edge resolves the conflict)
OE0402Unreachable defeater
OE0403Circular superiority relation
OE0501Circular negation
OE0502Unstratifiable program
OE1002Unknown defeat-ordering strategy
OE1007Cross-module superiority cycle
OE1013Unresolvable cross-module defeat target

ox explain <code> prints the long-form explanation and the canonical fix.

Why a stratified semantics

A naïve “first-rule-wins” or “most-recent-rule-wins” approach to defeasibility is brittle: rule order in source becomes load-bearing, refactors silently change behavior, and the result depends on evaluation order. The Governatori three-stratum compilation gives unambiguous semantics that is independent of evaluation order — multiple paths through the strata produce the same result, by construction.

This is what makes the defeasibility layer composable. A package can ship rules with their own @[override] annotations; a downstream package can add more rules, more annotations, even override rules from the upstream package; the resulting superiority graph is the union; the semantics stays well-defined; and the compiler tells you when the union has a cycle or an ambiguity.

What’s next

Chapter 5.3 covers the tier ladder; defeasibility’s stratification interacts with tier classification (NaF can lift a rule up the ladder). Chapter 5.4 covers standpoints; defeasibility orderings differ between standpoints, and the same rule pair can have different superiority outcomes depending on which standpoint the query runs against. Chapter 5.1 covers the meta-property calculus that powers Tier-0 reasoning — a substrate the defeasibility layer reads but does not modify.

Decidability Tiers

A rule that the reasoner cannot evaluate is a rule the reasoner cannot use. This is not a complaint — it is a design fact. Description Logic, Datalog, and full first-order logic occupy different points on the tractability/expressiveness curve, and a language that wants to run its rules has to make those points visible to the programmer.

Argon does that with a seven-tier decidability ladder. Every rule is automatically classified at elaboration time. Tiers from tier:structural through tier:recursive execute today. tier:fol admits semi-decidable rules through the unsafe logic { … } escape hatch. tier:modal and tier:mlt are reserved for std::kripke-driven modal reasoning and multi-level types respectively.

This chapter explains the ladder, shows one example per tier, and walks through the three related mechanisms that hang off it: #dec(<tier>) directives that scope ambient ceilings at module / block / declaration granularity, unsafe logic { … } blocks for the FOL escape hatch, and the recognizer table that lets carefully-shaped FOL bodies pass under stricter ceilings via shape-driven tier reduction.

The ladder

TierAddsBacking
tier:structuralSubsumption (<:), disjointness, role hierarchies, partition.Pure meta-property lookup; polynomial classification.
tier:closureTransitive closure, role composition, functional / inverse roles.Polynomial; nous saturation + recognized-shape fast-paths.
tier:expressiveArbitrary class expressions, qualified cardinalities, full negation.Decidable; exponential worst case.
tier:recursiveDatalog rules with stratified negation.DataFrog evaluator; recursion-aware.
tier:folUniversal / existential quantification, full equality.Semi-decidable; gated behind unsafe logic { … }.
tier:modalModal operators over std::kripke.Composes with structural / closure tiers.
tier:mltMulti-level instantiation; n-order types.Bounded-order is decidable; unbounded is not.

The ladder stands on its own — each tier corresponds to a real engine and a real cost model, grounded in the complexity classes those engines target. tier:structural is the polynomial concept-graph saturation regime described in Baader et al., Pushing the EL Envelope (IJCAI 2005); tier:recursive is stratified Datalog as formalized in Abiteboul, Hull & Vianu, Foundations of Databases (1995); tier:fol is unrestricted first-order logic; the four remaining tiers sit between, each with its own engine and acceptance criterion. The names describe what each tier admits and what it costs; they do not align to any other language’s expressivity slices.

Classification is automatic. There is no tier: annotation on a rule; the elaborator walks the body, assigns each atom and operator an intrinsic tier, and takes the maximum:

where the per-atom function is a structural recursion:

The recursion is implemented in argon/oxc/src/meta/classify.rs and surfaces through OI0804 at elaboration time. Aggregates compose by max; the binding-form forall is the single construct that can force a rule to tier:fol unilaterally — unless the recognizer (see below) matches the body against a known shape and reduces the effective tier.

The classification surfaces through OI0804 — an info-level diagnostic emitted at elaboration time so you can see what tier each derive rule landed at:

$ ox check
OI0804

  > [OI0804] derive rule `ActiveLease` classified at tier:recursive
    ,-[11:1]
 11 | ,-> pub derive ActiveLease(l: Lease) :-
 12 | |       l: Lease,
 13 | |       l.start_date <= today(),
 14 | |       l.end_date > today()
 15 | |->
    : `---- tier:recursive
    `----
  help: for more information, try `ox explain OI0804`

ox explain OI0804 prints the long-form explanation: which sub-expressions contributed which intrinsic tiers, and how they composed.

One example per tier

The seven tiers, illustrated against the running lease ontology and standard-shape predicates over it.

Tier 0 — MetaPropertyOnly

Pure meta-property lookups. No traversal of the structural graph; no value comparisons; no arithmetic.

pub derive RigidConcept(c: ⊤) :-
    A.rigidity(c) == rigid

The reasoner answers this from the meta-property calculus’s IS-state (Stratum 0). Polynomial in the number of axes × the number of concepts. The classifier rejects nothing here.

Tier 1 — HierarchyTraversal

Subtype reasoning, transitive closure, simple sibling-axiom checks.

pub derive AnyResidentialSibling(l: Lease) :-
    l: Lease,
    l specializes ResidentialLease

The nous saturator handles tier-1 rules by traversing the type hierarchy.

Tier 2 — CountingAndPaths

Bounded-length path queries through the structural graph.

pub derive HasGuarantorWithin2Hops(l: Lease) :-
    l: Lease,
    l connected_by mediates within 2 to Guarantor

within N caps the traversal depth; the connected_by operator becomes a bounded path expression.

Tier 3 — ValuePredicates

Comparisons over value-typed fields plus integer arithmetic. The QF-LIA fragment.

pub derive ActiveLease(l: Lease) :-
    l: Lease,
    l.start_date <= today(),
    l.end_date > today()

Tier 3 is the highest of the executable tiers — the running edge of what the saturator + value-predicate engine produces results for today. Most real domain rules sit at tier 3. OI0804 for any rule in the lease tutorial reports tier 3 because of the date comparisons.

Tier 4 — Temporal

Allen-interval operators (before, after, meets, overlaps, during, contains, …) — DatalogMTL territory.

pub derive OverlapsActive(a: Lease, b: Lease) :-
    a: Lease, b: Lease,
    a overlaps b,
    a end_date > today()

The Allen operator forces tier 4. Today the elaborator parses, type-checks, and emits the rule into the IR; the saturator does not produce derivations from tier-4 rules until the DatalogMTL execution path ships. ox check reports OI0804: Tier 4 (temporal) and the rule’s body becomes design intent rather than a live derivation.

Tier 5 — BoundedFOL

First-order rules with bounded-cardinality scope annotations. Classical Kodkod territory.

@[scope(leases: 100)]
pub derive AtLeastOneLeaseHasNoTenant() :-
    exists l: Lease where not exists t: Tenant where l.tenant == t

The exists introduces a true binding-form quantifier; the @[scope] annotation tells the classifier it is bounded. Tier 5 is syntax-only today; without @[scope] the same rule is tier 6.

Tier 6 — FullFOL

Unbounded forall / exists, full first-order logic. Semi-decidable.

unsafe logic {
    pub derive transitivity_of_specialization(x: ⊤, y: ⊤, z: ⊤) :-
        forall x, y, z: ⊤ where x specializes y, y specializes z
        => x specializes z
}

The forall binding-form forces tier 6. A tier-6 rule outside an unsafe logic { … } block is a hard error — OE0809: Tier 6 rule outside unsafe logic block. Inside the block, the rule parses, elaborates, and lands in the IR with OI0808 (“unsafe logic rule gated”) — the rule is a property of the source, not of any saturation result.

The decision to gate tier 6 behind unsafe logic is deliberate: full FOL is an escape hatch for rare cases (theorem-shaped invariants you intend to mechanically verify but not execute). It is not a programming idiom.

#dec(<tier>) — capping an effective fragment by scope

A mature ontology project usually wants to be conservative: “we only commit to tier:recursive-or-below reasoning in this module; flag anything that creeps higher.” The #dec(<tier>) directive declares an ambient ceiling at module, block, or declaration scope:

#dec(tier:recursive)

mod core_rules {
    pub derive ActiveLease(l: Lease) :-
        l: Lease,
        l.start_date <= today(),
        l.end_date > today()

    #dec(tier:closure)
    pub derive Ancestor(p: Person, c: Person) :-
        p parent_of c
        => p ancestor_of c
}

unsafe logic {
    pub derive transitivity_of_specialization(x: ⊤, y: ⊤, z: ⊤) :-
        forall x, y, z: ⊤ where x specializes y, y specializes z
        => x specializes z
}

The directive scopes:

  • Module head. #dec(<tier>) at the top of a .ar file binds the ambient ceiling for every item in the file.
  • Per-block. #dec(<tier>) { … } binds an ambient inside a block — handy for a research subsection that uses one stricter-or-looser tier than its surroundings.
  • Per-declaration. #dec(<tier>) immediately preceding a pub derive (or any other tier-classified declaration) overrides the enclosing ambient for that declaration only.
  • unsafe logic { … }. Injects a whole-block tier:fol ambient, the FOL escape hatch. Every item nested inside — derive rules, nested concept declarations, anything else admitted in block scope — inherits FOL regardless of any outer ceiling.

Stricter wins: the effective ceiling at any point is min over the entire ambient stack. A #dec(tier:closure) declaration nested inside a #dec(tier:recursive) module compares its rules against the stricter closure ceiling, not the looser ambient.

A rule whose effective syntactic tier exceeds the stack’s ceiling fires OE0604 TierViolation. The check runs at elaboration, before any reasoning, so the build fails with a precise pointer at the offending rule.

There is no [reasoning] max_tier manifest field. Per-scope tier concerns live in source directives; package-wide concerns (no-std, edition, dependencies) live in the manifest. The two never mix.

Recognized-shape tier reduction

A rule’s effective tier is min(syntactic_tier, recognized_tier). Carefully-shaped FOL bodies pass under stricter ceilings:

pub decorator transitive(r: rel) on rel = {
    semantics: forall x y z. r(x, y) and r(y, z) -> r(x, z)
}

#dec(tier:closure)
@[transitive]
pub rel parent_of(p: Person, c: Person)

The decorator’s body is syntactically tier:fol (forall is a binding-form quantifier). But the recognizer matches the body against RecognizedShapeKind::Transitive { rel } — the canonical FOL shape for transitivity — and the effective tier collapses to tier:closure. The parent_of rule passes the #dec(tier:closure) ceiling. The reasoner takes the transitive-closure fast-path.

The recognizer admits ten canonical shapes (Transitive, Symmetric, Asymmetric, Reflexive, Irreflexive, Functional, InverseFunctional, QualifiedCardinality, DisjointClasses, CoveringClasses). Six normalization passes (alpha-rename, flatten-associative, push-negations-inward, implications-to-disjunctions, canonicalize-equality, sort-conjuncts) ensure that semantically-equivalent bodies with different syntactic surfaces match the same canonical form. Bodies that don’t match any shape — the common case for hand-written forall — fall through to generic FOL handling and require unsafe logic { … } to lift the FOL ceiling.

@[theorem] — mechanical verification at compile time

Some derive rules are theorems — claims that hold over the entire model rather than a particular saturation. For those, mark the rule with @[theorem]:

@[theorem]
pub derive specialization_is_transitive(x: ⊤, y: ⊤, z: ⊤) :-
    x specializes y,
    y specializes z
    => x specializes z

@[theorem] triggers the compiler to attempt mechanical verification at ox check / ox build time, using the saturator + the meta-property calculus. If the proof succeeds, the rule is a verified invariant. If the verifier cannot decide it, you get a warning and the rule continues to act as a regular derivation.

Two restrictions, both enforced as warnings:

  • OW0821@[theorem] is only valid on derive items. Placing it on compute, mutation, query, etc. is rejected.
  • OW0822@[theorem] declared twice on the same derive is also rejected.

@[theorem] is the right way to raise confidence above the executable cutoff: a tier-5 or tier-6 derive that is mechanically verified is more trustworthy than the same rule running, even if execution were available.

#[unproven] and #[assumed] — test markers

Two related decorators, both applied to test blocks:

  • #[unproven] — the test asserts a theorem-shaped claim without mechanical verification. The test runs as usual; the marker is a future-reader signal that the claim deserves verification.
  • #[assumed] — the test injects the body’s claim as a postulate within the test’s scope. Useful when a real-world axiom (for example, “every SSN is unique”) is needed downstream but cannot be derived from the model.
#[unproven]
test "subtyping is transitive across the entire lattice" {
    /* claim — not yet mechanically verified */
}

#[assumed]
test "every SSN is unique" {
    assert forall p1, p2: Person where p1.ssn == p2.ssn => p1 == p2
}

Both attributes are restricted to test blocks. Placing #[unproven] or #[assumed] on a non-test item produces OW0823 TestAttributeNonTest.

unsafe logic — escape hatch for tier 6

unsafe logic { … } is the only place tier-6 rules are admitted. Inside the block:

unsafe logic {
    pub derive transitivity_of_specialization(x: ⊤, y: ⊤, z: ⊤) :-
        forall x, y, z: ⊤ where x specializes y, y specializes z
        => x specializes z
}

Three diagnostics shape this surface:

  • OI0808 — informational; “unsafe logic rule gated” — emitted for every rule in the block, confirming the rule is parsed and lands in the IR but does not execute.
  • OE0809 — error; “Tier-6 rule outside unsafe logic block” — emitted if you forget the block.
  • OW0806 — warning; “Tier-6 rule without heartbeat budget” — emitted when a tier-6 rule lacks a @[budget] annotation. Once tier-6 execution lands, budgets become mandatory.

The block is for design intent: you have a property the model needs, you can express it in FOL, and you want it in the source so future readers see the trajectory. You do not get a result at evaluation time today, and that is the contract.

Configuring the tier classification

Three annotations let you tune what the classifier does with a particular rule:

  • @[complexity(<class>)] — pin the rule’s tier explicitly. Useful for the OE0805 check: if your asserted complexity contradicts what the classifier computes, the build fails. Treat this as a regression-guard for refactors that would silently raise a rule’s tier.
  • @[budget(N)] — heartbeat budget for a tier-6 rule (and, when tier-5/6 execution lands, a runtime fairness limit).
  • @[scope(domain: N)] — bounded-cardinality annotation that lets a tier-5 rule stay at tier 5 instead of escalating to tier 6.

Invalid keys in @[budget] are OE0810; in @[scope], OE0811. Unknown complexity classes in @[complexity] are OE0812.

A worked example showing all three at once. The catalog’s _catalog/decorator-{complexity,monotone,scope,budget}/ directories ship runnable variants for each.

// @[complexity(ptime)] pins a rule to Tier ≤ 2. A body that
// classifies higher triggers OE0805 at ox check time.
@[complexity(ptime)]
pub derive simple_subsumption(p: Person) :-
    p: Person                                // tier:closure — passes

@[complexity(ptime)]
pub derive AgeFloor(p: Person) :-            // OE0805: ptime asserted,
    p: Person, p.age > 0                     // tier:recursive classified
// @[monotone] asserts the rule body uses no negation / no
// is-ambiguous / is-unknown / is-timeout atoms. The elaborator's
// find_non_monotone_atom walker checks; OE0807 fires on violation.
@[monotone]
pub derive HasAdult(p: Person) :- p: Person, p.age >= 18
// @[scope] bounds a tier-5 quantifier. Without it the same rule
// would classify at tier:fol; the bound keeps it in BoundedFOL.
@[scope(max: 16)]
pub derive AtLeastOneActiveLease() :-
    exists l: Lease where ActiveLease(l)

The @[complexity] annotation in particular is the canonical regression-guard pattern: assert your intended ceiling, and let OE0805 flag the refactor that quietly raises a rule’s effective tier.

What every tier-classifier diagnostic means

Quick reference; see Appendix C for the canonical registry.

CodeMeaning
OI0801Non-polynomial reasoning (informational)
OE0802Variable-binding quantifier outside unsafe block
OW0803Bounded FOL rule without scope annotation
OI0804Derive-rule decidability classification
OE0805Asserted complexity contradicts classified tier
OW0806Tier-6 rule without heartbeat budget
OE0807Monotone assertion violated by non-monotone atoms
OI0808unsafe logic rule gated
OE0809Tier-6 rule outside unsafe logic block
OE0810Invalid key in @[budget] annotation
OE0811Invalid key in @[scope] annotation
OE0812Unknown complexity class
OW0821@[theorem] applied to a non-derive item
OW0822@[theorem] declared multiple times on the same derive
OW0823#[unproven] / #[assumed] applied to a non-test item
OE0604Tier violation — declaration’s effective tier exceeds the #dec(<tier>) ambient ceiling

Why a tier ladder rather than profiles

Description Logic profiles (EL, QL, RL, plus EL++ and DL-Lite variants) come from a tradition of “pick the maximal sublanguage you can decide in time T.” They are ontology-engineering choices, and they apply to a whole language at once.

Argon’s ladder is a programmer’s surface. Each tier corresponds to a real engine that the toolchain ships (or will ship), with a real cost model. The classifier tells you which engine your rule lands on. #dec(<tier>) lets you pin scopes — module, block, declaration — to a tractability target. The recognizer collapses recognized-shape FOL bodies under stricter ceilings without forcing every rule into the unsafe logic escape hatch. @[theorem] raises confidence above the executable cutoff for the rare cases that need it.

The seven-tier ladder is the lever; the per-rule classification is what makes it useful. Decidability moves from a property of the language to a property of the rule, made visible at compile time.

What’s next

Chapter 5.1 explains the meta-property calculus that powers tier 0. Chapter 5.2 covers @[default] / @[override] / @[defeat], which interact with tier classification (defeasibility is itself stratified). Chapter 5.4 covers the bitemporal axes — orthogonal to tiers but reasoning-relevant.

Standpoints and Bitemporal Reasoning

A real-world domain has more than one viewpoint, and a real-world fact has more than one timestamp. A Lease looks different to the tenant than to the landlord. A regulation that takes effect on January 1 was added to the system on December 14. A medical record that names a diagnosis on Tuesday was retracted on Thursday because of new lab results — but the patient was treated under the original diagnosis for two days.

Argon makes these dimensions first-class. Standpoints are named perspectives the runtime tracks alongside every fact. Bitemporal axes — valid-time and transaction-time — are recorded for every event in the append-only log. Forks let you branch the world structurally without copying data. This chapter covers all three.

The mechanism: standpoints are a project-level partial-order lattice; modules can declare themselves contributing to a particular standpoint; rules in a child standpoint can override or extend rules from a parent. Bitemporal axes record both when a fact is true in the world and when the system learned it. Forks branch the event log structurally with copy-on-write semantics. Multi-standpoint bridge rules let a child standpoint pull facts from incomparable siblings via a shared parent. Refinement-type membership under open-world assumption uses (Kleene’s strong three-valued logic), so CAN for one standpoint can be IS for another. Cross-standpoint federated queries lift to the four-valued extension where source-level disagreement surfaces as .

Standpoints — what they are

A standpoint is a named view over the model. Two standpoints can disagree about whether a fact holds, what its provenance is, or whether a derivation fires — without either being wrong. The runtime keeps them simultaneously and lets the same query answer differently from each.

Declare standpoints in [standpoints]:

[standpoints]
default = []
tenant_view = ["default"]
landlord_view = ["default"]
auditor_view = ["tenant_view", "landlord_view"]

Each entry is name = [parents] — the standpoint’s parents in the lattice. Children inherit from parents; children can add facts, override defaults, and disagree with parents on derivations. The lattice is a partial order, not a partition: a fact in default is also visible in tenant_view (unless tenant_view retracts it); a fact added in tenant_view is not visible in landlord_view.

This is the load-bearing distinction: standpoints discriminate, they do not partition. A query without a standpoint flag runs against default. A query with --standpoint tenant_view runs against the join of default and tenant_view-local facts. Multiple standpoints active at once is admitted; the engine answers by lattice meet.

Standpoint-discriminated rules

Inside an [modules] block, a module can declare itself contributing to a particular standpoint:

[modules.tenant_only]
path = "tenant_only"
standpoint = "tenant_view"
default-world = "open"

A rule defined in tenant_only.ar is visible only when querying from tenant_view (or its descendants). The same predicate can have different rules across standpoints — for example, MonthlyOwed in tenant_view could include security-deposit interest, while in landlord_view it could not.

This is how regulatory layering works in practice: a federal rule lands in one standpoint, a state-specific rule in a child standpoint, and a query at the state-specific standpoint sees the conjunction. The engine’s stratified semantics keeps the layers honest.

World assumption per (module, predicate)

Open-world assumption (OWA) is the default for ontology work — the absence of IS(p, q) does not mean NOT(p, q). Closed-world (CWA) is the default for computational reasoning — what is not derivable is treated as false.

Argon refuses to pick one globally. Instead, world assumption is configured per (module, predicate):

[modules.tenant_only.default-world]
default = "open"

[modules.tenant_only.world]
ActiveLease = "closed"
HasGuarantor = "open"

Within tenant_only, the predicate ActiveLease runs under CWA — if the engine cannot derive ActiveLease(l), the answer is NOT(ActiveLease, l). HasGuarantor runs under OWA — failure to derive means we don’t know yet. The runtime tracks which assumption applied to each derived fact and surfaces it via the why-provenance.

CWA in an OWA module triggers OW1008 — a warning, not an error. The mix is sometimes intentional (regulatory queries sometimes want CWA for “did the tenant make all 12 payments” while staying OWA for “is there a guarantor we don’t know about”).

Bitemporal axes

Every fact in the runtime carries two timestamps. The pair-of-timestamps design follows Snodgrass & Ahn’s Temporal Databases (1986) and the bitemporal model formalized by Jensen, Soo & Snodgrass in Unifying temporal data models via a conceptual model (1994). Argon’s specific commitment: retraction is a new event with later transaction time, never an in-place modification.

  • Valid-time — when the fact is true in the world. The lease is active from 2026-01-01 to 2027-01-01, regardless of when the system learned about it.
  • Transaction-time — when the system learned the fact. The lease was recorded on 2025-12-14. If the system was updated three days later to correct the rent figure, the new fact has the same valid-time interval but a later transaction-time.

Formally, the event log is a sequence

where is the event payload, its valid-time interval, its transaction-time, the principal who produced it (e.g., --principal alice-001), and the standpoint it landed in. The log is monotonically ordered by : appends never reorder.

The query semantics is a filter over the log. Given a query , a target valid-time , and a target transaction-time :

The query runs against the subset of events whose valid-time is at-or-before , whose transaction-time is at-or-before , and whose standpoint is at-or-below in the lattice. Both axes are recorded for every event in the append-only axiom-event log. Both are queryable independently.

--as-of-valid <RFC-3339> answers a question against the world as it was at the named valid time:

$ ox query active_leases --as-of-valid 2026-06-01T00:00:00Z

returns the leases that were active on June 1, 2026, even if some of them have since been terminated.

--as-of-tx <RFC-3339> answers as-of the system’s knowledge at a particular transaction time:

$ ox query active_leases --as-of-tx 2025-12-14T00:00:00Z

returns the leases the system knew about on December 14, 2025 — even if subsequent corrections changed the picture.

The two flags compose: --as-of-valid X --as-of-tx Y answers “what did the system, as of Y, believe was true on X?” — useful for audit reconstruction.

Retraction as a new event

A fact is never modified in place. A correction is a new event — same valid-time, later transaction-time, marked as a retraction of the prior fact. The old fact stays in the log; queries before the retraction’s transaction-time still see it.

pub mutation retract_lease_signing(l: Lease, retracted_at: Date) -> Lease {
    do {
        retract LeaseSigned(l, _)
    }
    emit LeaseSigningRetracted(l, retracted_at)
    return l
}

The retract clause does not delete: it appends a retraction event whose tx-time is now. A --as-of-tx query before now still sees the original signing; after now, it sees the retraction and LeaseSigned(l, _) is no longer derivable.

This is what makes Argon’s bitemporal semantics audit-friendly: the history of corrections is itself queryable. A regulator can ask “when did you learn that this fact was false?” and get a precise answer.

Forks — branching the world

Sometimes you want to ask a counterfactual: “what would the model look like if we mutated l1 instead of l2?” or “what does the model say if we add this hypothetical regulation?” Forking lets you branch the runtime structurally.

A fork is a copy-on-write branch of the event log. Reads against a fork see the parent’s state up to the fork point plus any fork-local events. Writes to a fork stay local; the parent does not see them. Forks can be nested.

Today, forks are created programmatically through the kernel API; once a fork exists, queries discriminate to it via --fork:

$ ox query active_leases --fork what-if-rent-control --as-of-valid 2026-06-01T00:00:00Z

The structural-sharing implementation means a fork is cheap — it does not duplicate the data, it shares it under the hood. Useful for the long-running “what-if” analyses that ontology projects keep running for months. A CLI surface for creating forks (ox fork) is on the roadmap; until it lands, forks are minted via the kernel API and the application layer that owns the runtime instance.

Multi-axis queries

Standpoint, valid-time, transaction-time, fork — four orthogonal dimensions, each with its own query syntax:

$ ox query active_leases \
    --standpoint tenant_view \
    --as-of-valid 2026-06-01T00:00:00Z \
    --as-of-tx 2025-12-14T00:00:00Z \
    --fork what-if-rent-control

The engine answers from the conjunction: rules in tenant_view, the world as it stood at June 1 (valid-time), what the system knew on December 14 (transaction-time), in the rent-control fork. Provenance comes back tagged with all four axes; you can trace which derivation came from which standpoint, what tx-time the underlying fact has, and which fork it lives in.

The bridge-rule mechanism lets a derivation in one standpoint demand-drive results in a parent standpoint: a fact derived in auditor_view can pull through facts from both tenant_view and landlord_view because auditor_view is the meet of both in the lattice. The engine resolves the join lazily — only the standpoints actually needed by the query get evaluated.

Per-tenant scoping

In multi-tenant deployments — the typical case for a Sharpe-internal ontology runtime — standpoints carry an additional axis: the tenant. Tenant-local standpoints are scoped so that one tenant’s default does not see another tenant’s facts. The configuration lives in the runtime’s tenant manifest, not in the package’s ox.toml; from the language’s point of view, a tenant is an outermost standpoint that wraps everything.

A package author writes against the default standpoint and the lattice declared in [standpoints]. The runtime layers tenant-scoping over that. The query syntax is unchanged.

Worked example: cross-package standpoints

The _catalog/manifest-modules/ workspace family exercises the [standpoints] + [modules] machinery in isolation. Five variants ship:

  • minimal/ — single standpoint (default), single module — the baseline.
  • nested-lattice/ — three-level lattice (default → tenant_view → tenant_admin_view) showing inheritance and meet semantics.
  • cross-package/ — two packages, each contributing rules to the same standpoint via separate modules; resolves under the direct-dependencies rule from Chapter 4.1.
  • mixed-world/default-world = "open" at module top with per-predicate closed overrides; surfaces OW1008 for the intentional CWA-in-OWA mix.
  • negative-cycle/ — cyclic [standpoints] declaration that triggers OE1001.

Each variant ships a runnable ox check so you can see the diagnostic codes from the table above fire on real input rather than reading the codes in isolation.

Diagnostics worth knowing

CodeWhen
OE1001Cyclic standpoint dependency in [standpoints]
OE1002Unknown defeat-ordering strategy
OE1004Module references an undeclared standpoint
OW1008CWA concept inside an OWA module (intentional mix surfaced)
OW1009Standpoint DAG has no root (often a configuration mistake)
OE1010Bridge rule references a private concept
OE1011Module discriminator collision (two modules contribute to the same standpoint with conflicting names)

ox explain <code> prints the long form.

How it composes with the rest of the language

The bitemporal axes interact with mutations: every mutation event lands in the log with the current tx-time and the valid-time the mutation declares. Mutations in Chapter 2.5 are the surface; the bitemporal log is the substrate.

The standpoint mechanism interacts with the meta-property calculus: each standpoint can have its own world-assumption configuration, and the calculus runs once per standpoint when needed. Refinement-type membership under OWA uses (Kleene’s strong three-valued logic), and CAN for one standpoint can be IS for another. Each standpoint also declares a consistency policy (strict default; paraconsistent opt-in) controlling within-standpoint append-time invariants — strict standpoints reject conflicting writes, paraconsistent standpoints persist them as -valued cells in the FDE fragment. Cross-standpoint disagreement is preserved at federation time regardless of policy.

Forks interact with mutations: a mutation against a fork is fork-local; a mutation against default propagates to all forks (because forks share the event log up to their branch point). The “what-if” pattern usually works the other way — fork first, mutate, query — and the pattern is the kind of thing that makes the bitemporal+standpoint+fork triple worth the design budget.

What’s next

This is the last of the Part 5 chapters. The book proper is complete: Parts 1–4 teach the language end-to-end, Part 5 covers the formal foundations the working programmer can use without studying directly. The reference Appendix A (keywords) and Appendix B (operators) collect the syntax surface; Appendix C is the diagnostic registry; Appendix D records where the language is moving; Appendix E is the curated decision log.

The future Argon Book editions will fill in the gaps Part 5’s chapters flagged: the tier-4-and-above execution side as it lands, the multi-axis-query semantics as the implementation matures, and the worked examples from real UFO-team projects as they mature.

For Agents — Argon Language Reference

This part answers: as an agent writing, debugging, or exploring Argon code, what do I need to know that the rest of the book doesn’t already give me?

The rest of the Argon book is a tutorial walkthrough for human readers. This part is dense, info-rich, agent-oriented reference — designed for fast lookup over linear reading. Each subpage covers one focused concept with a “this page answers:” one-liner at the top and dense cross-links to related pages.

When to read what

You want toGo to
Look up the syntax for any declaration formQuick Reference
Step-by-step recipes for common modeling jobsTask Recipes
Pick between alternatives (derive vs compute, which compute form, etc.)Decision Guides
Pick the right decidability tierTier Selection
Resolve a specific diagnosticError Recovery
Avoid an Argon-specific mistakeAnti-Patterns
Recognize a canonical Argon idiomIdioms
Integrate via CLI / MCP / IDE / skillsTooling
Look up a termGlossary
Look up a keywordAppendix A — Keyword Reference
Look up an operatorAppendix B — Operator Reference
Look up a diagnostic codeAppendix C — Diagnostic Codes
Understand the language end-to-endFoundations, Composition, Beyond

Scope

This part covers writing and reading Argon-the-language. It does not cover:

  • Working on the compiler / LSP / toolchain — that’s the AGENTS.md tree at the repo root and per-crate.
  • Why Argon was designed this way — that’s the RFDs at /rfd/.
  • What’s currently shipping or open — that’s GitHub Issues + Releases.
  • Compiler-internal API — that’s rustdoc on oxc.

Pages in this part

Quick Reference

This page answers: what’s the syntax for every declaration form, with one minimal example each?

For semantics and rationale, follow the cross-links into the book chapters and the RFDs.

Substrate primitives (declaration forms)

FormSyntax (minimal)What it declaresSee
metatypepub metatype kind = { rigid, sortal, provides }A user-defined metatype with axis assignmentsch02-01
metaxispub metaxis rigidity for type { rigid, anti_rigid }A user-defined axis a metatype can take values on. Typed-domain form: pub metaxis order : Nat for type (optionally where { _ > 0 }). Cross-cutting: for [type, rel]. Valid kinds: type, rel, dec, field, individual, frame, metarel.ch02-01, ch02-06
metarelpub metarel mediates = { source: relator, target: kind, ... }A user-defined relation kindch02-01
decoratorpub decorator transitive() on rel { lowers_to: shape::Transitive }A user-defined decorator that recognized-shape lowersch02-01

Concept hierarchy

FormSyntax (minimal)See
Conceptpub kind Person <: Agentch02-02
Concept with constraintspub kind Person <: Agent where { age >= 0 }ch02-02, RFD-0018
Anonymous conceptlet _: Concept; (synthesizes __anon_<hash>_<n>)ch02-02
Group axiomdisjoint(A, B, C) / complete(A, B, C) / partition(A, B, C)ch02-02
Specializationatom: Tenant <: Person (typeset alias ). In a concept-header supertype slot, : is admitted as documented sugar for <: (pub subkind X : Ypub subkind X <: Y).ch02-02, RFD-0017
Instantiationatom: alice : Tenant (instance-of-type). : at every non-header position is the instantiation operator and never overlaps with <:.ch02-02

Relations and properties

FormSyntax (minimal)See
Relationpub rel works_at(p: Person, o: Organization)ch02-03
Relation with metarelpub rel mediates(c: Commitment, p: Person) :: Mediationch02-03
Relation with decorator@[transitive] pub rel ancestor_of(a: Person, b: Person)ch02-03
Propertypub property age(p: Person) -> Natch02-03
Cardinalitypub rel hires(o: Organization, p: Person) { cardinality: 1..* }ch02-03

Rules and reasoning

FormSyntax (minimal)See
Derive rulepub derive adult(P) :- Person(P), age(P, A), A >= 18ch02-04
Multi-head deriveMultiple pub derive same_head(...) :- ... rulesch02-04
Strict rulepub strict adult(P) :- ...ch02-04
Defeasible rule(default) — same as pub derivech02-04
Override@override(other_rule) pub derive ...ch02-04
Theorem mark@[theorem] pub derive ...ch02-04
Modalbox(P), diamond(P) operatorsRFD-0018
FOL escapeunsafe logic { ... }RFD-0018

Reflection

The compiler exposes the type lattice through one intrinsic and seven std::meta predicates. All evaluate at tier:structural. Import via use std::meta::*.

FormSyntax (minimal)What it does
meta() intrinsicmeta(c) == kindType-of subject. Sugar form c :: kind lowers to the same atom. No use required (substrate, not a std::meta predicate). Ground argument required (OE0212 if free).
is_type(X)is_type(c)Universal classification — every concept satisfies it.
metatype(X, M)metatype(c, m)Bind M to the metatype keyword as a string.
supers(X, S)supers(c, s)Direct supertypes.
ancestors(X, A)ancestors(c, a)Transitive ancestors.
fields(X, F)fields(c, f)Declared field names.
axioms(X, A)axioms(c, a)Declared axioms.
module(X, M)module(c, m)Owning module path as a string.

See ch02-06, ch03-03.

Computes

FormSyntax (minimal)When to use
Form 1 (expr)compute capital_gain(s: Sale) = s.proceeds - s.basisOne-shot calculation
Form 2 (inline)compute f(x: T) { input { ... } body { ... } out { ... } }Multi-step, tier-bounded, dynamically loadable
Form 3 (Rust)compute f(x: T) -> R { ... impl rust("crate::path::fn") }Native FFI; requires Rust rebuild

See ch02-05, RFD-0006.

Queries and mutations

FormSyntax (minimal)See
Queryquery adults() :- Person(P), age(P, A), A >= 18 => Pch02-05
Mutationmutation hire(o: Organization, p: Person) { do { works_at(p, o); } return p; }ch02-05

Singular clause names: require, do, retract, emit, return. See RFD-0007.

Tests

FormSyntax (minimal)See
Testtest t1 { ... expect { diagnostic OE0211 at //~ marker } }ch03-03
Fixturefixture { let p: Person = ...; } (test-local mini-module)ch03-03
Framepub frame F { ... } + using F in testch03-03
Proof status#[unproven] test ..., #[assumed] test ...RFD-0008
Source marker//~ <label> (anchors expect blocks)ch03-03

Patterns

FormSyntax (minimal)See
Pattern declpub pattern correlative<R, D> { ... }ch03-01
Pattern instantiationuse pattern correlative<Right, Duty> as RDch03-01
Bespoke surfacecorrelative_pair(Right, Duty)ch03-01

See RFD-0019.

Modules and packages

FormSyntaxSee
Module declarationmod foo; (file foo.ar)ch04-01
Single-symbol importuse std::math::Nat;ch04-01
Glob importuse ufo::prelude::*;ch04-01
Re-exportpub use ufo::prelude::*;ch04-01
Visibilitypub (package-public) / unmarked (module-internal)RFD-0011

Tier directives

FormSyntaxEffectSee
Module-scope#dec(tier:closure) at file topCaps every declaration in this modulech05-03
Block-scope#dec(tier:fol) in blockCaps the surrounding blockch05-03
Decl-scope#dec(tier:expressive) on declarationCaps the single declarationch05-03

See RFD-0004 and tier-selection.md.

Collections

The five type constructors and their expression-level surface. See ch02-08 and RFD-0039.

FormSyntax (minimal)What it does
Type constructorsSet[T], List[T], Map[K, V], Optional[T], Range[T]Closed-set parametric type constructors. Brackets, not angle brackets.
Optional sugarT?Optional[T]Same type; reach for T? for the common 0-or-1 case.
Method callxs.size(), xs.contains(x)Desugars to <TypeOf(xs)>::m(xs, args) at elaboration.
Indexingxs[i]Desugars to List::at(xs, i) -> Optional[T] (or Map::get). Set[T] rejects with OE2406.
Slicingxs[i..j], xs[i..], xs[..j]Desugars to List::slice(xs, Range::new(i, j)). Literal bounds checked at elaboration.
Range literali..j (half-open) / i..=j (inclusive)Constructs Range[T]. Doesn’t materialize in v1.
Membershipx in xs, x not in xsDesugars to <TypeOf(xs)>::contains(xs, x) (and negation).
Comprehension[expr for x in xs where pred]Desugars to xs.filter(<pred>).map(<expr>). Binder shadowing fires OW2403.
Constructor callsSet::of(a, b), List::of(a, b), Map::of((k, v), ...), Some(x), None()Variadic / per-arity.
Mutation idiomdo { l.parents = l.parents.append(p) }Functional rebuild-and-assign; no in-place mutators in v1.

Diagrams

FormSyntax (minimal)See
Diagram blockdiagram T { concepts { Person, Organization }; layout: sugiyama; }ch03-04

See RFD-0026.

See also

Task Recipes

This page answers: I have a specific modeling job to do — what are the steps?

Recipes are minimal: enough to get started, with cross-links to the chapter or RFD that explains the why.

I want to declare a new concept

  1. Pick the metatype from your foundational-ontology package (most commonly UFO’s kind, subkind, role, phase, etc.).
  2. Decide whether it specializes an existing concept and what its world assumption is.
  3. Add where clauses for structural constraints (RFD-0018).
  4. Make it pub if downstream packages should use it.
use ufo::prelude::*;
use std::math::Nat;

pub kind Person <: Agent
  where {
    age >= 0,
  }

See ch02-02.

I want to declare a relation between concepts

  1. Decide endpoints (source and target concepts).
  2. If it’s a recognized algebraic shape (transitive, symmetric, …), add the matching decorator.
  3. If it has metarel semantics (mediation, externally-dependent), use :: <Metarel>.
  4. Add cardinality constraints if needed.
@[transitive]
pub rel ancestor_of(a: Person, b: Person)

pub rel mediates(c: Commitment, p: Person) :: Mediation {
  cardinality: 1..1,
}

See ch02-03, RFD-0018.

I want to write a derivation rule

  1. Identify the head — what fact is being derived.
  2. Identify the body — what facts must hold for the head to follow.
  3. Write pub derive head(args) :- body_atoms.
  4. If the rule is meant to apply only when no override fires, leave it pub derive (defeasible). If it’s an unconditional theorem, use pub strict.
  5. Add @[theorem] if the rule should be machine-verified.
pub derive adult(P) :- Person(P), age(P, A), A >= 18

Multi-head disjunction: write multiple rules with the same head and consistent strength.

See ch02-04, RFD-0007.

I want to retrieve facts by pattern

  1. Write a query declaration with a Prolog-style :- body.
  2. Project the result with => head_expression.
query adults() :-
  Person(P),
  age(P, A),
  A >= 18,
  => P

Every result carries why-provenance. See ch02-05, RFD-0007.

I want to apply a state transition

  1. Write a mutation declaration with require (preconditions), do (assignments), optional retract (negative facts), optional emit (events), return (response).
  2. The mutation will record its transition trace as provenance.
mutation hire(o: Organization, p: Person) {
  require { not works_at(p, _) }
  do { works_at(p, o); }
  emit { Hiring(o, p) }
  return p;
}

See ch02-05.

I want a calculation that returns a value

  1. Pick the compute form (see decision-guides.md#which-compute-form).
  2. Form 1 for one-shot, Form 2 for multi-step tier-bounded, Form 3 for native FFI.
// Form 1
compute gain(s: Sale) -> Money = s.proceeds - s.basis

// Form 2
compute classified_gain(s: Sale) {
  input { s: Sale }
  out { kind: String, amount: Money }
  ensure { s.proceeds >= 0 }
  body {
    let amount = s.proceeds - s.basis;
    let kind = if amount > 0 { "gain" } else if amount < 0 { "loss" } else { "wash" };
    { kind: kind, amount: amount }
  }
}

See ch02-05, RFD-0006.

I want to write a test

  1. Use test <name> { ... }.
  2. Set up state with assertions or with a fixture { ... } block.
  3. Use expect to assert specific diagnostics or values.
  4. Mark with #[unproven] or #[assumed] if appropriate.
test adult_classification {
  fixture {
    assert Person(alice);
    assert age(alice, 25);
  }
  expect { result adult(alice) at //~ check }
}  //~ check

See ch03-03, RFD-0008.

I want to model a recurring shape (correlative pair, part-whole, etc.)

Use a pattern. Either reach for a bespoke surface form (correlative_pair, part_whole) or write pub pattern <name> { ... } and instantiate with use pattern <name><types> as <instance>.

See ch03-01, RFD-0019.

I want to escalate beyond the default tier

  1. Identify which tier you need (see decision-guides.md#when-to-escalate-tier).
  2. Add #dec(<tier>) at module / block / declaration scope.
  3. For full FOL, wrap the relevant block in unsafe logic { ... }.
#dec(tier:expressive)

pub derive complex_classification(X) :- ...

See tier-selection.md, ch05-03, RFD-0004.

I want to include a foundational ontology

  1. Add the package to your ox.toml [dependencies].
  2. use the foundational ontology’s prelude in your modules.
  3. Refer to its metatypes by name.
# ox.toml
[dependencies]
ufo = "0.3.0"
use ufo::prelude::*;

pub kind Person <: Agent { ... }

See ch04-01, RFD-0014.

I want to render a diagram

  1. Add a diagram block.
  2. Specify which concepts/relations to include and a layout algorithm.
diagram TaxRoles {
  concepts { Person, Taxpayer, Withholding_Agent }
  relations { works_at, withholds_from }
  layout: sugiyama;
}

See ch03-04, RFD-0026.

I see an OE#### error and don’t know what it means

  1. Run ox explain OE#### for the canonical long-form explanation.
  2. Cross-reference error-recovery.md for common diagnostics with action hints.
  3. Full reference: Appendix C.

See also

Decision Guides

This page answers: when faced with multiple ways to express something, how do I pick?

Each guide is a short decision tree. Follow the tree to reach a recommendation; cross-link explains why.

derive vs compute vs query

What's the output?

├── A new fact (or facts) that the kernel should treat as derived,
│   visible to other rules and queries
│   → use `derive`
│
├── A scalar / record / collection computed from inputs,
│   not stored as a fact in the knowledge graph
│   → use `compute`
│
└── A retrieval that answers "what facts match this shape, right now?"
    → use `query`

A derive rule’s head facts participate in saturation and become inputs to other rules. A compute returns a value at the call site without entering the fact base. A query projects existing facts (asserted or derived) into a result set without producing new facts.

See RFD-0007, RFD-0006.

Which compute form?

Is the body a single expression?

├── Yes
│   → Form 1: `compute f(args) = expr`
│
└── No — needs multiple statements
    │
    ├── Per-tenant customization required without a Rust rebuild?
    │   → Form 2: `compute f(args) { input ... body ... out ... }`
    │
    ├── Native library access or unbounded performance flexibility needed?
    │   → Form 3: `compute f(args) -> R { ... impl rust("path::to::fn") }`
    │
    └── Otherwise (multi-step but tier-bounded math)
        → Form 2 (default): inline body in the constraint sublanguage

Form 2 is the recommended default for non-trivial computes. Form 3 is the escape hatch when Form 2 isn’t expressive enough; it carries a Rust-rebuild cost.

See RFD-0006, ch02-05.

query vs mutation

Does this operation change kernel state?

├── No — pure retrieval, no side effects
│   → `query`
│
└── Yes — asserts new facts, retracts existing facts, or emits events
    → `mutation`

A query is read-only: it never changes the fact base. A mutation runs require / do / retract / emit clauses against the current state and returns a result. Mutations carry transition-trace provenance; queries carry derivation provenance.

See RFD-0007.

frame vs fixture vs world (world is deferred — see backlog)

Where does this test scaffolding apply?

├── To this single test only — diagnostics scoped, no reuse
│   → `fixture { ... }` inline in the test
│
└── To multiple tests — composable mini-vocabulary
    → `pub frame F { ... }` declaration; tests reference via `using F`

Fixtures are inline mini-modules elaborated under capture mode; their diagnostics don’t leak to the parent. Frames are reusable; multiple tests can using F and they compose with conflict detection (OE0214-OE0218).

See ch03-03, RFD-0008.

When to escalate tier

Is the rule expressible in the structural / closure tier?

├── Yes
│   → Default tier (no `#dec` needed)
│
└── No
    │
    ├── Recursion needed?
    │   → `#dec(tier:recursive)`
    │
    ├── Negation, disjunction, complex quantification?
    │   → `#dec(tier:expressive)`
    │
    ├── Full FOL needed (existential quantification, complex Skolemization)?
    │   → `unsafe logic { ... }` — carries `tier:fol`
    │
    ├── Modal (necessity / possibility) reasoning?
    │   → `#dec(tier:modal)` — `box()` / `diamond()` operators
    │
    └── Multi-level types (a metatype's instances are themselves types)?
        → `#dec(tier:mlt)` — escape hatch for foundational-ontology bootstrapping

The tier ladder is graduated by evaluation cost. Default is fast and ergonomic; escalation costs decidability and runtime predictability. Recognized shapes (transitive, symmetric, etc.) inhabit the recognized tier regardless of syntactic surface — see RFD-0004.

See tier-selection.md, ch05-03.

where clause vs decorator

What kind of constraint?

├── Structural — references only the concept's own fields and ancestors
│   → `where { ... }` clause in the concept body
│
└── Algebraic — relation property like transitivity, symmetry, asymmetry
    → `@[decorator]` on the relation declaration

where clauses lower to structural validation rules. Decorators lower to recognized shapes that the reasoner dispatches to specialized fast-paths.

See RFD-0018.

Functor module vs pattern vs nothing (one-off concepts)

How much structure do you need to reuse?

├── A single declaration shape, parameterized
│   → Just write the concept directly, possibly with one or two parameters
│
├── A small recurring shape (3–10 declarations) that pairs related concepts
│   → `pattern` declaration (RFD-0019)
│
└── A whole module's worth of declarations parameterized by other modules
    → Functor module (RFD-0009)

See RFD-0019, RFD-0009.

assert vs pub declaration

Are you stating a fact about an individual, or declaring a type/concept/rule?

├── Fact about an individual (ABox)
│   → `assert Person(alice)` or similar
│
└── Type, concept, relation, property, rule, etc. (TBox)
    → `pub <metatype> Foo ...` etc.

ABox assertions become axiom events in the kernel’s event log (see RFD-0021). TBox declarations become schema events.

Set vs List vs Map vs Optional[Set] — which collection?

What's the shape?

├── 0 or 1 value
│   → `Optional[T]` (sugared `T?`)
│
├── Many values, no order, no duplicates, membership-driven
│   → `Set[T]`
│
├── Many values, ordered, indexable, duplicates meaningful
│   → `List[T]`
│
├── Key-value lookup, K orderable
│   → `Map[K, V]`
│
└── 0 or many values where presence-of-the-set matters
    (an absent set is distinct from an empty set)
    → `Optional[Set[T]]`

The last case is rare but real: an absent set means “this field has not been determined” while an empty set means “this field is determined to have no elements.” If the model doesn’t distinguish the two readings, use bare Set[T] and let an empty set carry both meanings.

For relation-shaped fields (a building’s tenants; an organisation’s employees), [T; <bound>] is the conventional surface; collection-typed Set[T] works when you want method-call access on the field’s value.

See ch02-08, RFD-0039.

Comprehension vs method chain

Is the projection a small expression (field access, arithmetic, constructor)?

├── Yes
│   → comprehension: `[u.number for u in b.units where pred(u)]`
│
└── No — each step has a name worth preserving
    → method chain: `b.units.filter(pred).map(unit_to_number).filter(...)`

Comprehensions read closer to the intent when the projection is small. Method chains preserve named intermediates. They desugar to the same form.

See ch02-08, idioms.md.

See also

Tier Selection

This page answers: which decidability tier should I use for this rule, and when do I need to escalate?

Argon’s decidability ladder is graduated by evaluation cost. Tiers are independent of OWL profiles (RFD-0004).

The seven tiers

TierAdmitsCost profileDefault?
tier:structuralMeta-property lookup, simple subsumption, atomic constraintsPolynomial; fastYes
tier:closureTransitive closure, counting, classificationPolynomial; saturation costNo
tier:expressiveGNFO (Guarded Negation First-Order)Polynomial in expressive class; richer than closureNo
tier:recursiveQuantifier-free Linear Integer Arithmetic + recursionPolynomial in QF-LIA; recursion adds bounded costNo
tier:folDatalogMTL, bounded FOL via Kodkod (@[scope]); unsafe logic { } admits unbounded FOLDecidable per-bound; unbounded FOL is undecidableNo (escape hatch)
tier:modalModal operators (box(), diamond()); modal axiomsDecidable for bounded modal logicNo
tier:mltMulti-Level Theory: a metatype’s instances are themselves typesReserved for foundational-ontology bootstrappingNo (exotic)

Picking a tier

Is the rule structural — references only ancestors, fields, atomic predicates,
no recursion, no aggregates, no negation?
  → tier:structural (default; no #dec needed)

Need transitive closure (e.g., ancestor-of), counting, or saturation-driven classification?
  → tier:closure

Need negation, disjunction, complex universal/existential quantification (still bounded)?
  → tier:expressive

Need recursion that can't be expressed via transitive closure?
  → tier:recursive

Need full FOL with bounded scopes (Kodkod-class)?
  → #dec(tier:fol) + @[scope] annotations

Need modal reasoning (necessity, possibility)?
  → #dec(tier:modal)

Need full unbounded FOL (proof-theoretic, undecidable)?
  → unsafe logic { ... } block (which carries tier:fol)

Need multi-level types?
  → #dec(tier:mlt) — exotic; usually only foundational-ontology authoring

How to declare a tier

// Module-scope: caps every declaration in this module
#dec(tier:closure)

// Declaration-scope: caps a single declaration
#dec(tier:expressive)
pub derive complex_classification(X) :- ...

// Block-scope: caps a single block
#dec(tier:recursive) {
  pub derive ancestral(P, A) :- parent(P, A)
  pub derive ancestral(P, A) :- parent(P, M), ancestral(M, A)
}

// FOL escape hatch
unsafe logic {
  // full FOL admitted; carries tier:fol
}

The compiler classifies each expression’s natural tier and rejects expressions that exceed the surrounding #dec annotation with OE0604 TierViolation. Recognized shapes (transitive, symmetric, …) inhabit the recognized tier regardless of their syntactic appearance.

Recognized shapes inhabit the recognized tier

A @[transitive] decorator on a relation lowers to a recognized shape with a known polynomial-time fast-path. The expression inhabits the recognized tier (often closure-equivalent) even though its naive interpretation might lift it higher. This is structural and intentional (RFD-0004).

The 10 canonical recognized shapes:

Transitive       Symmetric     Asymmetric    Reflexive    Irreflexive
Functional       InverseFunctional
QualifiedCardinality
DisjointClasses  CoveringClasses

If an expression matches one of these (after the recognizer’s 6 normalization passes), the reasoner dispatches to a fast-path; tier classification reflects that.

Collection operations and per-context admission

The collection substrate (ch02-08) ships each operation with an explicit tier. The tier interacts with the surrounding evaluation context: different bodies admit different operation sets.

Contextclosure-tier opsexpressive-tier ops (map, filter, Optional::map)recursive-tier ops (fold)
pub compute bodyyesyesyes
pub mutation do { }yesyesyes
pub derive bodyyesreject (OE2408)reject (OE2408)
query bodyyesreject (OE2408)reject (OE2408)
Refinement where { }reject (OE2408)rejectreject
test blockyesyesyes

Closure-tier ops (size, contains, at, append, slice, union, get, Some, None, is_some, is_none, unwrap_or, Range::new, Range::contains) are admitted freely in rule bodies and queries. Expressive- and recursive-tier ops (map, filter, fold) belong inside a pub compute body; call the compute from the rule.

Refinement bodies admit the metatype calculus only; every collection op is out of fragment there.

See ch02-08 — tier dispatch matrix, RFD-0039.

Common tier mistakes

  • Reaching for tier:fol when tier:expressive would do. Most “I need negation” cases fit GNFO. Try the lower tier first.
  • Recursion in tier:closure. tier:closure admits transitive closure but not arbitrary recursion. If your rule recurses through aggregates or arithmetic, it’s at least tier:recursive.
  • unsafe logic without an obvious need. The escape hatch is for genuinely unbounded FOL. Most “I need full FOL” turns out to be “I need bounded FOL” and tier:fol with @[scope] annotations is enough.

See also

Error Recovery

This page answers: I see a diagnostic — what does it mean and what do I do?

This page covers the most common diagnostics with concrete recovery actions. For the full enumeration, see Appendix C. For long-form explanations of any code, run ox explain <code>.

Resolution + import errors

OE0101 — Unresolved name

What it means: a name doesn’t resolve to anything in scope. Most common cause: missing use import.

Recovery:

  • If the name is a primitive (Nat, Int, Real, Bool, String, Decimal, Money, Date, etc.): add use std::math::<Name>; (no implicit prelude — see RFD-0014).
  • If it’s a foundational-ontology metatype (kind, role, phase, …): add use ufo::prelude::*; (or your foundational-ontology’s prelude).
  • If it’s from a sibling module: add use crate::path::to::Name;.
  • If it’s from another package: ensure the package is in ox.toml [dependencies], then use <package>::path::to::Name;.

OE0102 — Ambiguous import

Two use statements bring the same name into scope. Pick one or rename via as.

OE0103 — Cyclic module dependency

Module A imports B which imports A (transitively). Restructure to break the cycle — usually one of the modules should host the shared declarations and the others import from it.

Type system + structural

OE0201 — Concept used as relation, or vice versa

You wrote Person(alice, bob) but Person is a concept (1-ary), not a relation (2-ary). Or you wrote works_at(alice) but works_at is a relation (2-ary). Check the declared arity.

OE0203 — Non-exhaustive match or cardinality violation

For match: add a wildcard arm or a guardless named arm to handle remaining cases. For cardinality: a relation with cardinality: 1..1 was instantiated zero or two-or-more times for the same source.

OE0206 — Duplicate declaration

Two declarations share a qualified name. Rename one, or move one to a different module.

OE0207 — Mixed-strength rules on same head

You have pub strict foo(X) :- ... and pub derive foo(X) :- ... for the same head. Pick one strength; same-head rules must agree on whether they’re strict or defeasible.

OE0208compute call arg-count mismatch

The compute is declared with N parameters; you called it with M. Match the signature.

OE0210 — Recursive compute-call cycle

Compute A calls B which calls A. Refactor to break the cycle (often via a derive rule that captures the recurrence pattern).

OE0211derive head’s consequence atoms cannot lower

The rule’s head is structurally invalid (often: a head atom references a concept the body doesn’t bind, or the head shape doesn’t match a known consequence form). Check that every variable in the head is bound by a body atom.

Meta-property and metatype

OE0603 — Metaxis value type mismatch

A metatype <T> = { axis = <literal> } assignment has a literal whose type doesn’t match the axis’s declared type. If the axis is : Nat, the value must be a non-negative integer literal.

OE0604 — Tier violation

An expression’s natural tier exceeds the surrounding #dec(tier:...) annotation. Either escalate the surrounding tier or rewrite the expression to fit.

OE0605 — Unknown metatype

A declaration references a metatype name that isn’t in scope. Common causes:

  1. Missing use ufo::prelude::*; (or whichever foundational-ontology package).
  2. Typo in the metatype name.
  3. The metatype was renamed (check the package’s CHANGELOG).

OE0606 — Metaxis refinement violation

A metaxis carries a where { ... } refinement and a value being assigned doesn’t satisfy it.

Test framework

OE0213expect references unknown diagnostic code

The code in expect { diagnostic OE#### } doesn’t exist. Verify against Appendix C.

OE0214 / OE0215 / OE0216 / OE0218 — Frame conflicts

OE0214: using <name> references an unresolved frame. OE0215: two composed frames declare the same-named member. OE0216: an inline fixture redeclares a frame member. OE0218: frame include chain forms a cycle.

For 0215/0216: rename one of the conflicting members. For 0218: break the include cycle.

Decorator + recognizer

OE0229 — Unknown decorator

The decorator name doesn’t resolve. Same recovery shape as OE0101 — usually a missing use.

OE0230 / OE0231 / OE0234 — Decorator arity / target / argument-type mismatch

Check the decorator’s declaration for expected arity, target type, and argument types.

OE0232 — Hint-body mismatch in recognized shape

The decorator’s lowers_to: <shape> claims a shape the body doesn’t match. Either fix the body to match or change the lowering hint.

OE0233 — Tier-clause divergence

A decorator’s tier annotation conflicts with an inferred tier. Pick one.

What to do when none of these match

  1. Run ox explain <code> for the canonical explanation + suggested fix.
  2. Check Appendix C for the full code reference.
  3. Look at the source span — the diagnostic anchors at the offending location.
  4. If the message is unclear, the diagnostic message itself is a bug — file an issue (the message must stand on its own).

See also

Anti-Patterns

This page answers: what should I NOT do when writing Argon, and why?

Each anti-pattern names the wrong move, explains why it’s wrong, and points at the right alternative.

Naming Argon’s tiers as OWL profiles

Don’t: describe a tier as EL++ / OWL 2 EL / OWL 2 QL / OWL 2 RL / OWL 2 DL / SROIQ / ALC. Don’t call nous “the EL++ reasoner.”

Why: Argon’s decidability ladder is independent of OWL (RFD-0004). OWL profile names import OWL framing and constraints that aren’t load-bearing. Coincidental expressivity overlap is a coincidence of math, not a design choice.

Do instead: use Argon’s own tier vocabulary (tier:structural, tier:closure, …, tier:fol, …, tier:mlt). Describe nous by what it does (classification, subsumption, role-closure), not by OWL vocabulary.

Hardcoding foundational-ontology content

Don’t: match on metatype.as_str() == "kind" or write rules that only make sense if a specific foundational ontology is loaded.

Why: Argon is foundational-ontology-neutral (RFD-0002). The compiler privileges no specific foundational ontology; cross-foundational-ontology programs (UFO + BFO smoke test) must compile against the same compiler.

Do instead: treat metatype names as identifiers loaded from the active foundational-ontology package. Code that needs to reason about a specific metatype’s properties does so via the meta-property axes the package declares (e.g., axis values like rigidity = rigid), not via metatype-keyword string matching.

Implicit-prelude expectations

Don’t: write pub kind Person { age: Nat } without use std::math::Nat; somewhere in the module.

Why: there’s no implicit prelude. Bare primitive names produce OE0101 (RFD-0014). The diagnostic carries a contextual hint suggesting the right import.

Do instead: add an explicit use line at the top of every module that references primitive types or foundational-ontology names.

Recursive compute calls

Don’t: write compute fib(n) = if n < 2 { n } else { fib(n - 1) + fib(n - 2) } (compute calling itself).

Why: the elaborator runs validate_compute_call_acyclicity and rejects recursive cycles with OE0210. Computes are not the right home for recursion.

Do instead: express recursion as a derive rule. Datalog handles transitive cases natively; for arbitrary recursion, escalate to tier:recursive.

Conflating decorator with structural constraint

Don’t: use a decorator to encode a constraint like “Person’s age must be non-negative.”

Why: decorators (RFD-0018) are reserved for algebraic relation properties (transitive, symmetric, asymmetric, etc.) that lower to recognized shapes. Using them for arbitrary constraints conflates two mechanisms.

Do instead: use a where { ... } clause in the concept body for structural constraints.

Cross-tenant shortcuts

Don’t: write a query or mutation that touches another tenant’s data, even if you have a shared-cache reason.

Why: cross-tenant isolation is a structural invariant (RFD-0020). The type system doesn’t even let you accidentally do this; if you find a way, it’s a bug to file, not a feature to use.

Do instead: if you genuinely need cross-tenant data (audit, aggregation, regulatory reporting), that needs an explicit RFD authorizing the boundary crossing and an API that’s tenant-aware.

Editing a committed RFD

Don’t: modify a state: committed RFD’s body to change the position.

Why: RFD supersession (RFD-0001) preserves history. A reader looking at code from a year ago needs to know what the rationale was at the time. Editing destroys that.

Do instead: open a new RFD that supersedes the old one. The new RFD cites the prior in its body and explains what changed. On merge, flip the prior’s state to superseded.

Citing decision IDs in tracked code

Don’t: put // per D-NN, we do X in code or in tracked docs.

Why: decision identifiers are out-of-tree references that future readers can’t easily resolve. The code should describe the behavior directly.

Do instead: describe the behavior directly. If rationale is needed, link to the RFD by number (see RFD-0007).

Catching errors silently

Don’t: write a constraint that always passes when input is malformed.

Why: Argon constraints exist to catch model errors. A constraint that fails open eats the error and produces wrong derivations downstream.

Do instead: let constraints fail closed. OE0606 MetaxisRefinementViolation and friends are the right behavior; downstream consumers see the diagnostic and can decide.

Bypassing unsafe logic for full FOL

Don’t: structure a rule with deeply-nested negation and existentials to “avoid unsafe.”

Why: the FOL escape hatch exists because some constraints really do need full FOL (RFD-0018). Avoiding it via grammatical contortion produces brittle rules that the tier classifier might still flag as tier:fol and the reasoner might still evaluate as full FOL — just less readable.

Do instead: wrap the genuine-FOL section in unsafe logic { ... } and keep the rest of the rule in lower tiers.

“It’s just like OWL X”

Don’t: explain an Argon construct by saying “it’s just like OWL <thing>” or document a feature in OWL terms.

Why: the framing is wrong even when the math overlaps (RFD-0004). Argon and OWL are independent designs; explaining one in terms of the other imports framing that doesn’t fit.

Do instead: describe the Argon construct directly. If interop matters, that’s a separate concern handled by ox-translate (see RFD-0003).

Plurals in keyword choice

Don’t: assume keywords are plural (“requires”, “inputs”, “outputs”).

Why: Argon prefers singular keywords (RFD-0015).

Do instead: check Appendix A — require, input, out, etc.

See also

Idioms

This page answers: what’s the Argon way to do common things?

Each idiom names a common need and the canonical way to express it. Cross-links to the chapter or RFD that explains the underlying reasoning.

Re-export a foundational-ontology prelude

// In a leaf-package's prelude.ar
pub use ufo::prelude::*;

A package that builds on UFO re-exports UFO’s prelude so its consumers get the foundational-ontology vocabulary without explicit use ufo::prelude::* in every module. Same pattern works for BFO or any other foundational-ontology package.

See RFD-0011, ch04-01.

Group axiom declarations for symmetric constraints

disjoint(Person, Organization, System)

When multiple concepts share a symmetric structural relationship, prefer the dedicated declaration form (disjoint, complete, partition) over multiple pairwise where clauses.

See ch02-02, RFD-0018.

Anonymous individuals via let _: T;

let _: Sale;

When you need an individual to satisfy a constraint or pattern-match without naming it, let _: T; synthesizes an anonymous individual with a content-hash-derived identifier (__anon_<8-byte-blake3-hex>_<counter>). For block-local aliasing:

let _ as my_sale: Sale;

The handle my_sale is local to the enclosing block.

See ch02-02.

Diagnostic assertions in tests

test rejects_negative_age {
  expect { diagnostic OE0606 at //~ negative }
}

assert age(alice, -5)  //~ negative

expect { diagnostic <code> at //~ <marker> } matches the diagnostic against the source marker. This is the canonical way to test that a constraint or rule fires.

See ch03-03, RFD-0008.

Frame composition for shared test scaffolding

pub frame BasicCorporate {
  assert Organization(acme);
  assert Person(alice);
  assert works_at(alice, acme);
}

test scenario_a {
  using BasicCorporate;
  // ... test body ...
}

test scenario_b {
  using BasicCorporate;
  // ... different test body, same scaffolding ...
}

When multiple tests share setup, declare a pub frame and using it. Frames compose; multiple frames can be combined with conflict detection.

See ch03-03, RFD-0008.

Per-tier scoping at the granularity that matches need

// Module-scope (least granular)
#dec(tier:closure)

// vs declaration-scope (more granular)
#dec(tier:expressive)
pub derive complex_thing(X) :- ...

// vs block-scope (most granular)
#dec(tier:recursive) {
  pub derive ancestral(...) :- ...
}

Prefer the most granular scope that captures the actual need. Module-wide tier escalation hides the fact that one rule needed it; per-declaration scoping makes the requirement visible.

See tier-selection.md, ch05-03.

Why-provenance on derived facts

Every derive rule’s output carries why: PosBool(M) provenance automatically. To inspect:

query trace_classification(P) :-
  adult(P)
  => { person: P, why: meta(why_of(adult(P))) }

Provenance is a value field, not a separate query mechanism. It composes naturally with the rest of the expression grammar.

See RFD-0007.

Theorem marking on critical derivations

@[theorem]
pub derive every_adult_has_age(P) :- adult(P), exists(A, age(P, A))

When a rule encodes a structural invariant the system should mechanically verify, mark it @[theorem]. The compiler attempts proof; failure surfaces as OW0 warning. For tests asserting unprovable claims, use #[unproven] on the test instead.

See ch02-04, RFD-0008.

Multi-head defeasible disjunction

pub derive vehicle_class(V, "compact") :- size(V, "small")
pub derive vehicle_class(V, "midsize") :- size(V, "medium")
pub derive vehicle_class(V, "large") :- size(V, "large"), price(V, P), P > 30000

Multiple pub derive rules sharing a head act as disjunctive cases. They must agree on rule strength (all pub derive or all pub strict); mixing strengths produces OE0207.

See ch02-04.

Aggregate with where filter

pub derive total_revenue(O, T) :-
  Organization(O),
  T = sum(R) {
    Sale(S),
    sale_org(S, O),
    revenue(S, R)
    where status(S, "completed")
  }

Aggregate expressions admit a where clause that filters the witness set without changing the aggregate’s signature.

See ch02-04.

Compute with ensure for preconditions

compute capital_gain(s: Sale) {
  input { s: Sale }
  out { gain: Money, kind: String }
  ensure { s.proceeds >= 0, s.basis >= 0 }
  body {
    let gain = s.proceeds - s.basis;
    let kind = if gain > 0 { "gain" } else { "loss" };
    { gain: gain, kind: kind }
  }
}

ensure clauses validate inputs at call time; failures surface as compute-call diagnostics rather than corrupted outputs.

See RFD-0006, ch02-05.

Iterate-and-collect via comprehension

pub compute active_unit_numbers(b: Building) -> List[Text] =
    [u.number for u in b.units where unit_is_active(u)]

When the projection is a small expression — a field access, an arithmetic combinator, a constructor call — comprehensions read closer to the intent than the equivalent filter + map chain. The where clause is optional; the binder is fresh and local. Multi-source comprehensions are deferred to v2; in v1 chain two computes or compose two filter-maps when you need them.

See ch02-08, RFD-0039.

Optional unwrap with fallback

pub compute contact_or_placeholder(p: Person) -> Text =
    p.contact_email.unwrap_or("(no contact)")

unwrap_or covers presence-based defaulting. The fallback applies only to absence — unwrap_or(None(), d) returns d; unwrap_or(Some(undefined), d) returns the inner undefined, not d. K3-faithful semantics: presence is classical, inner truth state passes through.

When the intent is “either way, substitute a default,” chain a separate operation that operates on the inner value’s truth state.

See ch02-08, RFD-0037.

Rebuild-and-assign for collection-field mutation

pub mutation add_parent(l: Lease, p: Person) {
    do {
        l.parents = l.parents.append(p);
    }
    return l;
}

Collection operations are pure: append / union / filter / map return new collections, never mutating in place. In a do { } body, the idiomatic way to “modify” a collection field is to rebuild it and assign. Each assignment becomes a single GroundAssertion in the kernel’s append-only event log.

In-place mutators (l.parents.insert!(p)) are deferred. Use rebuild-and-assign in v1.

See ch02-08, RFD-0039.

Set vs List decision

Reach forWhen
Set[T]Order does not matter; duplicates are not meaningful; membership / union are the principal operations.
List[T]Order matters; positional access by index makes sense; duplicates are meaningful.
Map[K, V]Lookup by key is the principal operation; K is orderable.
Optional[T]The field is 0-or-1. Sugar T? is preferred for the common case.
[T; <bound>]The field is conceptually a relation between the owning concept and others, with a structural cardinality constraint.

[T; <= 1] triggers OW2402 suggesting T?. Take the suggestion when the intent reads as “value that may be absent”; keep [T; <= 1] only when the intent reads as “relation, capped at one occurrence”.

See ch02-08.

Naming conventions

  • Concepts: PascalCase (Person, CapitalGainSale).
  • Relations and properties: snake_case (works_at, formation_date).
  • Computes / queries / mutations: snake_case.
  • Patterns: snake_case for the pattern name.
  • Modules and packages: snake_case.
  • Metatype names: chosen by the foundational-ontology package (UFO uses lowercase kind, role, etc.).

See also

Tooling

This page answers: as an agent integrating with Argon (writing code, calling diagnostics, wiring an MCP harness), what does the tooling surface look like and how do I use it?

For the language itself, see Quick Reference. For why this surface exists in the shape it does, see RFD-0027. For the diagnostic schema’s stability commitment, see RFD-0028.

Architecture in one paragraph

Argon’s agent-facing tooling is layered. Tier 0 is the universal floor: every ox subcommand relevant to agents accepts --json and emits payloads conforming to schemas under share/argon/schemas/. Any harness that can run a shell command and parse JSON is integrated. Tier 1 is the MCP transport: ox mcp exposes the same operations as MCP tools, bytewise-identical to the CLI JSON. Tier 2 is the VS Code / Cursor extension for in-editor humans (and IDE-mode agents). Tier 3 is per-agent skill / rules files (share/agents/_common/argon-skill.md plus per-agent variants) that teach agents which Tier 0 / Tier 1 capability to reach for.

All four tiers are backed by a single in-process surface library, oxc-agent-surface. Transports never reimplement operations; they’re wire-format projections of typed Rust function calls.

Tier 0 — universal CLI + JSON schemas

Every agent-relevant subcommand accepts --json:

CommandSchemaNotes
ox check --jsondiagnostics.schema.json (v1.0, ratified by RFD-0028)Type-check + meta-property calculus + package constraints. Returns a DiagnosticsReport.
ox explain <code>(planned: explain.schema.json)Diagnostic code → prose with example fix. Currently text output.
ox query <name> --json(planned: query-result.schema.json)Run a named query against compiled state. Bindings + provenance.
ox tree --json(planned: package-tree.schema.json)Resolved package tree + lockfile state.
ox why <package>(planned: provenance.schema.json)Path-to-package explanation; provenance walk.

Schema files ship inside the toolchain at ~/.argon/toolchains/<version>/share/argon/schemas/. Their version manifest is version.json:

{
  "schema_set": "0.1.0",
  "schemas": { "diagnostics": "1.0.0" },
  "stable": ["diagnostics"]
}

Schemas listed in stable have crossed SemVer 1.0 via their ratification RFD; pin against those with confidence. Pre-1.0 schemas may break between minor toolchain releases.

Example: ox check --json

$ ox check --json | jq '{schema_version, summary}'
{
  "schema_version": "1.0.0",
  "summary": { "errors": 1, "warnings": 0, "infos": 0 }
}

The full payload:

{
  "schema_version": "1.0.0",
  "diagnostics": [
    {
      "code": "OE0226",
      "severity": "error",
      "message": "metarel endpoint mismatch",
      "primary_span": {
        "file": "src/lease.ar",
        "byte_start": 142, "byte_end": 154,
        "line_start": 8, "col_start": 5,
        "line_end": 8, "col_end": 17
      },
      "primary_label": "expected `kind`, found `role`",
      "secondary_labels": [],
      "help": null,
      "package_origin": "ufo",
      "provenance_chain": []
    }
  ],
  "summary": { "errors": 1, "warnings": 0, "infos": 0 }
}

Every Diagnostic carries the same nine fields. severity is one of "error" | "warning" | "info". primary_span is null for spanless diagnostics (CLI-layer cross-format errors with no .ar source); when present, it carries both 0-indexed UTF-8 byte offsets and 1-indexed UTF-16 line/column. Column units are UTF-16 code units, matching LSP’s code-unit-width convention so multi-byte characters land in the same column an LSP-aware editor would compute. Indexing diverges from LSP — LSP’s Position is 0-indexed for both line and character — so an agent passing these into LSP Position fields must subtract 1 from each. Argon emits the editor-display form (1-indexed) per RFD-0028.

Exit code: 0 when summary.errors == 0, non-zero otherwise. Configuration errors in ox.toml short-circuit before any diagnostic compilation runs and surface as their own diagnostics in the same payload shape.

Tier 1 — MCP transport (ox mcp)

ox mcp is the Model Context Protocol server. Speaks JSON-RPC 2.0 over stdio. Configure your agent (Cursor, Claude Code, opencode, Codex, Continue, Aider) to launch it as an MCP server. oxup agents register writes the relevant config for detected agents on toolchain install — see Tier 4 below.

Tool surface

ToolWrapsReturns
argon_checkox check --json over a workspaceDiagnosticsReport (text content with JSON body)
argon_explainox explain <code>Text
argon_check_file(planned) file-scoped checkDiagnosticsReport
argon_queryox query --jsonBindings + provenance
argon_packagesox tree --jsonPackage tree
argon_whyox why --json (post-provenance)Provenance walk

Tool payloads are bytewise-identical to CLI JSON because both transports route through the same oxc-agent-surface library — drift between channels is structurally impossible.

Lifecycle (v0 vs v1)

ox mcp ships in two scheduled phases within one release cycle.

  • v0 (current): thin spawning transport. Each MCP tool call invokes the corresponding ox CLI subcommand as a child process and streams its JSON output back. No persistent compiler state. Cold cache on every call. Simple lifecycle.
  • v1 (next): persistent OxcContext. Holds one Salsa database across tool calls; warm caches across requests; correct cache invalidation against file changes between calls. Production-traffic shape. Same wire surface — clients targeting v0 work unchanged on v1.

See RFD-0027 for the rationale on shipping v0 first.

Wire example

→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0.0"}}}
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","serverInfo":{"name":"argon","version":"0.4.x"},"capabilities":{"tools":{}}}}

→ {"jsonrpc":"2.0","id":2,"method":"tools/list"}
← {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"argon_check","description":"...","inputSchema":{...}},...]}}

→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"argon_check","arguments":{"workspace":"/abs/path/to/project"}}}
← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{\"schema_version\":\"1.0.0\",...}"}],"isError":false}}

Workspace is optional; omitted falls back to the server’s launch CWD. Absolute paths only — relative paths surface a -32602 invalid params error to avoid CWD-drift surprises across tool calls.

Tier 2 — IDE extension

The Argon VS Code / Cursor extension wraps ox-lsp for in-editor humans and IDE-mode agents that pick up diagnostics via the editor’s extension-to-context glue. Install with:

oxup extension install

Or cursor --install-extension <vsix> against a release VSIX. Once installed, the language server activates on .ar files, populates the InfoView panel, and surfaces diagnostics through the editor’s standard publishDiagnostics channel. Agents running inside Cursor / VS Code see those diagnostics as part of their normal context.

Tier 2 is unchanged by RFD-0027 — it’s the existing surface, retained.

Tier 3 — per-agent skills / rules

Skill / rules files teach an agent which Tier 0 / Tier 1 capability to reach for. They’re documentation pointers, not implementations.

oxup agents register (called automatically by oxup install / update / default) walks every share/agents/<id>/ directory in the active toolchain and drops the skill file at the agent-specific config location:

AgentTarget
Cursor~/.cursor/rules/argon.md
Claude CodeSkill plugin via ~/.claude/settings.json marketplace pointer
Codex~/.codex/AGENTS.md
opencoderule file under opencode’s rule directory

Single-source content lives at share/agents/_common/argon-skill.md. Per-agent variants under share/agents/<id>/ wrap that body with the frontmatter each agent expects. Substantive edits land in _common first.

If you maintain agent integrations in your project, you can override the global skill with a project-local file at <workspace>/.cursor/rules/argon.md (or the equivalent for your agent). Project-local rules win over global ones.

Tier 4 — registration as data

share/agents/<id>/registration.toml is a per-agent descriptor read by oxup agents register. Adding a new agent that uses an existing handler kind is a share/ data drop — no Rust changes required.

Descriptor shape:

[agent]
id = "cursor"
name = "Cursor"

[detect]
config_dir = "~/.cursor"   # ~ expanded against $HOME

[[register]]
kind = "mcp-server"
config_relative = "mcp.json"
server_name = "argon"
argv = ["ox", "mcp"]

Detection is per-agent presence — missing config dir means the registration step is a silent no-op. Registration steps are idempotent on re-run; toolchain bumps re-aim transparently.

Currently supported handler kinds:

  • claude-marketplace — merge a marketplace pointer + enabledPlugins entry into Claude Code’s ~/.claude/settings.json. Used by share/agents/claude-code/registration.toml.
  • mcp-server — merge an mcpServers.<name> entry into the agent’s MCP JSON config. Used by share/agents/cursor/registration.toml. The same kind works for any agent consuming the standard MCP mcpServers shape.

New kinds extend the RegistrationStep enum + dispatch in oxup::agents. Adding a kind is a Rust change; adding a new agent that uses an existing kind is purely a share/ data drop.

I want to … do Y

TaskReach for
Get diagnostics on a workspaceox check --json (or argon_check over MCP)
Understand what a diagnostic code meansox explain <code> (or argon_explain over MCP)
See what packages a workspace depends onox tree --json (or argon_packages over MCP)
Run a named query against compiled stateox query <name> --json (or argon_query over MCP)
Wire my agent up to call Argon’s MCP toolsoxup agents register (runs automatically on oxup install)
Install the editor extensionoxup extension install
Pin against a stable wire formatcheck share/argon/schemas/version.json’s stable array
Override the global skill content for one projectdrop a project-local rule file (e.g. .cursor/rules/argon.md)

Stability discipline

  • Schemas in stable: SemVer-locked. Breaking change → major bump; additive change → minor bump. Consumers pin.
  • Schemas not in stable: pre-1.0, may break between minor toolchain releases. Each schema graduates to 1.0 via its own follow-up RFD as the underlying surface stabilizes.
  • MCP wire shape: tracks the published MCP spec revision. The server announces its supported protocolVersion during initialize; clients negotiate.
  • Tool names + argument shapes: stable within a major. Adding a new tool is non-breaking; renaming or removing a tool requires a major bump.
  • CLI flags: --json is stable; per-command flag changes follow the standard ox CLI deprecation cycle.

Anti-patterns

  • Don’t infer diagnostics from prose. Parse the JSON. The wire format is the contract.
  • Don’t fork wire shapes between transports. CLI JSON, MCP tool payloads, and (future) LSP-extended response bodies are all the same data, projected from the same in-process surface. If you find yourself reconstructing a payload differently in different places, you’ve drifted.
  • Don’t depend on a non-stable schema in production. Pre-1.0 schemas may evolve. Use them, but pin to the toolchain version, and watch the next ratification RFD.
  • Don’t hand-author files generated by oxc-codegen. Schemas, TypeScript bindings, and the diagnostic registry are emitted from Rust source-of-truth types via schemars / ts-rs. Drift is gated by oxc-codegen check in CI.
  • Don’t run ox mcp with relative workspace paths. v0 rejects them with -32602 invalid params; this is intentional and stays in v1.

Where this ships

  • Schemas: ~/.argon/toolchains/<version>/share/argon/schemas/
  • Skill content (single-source): ~/.argon/toolchains/<version>/share/agents/_common/argon-skill.md
  • Per-agent variants: ~/.argon/toolchains/<version>/share/agents/<id>/
  • Registration descriptors: ~/.argon/toolchains/<version>/share/agents/<id>/registration.toml
  • ox binary: ~/.argon/toolchains/<version>/bin/ox
  • LSP binary: ~/.argon/toolchains/<version>/bin/ox-lsp

oxup proxies (~/.argon/bin/ox, etc.) resolve the active toolchain via ox-toolchain.toml walk-up and exec the version-specific binary; integration code never hard-codes an absolute path.

See also

  • RFD-0027 — the architecture rationale (why CLI is the floor, why MCP is Tier 1, why registration is data).
  • RFD-0028 — diagnostics schema 1.0 ratification.
  • Anti-Patterns — Argon-language anti-patterns (vs the tooling-specific ones above).
  • Decision Guides — choosing between Argon language constructs.

Glossary

This page answers: I see a term — what does it mean?

Terse one-line definitions. Cross-links to the chapter or RFD that gives the full treatment.

A

  • ABox — the assertional layer. Facts about specific individuals (e.g., Person(alice)). Contrast with TBox. See ch02-02.
  • AFT — Approximation Fixpoint Theory (Denecker, Marek, Truszczynski 2000). The bilattice-parametric framework Argon’s truth-value semantics is grounded in. Subsumes K3, FDE, Boolean, and every CWA variant under one algebraic structure. See RFD-0037.
  • Aggregatesum, count, min, max, avg over a body of atoms. May carry a where filter. See ch02-04.
  • Anonymous individual — an individual created without an explicit name; identifier synthesized from content hash. See ch02-02.
  • Anti-rigid — a metatype property: instances of an anti-rigid type can stop being instances over time without ceasing to exist (UFO’s roles, phases). Compare rigid.
  • Argon — the language. The compiler is oxc. See the substrate chapter.
  • Axiom — a structural rule attached to a concept or relation. See ch02-04.

B

  • BFO — Basic Formal Ontology. A foundational ontology; loads as a peer package, not a built-in. See RFD-0002.
  • Belnap-Dunn FDE — see FDE.
  • Bilattice — an algebraic structure with two independent partial orders (truth and information) on the same carrier. Fitting (1991). The substrate Argon’s Truth4 realizes. See RFD-0037.
  • Bivalent lockfile — a lockfile carrying both a content hash and a constructs-hash for each entry. See RFD-0013.
  • Bitemporal — both valid time and transaction time. The kernel’s event log is bitemporal. See RFD-0021.

C

  • Closed World Assumption (CWA) — the modeling assumption that absence of evidence is evidence of absence. Opposite of OWA. See RFD-0037 (which supersedes RFD-0016).
  • Compute — a declarable item that returns a value at the call site. Three forms: expression, inline body, opaque Rust FFI. See RFD-0006.
  • Concept — the language’s term for a class / type. Specializes other concepts via <:. See ch02-02.
  • Constructs hash — a Merkle root over a package’s exported declaration signatures. Catches semantic identity. See RFD-0013.
  • Content hash — byte-level hash of source. Catches byte identity. Pairs with constructs hash. See RFD-0013.
  • CQRS — Command-Query Responsibility Segregation. Reads go through projections; writes append to the event log. See RFD-0022.

D

  • Decidability tier — Argon’s graduated decidability ladder. Independent of OWL profiles. See RFD-0004.
  • Decorator — a user-declared annotation that attaches algebraic / structural properties to relations. Lowers to a recognized shape. See ch02-01, RFD-0018.
  • Defeasible — a rule whose conclusion can be overridden by a more specific rule. Default rule strength. Contrast strict.
  • Derive — a rule that produces new facts from existing facts. Form: pub derive head(args) :- body. See ch02-04.
  • Diagnostic — a compiler-emitted message (error, warning, info). Codes: OE / OW / OI. See Appendix C, RFD-0024.
  • DRedc — Delete-Rederive with counting. The current IVM algorithm. See RFD-0022.

E

  • Edition — an integer version of the source-syntax dialect. Parse-time only. See RFD-0012.
  • Elaboration — the compiler phase that resolves names, type-checks, and produces Core IR. See oxc rustdoc on the elaborate module.
  • Event logont.axiom_events, the kernel’s append-only assertion / retraction record. See RFD-0021.
  • Exactly True Logic / ETL — Pietz & Rivieccio (2013). The four-valued logic where only T is designated. The rigorous published anchor for Argon’s “fail-closed on unknown” rule. See RFD-0037.
  • Expect — a test block matching specific diagnostics or values against source markers. See ch03-03.

F

  • FDE — First-Degree Entailment / Belnap-Dunn four-valued logic (Belnap 1977 / Dunn 1976). Truth values: T (told true), F (told false), N (told neither / unknown — Argon’s can), B (told both — Argon’s both). Argon’s storage substrate; cross-standpoint federated queries surface FDE values. See RFD-0037.
  • Federation — aggregating per-standpoint query results across multiple standpoints via AFT info-join. Lifts K3 contributions into FDE outputs; BOTH arises exactly when sources disagree. See RFD-0037.
  • Fixture — a test-local mini-module elaborated under capture mode. See ch03-03.
  • Fork — a copy-on-write branch of the kernel’s event log. Supports hypothetical reasoning. See RFD-0021.
  • Foundational ontology — a top-level conceptual scheme (UFO, BFO, DOLCE, GFO) that the package loads. The compiler privileges none. See RFD-0002.
  • Frame — a reusable test-scaffolding declaration. Composes with using F. See ch03-03.
  • Functor module — a module parameterized by other modules. See RFD-0009.

G

  • GFO — General Formal Ontology. A foundational ontology; loads as a peer package.
  • Group axiom — a structural declaration over multiple concepts (disjoint, complete, partition).

H

  • Hypothetical — a speculative-reasoning block. Operates under a forked event log. See ch02-04.

I

  • Inert — the property that the compiler privileges no specific foundational ontology. See RFD-0002.
  • Intent node — what an AGENTS.md file is. Each declares the directory’s purpose and operating rules.
  • IVM — Incremental View Maintenance. Algorithm class for keeping derived projections in sync with assertions. See RFD-0022.

K

  • K3 — Kleene’s strong three-valued logic (Kleene 1952). Truth values: T (true), F (false), U (unknown). Argon’s per-standpoint refinement-membership lattice context; the consistent fragment of FDE. The conditional propagates U → U to U (versus Łukasiewicz Ł3 which makes it T). See RFD-0037.
  • Kernel — the runtime (storage + reasoning + multitenancy). Per-tenant instances. See RFD-0020.
  • Kernel API v2 — resource-oriented HTTP API at /api/v2/*. See RFD-0023.
  • Kleene-Belnap — informal compound naming. K3 (the Argon lattice for refinement membership) is the consistent fragment of FDE motivated as in Belnap (1977)’s question-answering argument; the Pietz-Rivieccio “Exactly True” designation pins down “only T succeeds.” See RFD-0037.
  • Kripke — modal-logic semantics. Argon’s box() / diamond() operators correspond to Kripke necessity / possibility.

M

  • Metatype — a user-declared classification with axis assignments (e.g., UFO’s kind = { rigid, sortal, provides }). See ch02-01.
  • Metaxis — a user-declared axis along which metatypes can take values. See ch02-01.
  • Metarel — a user-declared kind of relation (e.g., UFO’s Mediation). See ch02-01.
  • Module — a single .ar file. The package’s top-level module is prelude.ar. See RFD-0010.
  • Mutation — a first-class declarable item that runs require / do / retract / emit / return clauses. See RFD-0007.

N

  • Nous — the structural reasoner. Pure, synchronous, no I/O. See crates/nous rustdoc.

O

  • Open World Assumption (OWA) — the modeling assumption that absence of evidence is not evidence of absence. Argon supports it; refinement under OWA is in (Kleene strong three-valued); cross-standpoint federated queries lift to . See RFD-0037.
  • OntoUML — a UML-derived modeling notation. Supported via ox-ontouml bridge. See RFD-0003.
  • Overlay — per-tenant kernel state layered over the shared base. See RFD-0020.
  • OWL — Web Ontology Language. A foreign format; supported via ox-owl bridge. Argon’s tier ladder is independent of OWL profiles. See RFD-0004, RFD-0003.
  • Oxide — the toolchain that ships Argon. Internal identity; binaries are ox, oxc, ox-lsp, oxup.

P

  • Pattern — a parameterized template that emits declarations. Either via pattern declaration or bespoke surface form (correlative_pair, part_whole). See RFD-0019.
  • Phase — a UFO metatype: a temporary state of an instance (e.g., Adult, Employed). UFO-specific.
  • PosBool(M) — the provenance encoding. DNF over assertion identifiers. Stored as a JSONB value field. See RFD-0022.
  • pub — visibility modifier promoting a declaration to package-public. Default is module-internal. See RFD-0011.

Q

  • Query — a first-class declarable item that retrieves facts matching a body pattern. Carries why-provenance. See RFD-0007.

R

  • Relation — a multi-place predicate between concepts. Same as rel in source.
  • Recognized shape — an algebraic structure (transitive, symmetric, …) the recognizer fast-paths via specialized algorithms. Inhabits the recognized tier. See RFD-0004.
  • RFD — Request for Discussion. The form this monorepo records design rationale. See RFD-0001.
  • Rigid — a metatype property: instances of a rigid type cannot stop being instances without ceasing to exist (UFO’s kinds, subkinds). Compare anti-rigid.
  • Role — a UFO metatype: a relational role an individual plays (e.g., Customer, Employee). UFO-specific.

S

  • Saturation — the fixpoint of applying derivation rules until no new facts are derivable.
  • Sortal — a metatype property: instances of a sortal type carry their own identity criteria.
  • Standpoint — a perspectival viewpoint in the knowledge base. First-class; arranged in a partial-order lattice. See RFD-0010.
  • Strict — a rule strength: the rule’s conclusion is unconditional, cannot be overridden. Contrast defeasible.
  • Substrate — the language’s foundational layer of declaration forms (metatype, metaxis, metarel, decorator). See ch02-01.
  • Subkind — a UFO metatype: a sortal whose identity is inherited from a kind ancestor. UFO-specific.

T

  • TBox — the terminological layer. Declarations of concepts, relations, properties. Contrast ABox.
  • Test — a first-class declarable item containing assertions and expect blocks. See ch03-03, RFD-0008.
  • Tier — Argon’s decidability classification. See decidability tier.
  • Top type — the universal upper bound. Notation: (with Top as ASCII alternate). See RFD-0017.
  • Transaction time — when a fact was recorded in the system. Pairs with valid time for bitemporal queries. See RFD-0021.

U

  • UFO — Unified Foundational Ontology. A foundational ontology; ships as a peer package, not a built-in. See RFD-0002.
  • unsafe logic { ... } — the FOL escape hatch block. Inhabits tier:fol. See RFD-0018.
  • unproven — a test attribute marking a theorem claim the system cannot mechanically verify. See RFD-0008.
  • use — module import statement. Single-symbol Named or glob form. See RFD-0010.

V

  • Valid time — when a fact was true in the modeled world. Pairs with transaction time. See RFD-0021.

W

  • where clause — structural constraint clause in a concept body. See ch02-02, RFD-0018.
  • Why-provenance — derivation provenance carried as a value field on every derived fact. See RFD-0007.
  • Workspace — a multi-package directory with a shared lockfile and target/. See RFD-0013.

RFDs — Requests for Discussion

Design rationale for the orca-mvp monorepo. RFDs cover any system in this tree: argon (language + Oxide toolchain), kernel, tide, platform, tools, userspace.

What an RFD is

An RFD captures a single design position and the rationale behind it. One question per RFD. Once committed, the rationale is the durable answer; if a future RFD changes the answer, the old RFD is marked superseded and the new one cites it.

RFDs are not work-tracking artifacts. Implementation work that flows from a committed RFD lives in GitHub Issues that cite the RFD. Closing all the issues does not close the RFD; the RFD stays committed as the rationale of record.

What an RFD is not

  • Not a spec. Specs live in rustdoc (code surfaces) and the Argon book (language surface). RFDs reference those for the “what got built” but don’t redefine them.
  • Not a task list. Use GitHub Issues + Project boards for active work.
  • Not a status board. Cluster status, release readiness, in-flight work — all live in GitHub Projects.

Layout

rfd/
├── README.md                       this file
├── 0001-rfd-process.md             how RFDs work
├── NNNN-<slug>.md                  one position per file
└── archive/                        superseded or abandoned RFDs (optional; usually we just leave them in place with state field set)

Numbering

  • Four-digit zero-padded, monotonic.
  • Numbers are assigned at draft time in the PR that introduces the RFD.
  • Once a number is assigned and the PR opens, the number is burned even if the RFD is abandoned (the file lands with state: abandoned).
  • Gaps are normal; never renumber.

Lifecycle

Each RFD carries a state field with one of:

  • discussion — drafted, open for comment. The PR is the discussion forum.
  • committed — ratified; the rationale is in force. Body frozen except clarifications.
  • abandoned — opened but the position was dropped. Body frozen.
  • superseded — replaced by a later RFD. superseded_by: field carries the replacement’s number. Body frozen.

State transitions:

  • discussion → committed on PR merge after ratification.
  • discussion → abandoned on PR close-without-merge or explicit abandonment.
  • committed → superseded happens only when a new RFD lands that explicitly supersedes this one.

Metadata block

Each RFD opens with a small HTML metadata block right after the H1. argon-theme.css styles it as a colored status pill plus a muted meta strip; in the canonical /rfd/ markdown view it renders as plain text. The shape:

<div class="rfd-meta">
  <span class="rfd-status rfd-status-committed">Committed</span>
  <span class="rfd-meta-line">Opened 2026-05-03 · Committed 2026-05-03</span>
</div>

Status-pill classes correspond to lifecycle states:

  • rfd-status-discussion — open, drafting (yellow)
  • rfd-status-committed — settled, in force (green)
  • rfd-status-superseded — replaced by a later RFD (slate, line-through)
  • rfd-status-abandoned — dropped without resolution (orange)

The meta line carries dates inline (Opened YYYY-MM-DD · Committed YYYY-MM-DD). For a superseded RFD, append · Superseded by <a href="NNNN-slug.html">RFD-NNNN</a>.

Body shape

Lightweight; not bureaucratic. Use the sections that fit the question; skip ones that don’t apply.

  1. Question — one paragraph stating what’s being decided.
  2. Context — what’s hard about this, what changed, what depends on it.
  3. Options — alternatives considered (skip if there’s only one obvious answer).
  4. Decision — the answer (only when state is committed).
  5. Rationale — why; the load-bearing reasoning future readers need to make consistent calls.
  6. Consequences — what changes downstream (which crates, docs, tests, conventions).

How to land an RFD

  1. Pick the next free number. Create rfd/NNNN-<slug>.md with state: discussion.
  2. Open a PR titled RFD-NNNN: <title>.
  3. Discuss on the PR. Body iterates as the discussion converges.
  4. Once ratified: flip state to committed, set committed: date, merge.
  5. Cite the RFD from any code, doc, or issue that depends on the rationale.

How to supersede an RFD

  1. Open a new RFD (next free number) with the new position.
  2. New RFD cites the prior in its body and explains what changed.
  3. On merge: flip prior RFD’s state to superseded, set superseded_by field. Do not modify prior body content beyond that.

Index

RFDTitleState
0001RFD processdiscussion
0002Argon is foundational-ontology-neutralcommitted
0003Foreign-format support lives in ox-* crates, not in the languagecommitted
0004Decidability ladder is Argon’s own, never framed in OWL termscommitted
0005One grammar, multiple evaluation contextscommitted
0006Three compute forms — expression, inline body, opaque FFIcommitted
0007Queries and mutations are first-class items; every result carries why-provenancecommitted
0008Tests are first-class items; proof-status attributes for theorem claimscommitted
0009Generics are functor modules and generic computations, not parametric conceptscommitted
0010Module system and standpointscommitted
0011Two-tier visibility and direct-dependency rulecommitted
0012Package manifest is unified; editions are parse-time onlycommitted
0013Bivalent lockfile (content-hash + constructs-hash); workspace-local resolutioncommitted
0014std ships with the toolchain; explicit imports — no auto-preludecommitted
0015Surface naming prefers PL idiom over proof-assistant idiomcommitted
0016Refinement under OWA is three-valued (Kleene-Belnap)superseded
0017Universal top type is / Top; no explicit Thing wrappercommitted
0018where clauses, unsafe blocks, modal operators — one mechanism per concerncommitted
0019Patterns are first-class parameterized templatescommitted
0020Per-tenant kernel runtime — shared base + per-tenant overlaycommitted
0021Unified axiom event log; bitemporal; CBOR axiom-ADT bodiescommitted
0022CQRS projections + DRedc IVM behind a ProjectionMaintainer traitcommitted
0023Kernel API v2 — resource-oriented redesigncommitted
0024Diagnostic codes — OE / OW / OI severity prefix; X* for external bridgescommitted
0025Toolchain architecture — oxup argv[0] dispatch; single-root state; rustup-stylecommitted
0026Living diagrams — diagram blocks as graduated language extensioncommitted
0027Agent-facing tooling — universal CLI + JSON contract, MCP as transport, registration as datacommitted
0028Diagnostics schema 1.0 — agent wire-format ratificationcommitted
0029Doc comments and ox doc//////! semantics, intra-doc links, doc tests, JSON IRcommitted
0030ox doc --document-deps and external-package documentation — Cargo --no-deps parity; deps rendered from local cachediscussion
0031Concept-reference endpoints on pub metarel — subtype-aware validation alongside metatype-keyword and axis-value matchesaccepted
0032Shared-base package classification for workspace-vendored deps — SharedBasePolicy consulted for workspace members, not just locked depsaccepted
0033Sequenced test statements: mutate and cleanup in test blocks — per-statement saturation, canonical emit semantics, ordered teardowndiscussion
0034Composition pipeline and the oxc / ox boundary — two-phase pipeline, per-package .oxc, workspace .oxbin, Engine/Module/Storecommitted
0035Binary artifact .oxbin and the execute layer — section model, three-axis versioning, Merkle content-addressing, runtime trait surface, hot replacement contractcommitted
0036Generic declarations for derive, mutation, and compute<T: Bound> lifted from pub constraint, monomorphization at compose time, Datalog-alternatives composition, substrate-neutral boundsdiscussion
0037AFT-grounded truth-value semantics — bilattice substrate, K3/FDE/Boolean lattice contexts, fail-closed projection, per-standpoint consistency policycommitted
0038Runtime capability advertisement — what OxbinRuntime::capabilities() reports about supported AFT operators and lattice contextsdiscussion
0039Collections and collection operators — Set / List / Map / Optional / Range as closed type constructors, UFCS method-call sugar, comprehensions, indexing, slicing, membership; graduated-context tier dispatchcommitted
0040Substrate atoms and the explicit-writes principle — six atoms (metatype, concept, rule, trait, decorator, macro); four surface keywords (fn, derive, query, mutation) as one rule primitive; call-purity ladder; explicit-writes via insert / update / delete / emit with declared sink; : vs <: cleanup; braces-required; MLT via InstanceOfdiscussion
0041Traits and behavioral polymorphism — pub trait declarations with required fn / derive / query / mutation items; default impls; trait inheritance via <:; impl Trait for Concept blocks; generic bounds <T: Trait> with multiple-bound composition via +; mixed concept/trait bounds via where-clauses; static dispatch via monomorphization at compose time; orphan rule for coherence; concept/trait orthogonalitydiscussion
0042Macros and compile-time code generation — declarative macros (macro!-style, syntactic pattern matching) and procedural macros (@[…]-style, compiled Rust with resolved-AST and metatype-calculus access); expansion at oxc elaboration time within per-package boundary; hygiene; spans through expansion; sandbox + capability grants; state machines and @[derive(...)] as canonical examplesdiscussion
0043Query and mutation surface — select / insert / update / delete / with / emit / scenario grammar replacing require / do / retract / emit / return clauses; EdgeQL-style shape grammar shared between reads, writes, and computed fields; .field scope-relative resolution; aggregations + group by / order by / limit; match form for multi-hop traversal; emit-sink declarations; tier ceilings per body; lease-story worked migrationdiscussion
0044Cross-level instantiation (MLT) — Instantiates IR atom generalized to InstanceOf { instance: EntityRef, type_concept: u64 } with EntityRef = Individual / Concept / Statement; surface pub kind A : B admission for concept-of-concept; order derived from instantiation closure via new phase_mlt; vocabulary constraints via @[order(=N)] / @[bounded_order(N)]; Lean extension TypeGraphMLT with instantiates relation, acyclicity, order monotonicity; Tonto compatibilitydiscussion

RFD-0001 — RFD process

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

How does this monorepo capture design rationale, and how does that artifact relate to the rest of the documentation surface?

Context

The orca-mvp tree had accreted ~8 substrates that each claimed authority over some flavor of design content: a decision registry (141 D-NNN files), cluster RFC folders that mixed spec / decision / work-tracking, a master design doc duplicated between codebase and other docs, scattered AGENTS.md files carrying global rules, plus 12 mega-plan docs in /docs/. Status lived in 4–6 mirror sites that drifted independently. The book has been actively wrong because contradicting sources fed into it.

The fix is to define one canonical home per artifact type and stop maintaining the duplicates. RFDs replace decisions/ + RFCs/ + master-doc decision logs as the single home for design rationale. They do not subsume specs (those live in rustdoc + book) or work tracking (GitHub).

Decision

RFDs as defined in rfd/README.md are the only place this monorepo records committed design rationale. One question per RFD, one position per RFD, immutable once committed except for typo and clarity fixes. When a position changes, a new RFD supersedes the old one.

RFDs do not track work. A committed RFD that requires implementation produces zero, one, or many GitHub Issues that cite the RFD; those issues track the work, not the RFD itself.

RFDs cover the whole monorepo. Single namespace at /rfd/ at the repo root. Argon, kernel, tide, platform, tools, userspace — all share the namespace. Cross-cutting decisions are common enough that per-system RFD trees would just create boundary problems.

Discussion happens on the PR that lands the RFD. PRs are the durable forum. Discussion outside the PR is allowed but the PR comment thread is the canonical record.

Rationale

Why “Discussion” and not “Comment”. The Oxide framing — Request for Discussion — names the value. The decision matters; the rationale matters more. RFDs that only record “we decided X” without “and here’s why we ruled out Y” are useless when context shifts. Treating each RFD as a discussion document forces the author to surface tradeoffs.

Why one position per RFD, not one topic. Topics drift. Positions don’t. “How do packages work” is a topic; “Bivalent lockfiles use content-hash + constructs-hash” is a position. The latter is what an agent or contributor needs to look up later.

Why work tracking is separate. Cluster RFCs in the previous system tried to be spec-and-decision-and-task-list-and-status-tracker simultaneously. Each role wants different update cadence: specs evolve continuously, decisions land once and freeze, task lists shift weekly, status changes daily. Bundling them produced mirror-site drift. RFDs commit to the freeze-once role; everything else moves to its native home.

Why a single namespace, not per-system. Many positions are cross-cutting (multitenancy model, error code conventions, version-bump grammar, how foreign-format support is exposed). Per-system trees force every cross-cutting decision into a “where does it go” debate. Single namespace means the question never comes up.

Why supersession instead of editing. A reader looking at code from a year ago needs to know what the rationale was at THAT time, not what we think now. Edit-in-place destroys that history. Supersession preserves it: the old RFD stays, marked superseded, and the reader can follow the chain.

Why RFDs do not deprecate the AGENTS.md tree. AGENTS.md files are intent nodes for the system that help organize agents working on each part of it. That’s an orthogonal axis from RFDs (which capture why we chose X); the two surfaces don’t overlap.

Consequences

  • The book’s appendix-e-decision-archive.md is deleted (decisions live in /rfd/, not in the book).
  • Master design doc §9 Decision log deletes (decisions D-128 through D-131 migrate to RFDs).
  • New decisions land as RFDs. No more D-NNN registry.
  • Code, docs, and issues cite RFDs by number: RFD-0014 or rfd/0014-foo.md.

RFD-0002 — Argon is foundational-ontology-neutral

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Does Argon (the language) embed a specific foundational ontology — UFO, BFO, DOLCE, GFO — or does it treat all foundational ontologies as first-class peer packages?

Decision

Argon is a foundational-ontology-neutral substrate. The compiler has zero hardcoded foundational-ontology content. UFO, BFO, GFO, DOLCE, and any future foundational ontology load as ordinary packages with no compiler privilege. The language provides primitives (declaration forms, sealed Metatype, instantiates / specializes relations, reflection intrinsics, decidability tiers) that any foundational ontology can build on; none of those primitives encode opinions specific to any foundational ontology.

Rationale

The compiler must outlive the foundational ontology choice. Argon is the substrate every domain — tax, compliance, GAAP, lease, insurance, legal — gets expressed in. If UFO turns out to be the wrong upper ontology for some domain, or if a customer’s regulatory regime requires BFO, or if a research direction wants DOLCE, the compiler cannot need a rebuild to switch. Every foundational ontology has to be swappable at the package layer.

Inertness is structural, not aspirational. “Inert” means there is no place in the compiler where a UFO keyword (kind, role, phase) could acquire a special meaning, because the compiler doesn’t know those words. They lex as identifiers. They resolve against whatever metatype the loaded vocabulary declared. A UFO package becoming privileged would require new compiler code, which is the kind of change that gets noticed in review.

Foundational ontologies are not removable from the system, only from the language. Real users still need a foundational ontology to model anything; UFO is the default load. The neutrality applies to the compiler and the language semantics, not to the recommended workflow.

Consequences

  • The compiler must never hardcode keyword-style metatypes, axis names, or relation names from any foundational ontology. Anti-pattern: matching on metatype.as_str() == "kind" in compiler code.
  • All foundational ontology content ships as packages (e.g., ufo, bfo) installable through the registry.
  • Cross-foundational-ontology validation: a BFO smoke-test package compiles against the same compiler that compiles UFO, with no compiler-side configuration.
  • Bridge to OWL / OntoUML / Tonto / etc. lives outside the language entirely (RFD-0003).

RFD-0003 — Foreign-format support lives in ox-* crates, not in the language

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Where do OWL / OntoUML / Tonto / RDF / Turtle / Manchester / OBO interop crates live in the workspace, and what does the Argon language know about them?

Decision

The Argon language is foreign-format-neutral. The compiler (oxc) and the project manager (ox) carry zero foreign-format knowledge. All format-specific code lives in ox-* toolchain crates: ox-io (RDF-family IO substrate), ox-owl (OWL 2 structural types), ox-ontouml (OntoUML JSON), ox-translate (OWL-family translator), ox-catalog (IRI catalog), and any future format crate.

User-facing CLI (ox import --from=<format>, ox export --format=<format>) discovers format bridges through a toolchain extension mechanism. The language and project-manager crates depend on the foreign-format crates only at the wire-up boundary, never via direct source dependency in the elaboration or compile path.

Rationale

Formats are not the language. OWL is a serialization family for description logics. OntoUML is a UML-derived modeling notation. Tonto is a textual surface for OntoUML. None of these are Argon. Building any of them into the compiler would mean every Argon release has to coordinate with every format spec’s evolution, and every format-specific bug becomes a compiler bug.

Naming reservation enforces the rule. The ox-* namespace is reserved for the Argon Oxide toolchain. A crate named ox-something is by convention a toolchain extension, not a language extension. The reverse: argon-* crate names would imply the language depends on whatever the crate handles, which it doesn’t.

Bridges are bidirectional but asymmetric. oxc::ast and oxc::core_ir are stable public surfaces that bridge crates depend on. The bridge crates expose import/export functions consuming or producing those types. The language does not depend on the bridge crates; the bridges depend on the language.

Consequences

  • New foreign formats land as new ox-* crates. They do not touch oxc or ox.
  • Foreign-format diagnostic codes use the X* family (e.g., XW0841 for OntoUML import warnings) registered through oxc-protocol::core::codes::StaticCode + inventory::submit!. The compiler’s built-in OE / OW / OI codes do not host foreign-format variants.
  • An issue or RFD titled “Argon: support format X” is misnamed. Format support is a toolchain feature, not a language feature.
  • Existing layering violation: ox/src/cli/import.rs and ox/src/cli/export.rs currently link ox-ontouml::bridge directly. The pending extraction makes ox discover format bridges through an extension mechanism instead of compile-time linking.

RFD-0004 — Decidability ladder is Argon’s own, never framed in OWL terms

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Argon offers a graduated decidability ladder. Should its tiers be named or described using OWL profile terminology (EL++, OWL 2 EL/QL/RL/DL, SROIQ, ALC), or in Argon’s own vocabulary?

Decision

Argon’s decidability ladder uses Argon’s own tier names exclusively: tier:structuraltier:closuretier:expressivetier:recursivetier:foltier:modaltier:mlt. OWL profile names and adjacent description-logic vocabulary (fragment, profile) are forbidden when describing the ladder, the reasoner, or any expressivity-bounded sublanguage of Argon.

Recognized shapes inhabit the recognized tier — when an expression matches a canonical shape with a known fast-path implementation, its tier classification is the shape’s tier, not the syntactic tier of the source expression.

Rationale

The ladder is independent of OWL. Argon’s tiers are designed around the algorithmic complexity profile of evaluating expressions in each tier, not around any specific description-logic profile. Coincidental expressivity overlap with an OWL profile is an artifact of mathematics, not a design choice. Surfacing the equivalence in user-facing text imports OWL framing, OWL constraints, and OWL idioms that are not load-bearing for the language.

Tier-as-OWL-profile leaks foundational opinions. OWL profiles are designed around specific RDF-graph evaluation patterns. Argon’s reasoner (nous) is not an OWL reasoner; describing it as “the EL++ reasoner” misrepresents its architecture and binds it to a specification it does not implement. Describe the reasoner by what it does (classification, subsumption, role-closure, saturation), never by OWL vocabulary.

Recognized-shape tier classification. A transitive decorator on a relation lowers to a recognized-shape Datalog rule with a known polynomial-time algorithm. Even though the syntactic representation might appear at a higher tier, the recognized shape’s known evaluation cost is the operative classification. This keeps the ladder honest: tier reflects evaluation cost, not syntactic surface area.

Consequences

  • Any document, comment, or diagnostic that names a tier as EL++ is incorrect and gets fixed when noticed.
  • The reasoner is described as “the structural reasoner” or “nous” — never “the EL++ reasoner.”
  • tier:fol is the FOL escape hatch tier; unsafe logic { … } blocks live there. There is no tier:OWL_2_DL.
  • Recognizer table dispatch (transitive, symmetric, asymmetric, reflexive, irreflexive, functional, inverse-functional, qualified-cardinality, disjoint-classes, covering-classes) classifies recognized expressions into the recognized tier.

RFD-0005 — One grammar, multiple evaluation contexts

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Argon admits expressions in many situations: declaring concepts, computing derived values, querying state, mutating state, asserting tests, building diagrams, framing hypotheticals, exemplifying in doc-tests. Should each situation have its own sublanguage, or one shared expression grammar evaluated under different contexts?

Decision

One grammar. The same expression grammar serves the declare / compute / query / mutate / test / diagram / hypothetical / doc-test contexts. What differs across contexts is the evaluation semantics — what an expression means in declare-context (a structural assertion) versus query-context (a Prolog-style retrieval) versus mutate-context (a side-effecting transition). The grammar does not change.

The context system is extensible. Adding a new evaluation context (a future workflow primitive, a new meta-reflection mode) does not require a new grammar; it requires a new context-evaluation rule.

Rationale

Sublanguages multiply learning surface. A user who has learned how to write a derive rule has also learned, almost for free, how to write a query, a mutation, and a test assertion. Each context-specific extension would have re-introduced the cost of learning a new syntax for the same conceptual machinery (binding variables, navigating relations, applying predicates).

Context is what shifts, not syntax. A predicate married(X, Y) is the same syntactic shape whether it appears as a fact assertion, a rule body, a query head, a test expectation, or a hypothetical premise. The semantics of “what this means right now” is determined by the surrounding block. The reader’s mental model is “what context am I in” once, then everything inside is the same language.

Evaluation contexts are first-class and composable. Test contexts can nest queries. Hypothetical contexts can nest mutations. Doc-test contexts evaluate compute calls. The shared grammar is what makes nesting work: the inner context inherits the outer’s variable bindings without re-parsing.

Consequences

  • Adding a new evaluation context (e.g., simulation { … }, replay { … }, future kernel primitives) is a context-evaluation extension, not a grammar extension. No tree-sitter changes; no new keywords beyond the context’s introducer.
  • The compiler classifies each expression’s evaluation context during elaboration. The same Core IR atom may evaluate differently depending on the surrounding block.
  • Diagnostic codes like OE0211 (assertion-consequence-unlowerable) attach to the expression-in-context, not the expression itself. An expression that’s valid in query-context may be invalid in declare-context with a clear, context-specific diagnostic.

RFD-0006 — Three compute forms — expression, inline body, opaque FFI

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Domain-specific computation in Argon (capital-gain calculation, depreciation schedule, lease-term accrual, etc.) can be expressed at varying levels of complexity. How many implementation forms does compute admit, and what’s the tradeoff for each?

Decision

compute admits three implementation forms:

  1. Expression formcompute f(args) = expr. The body is a single expression. Suitable for one-shot calculations; lowers to a Core IR expression node.

  2. Inline body formcompute f(args) { input { … } out { … } ensure { … } body { … } }. The body is a constraint-sublanguage block expressed in Argon’s own grammar (RFD-0005). Lowers to CoreComputeBody::Inline. The inline interpreter (a tree walker over KernelExpr) dispatches inline bodies through DomainComputation via InlineComputeAdapter. Tier-classified. Dynamically loadable per-tenant without a Rust rebuild.

  3. Opaque Rust FFI formcompute f(args) { … impl rust("path::to::fn") }. The body is an opaque pointer to a registered Rust function. Lowers to CoreComputeBody::Rust. Requires a Rust rebuild; offers maximum flexibility and access to native libraries.

Form 2 dynamic loading uses an overlay mechanism: the kernel loads the inline-form body into a per-tenant registry at hydration time, and the inline adapter minter wires up dispatch. Per-tenant custom math ships without a code release.

Rationale

Three forms cover the cost-flexibility frontier. Expression form is easy to write, easy to read, easy to verify, but limited. Inline body form is more flexible (multi-statement, type-checked against Argon’s own expression grammar) and dynamically loadable, but tier-bounded. Rust FFI is unbounded but requires Rust expertise and a release cycle.

Form 2 is the load-bearing form for tenant customization. Tax law differs per jurisdiction. Tenants need to author math their own way without coordinating release cycles. The inline-body form lets that happen against Argon’s own grammar, with the same tier guarantees and the same provenance-tracking the language’s structural rules get.

Why not just two (expression + Rust). The middle ground matters: tenants are not Rust programmers. Inline body form gives them a mid-tier escape hatch — more than one expression, less than full Rust — that’s still tier-classified and provenance-tracked.

Why a tree walker, not a JIT. Inline-form bodies evaluate at kernel-runtime per-fact. JIT compilation would multiply complexity for marginal latency gains in a workload dominated by I/O and saturation passes. The tree walker keeps the evaluator small, debuggable, and consistent with the rest of the inline interpreter.

Consequences

  • Inline interpreter at crates/nous/src/runtime/interpreter.rs evaluates CoreComputeBody::Inline.
  • mint_inline_adapters_for_overlay runs at kernel-api hydration time to wire per-tenant adapters.
  • validate_compute_call_acyclicity rejects recursive compute-call cycles (OE0210). Arity mismatch surfaces as OE0208. Unresolved call targets surface as OE1406.
  • Compute-bodies compose: function calls (path(args)) over other computes, arithmetic over Int / Decimal / Money, multi-atom negation via aux-rule hoisting, complex-scrutinee match lowering.
  • Justification granularity is per-leaf-read with transparent combinators. Incremental recomputation has no interpreter-level cache; correctness defers to the kernel’s IVM substrate.

RFD-0007 — Queries and mutations are first-class items; every result carries why-provenance

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

How does Argon expose retrieval and state-transition operations? As external API surfaces wired up in Rust, or as language-native declarable items that ship with the same testing, type-checking, and provenance infrastructure as derive rules?

Decision

Queries and mutations are first-class items in the language. They declare with query <name>(args) :- body => head and mutation <name>(args) { require, do, retract, emit, return } shapes. Both are type-checked against the TBox at compile time. Both compose with patterns, computes, aggregates, and the rest of the expression grammar.

Every query and mutation result carries why-provenance. Derived facts include their derivation chain — which input facts contributed, which rules fired, what the substitutions were. The provenance is a value field (PosBool(M) DNF encoding stored as JSONB) carried alongside the fact. Aggregate expressions carry witness lists. Compute calls carry per-leaf-read justification with transparent combinators.

Query surface uses Prolog-style :- syntax for the body and => head for the projected output. Mutations use singular clause names: require (preconditions), do (assignments), retract (negative facts), emit (events), return (response payload).

Rationale

Provenance is not optional. This system processes hundreds of billions of dollars in financial contracts. Every derived fact must be defensible: what inputs led here, which rule version applied, what substitutions filled in the gaps. Bolting provenance on as a per-feature concern would have left blind spots; making it a structural property of every result means the question never comes up.

First-class items, not bolt-on APIs. A query authored in Argon participates in the type system. The compiler verifies the query’s body atoms resolve against declared concepts, that bound variables are consistent, that the projected head is well-typed. A query expressed as a Rust function with hand-written SQL has none of those guarantees and is the thing that produces the bug we just paid an audit team to find.

Singular mutation clauses. require not requires. do not does. The grammar prefers singular keywords (RFD-0015 framing). Reads as English when the mutation is simple, scales to compound clauses when needed.

PosBool(M) value, not weight semiring. PosBool stores the derivation expression as a value field on each derived fact. This composes with DBSP-style IVM (RFD-0022) where the weight semiring is reserved for Z-set arithmetic. Mixing the two semantics would have constrained provenance encoding to whatever shape DBSP-the-library exposes.

Consequences

  • query and mutation are reserved item kinds with their own AST nodes (Item::Query, Item::Mutation).
  • Result objects from queries carry why: PosBool(M). Result objects from mutations carry the same plus a transition-trace.
  • The compiler enforces that every derived fact has a recoverable provenance encoding; rules that produce facts without traceable derivation are rejected.
  • Aggregate expressions admit where clauses for filtering and emit witness lists alongside the aggregate value.
  • Compute call graphs cannot have cycles (OE0210). Arity must match (OE0208). Tier must be consistent.

RFD-0008 — Tests are first-class items; proof-status attributes for theorem claims

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Should Argon source files carry tests directly, or rely on an external test harness?

Decision

Tests are first-class language items declared as test <name> { … } blocks. Test bodies use Argon’s expression grammar (RFD-0005) with expect { … } and fixture { … } sub-blocks. The compiler elaborates tests under capture mode so fixture-internal diagnostics do not leak to the parent’s stream. The test runner reports five status categories: Pass, Fail, Unproven, Assumed, Ignored.

Proof-status attributes mark theorem claims:

  • #[unproven] — the test states a theorem the system cannot mechanically verify in the current tier. Reported as a distinct Unproven status; carries an unproven_verified bit when the system later succeeds.
  • #[assumed] — the test asserts a postulate; the body is injected as an axiom in the test’s scope. Reported as Assumed status.

Both attributes admit @[…] AttrList and @<ident> / #<ident> Decorator surface forms.

Rationale

Tests live with the code. Splitting language-level invariants from a separate test harness would have allowed the two surfaces to diverge. Putting tests as first-class items means they’re checked by the same compiler that compiles the rules they test, parsed by the same parser, type-checked against the same TBox.

Capture mode for fixtures. A fixture block is a test-local mini-module. Diagnostics inside the fixture are scoped to the fixture; they do not propagate to the parent test’s stream. This lets a test verify “this code raises diagnostic OE0211” without polluting unrelated tests’ output.

Five status categories, not three. Pass / Fail covers mechanical execution. Unproven / Assumed exposes the theorem-marking discipline: there are claims we believe but cannot verify, and there are postulates we assert. Conflating any of them into Pass or Fail erases a real distinction. Ignored is the explicit-skip path.

Why proof-status attributes replace #[expect_sorry]. The original #[expect_sorry] attribute conflated two concepts: “we know this can’t be proven” (unproven) and “we’re treating this as an axiom for the test” (assumed). Splitting them makes the runner’s report unambiguous and the test author’s intent clearer.

Consequences

  • test is a reserved item kind with its own AST node (Item::Test).
  • expect { diagnostic <code> [severity:<sev>] [at <marker>] } declarative matching with //~ <label> source markers and span-overlap semantics.
  • fixture { … } blocks elaborated under capture mode.
  • [pub] frame Name { … } declaration form composes test contexts with using F[, F2…]. Conflict surfaces as OE0214OE0216, OE0218.
  • Runner CLI: ox test --package <prefix> qname-prefix filter.
  • Test attributes lower to CoreTest.proof_status. The runner’s report distinguishes the five categories.

RFD-0009 — Generics are functor modules and generic computations, not parametric concepts

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Argon needs a way to abstract over types — re-use the same modeling pattern for Lease and Loan and RentalAgreement without copy-pasting. Should the language admit parametric concepts (concept Container<T>), or restrict generics to module-level functor abstractions and per-compute generics?

Decision

Generics are admitted at two levels:

  1. Functor modules — a module parameterized by other modules. Instantiation produces a fresh module with the parameter bound. Used for “this whole package shape, but with Lease substituted for Loan.”

  2. Generic computations — a compute parameterized by type variables. Used for “this calculation works on any concept that has these properties.”

Generics are not admitted at the concept level. There is no concept Container<T> in Argon. A concept that needs polymorphic behavior models that behavior through relations or through a functor module instance.

Rationale

Parametric concepts collide with the foundational-ontology model. A concept Container<T> introduces a class of types with no canonical instantiation in the foundational ontology. Is Container<Person> a kind? A subkind? Does it provide identity? The answer depends on how UFO (or whichever foundational ontology) handles parametric type families, and the answer differs across foundational ontologies. Building parametric concepts into the language would have leaked foundational-ontology assumptions into the compiler (RFD-0002).

Functor modules are the right granularity. The unit of reuse in real ontology engineering is rarely “one polymorphic concept.” It’s “one whole modeling pattern — these concepts, these relations, these constraints — instantiated for a specific domain.” Functor modules express that directly. The result is more than one concept, but the parameterization is at the module boundary, not the concept boundary, so the foundational-ontology layer stays clean.

Generic computations are uncontroversial. A compute function over “anything with a value: Money property” is a Hindley-Milner-style type variable that ranges over concepts satisfying a structural constraint. No foundational-ontology coupling.

Consequences

  • Concept declarations do not accept type parameters in their syntactic surface.
  • Module declarations admit a parameter list: mod functor(M: SomeSignature) { … } (concrete syntax to be specified during implementation).
  • Compute declarations admit type variables in their parameter and return types: compute f<T: HasValue>(x: T) -> Money.
  • Patterns (RFD-0019) provide the lighter-weight reuse mechanism for shapes that don’t need full module-level abstraction.

RFD-0010 — Module system and standpoints

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

How are Argon source files organized into modules, and how do standpoints (the language’s mechanism for perspectival modeling) integrate with the module system?

Decision

Module organization follows Rust conventions:

  • Every .ar file is a module. The module’s name is the file stem; the module path is the relative directory path.
  • The package’s entry file is prelude.ar at the package root. The entry file resolves as pkg::prelude and acts as the package-module primary file.
  • Imports use Rust-style use vocab::path syntax. There is no implicit prelude (RFD-0014); every name visible in a module has a corresponding use line or fully-qualified path.
  • Default visibility is module-internal (file-scoped). The pub keyword promotes a declaration to package-public visibility. Two-tier visibility (RFD-0011) — no fine-grained access modifiers.
  • Module configuration splits across two scopes: package-level structure (standpoints { }, defeats { }, etc. in ox.toml) and module-level locals.

Standpoints are first-class. A standpoint is a perspective on the knowledge base — a specific viewpoint under which facts may hold or not. Standpoints declare in a project-level partial-order lattice; bridge rules and standpoint compositions follow the lattice. Standpoint scoping integrates with per-tenant overlays (RFD-0020): each tenant carries a primary standpoint sourced from their tenant config.

use foo::bar::Sym; parses as single-symbol Named import (Rust-style), not as a glob or module import.

Rationale

Filesystem hierarchy is the module hierarchy. Forcing the language’s namespace structure to match the filesystem layout removes a degree of freedom that produces drift. A reader looking at ufo/properties/identity.ar knows the qualified path is ufo::properties::identity without having to read the file.

No implicit prelude. Auto-imported names are convenience that compounds confusion: which module is Nat from? Adding the explicit use line takes one second when writing and saves an hour when debugging. Bare primitive names produce diagnostic OE0101 with a contextual hint pointing at the appropriate std::math::* import.

Standpoints as a lattice, not a flag. Real perspectival modeling has multiple cross-cutting viewpoints (legal vs. accounting, insurer vs. policyholder, federal vs. state). A standpoint flag would have collapsed all these into one axis. The lattice admits multiple orthogonal viewpoints with explicit composition.

Module-internal default reduces accidental coupling. Most declarations should not be public. Defaulting to module-internal means that promoting something to pub is a deliberate act, and the declaration’s visibility is visible in its declaration line.

Consequences

  • File names map directly to module names; rename a file → rename the module.
  • prelude.ar is the convention; library entry + package-module primary file in one.
  • Re-exports use pub use. Re-export resolution is a fixpoint over local ∪ re_exports in the elaborator’s import-resolution pass; chain depth is bounded by module_count + 1 (cycles surface as OE0903).
  • Package-name canonicalization: manifests admit hyphens or underscores; canonical form is underscored; transform at parse.
  • Standpoints integrate with the kernel’s bitemporal event log (RFD-0021) as discriminator axes (standpoint_id, module_id, fork_id).

RFD-0011 — Two-tier visibility and direct-dependency rule

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

How fine-grained should Argon’s visibility system be, and what’s the rule for using items from packages that aren’t direct dependencies?

Decision

Two tiers of visibility. A declaration is either pub (package-public) or module-internal (the default). No pub(crate), no pub(super), no fine-grained access modifiers (yet). I (@ivan_sharpe) do have plans to introduce pub(pkg) and pub(mod) and pub(super), but these are not a priority at the moment until a need arises.

Direct-dependency rule. A package may use an item only if that item’s owning package is a direct dependency in the consuming package’s manifest. Items reachable through transitive dependencies are not usable without an explicit declaration.

Rationale

Two tiers cover the cases worth distinguishing. The case for fine-grained visibility (“only this neighboring module can see this”) almost never holds in practice; the situations that motivate it are usually solved better by reorganizing the module structure. Two tiers (visible at package boundary, visible only inside this module) match the actual modeling decisions ontology authors make.

Direct-dependency rule prevents accidental coupling. A package that uses items from a transitive dependency creates an implicit dependency that breaks when the intermediate package’s authors restructure their re-exports. Forcing every used package to appear in the consuming manifest makes the dependency graph explicit and the consequences of upstream changes predictable.

Re-exports are how transitive items become usable. A package that wants to expose items from its dependencies for downstream consumption uses pub use. The downstream consumer then declares the re-exporting package as a direct dependency, not the original one. This is how UFO-prelude-style patterns work: pub use ufo::prelude::* in a package’s prelude.ar creates a controlled re-export surface.

Consequences

  • Manifest’s [dependencies] table is the authoritative list of packages this package may import from.
  • The compiler rejects imports from packages not listed as direct dependencies, even when the symbol is reachable through transitive re-exports without a re-export declaration.
  • pub use foo::bar::* is the canonical re-export pattern.
  • No language-level access modifier between pub and module-internal. If a finer split is needed, the right answer is usually a module reorganization.

RFD-0012 — Package manifest is unified; editions are parse-time only

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Argon packages come in several flavors: leaf libraries, runnable projects, multi-package workspaces. Should each flavor get its own manifest schema, or one unified schema with optional sections? And how does the language handle source-syntax evolution across releases?

Decision

Unified manifest schema at ox.toml. Library, project, and workspace flavors use the same top-level schema with different sections populated. A library has [package] only; a project adds [bin] or runtime sections; a workspace has [workspace] listing members. The schema is one document; the flavor is determined by which sections are present.

Editions are integer-numbered, opt-in per package, parse-time only. A package’s edition = 2026 field selects a parser dialect. Semantics are stable across editions — only syntactic surface evolves. The compiler’s IR shape, type system, and runtime semantics are edition-independent.

Rationale

One manifest schema removes a class of “which schema is this” questions. Tooling that processes manifests (resolver, registry uploader, diff tools) walks one structure. Authors learn one schema. The flavor distinction is a presence-check, not a parser fork.

Editions confined to parse-time. The motivation for editions is “we want to change the syntax without breaking existing code.” That’s a parser concern. Letting editions affect IR or runtime semantics introduces a combinatorial space (which IR for which edition?) that compounds with every release. Confining the difference to parse-time means the rest of the compiler operates on a single canonical IR regardless of source edition.

Integer numbering, not date-based. Editions don’t ship at predictable cadences. edition = 2026 at year boundaries would imply something the cadence doesn’t match. Sequential integers (edition = 1, edition = 2) decouple the edition number from the calendar.

Consequences

  • Manifest parser admits all top-level sections; the flavor check happens after parsing.
  • The compiler’s parser dispatches on the package’s edition field; downstream phases see one canonical AST.
  • Edition migration is a syntactic transformation, never a semantic one. A migration tool can rewrite source from edition N to edition N+1 without changing meaning.
  • Workspace members’ editions are independent; one workspace can host packages on different editions.

RFD-0013 — Bivalent lockfile (content-hash + constructs-hash); workspace-local resolution

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

How does Argon pin dependency versions, what does the lockfile capture, and how do workspaces resolve member packages?

Decision

Bivalent lockfile. Each lockfile entry carries two hashes:

  • Content hash — byte-level identity of the source. Catches “did the file content change.”
  • Constructs hash — Merkle root of the package’s exported declaration signatures. Catches “did the meaning change.”

Two packages with identical content hashes always have identical constructs hashes. Two packages with the same constructs hash but different content hashes mean cosmetic edits (whitespace, comments) without semantic change. Two packages with different constructs hashes always have different content.

Per-workspace lockfile, not per-package. A workspace’s ox.lock pins every member’s transitive dependencies. Workspaces with overlapping members share resolution.

Workspace members ALWAYS resolve locally. When a workspace contains both foo (member) and a transitive resolution that would have pulled foo from the registry, the local member wins. There is no version-pinning override that pulls a registry version of a workspace member.

Merkle root canonical ordering: alphabetical by qualified name. Constructs are sorted by their canonical qualified name before hashing. Reordering source-file declarations does not change the constructs hash.

Rationale

Two hashes for two distinct invariants. Content hash answers “did the bytes change.” That matters for cache invalidation and reproducible builds. Constructs hash answers “did the meaning change.” That matters for “is this package compatible with what depended on it.” A single hash conflates both into either over- or under-invalidation.

Workspace-local always wins. A developer working in a multi-package workspace expects edits to a member package to be visible across the workspace immediately. Resolving a member through the registry instead would silently snapshot the registry version, leading to “why aren’t my edits showing up” debugging cycles.

Alphabetical Merkle ordering. Any deterministic ordering would suffice; alphabetical is the convention with the lowest cognitive overhead. Authors don’t need to reason about declaration order to predict the hash.

Per-workspace, not per-package. Workspace members share dependencies. Per-package lockfiles would pin them independently and produce conflicts. One workspace lockfile means one resolution per workspace, consistent across members.

Consequences

  • ox.lock schema includes both hashes per entry.
  • Cosmetic edits to a package (formatting, comments) bump content hash without bumping constructs hash. Downstream consumers do not need re-resolution.
  • Semantic edits bump both hashes. Downstream consumers re-resolve.
  • Workspace dependency resolution prefers workspace members over registry resolutions for any member package. There is no “pin to registry version” escape hatch for workspace members.
  • Build artifacts go to target/ per-workspace, shared across members.

RFD-0014 — std ships with the toolchain; explicit imports — no auto-prelude

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Argon has a standard library covering math primitives, collections, meta-reflection, Kripke patterns, and test infrastructure. How does it ship — through the registry as packages, or bundled with the toolchain? And does the language auto-import any of it implicitly?

Decision

std ships with the toolchain. The standard library is part of the toolchain tarball alongside oxc, ox, ox-lsp. Substrate types (primitives — Nat, Int, Real, Bool, String, Decimal, Money, Date, DateTime, Duration) are Rust-side; library content (std::collection, std::meta, std::kripke, std::test) lives as .ar source under share/std/ resolvable by the toolchain.

Cargo-style layout for std internals. std mirrors the conventions of Cargo’s std layout: substrate types declared once in the compiler core; library content path-resolvable through the toolchain’s std bundle.

No implicit prelude. Modelers must use std::math::Nat; per module to reference primitives. Bare primitive names (Nat, Int) without an import produce OE0101 with a contextual hint that points at the appropriate std::* import.

[package] no_std = true opts out of std entirely. A no_std package gets no implicit primitives; everything resolves against the package’s own declarations and explicit imports.

use foo::bar::Sym; parses as a single-symbol Named import — Rust-style. The trailing Sym is the symbol being imported, not a module path being walked into.

Rationale

Toolchain-bundled std beats registry-distributed std. Registry-distributed std would have made std versioning and toolchain versioning independent, producing combinations that aren’t tested. Bundling them means every toolchain release ships with one specific std, and the combination is the unit of release.

Explicit imports beat auto-prelude. Auto-prelude saves five characters per module and costs an unbounded amount of “wait, where did this name come from” debugging when names collide or get shadowed. Explicit imports make every reference traceable to its source via grep.

Diagnostic hint over silent error. A bare Nat reference firing OE0101 with “did you mean use std::math::Nat;?” surfaces the fix immediately. Without the hint, modelers would have had to discover the import requirement by reading documentation.

Cargo-style layout. Argon’s package model already echoes Cargo (RFD-0012, RFD-0013). Aligning std layout with that convention means modelers who know Cargo know roughly how std works without re-learning.

Consequences

  • argon/oxc/src/std_meta.rs, std_math.rs, etc. carry the substrate-type registrations.
  • Toolchain tarball includes a share/std/ directory with the library-side .ar source.
  • ox-toolchain.toml resolution points the toolchain’s std bundle at the active toolchain.
  • Diagnostic OE0101 (bare-keyword fallthrough) generates four hint variants: known primitive, known collection, no_std-package context, none.
  • Existing .ar files migrated to explicit imports: argon-packages/ and argon/examples/lease-story/ carry use std::math::Nat; etc. on every module that references primitives.

RFD-0015 — Surface naming prefers PL idiom over proof-assistant idiom

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Argon’s expressivity ceiling is Lean-class (tier:fol, tier:modal, tier:mlt admit propositions-as-types-equivalent reasoning). Should the surface vocabulary borrow from proof-assistant traditions (Lean / Coq / Agda) or from programming-language traditions (Rust / TypeScript / Haskell)?

Decision

When a construct, attribute, or keyword in Argon has both a programming-language analogue and a proof-assistant analogue, the surface uses the PL term. Target mental model is Rust / TypeScript / Haskell / Scala — not Lean / Coq / Agda — even when the underlying semantics derive from proof-assistant foundations.

Examples:

  • derive (PL: data-flow / Datalog) over lemma or theorem (proof-assistant).
  • query and mutation (PL: GraphQL / database) over goal and tactic.
  • pub and module-internal visibility (PL: Rust) over private / protected (Java) or Local / Global (Coq).
  • unsafe { … } for the FOL escape (PL: Rust) over Sorry or Admitted (proof-assistant; though #[unproven] and #[assumed] capture related but distinct concepts).
  • Singular keywords (input, not inputs; require, not requires).

Rationale

Daily-driving feel matters more than expressivity ceiling. Most Argon code is structural / closure / recursive tier — the ergonomic ground floor. Authors at that tier are working on tax law, lease accrual, GAAP recognition. Naming the language for proof-assistant audiences would have made the daily 90% feel foreign.

Lean expressivity, Rust feel. The expressivity ceiling is Lean-class because the semantic substrate supports it, not because we want every modeler to think in proof-theoretic terms. The surface should fade into the background for the structural tier and only surface its expressiveness when an author deliberately reaches for it.

Singular keywords scan as English. “require this, do that, retract those, return result” reads like instructions. “requires X, does Y” forces the reader to mentally drop the s to parse the structure.

No Lean references in examples. Even when an example draws inspiration from a Lean mechanization, the example doesn’t reference Lean by name in tracked artifacts. The reader’s mental model stays in PL territory.

Consequences

  • New language surface (keywords, attributes, decorator names) goes through this filter. PL term wins ties.
  • Existing surface that drifted toward proof-assistant idiom (some early decorator names) gets renamed when noticed.
  • Documentation and the book describe constructs in PL terms, even when they have proof-theoretic interpretations.
  • Reasoner output describes derivations in PL-friendly language (“derived”, “applied”), not proof-theoretic language (“inferred”, “concluded”).

RFD-0016 — Refinement under OWA is three-valued (Kleene-Belnap)

Superseded Opened 2026-05-03 · Committed 2026-05-03 · Superseded by RFD-0037 on 2026-05-08

Superseded. RFD-0037 supersedes this RFD by formalizing it as the K3 special case of an AFT-grounded bilattice framework. The decision of this RFD — that refinement membership under OWA is three-valued, success requires true — is preserved verbatim and proven mechanically in ArgonFormal/Standpoint/BackwardCompat.lean. The new RFD adds: rigorous K3 / FDE / Boolean lattice contexts, Pietz-Rivieccio Exactly True designation, fail-closed FDE→K3 projection with explicit policies, per-standpoint consistency policy, cross-standpoint federation via AFT info-join, and the K3-vs-Ł3 conditional choice. Read RFD-0037 for the current commitment; this RFD is preserved for historical record.

Question

When a module operates under the Open World Assumption (OWA), refinement-type membership checks have a third possible answer: not true, not false, but unknown (the system has insufficient information). How does the language treat unknown results in refinement contexts?

Decision

Refinement membership under OWA is three-valued (Kleene-Belnap). The three values are true, false, and unknown. Membership succeeds only on true. unknown does not satisfy the refinement; the call site sees a refinement-violation diagnostic, not a refinement-pass.

Under CWA (Closed World Assumption), unknown collapses to false. A CWA-mode module’s refinement check returns true or false only.

A module’s world assumption is part of its declaration; refinement contexts inherit the surrounding module’s mode.

Rationale

Three values match the underlying semantics. OWA is the assumption that absence of evidence is not evidence of absence. A refinement check that doesn’t know the answer can’t conclude false; it has to admit unknown. Forcing two-valued logic under OWA would have either silently treated unknown as true (unsafe; admits unverified facts) or silently as false (over-restrictive; rejects valid facts that just weren’t proven yet).

Success requires true, not “anything but false.” The unknown value represents “we don’t have enough information.” A refinement-typed value carrying unknown membership is one whose claim hasn’t been verified. The refinement should not pretend it has been verified; it should fail closed and let the author decide whether to add evidence or relax the refinement.

CWA collapse is automatic. When a module declares CWA, the standard interpretation is “facts not asserted are false.” unknown is meaningless in that mode; collapsing to false matches the modal commitment. This means CWA modules can use refinement membership as classical two-valued logic without writing out the collapse.

Consequences

  • Refinement membership returns a Kleene-Belnap value. Type checking treats unknown as a violation.
  • Diagnostic codes distinguish “definitely violates” (OE0606 MetaxisRefinementViolation) from “cannot verify” (an OW0 warning when a CWA-style audit would have admitted, but OWA cannot).
  • Patterns that build on refinement (membership-conditioned narrowing, cardinality assertions) inherit the three-valuedness.
  • Module declarations carry world-assumption metadata (world: open / world: closed); refinement evaluation reads that metadata.

RFD-0017 — Universal top type is / Top; no explicit Thing wrapper

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Argon needs a universal top type — the type that every other concept transitively specializes. Should the top type be named for a foundational ontology’s convention (Thing from OWL, Entity from UFO), or use a foundational-ontology-neutral notation?

Decision

The universal top type is (the typeset character) with Top as an ASCII alternate. Every concept transitively specializes . There is no explicit Thing wrapper concept in the language.

Foundational ontologies that have their own top concept (UFO-A’s Thing, BFO’s Entity, DOLCE’s Particular) declare those at the package level as ordinary concepts that specialize . A user-facing UFO-A taxonomy roots at UFO-A’s Thing; that Thing itself specializes .

Rationale

is foundational-ontology-neutral. Argon does not pick a foundational ontology (RFD-0002). Naming the top type for any specific foundational ontology’s convention would have leaked that ontology’s framing into the language’s universal vocabulary. is a notation from set theory and lattice theory; it predates and outlasts any specific ontological commitment.

No wrapper layer. Building a Thing concept into the compiler that everything transitively specializes through would have required the compiler to know about Thing. That’s the kind of foundational-ontology hardcoding RFD-0002 forbids. Letting be the universal top means the compiler is the only thing that knows , and every foundational ontology’s top concept is just an ordinary concept that specializes it.

Typeset plus ASCII Top. Code editors handle both; is the canonical form in published material; Top is the typeable alternate.

Consequences

  • Concept declarations that don’t explicitly specialize anything implicitly specialize .
  • The specializes relation <: (with accepted as an alternate) closes transitively at .
  • UFO-A’s Thing, BFO’s Entity, etc. are declared in their respective packages as concept Thing <: ⊤ and so on.
  • The book and rustdoc use in formal notation; user-facing prose and editor surfaces accept Top.

RFD-0018 — where clauses, unsafe blocks, modal operators — one mechanism per concern

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Argon needs to express structural constraints on concepts, escape hatches for full FOL, and modal axioms (necessity / possibility). Should each get its own dedicated syntax, or are there unifying mechanisms?

Decision

Structural constraints via where clauses. A concept’s body admits where { … } blocks containing the constraint expressions. There is no @requires decorator alongside where; one mechanism, structural goes in the concept body.

Group axiom declarations (disjoint, complete, partition) admit dedicated declaration forms when the constraint involves multiple concepts symmetrically. These compose with where for per-concept refinement.

FOL escape via unsafe { … } blocks. When a constraint can’t be expressed within the structural / closure / recursive tiers, an unsafe logic { … } block lifts to tier:fol (RFD-0004). Inside an unsafe block, the full FOL expression grammar is admitted; the block’s body inherits the same expression grammar (RFD-0005) — no new syntax. The block annotation declares the expressivity cost.

Modal axioms via box() / diamond(). Necessity (box(P) — “in every accessible world, P”) and possibility (diamond(P) — “in some accessible world, P”) are first-class operators in the language. Modal axioms compose with the rest of the expression grammar; no separate modal sublanguage.

Rationale

One mechanism per concern reduces surface area. Multiple decorators that did almost-the-same-thing produced “which one do I use here” questions. Picking where for structural constraints and reserving decorators for orthogonal concerns (theorem marking, transitivity) makes the choice mechanical.

unsafe is the right metaphor. Like Rust’s unsafe, the block does not change syntax — it changes the obligations of the author. An unsafe logic block admits expressions whose evaluation cost may be unbounded or undecidable; the author is acknowledging that and accepting the responsibility.

Modal operators as first-class. Modal logic is too useful to bury behind a sublanguage barrier. box() and diamond() integrate with rule bodies, query bodies, mutation preconditions — anywhere a propositional expression appears. The compiler enforces modal axiom rules at tier:modal.

Group axioms admit dedicated forms when the constraint is symmetric. disjoint(A, B, C) is clearer than three pairwise where clauses; the dedicated form is a sugar that lowers to the equivalent where constraints. Structural sugar is a feature, not a leak.

Consequences

  • Concept declarations admit where { … } blocks; no @requires decorator.
  • disjoint, complete, partition are reserved declaration-form keywords for group axioms.
  • unsafe logic { … } is the FOL escape; the block’s tier is tier:fol.
  • box() and diamond() are reserved operators; modal axioms compose with the rest of the grammar.
  • The decidability classifier auto-classifies expressions into the appropriate tier; expressions outside unsafe blocks that exceed the surrounding tier produce OE0604 TierViolation.

RFD-0019 — Patterns are first-class parameterized templates

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Ontology authors repeatedly model similar shapes — correlative pairs (rights / duties), part-whole relationships, role-mediating relations. Should the language admit lightweight reuse for these shapes that’s lighter than functor modules (RFD-0009)?

Decision

Patterns are first-class language items: parameterized templates that emit declarations when instantiated. Pattern declaration shape:

pattern <name>(<TypeArgs>) {
    <declarations that reference TypeArgs>
}

Instantiation:

use pattern <name><TypeArgs> as <instance_name>

Patterns can also surface via bespoke per-pattern syntax for common shapes (e.g., correlative_pair(...), part_whole(...)).

Patterns are not generic concepts (RFD-0009 still applies). A pattern instantiation produces a fresh set of concept / relation / property declarations bound to the type arguments — flat declarations, no parametric type machinery in the resulting concepts.

Rationale

Patterns hit the middle ground. Functor modules abstract over whole module shapes; one-off concepts handle one-off cases. Patterns capture the recurring 3-to-10-declaration shape that doesn’t justify a full module split but does deserve a name and reuse mechanism.

Bespoke syntax for popular shapes. Some patterns are common enough that a reader benefits from immediate recognition. correlative_pair(Right, Duty) reads as “the Right/Duty correlative pair” instantly; use pattern correlative<Right, Duty> as ... reads less directly. The bespoke surface is sugar that lowers to the same pattern instantiation.

Patterns emit, they don’t parameterize. A pattern instance produces concrete declarations that the compiler treats as if the author had hand-written them. There’s no runtime cost to pattern instantiation, no parametric concept tracking, no constraint-propagation across instances.

Consequences

  • pattern is a reserved item kind with its own AST node.
  • Pattern bodies admit any declaration form (concepts, relations, properties, rules, computes, etc.).
  • Pattern instances expand at elaboration time; downstream phases see the expanded declarations.
  • Bespoke per-pattern surface forms (correlative_pair, part_whole) are language-recognized sugar; the compiler ships with a small set, and packages may declare their own.

RFD-0020 — Per-tenant kernel runtime — shared base + per-tenant overlay

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

The kernel serves many tenants concurrently. Should each tenant get an isolated kernel instance with its own copy of every loaded ontology, should everything share one global state with tenant-as-filter, or is there a hybrid?

Decision

Per-tenant kernel runtime: shared immutable base + per-tenant overlay.

  • A shared immutable base (UFO + Core + std) lives once under a reserved SHARED_BASE_TENANT_ID (UUID nil). Every tenant reads from this base; nobody writes to it.
  • Each tenant has a per-tenant overlay that adds tenant-specific concepts, roles, properties, rules, and compute registrations. The overlay is mutable per-tenant; cross-tenant writes are impossible by construction.
  • LayeredSchemaView resolves overlay-first with base fallback in a single SQL predicate: WHERE tenant_id IN (SHARED_BASE_TENANT_ID, $t).
  • Per-tenant Kernel instances exist as Arc<RwLock<BTreeMap<TenantId, Arc<RwLock<Kernel>>>>> in KernelState. Lazy load from Postgres + soft-cap eviction.
  • Tenant context propagates through the API layer via X-Tenant-Id header; TenantContextV2 extractor binds tenant scope per request.
  • Cross-tenant isolation is enforced at multiple layers: type-level (&mut self on Kernel carries tenant_id), runtime (every SQL scoped WHERE tenant_id IN (...)), integration-test verified.
  • Per-tenant domain registration via static Rust linking + per-tenant overlay registry. Form 2 / Form 3 computes (RFD-0006) live in the per-tenant overlay; name collisions impossible by construction.
  • Standpoints are tenant-local; primary standpoint sourced from tenant config.

Rationale

Shared base means UFO loads once. A naive isolated-instance design would have re-loaded every foundational ontology for every tenant — gigabytes of duplicated state, unbounded Postgres pressure. The shared base is the same content, read from one canonical location, with the SQL machinery making the read transparent.

Per-tenant overlay means tenant data stays tenant-local. Tenant A’s tax law amendments must not be visible to tenant B. The overlay scoping enforces this by default; cross-tenant reads require explicit join logic that has nowhere to live in normal code paths.

One-SQL read via reserved tenant ID. The shared base is queryable through the same SQL the per-tenant query uses, just with a wider IN predicate. No conditional logic in the read path; no “if shared else tenant” branching that would have produced the kind of bug we just paid an audit to find.

Type-level tenant carrying. Kernel instances carry their tenant ID in the type. A function that wants to operate on Kernel data must take &mut Kernel, which means it has a specific tenant in scope. There’s no API that lets you accidentally operate cross-tenant.

Consequences

  • KernelState (kernel/api/src/state.rs) maintains the per-tenant Kernel map with lazy load + soft-cap eviction.
  • All v2 API endpoints route through TenantContextV2. Write paths call require_principal(); missing principal surfaces as OE6005 PrincipalRequired.
  • Domain registration via static Rust linking. Per-tenant overlay registers in RustComputeRegistry.
  • Kernel Kripke commitments (RFD-0018’s modal operators) are preserved under per-tenant overlay; tenant-A and tenant-B see consistent modal evaluation against their respective overlays.
  • Cross-tenant queries are not a feature. If a future use case requires cross-tenant data, it would need an explicit RFD authorizing the boundary crossing.

RFD-0021 — Unified axiom event log; bitemporal; CBOR axiom-ADT bodies

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

How does the kernel persist the assertions and retractions that drive its derivations? As state snapshots? As event logs? With what temporal semantics?

Decision

Unified axiom event log. ont.axiom_events is the single source of truth for assertion / retraction / declaration events. All TBox and ABox changes — concept declarations, role declarations, fact assertions, rule registrations, retractions — append to this one table.

Axiom-ADT body encoding. Each event carries a typed payload encoded as CBOR with a variant tag (AxiomBody::ConceptDecl, AxiomBody::RoleDecl, AxiomBody::Assertion, etc.). Reading an event reconstructs the typed Rust value via serde deserialization. SHA-256 of the CBOR bytes acts as the body’s canonical hash for deduplication and integrity checks.

Bitemporal semantics: valid time × transaction time. Each event records both axes:

  • Valid time — when the asserted fact was true in the modeled domain.
  • Transaction time — when the assertion was recorded in the system.

Retraction is implemented as a new event whose op field is Retract and whose body matches the original event’s body. Skipping retracted events during reassembly removes the original assertion from the active set.

Standpoint, module, and fork as discriminator axes — not partitions. An event carries (tenant_id, standpoint_id, module_id, fork_id) as discriminators. Queries that scope to a standpoint or module filter on these axes; events not in scope are simply not selected.

Fork axis via fork_id + structural sharing. A fork is a copy-on-write branch of the event log. Forks share the lineage above the branch point and diverge below. Forks support hypothetical reasoning, what-if analysis, and rollback.

Rationale

One log beats many. Separate event logs for TBox vs. ABox vs. retractions vs. forks would have produced cross-log consistency questions (a TBox change retract in the ABox log? do they agree on temporal ordering?). One log with typed bodies and discriminators answers all the questions in one place.

CBOR with axiom-ADT. CBOR is a compact binary format with deterministic encoding (important for SHA-256 hashing and reproducible builds). The axiom ADT preserves type information through the encoding/decoding round-trip; readers get strongly-typed values, not generic JSON.

Bitemporal because audit requires it. Financial-contract reasoning needs to answer “what did the system know when this calculation was performed” (transaction time) AND “what was true in the world when the contract clause applied” (valid time). The two axes are independent and both required.

Discriminators not partitions. A standpoint isn’t a hard wall around events — it’s a perspective filter. Events outside the scope are still in the log, just not selected by the current query. This preserves the option to compose multiple standpoints (RFD-0010 standpoint lattice).

Consequences

  • ont.axiom_events is the storage primitive. All other projections (ont.axioms_current, materialized views) derive from this table.
  • Schema reassembly reads events filtered by kind; retraction events skip during reassembly to remove original assertions.
  • The kernel’s CBOR codec evolves with the AxiomBody enum; codec versioning tied to kernel-storage release cadence.
  • Forks compose with per-tenant overlays (RFD-0020); fork_id is orthogonal to tenant_id.
  • Bitemporal queries support “as of valid time T, transaction time T’” reads — required for audit trails on regulatory examinations.

RFD-0022 — CQRS projections + DRedc IVM behind a ProjectionMaintainer trait

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

The kernel computes derived facts from asserted facts via stratified Datalog rules + meta-property calculus. When asserted facts change, what algorithm maintains the derived facts incrementally, and how is that algorithm bound to the system so it can evolve?

Decision

CQRS architecture. Reads and writes are separated. Writes append to the event log (RFD-0021). Reads go through projections — materialized views maintained incrementally by an Incremental View Maintenance (IVM) algorithm.

DRedc IVM is the current algorithm. DRedc (Delete-Rederive with counting) handles the maintenance for stratified Datalog rules. It supersedes the simpler DRed by adding multiplicities; for symmetric / transitive clique predicates, DRedc detection at compile time enables a fallback path that avoids the pathological case.

Algorithm seam: ProjectionMaintainer trait. The IVM algorithm is bound behind a trait. The current implementation is DRedc; future implementations (FBF-2019 for the clique-pathology case, DBSP for streaming workloads) plug in at the same seam without touching projection consumers.

PosBool(M) provenance is a value field, not the IVM weight semiring. The provenance encoding (RFD-0007) lives on each derived fact as a JSONB column. DBSP-style weight semirings (when DBSP becomes the maintainer) operate on Z-set arithmetic separately; the two semantics don’t interfere.

v0.3 migration criteria for DRedc → DBSP are codified separately. Trigger conditions: clique-predicate workload pressure exceeds threshold; streaming derivation patterns become common; compile-time DRedc-pathology detection flags too many programs.

Rationale

CQRS is the only honest model. The kernel’s derived facts are a function of the asserted facts and the rules. Maintaining them in-place would have meant every assertion ran the full saturation pipeline. CQRS lets the saturation cost amortize across reads.

DRedc over DRed. Delete-Rederive is the standard incremental algorithm but degrades on symmetric / transitive clique predicates (every fact’s removal triggers re-derivation of every other fact in the clique). DRedc’s counting addresses this; FBF-2019 handles the residual cases. Compile-time pathology detection lets the system pick the right algorithm per program.

Trait seam future-proofs the algorithm choice. IVM is an active research area. The trait boundary means switching to DBSP, Differential Dataflow, or some future algorithm doesn’t require touching every consumer of derived facts.

PosBool as value field. Provenance is per-fact data, not part of the maintenance arithmetic. Mixing them would have constrained provenance encoding to whatever shape the IVM algorithm’s weight semiring exposes; keeping them separate preserves the freedom to change either independently.

TED (LogicBlox trace-edit-distance) ruled out. Considered as an alternative; the trace-management overhead doesn’t pay for itself at the workload sizes we anticipate.

Consequences

  • ProjectionMaintainer trait at crates/nous/src/reasoning/... (current implementation: DRedc).
  • Compile-time detection of symmetric + transitive clique predicates triggers FBF-2019 fallback.
  • Path-closure aggregates and multi-standpoint bridge rules use demand-driven caching projections (scope-setting; not yet fully landed).
  • DBSP feasibility tracked in v0.3 conditional roadmap; not committed.
  • Provenance remains a JSONB column orthogonal to the maintainer’s internal data structures.

RFD-0023 — Kernel API v2 — resource-oriented redesign

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

The original kernel HTTP API at /api/kernel/* accumulated as a flat command-style surface. As the system grew, the surface became hard to navigate and harder to evolve. What’s the v2 design?

Decision

Path-versioned at /api/v2/*. v1 stays operational during the migration window; v2 never loads v1 primitives directly.

Four-surface resource model:

  • /api/v2/schema/* — TBox: concept / role / property declarations, axiom decls, package bundle ingest.
  • /api/v2/canonical/* — ABox primitives: assertions, retractions, individuals, the foundational fact layer.
  • /api/v2/derived/* — derived facts produced by saturation; read-only; demand-driven.
  • /api/v2/scenarios/* — scenario / fork operations: copy-on-write branches, what-if reasoning, rollback.

ADT-honest primitive axiom layer plus an ergonomic composed instance layer. The primitives expose the underlying axiom shapes directly (one ABox primitive per AxiomBody variant). The composed layer provides ergonomic endpoints for common patterns (assert-with-justification, declare-and-axiomatize, etc.).

Named Argon ops as a secondary additive surface. Mutations (RFD-0007) registered in the active package expose as POST /api/v2/<name> endpoints alongside the primitives. The routing is dynamic per-tenant.

Typed via utoipa. SDK generated via orca-codegen. Integration tests carry the documentation.

Rationale

Resource-oriented over command-oriented. v1’s command-style design (/api/kernel/assert, /api/kernel/retract) made every operation a top-level verb. The resource-oriented design lets a single resource path host multiple operations (GET, POST, DELETE) and surfaces the underlying domain structure.

Four surfaces match the underlying architecture. TBox / ABox / Derived / Scenarios is how the kernel internally partitions its work (RFD-0021’s event log + RFD-0022’s projections). Mirroring that in the API makes the surface predictable: a developer who understands the kernel architecture finds the right endpoint by analogy.

ADT-honest primitives plus composed. The primitive layer matches the AxiomBody enum 1:1. The composed layer provides ergonomic shortcuts. Both are documented; the composed layer doesn’t lock developers out of primitive access.

v1 isolation during migration. v2 not loading v1 primitives directly means the migration is straightforward: v1 endpoints stay until clients have moved; no shared state ties v2’s correctness to v1’s continued operation.

Typed via utoipa. Generated OpenAPI spec at kernel/api-spec.v2.json with typed Rust SDK and TypeScript SDK. CI gates verify the spec, the SDKs, and the source agree.

Consequences

  • v2 endpoints in kernel/api/src/v2/.
  • kernel/api-spec.v2.json is the authoritative API contract; regenerated as the source evolves.
  • kernel/api-sdk/ (Rust) and userspace/kernel-api-client/ (TypeScript) generate from the spec.
  • v1 at /api/kernel/* continues to operate; deprecation timeline is a separate concern.
  • New endpoints land as v2-native; v1 receives only critical fixes.

RFD-0024 — Diagnostic codes — OE / OW / OI severity prefix; X* for external bridges

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

The compiler emits errors, warnings, and info notices. How are they identified, and how does the namespace accommodate diagnostics emitted by toolchain extensions like format bridges (ox-ontouml, ox-owl, etc.)?

Decision

Built-in compiler diagnostics use the O* family with a severity-prefix character + four-digit numeric category:

  • OE#### — error. Severity is unrecoverable; the build fails.
  • OW#### — warning. Build proceeds; the issue is surfaced.
  • OI#### — info. Informational notice, never blocks.

Numeric categories cluster diagnostics by phase or subsystem:

  • OE01XX — bare-keyword and primitive-name resolution.
  • OE02XX — elaboration and validation.
  • OE03XX — saturation and constraint violations.
  • OE04XX — lowering and stratification.
  • OE05XX — tier classification and enforcement.
  • OE06XX — meta-property calculus.
  • (etc., as the surface evolves.)

External bridge diagnostics use the X* family. A toolchain extension (RFD-0003) registers diagnostic codes through oxc-protocol::core::codes::StaticCode + inventory::submit!. The convention:

  • X* first-character marks external/bridge origin.
  • Per-bridge subrange is reserved (e.g., XW0841-XW0851 for OntoUML import warnings; XW0860-XW0869 reserved for the OWL translator).

The compiler’s enum-style ErrorCode retains a Registered(&'static str) escape hatch backed by the inventory registry; built-in compiler diagnostics use the dedicated enum variants.

Rationale

Severity in the prefix. A diagnostic code is read out of context (CI logs, ticket titles, search results). Embedding severity in the code itself means the reader sees OE0203 and knows it’s an error without further lookup.

Four-digit numeric category clusters by subsystem. When debugging, a developer who sees OE05XX knows it’s tier-related without having to look up the specific code. Clustering makes the namespace browsable.

O* vs X* separates origin. A diagnostic from oxc is the compiler’s claim about the source; a diagnostic from ox-ontouml is a bridge’s claim about a foreign-format input. Separating by prefix makes provenance clear and prevents the compiler enum from accumulating bridge-specific variants.

Enum + Registered hybrid. Compiler-built-in codes preserve enum exhaustiveness checks (the compiler can audit “do we handle every diagnostic kind”). External codes register dynamically (foreign-format-neutrality means the compiler doesn’t pre-know them). The Registered(&'static str) escape hatch carries external codes through the same surface without ballooning the enum.

Consequences

  • New compiler diagnostics get an OE / OW / OI code in the next available numeric slot in their subsystem cluster.
  • External bridges register codes in the X* namespace via inventory::submit!. The owning ox-* crate documents its subrange.
  • Code allocation tracks centrally in oxc/src/diagnostics/codes.rs for built-ins; external subranges document in their owning crate.
  • Reserved subranges: XW0841-XW0851 (OntoUML), XW0860-XW0869 (OWL translator), additional bridges as they land.
  • The book’s appendix C reference covers OE / OW / OI built-in codes; external code documentation lives with the owning bridge.

RFD-0025 — Toolchain architecture — oxup argv[0] dispatch; single-root state; rustup-style

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

How does the Argon toolchain ship — what binary is installed, how do users switch between versions, where does state live, and how does CI integrate?

Decision

Single Rust binary with argv[0] dispatch. oxup is the toolchain manager binary. Proxy binaries (ox, oxc, ox-lsp) are symlinks to the installed oxup. When invoked as ox, oxup dispatches to the actual ox binary in the active toolchain; same for oxc and ox-lsp. oxup init creates the proxies post-install.

Single root for managed state: ~/.argon/. Toolchain installs go to ~/.argon/toolchains/<channel>-<version>/. The active channel symlinks via ~/.argon/toolchains/active. Cache and component state co-locate.

Rustup-style channel resolution. ox-toolchain.toml resolves walking up from CWD; the closest toolchain manifest wins. Two channels in v0: stable and nightly.

Toolchain tarball contents:

  • Universal Mach-O binaries (lipo of arm64 + x86_64) for macOS; per-arch binaries for Linux.
  • std source bundle (RFD-0014).
  • Tree-sitter shared library.
  • Shell completions.
  • Manifest describing the contents.

Required components: core (oxc, ox, ox-lsp, std, tree-sitter, completions). Required components are always bundled; optional components (future) extend the manifest.

Auth strategy: gh CLI first. No oxup-managed credentials in v0; release pipeline auth flows through standard GitHub mechanisms.

Distribution: Homebrew tap at sharpe-dev/homebrew-tap (path corrected from earlier homebrew-ontolog which now redirects).

Release pipeline: argon-v* tag push on the monorepo triggers signing + notarization (macOS) + cross-platform tarball upload + tap auto-bump.

Diagnostics via oxup doctor: categorized checks (binary, filesystem, toolchain, auth, editor, configuration).

VS Code extension versioned lockstep with the toolchain. Extension and toolchain ship together; no version-skew compatibility matrix.

Telemetry: three levels — none, minimal (default), full. User-controllable; minimal collects anonymous usage signal only.

Offline mode: --offline flag and [network] offline = true setting in ox-toolchain.toml.

Nightly garbage collection: nightlies older than 30 days are deleted automatically.

Uninstallation: oxup uninstall removes everything oxup created — directory, rc-file edits (idempotent reverse), VS Code extension. Clean.

Shell integration: bounded edit region in user’s rc file with guard comments; idempotent; reverses cleanly on uninstall.

Rationale

argv[0] dispatch keeps the install surface small. One binary, three or four symlinks. No per-tool installer, no per-tool update mechanism, no version drift between ox and oxc.

Single root for state. All state under ~/.argon/. Backups, sandboxes, container-mounts handle one tree. Anti-pattern: state split across ~/.config/, ~/.cache/, ~/.local/, etc.

Rustup-style channel. Argon’s audience overlaps with Rust developers; the convention is familiar. Walking up from CWD lets per-project toolchain pinning work without environment variables.

Universal Mach-O. Apple Silicon and Intel Macs both work without per-arch downloads; the binary is bigger but the user experience is “one tarball just works.”

gh CLI for auth. GitHub’s auth machinery is mature, supports SSO and 2FA, and most contributors already have it. Reusing it avoids inventing parallel auth.

Homebrew tap. Mac users install via brew install. Linux gets the same tarballs from GitHub Releases.

30-day nightly retention. Long enough to bisect a regression; short enough that the cache doesn’t unboundedly grow.

Consequences

  • oxup owns: install / update / uninstall / channel switch / component management / doctor / proxy binary creation.
  • Release pipeline at .github/workflows/argon-release.yml runs on argon-v* tag push.
  • Tap repository at sharpe-dev/homebrew-tap; auto-bumped on release.
  • ~/.argon/toolchains/<channel>-<version>/ for installed toolchains; ~/.argon/toolchains/active for the active symlink.
  • Shell integration touches the user’s rc file inside guard comments only.

RFD-0026 — Living diagrams — diagram blocks as graduated language extension

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

Ontology engineers think visually. UML-style class diagrams, state machine diagrams, dependency graphs — these are how the domain reads. Should Argon support them, and if so, how does the visualization stay in sync with the source-of-truth declarations?

Decision

Text is truth, visuals are lenses. The authoritative representation is the text source. Diagrams are derived views — generated from the source, regenerated when the source changes. There is no diagram-first modeling surface where the diagram is canonical and text is generated.

diagram blocks are first-class language items. A diagram block declares a view: which concepts to include, which relations to render, which layout algorithm to apply, which metadata to surface. The block evaluates against the current source state and produces SVG / PNG / PDF output.

Diagram blocks compose with the rest of the language. The same expression grammar (RFD-0005) selects which concepts and relations to include. Filter expressions, aggregate over the diagram’s contents, mark concepts with metatype-derived styling — all use the standard expression vocabulary.

Per-diagram layout selection. Sugiyama-style hierarchical layout for taxonomies; force-directed for relation-dense graphs. The block declares the layout algorithm; future algorithms register without changing the block grammar.

Rationale

Diagram-first modeling drifts. Tools that treat the diagram as canonical (Visio, Enterprise Architect, some OntoUML editors) accumulate inconsistencies: the visual layer encodes information the underlying model doesn’t capture, or vice versa. Source-of-truth-as-text means the diagram cannot disagree with the model — it’s regenerated.

Diagrams as language items, not external configuration. A diagram block lives in the same source file as the concepts it visualizes. A change to the concept structure that breaks the diagram surfaces immediately at compile time, not when the diagram regenerates next week.

Graduated extension. A simple diagram block just lists concepts: diagram concepts { Person, Organization }. A complex one filters, aggregates, applies styling rules driven by metatype membership, composes with patterns. The simple case is approachable; the complex case is expressible in the same grammar.

Layout as a parameter, not the language. Different visualizations need different layouts. Hardcoding one layout would have constrained the language to one rendering model. Letting the block declare the layout means new algorithms (graphviz-style, custom application-specific) plug in without grammar changes.

Consequences

  • diagram is a reserved item kind with its own AST node and Core IR variant.
  • diagram blocks evaluate during compile, producing renderable artifacts as build outputs.
  • Layout algorithms ship in the compiler core (Sugiyama, force-directed) with a registration seam for additions.
  • Diagrams compose with where clauses and aggregates for selecting contents.
  • Editor surfaces (LSP InfoView, vscode extension) render diagram blocks live during editing.

RFD-0027 — Agent-facing tooling: universal CLI + JSON contract, MCP as transport, registration as data

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

How should Argon integrate with AI coding agents — Claude Code, Cursor (IDE + SDK), Codex, opencode, Continue, Aider, and whatever ships next year — such that the integration is general, stable, and additive rather than per-agent code in oxup?

Context

Argon ships a language server (ox-lsp) and a project-manager CLI (ox). The VS Code / Cursor extension wraps the LSP for human-in-IDE use. Today, agents reach Argon through whatever path the IDE happens to surface — when the agent runs in IDE mode with the Argon extension active, diagnostics flow through the editor’s extension-to-context glue. When the agent runs outside an IDE (Cursor SDK harness, cursor-agent CLI, opencode plugin loop, a custom automation harness), there is no Argon integration at all.

That gap is the load-bearing problem. Three things have to be true at once for an integration to be durable across the agent ecosystem:

  1. It works without an IDE in the loop.
  2. It works for any current and future MCP-supporting agent without per-agent code in oxup.
  3. The agent-facing data contract is stable enough that downstream tools can pin to it.

Configuration-blind harnesses (an SDK that doesn’t read external config files, a CI loop that boots an agent with no shell config, etc.) compound the problem: anything that requires a config file to land in the right place fails for them. The floor has to be reachable from a Bash tool with no setup beyond the toolchain being on PATH.

Options

Option A — IDE extension only. Lean on the existing VS Code / Cursor extension; rely on each editor’s extension-to-context integration. Rejected: fails immediately for headless harnesses, SDK-based agents, and CI-mode integrations.

Option B — MCP-only. Ship ox mcp and call it done. Rejected: configuration-blind agents and Bash-only loops still get nothing. The floor needs to be lower than MCP.

Option C — Per-agent native integrations. Implement Cursor SDK plugin, Claude Code plugin, opencode plugin, etc., each natively. Rejected: scales badly (every new agent is a code change in oxup), creates surface drift, and reimplements the same data flow N times.

Option D — Universal CLI + JSON schemas at the floor; MCP as an opt-in transport; both backed by a single in-process surface library; per-agent registration as data not code. The position taken in this RFD.

Decision

Argon’s agent-facing tooling is layered, with all tiers backed by a single in-process surface library so the operation a transport carries and the operation a transport implements are the same code. No tier reimplements another’s payloads; transports differ only in how they reach the surface.

Implementation locus — oxc-agent-surface library. Each agent-facing operation is a typed Rust function returning a Serialize + schemars::JsonSchema payload. The library lives in argon/oxc-agent-surface/ (or as an extension of oxc-runtime::OxcContext if module-internal scope wins out during implementation). CLI, MCP, and any future LSP-extended request are thin transports over these function calls — argument parsing, transport-specific envelope, then a single function invocation that returns the typed payload. JSON Schemas under share/argon/schemas/ are generated from the surface’s return types via schemars, the same pattern oxc-codegen already uses to keep oxc-protocol types in sync with their TypeScript bindings and the diagnostic registry. The Rust types are the source of truth; the schemas are their wire-format projection.

Tier 0 — universal CLI + JSON schema contract. Every agent-relevant ox subcommand has a --json flag. The flag selects a serializer; the operation is a call into oxc-agent-surface. Schemas (generated): diagnostics.schema.json, hover.schema.json, query-result.schema.json, package-tree.schema.json, provenance.schema.json, plus version.json carrying the schema-set semver. Any harness that can run ox check --json <path> and parse JSON is integrated. This is the floor.

Tier 1 — MCP transport (ox mcp). Each MCP tool delegates to the same oxc-agent-surface call as the equivalent CLI path; tool payloads are bytewise identical to CLI JSON because they pass through the same serializer. Tool surface: argon_check, argon_check_file, argon_explain, argon_hover, argon_query, argon_packages, plus argon_why once provenance walks ship. MCP-aware agents wire it via a single config entry pointing at ox mcp.

ox mcp lifecycle — two scheduled phases within one release cycle. v0 is a thin spawning transport: each MCP tool call invokes the corresponding ox CLI subcommand as a child process and streams its JSON output back. v1 holds a persistent OxcContext across tool calls, reusing Salsa caches for warm incremental analysis. v0 ships the contract and surface library before the lifecycle complexity; v1 is the production target. The promotion is scheduled, not gated on later telemetry — heavy agent traffic is the assumed workload, not a hypothetical.

Tier 2 — IDE extension. Unchanged. Continues to serve in-editor humans and IDE-mode agents that consume diagnostics via the editor’s extension-to-context glue.

Tier 3 — per-agent skill / rules files. Documentation pointers at Tier 0 / Tier 1 capabilities, not implementations. Content is single-sourced; per-agent variants differ only in file location and frontmatter wrapping.

Registration is data-driven. oxup agents register reads share/agents/<agent>/registration.toml describing the config-file path, write strategy, and detection predicate for each supported agent. Adding a new agent is a share/ data drop, not an oxup code change. Detection is per-agent presence (config directory exists or binary on PATH); missing agents are skipped silently.

Schema versioning. share/argon/schemas/version.json carries the schema-set semver; each schema also carries its own version. Pre-1.0 schemas are explicitly experimental and may break between minor toolchain releases; 1.0+ schemas commit to SemVer (breaking → major, additive → minor). Consumers pin. The agent contract gets the same stability discipline ConceptDef gets for the kernel — but each schema earns 1.0 through its own ratification RFD, not by default.

Rationale

Why CLI + JSON is the floor. Every agent that integrates with code has exactly two universal capabilities: read files, run shell commands. No agent universally speaks LSP, MCP, IDE-extension, or any other protocol. Picking anything richer than CLI as the floor leaves agents out. Configuration-blind harnesses can integrate at Tier 0 with zero installation magic, by literally calling ox check --json and parsing the output. Their problem reduces to “parse a documented JSON schema” — solvable in any language, by any harness, forever.

Why MCP is the right Tier 1, not the floor. MCP is converging fast as the cross-agent standard for tool exposure. Claude Code, Cursor (IDE and SDK), opencode, Codex, Continue, Aider — all support it or are adding support. Building Tier 1 as MCP means one implementation reaches every MCP-aware agent. Future MCP-adopting agents integrate for free. But MCP is not universal today, and never will be for harnesses that intentionally avoid configuration. Hence Tier 0 below it.

Why a single in-process surface is the implementation locus, not just shared schemas. Sharing JSON Schemas across transports prevents wire-format drift but doesn’t prevent implementation drift: three call sites for the same operation (one per transport) can subtly diverge in argument validation, error mapping, edge-case handling, and which fields get populated when. Pulling the operation into one typed function call that every transport invokes makes drift structurally impossible — the function and its return type are the operation. Schemas generate from the return types via schemars rather than being hand-authored alongside; this is the same drift-prevention pattern oxc-codegen already uses for oxc-protocol types and the LSP/TypeScript bindings, extended to the agent contract.

Why registration is data, not code. Agent ecosystems move faster than oxup releases. Cursor adds an SDK; Codex changes a config path; opencode forks. If every agent change requires a code update, oxup becomes a bottleneck on agent-ecosystem velocity. Data-driven registration (TOML descriptors plus shipped skill files) lets us add an agent in the next release tarball without touching Rust.

Why per-agent skills are pointers, not implementations. The skill/rules layer’s job is to teach the agent which capabilities exist and when to use them. The capabilities themselves live in Tier 0 / Tier 1. If skill content forks per agent, every fix requires N edits. Single-source content with per-agent wrapping (frontmatter, file location) means one update propagates everywhere.

Why ox mcp and ox-lsp stay distinct binaries. LSP and MCP have incompatible lifecycle semantics: LSP is a long-lived process talking JSON-RPC over stdio with bidirectional notifications; MCP is tool/resource semantics with retry-friendly one-shots. Conflating them inherits the worst of both. Each protocol gets its own binary that delegates into the same oxc-agent-surface library — and through it, into the same underlying oxc Salsa-tracked queries. Distinct binaries, single implementation locus.

Why ox mcp v0 ships before v1, even though v1 is the destination. v1 (persistent OxcContext + warm Salsa across tool calls) is the production-traffic shape: agents iterating on the same files across dozens of tool calls per session benefit substantially from cache reuse. But v1 inherits a class of bugs v0 doesn’t — process lifecycle, crash recovery, IPC robustness, cache-invalidation correctness against file changes between calls. Shipping v1 before the contract and surface library are stable means debugging persistence at the same time as debugging operations. v0 (thin spawning transport) lands the contract; v1 follows in the same release cycle as the next phase. Scheduled, not conditional — heavy warm-cache traffic is the assumed workload, not a maybe-later case.

Why the agent contract gets first-class stability discipline. Agents will be primary authors of Argon code; the agent surface is therefore upstream of language productivity itself. Diagnostic JSON quality, hover payload richness, query primitives — these shape what Argon is in practice for the bulk of users (other software), not just how its tooling feels. Schemas accordingly graduate at deliberate pace: each schema gets its own follow-up RFD as it stabilizes, not a single bulk-author-and-ship pass. The diagnostics schema is the highest-traffic surface and lands first; hover, query-result, package-tree, and provenance follow as their underlying surfaces do. Pre-1.0 schemas are explicitly experimental in version.json; 1.0+ schemas commit to SemVer.

Why “Ayush’s harness can’t read config” is not our problem to solve. A configuration-blind SDK harness sits on top of the universal floor (Tier 0) by definition: it can run ox check --json and parse output, and that’s all the contract requires. Anything more — auto-discovery of MCP servers, automatic skill installation — assumes a harness that reads config files. We don’t owe that to harnesses that intentionally don’t. The floor is what we owe; the rest is convenience for harnesses that opt in.

Consequences

  • New crate: oxc-agent-surface (or extension on oxc-runtime::OxcContext) defining each agent-facing operation as a typed Rust function. CLI / MCP / future LSP-extended requests all become thin transports over this surface. The crate is the implementation locus; transports are wire-format adapters.
  • Codegen extension. oxc-codegen grows an emitter that writes share/argon/schemas/*.schema.json from the surface’s return types via schemars. Drift between Rust types and schemas becomes a CI failure, same gate that already protects oxc-protocol.
  • New subcommand: ox mcp ships in two scheduled phases within one release cycle. v0 is a thin spawning transport; v1 holds a persistent OxcContext. The phasing is to land the contract before the lifecycle complexity, not to defer v1 indefinitely.
  • oxup changes. oxup agents register reads share/agents/<agent>/registration.toml and writes per-agent config. The existing Claude Code marketplace pointer migrates to this mechanism. oxup install invokes registration after toolchain laydown; users opt out via oxup agents register --none or per-agent flags.
  • share/ layout grows. New directories under the toolchain tarball:
    • share/argon/schemas/ — generated JSON Schemas + version meta
    • share/agents/_common/argon-skill.md — single-sourced skill content
    • share/agents/<agent>/registration.toml + share/agents/<agent>/<skill-file> per supported agent (cursor, claude-code, codex, opencode at v0)
  • Documentation. Argon book gains a “For Agents — Tooling” page covering JSON schema shapes, MCP tool names, and integration recipes per tier. Hosted in the existing “For Agents” part.
  • Contract stability. Schemas under share/argon/schemas/ follow SemVer once they hit 1.0. Pre-1.0 schemas are explicitly experimental in version.json. Each schema graduates via its own follow-up RFD as the underlying surface stabilizes; this RFD ratifies the architecture, not the per-schema shapes.
  • Cluster shape. Sized like a coherent multi-PR body of work — surface-library scaffolding, --json audit, ox mcp v0 + v1, oxup agents register data-drive, skill content authoring, book docs. Patch-line bumps absorb it; the substrate is purely additive (no breaking changes to existing surface).

Implementation issues that fall out

To be opened post-commit, each citing this RFD:

  1. Land oxc-agent-surface crate scaffolding. Enumerate operations as typed functions; derive return-type schemas via schemars; wire oxc-codegen to emit share/argon/schemas/*.schema.json from the surface types with a CI drift gate.
  2. Audit ox subcommand --json coverage. Route every --json path through oxc-agent-surface rather than ad-hoc serialization; remove direct serde_json calls in CLI handlers where the surface library covers them.
  3. Implement ox mcp v0 — thin spawning transport. Each MCP tool call invokes the corresponding ox CLI subcommand as a child process and streams JSON output back. Lands the MCP contract.
  4. Implement ox mcp v1 — persistent OxcContext. Hold one OxcContext across tool calls; warm Salsa caches across requests; correct cache invalidation against file changes between calls. This is the production target.
  5. Make oxup agents register data-driven. Read share/agents/<agent>/registration.toml; migrate the existing Claude marketplace pointer onto this mechanism; detect agents per descriptor; idempotent.
  6. Author single-source skill content. share/agents/_common/argon-skill.md carries the substantive content; per-agent variants in share/agents/<agent>/ carry frontmatter and any agent-specific guidance.
  7. Land “For Agents — Tooling” page in the Argon book. Tier-by-tier integration recipes; example diagnostic JSON, MCP tool invocations, registration commands.
  8. Open follow-up RFDs per schema as it stabilizes. Diagnostics first (highest traffic), then hover, query-result, package-tree, provenance. Each ratifies its schema for SemVer 1.0.

RFD-0028 — Diagnostics schema 1.0

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

What is the wire-format shape of the agent-facing diagnostics payload, and when does it commit to SemVer 1.0?

Context

RFD-0027 ratified the layered architecture for agent integration: CLI floor, MCP transport, IDE extension, per-agent skills — all backed by oxc-agent-surface whose typed return values generate JSON Schemas under share/argon/schemas/. Each schema graduates to SemVer 1.0 via its own follow-up RFD; this is that follow-up for the diagnostics schema.

The diagnostics schema is the highest-traffic agent surface. Agents writing Argon code call argon_check continuously while iterating. The schema’s shape determines what an agent can do with a diagnostic: render it, propose a fix, jump to it, file an issue about it. Get this wrong and every consumer rebuilds the missing context locally; get it right and consumers compose cleanly.

The shape shipped at 0.1.0 (under RFD-0027 Phase 1) carries 9 fields per diagnostic plus 7 fields per span. The question is whether that shape is the shape, ready for a SemVer 1.0 commitment.

Decision

Ratify the diagnostics schema at SemVer 1.0.0 on merge of this RFD. The shape is the one defined by oxc_agent_surface::types::diagnostic::DiagnosticsReport at the merge SHA; further evolution follows SemVer (additive → minor, breaking → major).

share/argon/schemas/version.json adds "diagnostics" to its stable array on this RFD’s merge.

Schema shape (load-bearing)

DiagnosticsReport is the top-level object: schema_version: string, diagnostics: Diagnostic[], summary: DiagnosticsSummary.

Diagnostic is the per-diagnostic record:

FieldTypeNotes
codestringStable error code, e.g. "OE0226". Code prefix encodes severity.
severity"error" | "warning" | "info"Mirrors the code prefix; surfaced explicitly so consumers don’t parse the string.
messagestringOne-line summary.
primary_spanSpanRef | nullNull for spanless diagnostics (CLI-layer cross-format errors).
primary_labelstring | nullOptional label rendered at the primary span.
secondary_labelsSecondaryLabel[]Each carries its own span + message.
helpstring | nullOptional remediation hint. CLI appends try ox explain <code>; wire form does not.
package_originstring | nullVocabulary package that authored the constraint. Compiler built-ins return null.
provenance_chainstring[]Why-chain from meta-property derivation; empty when not applicable.

SpanRef carries both byte offsets and 1-indexed line/UTF-16 columns:

FieldTypeNotes
filestringWorkspace-relative path, normalized to forward slashes.
byte_startu32UTF-8 byte offset (0-indexed, inclusive).
byte_endu32UTF-8 byte offset (0-indexed, exclusive).
line_startu321-indexed line of the span’s first byte.
col_startu321-indexed UTF-16 column of the span’s first byte.
line_endu321-indexed line of the span’s last byte.
col_endu321-indexed UTF-16 column one past the span’s last character (exclusive end).

DiagnosticsSummary carries errors: u32, warnings: u32, infos: u32 so consumers can summarize without iterating.

SecondaryLabel carries span: SpanRef + message: string.

Rationale

Why three-valued severity. Argon’s diagnostic codes already use a one-character severity prefix (E/W/I after the namespace character). Surfacing severity explicitly as an enum rather than parsing the string gives consumers structural access without coupling them to the code-prefix convention. Three levels match LSP’s DiagnosticSeverity 1/2/3 (Error/Warning/Information), making round-trip to LSP transports trivial.

Why both byte offsets and line/column. Renderers split into two camps: terminal/IDE renderers want line/column for cursor placement; programmatic consumers (refactor tools, jump-to-definition) want byte offsets to slice source text without re-parsing. Carrying both removes a class of off-by-one bugs at every consumer boundary.

Why UTF-16 columns specifically. LSP Position semantics use UTF-16 code units. Editors built atop LSP clients (VS Code, Cursor, Helix, neovim+coc) all assume UTF-16 columns. Picking anything else (UTF-8, codepoint) makes round-trip lossy at every IDE boundary. UTF-16 is the bad choice the world picked; we match.

Why 1-indexed lines/columns. Editor convention. Byte offsets stay 0-indexed (matching the compiler’s internal Span type) so the two coordinate systems remain visually distinguishable in the same payload.

Why package_origin is optional. Compiler built-in diagnostics (OE0001-class parse errors, type errors) don’t have a vocabulary package authoring them — they’re the language. Constraint-based diagnostics surface from a pub strict error rule in some package (UFO’s R01-R37, BFO’s continuant/occurrent disjointness, custom domain rules), and package_origin carries that package name so consumers can attribute / route / suppress per-package.

Why provenance_chain is string[]. The compiler’s meta-property calculus produces structured why-chains internally; for v0 we serialize them as strings rather than commit to a structured shape. Structuring the chain (axis names, derivation rule names, intermediate values) is its own design and would block ratification on more thinking. Strings are SemVer-safe additively: a future minor bump can introduce an optional provenance_structured: ProvenanceFrame[] | null field alongside the existing string array. Replacing the string array with a structured type — i.e. removing or renaming provenance_chain — is a major bump and out of scope for any minor evolution.

Why null for spanless / no-help / no-package — not absence. Schema consumers benefit from required fields with explicit null over optional fields whose presence is signal: a diagnostic that might have help but doesn’t is the same case whether help is missing-from-object or null. Forcing null in the wire format normalizes the parsing path. Required + nullable also generates cleaner TypeScript bindings (string | null vs string | undefined) for the eventual vscode-extension consumers.

Why ratify at 1.0 now rather than after #331 / #332 land. The shape is derived directly from oxc::diagnostics::OntologDiagnostic (legacy spelling — short for the pre-rename “Ontolog” language tag — and the canonical name of the internal compiler diagnostic struct), which has been stable since long before RFD-0027. The wire format adds nothing not already in the internal form — projecting Span to byte+line/col is mechanical, projecting Severity is identity, the rest is field-level rename. The risk of “consumers exercise the schema and find a missing field” is low because no compiler-side richness is being hidden. SemVer 1.0 commits to this shape — additive evolution stays minor; we can always bump major if we missed something.

Why this RFD is small. The schema description lives in types/diagnostic.rs; rustdoc and the generated JSON Schema are the source of truth. This RFD captures the rationale for committing to that shape, not a duplicate of the shape itself.

Consequences

  • share/argon/schemas/version.json stable array gains "diagnostics" on merge. The actual file update lands as a follow-up commit on the agent-tooling workstream branch — the file is generated from oxc_agent_surface::types::version::SchemaSetVersion::current(), which lives there. A trailing-newline chore commit on that branch (or directly on main if the branch has merged) updates current() to push "diagnostics" into stable and re-runs oxc-codegen emit.
  • Future changes to DiagnosticsReport / Diagnostic / SpanRef / Severity / SecondaryLabel / DiagnosticsSummary follow SemVer:
    • Adding an optional field (default-on-deserialize) → minor bump.
    • Renaming a field, removing a field, narrowing a type, changing semantics → major bump.
    • Adding a new Severity variant → major bump (consumer enums break).
  • Consumers (Cursor MCP, Claude Code skills, vscode-extension renderers, third-party harnesses) pin against the schema’s SemVer in version.json.
  • The CI drift gate via oxc-codegen check continues to enforce that the published JSON Schema matches the Rust types byte-for-byte. Drift here means the wire format moved without a SemVer bump; CI fails.

Out of scope

  • Hover, query-result, package-tree, provenance schemas — each gets its own ratification RFD as its underlying surface stabilizes.
  • Structured provenance_chain — punt to a future minor bump (additive optional field).
  • Code-action hints (LSP-style fix-it suggestions in the wire format) — punt to a future minor bump.

RFD-0029 — Doc comments and ox doc

Committed Opened 2026-05-04 · Committed 2026-05-04

Question

What is Argon’s API-reference documentation surface — comment syntax, parsed shape, intra-doc link semantics, doc-test treatment, visibility rules — and where does it live in the toolchain?

Context

Argon’s narrative documentation lives in the Argon book, an mdBook deployed at book.argon-lang.org. That covers the language tutorial + reference + the “For Agents” section. What it does not cover is the per-package API reference: the public concepts, metatypes, relations, rules, queries, mutations, and computations that authors export from their packages, with their doc strings, signatures, and cross-links rendered into a browseable surface.

Cargo + rustdoc is the reference design here — every public Rust item gets a doc page derived from /// comments, with intra-doc links resolved through the compiler’s name resolver, and code blocks executed as doc-tests. The Argon equivalent is ox doc. Today the lexer recognizes /// and //! (oxc::syntax::lexer) and the tree-sitter grammar admits doc_comment as a top-level statement, but elaboration drops them — they’re CST trivia with no structural attachment to declarations. Nothing reads them downstream.

This RFD locks the surface before any of that wiring lands.

Decision

Adopt the rustdoc model with three deliberate departures: (a) the parsed doc IR is the load-bearing artifact, not the HTML; (b) doc comments attach during elaboration, alongside pub visibility analysis; (c) ox doc is a thin renderer over a stable JSON IR that other tools (semver-check, doc-coverage, alternative renderers, the LSP InfoView) consume directly.

Comment syntax

FormRoleAttachment
/// …Outer doc comment.Concatenates with contiguous /// runs immediately preceding a declaration; attaches to that declaration.
//! …Inner doc comment.Valid only at module top (before the first declaration). Concatenates with contiguous //! runs and attaches to the enclosing module. Anywhere else → OW0815 MisplacedInnerDoc.
// …Regular line comment.Trivia. Never doc.
/* … */Block comment (no doc form).Trivia. Argon does not have /** … */ — keeping one outer-doc form keeps the surface tight.

Doc strings on items with no public visibility (pub keyword absent) elaborate but stay out of the rendered surface unless --document-private-items is passed (mirrors cargo).

Markdown flavor

CommonMark + the standard pulldown-cmark extensions Argon needs:

  • Tables (|---|---|).
  • Footnotes ([^id]).
  • Strikethrough (~~text~~).
  • Task lists (- [ ] item).
  • Heading anchors auto-generated from heading text (kebab-cased).

Smart-punctuation, math (KaTeX), and other extensions are deliberately not enabled at the doc-IR layer. Renderers may opt in (the book already enables KaTeX via mdBook config); doc strings stay portable across renderers.

Code blocks

Fenced code blocks tagged argon are first-class doc tests:

TagBehavior
```argonRun. Elaborates through the same capture-mode runner that powers fixture { }. Elaboration error → test fails.
```argon,ignoreRender only. Not executed; surfaces as a syntax-highlighted block. For partial snippets, illustrative non-Argon code, etc.
```argon,no_runParse + elaborate, do not test-execute. Useful for snippets that rely on external runtime state (a kernel-backed query against a live tenant).
```argon,setupSetup boilerplate. Concatenated as a prelude into every argon block in the same /// run. Not a runnable test on its own.
```argon,failExpected to fail elaboration. Passes if elaboration produces any diagnostic; fails if the snippet elaborates clean.

Lines starting with # (hash-space) inside any argon code block are hidden from rendered output but kept in the executable body. Mirrors rustdoc; lets authors elide setup ceremony in published docs while keeping examples runnable.

Code blocks tagged with anything other than argon[,…] (e.g. ```rust, ```text, ```) render verbatim and are never executed.

Two equivalent forms:

  • `[Name]` — bare reference.
  • `` [`Name`] `` — back-tick reference (matches rustdoc convention).

Name resolves through the same name resolver oxc::elaborate::resolve_imports uses, against the documented item’s lexical scope:

  • Bare identifier ([Concept]) — local module first, then use-imported paths, then package root.
  • Path ([pkg::module::Concept]) — fully qualified.
  • Method-style ([Concept::axis_value]) — concept-qualified field, axis, or relation reference. (v1: only resolves to declared concept members; deeper structural references queue for v2.)

Unresolved → OW0816 BrokenDocLink. Markdown links written [text](url) are explicit hrefs, never auto-resolved.

Visibility

ItemDefault in ox docOverride
pub <metatype-name> / pub rel / pub metarel / pub decorator / pub frame / pub query / pub mutation / pub deriveDocumented.#[doc(hidden)] opts out.
Items without pub (private to package).Hidden.--document-private-items includes them.
Items with #[doc(hidden)].Hidden.--document-private-items includes them.

#[doc(hidden)] is an attribute on the declaration, parsed alongside #[unproven] / #[assumed] / #[theorem]. It does not affect elaboration, name resolution, or runtime — only doc rendering.

Doc IR (JSON, stable)

The parsed shape lives in oxc-protocol::core::doc:

  • DocBlock { markdown: String, code_blocks: Vec<DocCodeBlock>, links: Vec<DocLink>, source_range: Range }
  • DocCodeBlock { kind: DocCodeKind, body: String, hidden_lines: Vec<u32>, source_range: Range }
  • DocCodeKind = Run | Ignore | NoRun | Setup | Fail
  • DocLink { text: String, target: ResolvedDocTarget, source_range: Range }
  • ResolvedDocTarget = Item { qualified_path: String } | External { href: String } | Unresolved
  • DocVisibility = Public | Hidden | Private

pub docs: Option<DocBlock> lives on every doc-eligible Core* item: CoreConcept, CoreRule, CoreMetatype, CoreMetarel, CoreDecorator, CoreFrame, CoreQuery, CoreMutation, CoreCompute, CoreAxiom. Module-level docs live on LoweredModule.docs.

The JSON serialization of DocBlock is the wire shape ox doc --emit-json produces. It’s the substrate three renderers consume:

  1. ox doc HTML renderer — static site, mirrors rustdoc’s information architecture.
  2. LSP InfoView + hover — same DocBlock, rendered into LSP MarkupContent.
  3. MCP argon_doc tool — agent-facing JSON pass-through (Phase 4).

The JSON commits to SemVer 1.0 in a follow-up RFD once ox doc ships and shape lands stable.

Diagnostics

CodeTrigger
OW0815 MisplacedInnerDoc//! outside module top.
OW0816 BrokenDocLinkIntra-doc link target unresolved.
OW0817 InvalidDocCodeBlockTagFenced block tagged argon,<unknown> — surfaces author typos in the kind tag.

All three are warnings, not errors — bad docs don’t break builds. ox doc --deny-warnings (mirrors cargo doc --deny-warnings via RUSTDOCFLAGS) escalates them to fatal for CI.

Rationale

Comment syntax matches Rust verbatim. /// outer + //! inner is the convention every Rust developer already has internalized; the UFO team is moving from OntoUML and a Rust-shaped surface flattens the learning curve. Block-doc form (/** */) is omitted — one form per concern (RFD-0018 idiom).

JSON IR before HTML. The technical reference Ivan provided is unambiguous: the highest-leverage artifact is the stable typed IR. HTML is one consumer; semver-check, doc-coverage tooling, alternative renderers, and the LSP InfoView all benefit from the same shape. Rustdoc’s --output-format json (still nightly-only after years) is the exact lesson — design it as the primary surface from day one.

Doc tests through capture-mode elaboration. Argon already has the runner (oxc::test_runner) with capture-mode for fixture { }. Doc tests reuse it: each ```argonblock is a synthetic fixture whose body is<setup_blocks_concatenated>\n<this_block>. No new elaboration path. The 5-status TestStatus` taxonomy (RFD-0008) covers doc-test outcomes too.

Visibility default is pub only. Anything else and packages would leak internals into their public docs by accident; that’s the rustdoc default for the same reason. --document-private-items is the escape hatch (cargo-equivalent).

Markdown extensions are limited. Math, smart-punctuation, custom directives, and other heavy extensions create source-portability problems and tie doc strings to the renderer that supports them. Doc strings should be readable in cat, in a code review, and in the editor’s hover panel — keeping the flavor close to portable CommonMark serves all three.

Cross-link resolution piggybacks on oxc::elaborate. This is Section 4.1 of the technical reference: do this through the real resolver or it will break under re-exports, generics-as-functors (RFD-0009), and standpoint-imported names. No separate doc-time resolver.

Consequences

  • New protocol module oxc-protocol::core::doc. ts-rs bindings + JSON Schema generated by oxc-codegen on the same drift gate as diagnostics.
  • New diagnostic codes: OW0815, OW0816, OW0817.
  • New #[doc(hidden)] attribute parsed by the existing AttrList pass; ignored by elaboration, consumed only by the doc renderer.
  • Option<DocBlock> field added to ten Core* types + LoweredModule; existing Salsa queries gain a doc-string input edge.
  • Doc-string parsing depends on pulldown-cmark (workspace dep — already in the tree via mdbook).
  • Phase 1 of the ox doc rollout (this RFD’s wiring) ships ahead of the HTML renderer; the JSON IR + diagnostics are useful on day one for IDE hover bodies and doc-coverage tooling.
  • Cross-package linking semantics (Section 4.4 of the technical reference) defer to a follow-up RFD landing with the HTML renderer — Phase 1 emits qualified paths in ResolvedDocTarget::Item; the renderer decides URL layout.

Historical lineage

None — new surface.

RFD-0030 — ox doc --document-deps and external-package documentation

Draft Opened 2026-05-05 · Status: Draft

Question

How does ox doc document a workspace whose dependencies resolve outside the workspace tree — registry, git, or path-deps — and how does the resulting site link out to those packages’ documentation?

Context

RFD-0029 ratified ox doc as a thin renderer over a stable JSON IR (oxc-doc::Site). RFD-0029 §3.2 + Phase 4 ship the React + Vite SSG that consumes that IR and emits the static HTML site. What neither RFD addressed is the cross-package navigation problem when the deps aren’t local.

Concretely, today (2026-05-05):

  • A workspace whose members are all path-deps inside the workspace tree (e.g. examples/lease-story with all 7 packages under packages/) renders correctly. Cross-package qname links resolve because the consumer site can find every dep’s Site.item_pages in the bundle envelope.
  • A typical project — single-[project] ox.toml declaring cofris = "0.2.2" from the registry — also “renders”: every concept page, every relation, every rule. But cross-package qname links 404. The renderer synthesizes URLs of the form cofris/0.2.2/cofris/concept.JournalEntry.html against the same target/doc/ tree, but cofris lives at ~/.argon/packages/cofris/0.2.2/ (the cache), not under target/doc/. Links break silently.
  • Workspaces mixing path-deps and registry-deps work for the path side and 404 for the registry side. The new external-deps row in the workspace dep graph (RFD-0030 prep, landed 2026-05-05) at least makes the externals visible, but they have no click target.

The user-facing surface needs to handle the registry case as the default — it’s how most projects will be structured once Argon’s package registry is populated.

Cargo’s design is the reference. cargo doc documents the workspace + its transitive dependencies by default; --no-deps opts out. The output tree at target/doc/ is self-contained. Argon should match this UX: ox doc produces a docs tree that works whether or not the user has internet access, with deps’ source coming from the local cache.

Decision

Adopt Cargo’s mental model with three concrete commitments:

  1. ox doc --document-deps builds docs for transitive intra-workspace + cached registry/git/path deps, producing a self-contained target/doc/ tree. Default-off behaviour matches the user’s current expectation. The flag flips when docs.argon.dev ships and --no-deps becomes the new default — at that point externals get rewritten to https://docs.argon.dev/<pkg>/<ver>/ URLs by the renderer.

  2. Dep docs come from the local resolver cache at ~/.argon/packages/<pkg>/<ver>/. The cache is already populated by ox install and content-hash verified against ox.lock. ox doc --document-deps walks the lockfile’s transitive closure, opens each dep’s manifest, builds its Site against the cached source, and includes it in the bundle envelope passed to argon-doc-ui. No network. No registry-side cooperation required.

  3. The IR doesn’t change. BundleEnvelope.sites: Vec<Site> already accepts arbitrary members; --document-deps just feeds it more. The workspace dep graph’s “external” row collapses to zero when every external dep is locally documented (the IR builder demotes externals into full WorkspaceMember-equivalents at envelope-construction time).

Mechanics

$ ox doc --document-deps          # documents workspace + every transitive dep
$ ox doc                          # default: workspace only (today's behaviour)
$ ox doc --no-deps                # explicit opt-out (post-docs.argon.dev default)

The implementation seam is argon/ox/src/main.rs::run_doc_workspace (and run_doc_single). After collecting the workspace’s own member sites, when --document-deps is set:

  1. Read ox.lock to get the transitive closure of every member’s deps with their pinned versions.
  2. For each external dep (source = registry / git / non-workspace path), locate the cached source: ~/.argon/packages/<pkg>/<ver>/. Verify content-hash against the lockfile entry; refuse divergence (same rule as ox audit).
  3. Build each dep’s Site via the same build_doc_for_project path used for workspace members. Each dep is treated as a single-package project for elaboration purposes — its own dep-resolution closure is what the lockfile already pinned, no fresh resolution needed.
  4. Append every external Site to BundleEnvelope.sites, mark them with is_external_dep: true on Site so the renderer can apply distinct breadcrumb/sidebar treatment (“From cofris 0.2.2 (registry)” subtitle on the package index, slightly muted card on the workspace landing).
  5. The bundle envelope is invoked once as today — the SSG just iterates more sites.

Lockfile interaction

The lockfile is the source of truth for which dep versions get documented. ox doc --document-deps against a workspace with ufo = "0.2.4" in ox.lock documents ufo@0.2.4 from the cache. If ox.lock is missing, --document-deps errors out with a pointer to ox install (consistent with ox audit).

Caching strategy

Per-dep doc rendering is content-addressable. The output of building cofris@0.2.2’s Site from ~/.argon/packages/cofris/0.2.2/ depends only on:

  • The dep’s source content-hash (already in ox.lock).
  • The IR schema version (oxc-doc::DOC_IR_SCHEMA_VERSION, currently 1.1.0).
  • The set of doc extensions in scope from packages the dep transitively depends on.

The natural cache path is ~/.argon/cache/doc/<source-hash>-<schema>.json carrying the serialized Site. ox doc --document-deps checks this cache before re-elaborating; cache hits skip the entire elaborate pipeline for that dep. Cache invalidation is automatic — content-hash and schema bumps both produce new keys.

This is a substantial perf win for the common case: a workspace’s deps don’t change between ox doc runs, so re-rendering only touches the workspace’s own changed members.

URL synthesis

Three regimes:

ModeExternal-dep qname URL synthesis
ox doc (default)Cross-package links to deps render as styled text with no href (current Phase 4 behaviour for unresolvable qnames).
ox doc --document-depsCross-package links resolve to relative paths in the same target/doc/ tree (../../<pkg>/<ver>/<pkg>/concept.X.html).
ox doc post-docs.argon.devCross-package links resolve to https://docs.argon.dev/<pkg>/<ver>/<pkg>/concept.X.html. The renderer reads the registry’s base URL from a workspace-level [doc] config or a built-in default.

The third regime is preserved as the eventual default once docs.argon.dev exists. Until then, --document-deps is the way to get clickable cross-package links.

Workspace dep graph

WorkspaceSite.external_dependencies carries the externals today. Under --document-deps, the IR builder demotes each documented external into a full WorkspaceMemberSummary (with depth = 0 since externals are by definition foundations from the workspace’s perspective; sub-depth ranking among externals reads the dep graph and is computed inside the document-deps walk). The “external” row collapses; the foundation tier expands. Visually: the dep graph shows everything as first-class.

Site.is_external_dep flag

Pages from external deps are rendered identically to workspace members but marked so the renderer can:

  • Add a “Dependency” subtitle on the package index (“Documented from ~/.argon/packages/cofris/0.2.2/”).
  • Disable doc-test execution (ox test --doc runs only on workspace-owned items per RFD-0029).
  • Skip the “View source on <host>” affordance once that lands.

The flag is a single optional field on Site; doesn’t change the wire shape for workspace-only renders.

Non-goals

  • Network fetching during ox doc. Deps must already be in the local cache. ox install is the network-touching command; ox doc reads from disk only. (Future: ox doc --document-deps --auto-install could implicitly invoke ox install first, but that’s a UX-layer convenience over this RFD’s mechanics.)
  • Per-dep doc-extension scoping. [package.docs.panel] declarations in dep manifests already scope by transitive workspace dep closure (RFD-0029 Phase 4 fix). --document-deps doesn’t change this rule — a dep’s own manifest panels apply to its own pages, governed by the same closure rule.
  • Cross-version doc rendering (e.g. documenting ufo@0.2.3 and ufo@0.2.4 simultaneously). The lockfile pins one version per package; that’s what gets documented. Multi-version is a cargo doc non-feature too.
  • Registry-side doc hosting. docs.argon.dev is out of scope here. This RFD specifies the local-side URL synthesis once that host exists; the host’s own architecture (build-on-publish, on-demand build, etc.) is a separate problem.

Open questions

  1. Should --document-deps be the default before docs.argon.dev ships? Cargo defaults to documenting deps; we currently don’t. Defaulting to --document-deps produces a working docs tree out of the box but slows down ox doc substantially (factor of N for an N-dep workspace, mitigated by the doc-cache). My current bias: default off until docs are fast enough that the perf hit is invisible, then flip. Open for discussion.

  2. Lockfile-less --document-deps. A workspace member may run ox doc --document-deps from a checked-out source tree that happens to have ox.lock present but never resolved (e.g., fresh clone, ox install not yet run). Hard error or silently skip externals? Current bias: hard error pointing to ox install, matching ox audit’s posture.

  3. Doc-cache eviction policy. Content-hashed entries never collide, but the cache grows monotonically. Evict by LRU once ~/.argon/cache/doc/ exceeds N MB? Track via size + atime, periodic compaction in ox install? Defer for now — wait for the cache to actually grow before solving.

  4. Path-dep externals semantics. A workspace member declaring cofris = { path = "../shared/cofris" } has a path-dep external. Should --document-deps document it? Cargo’s behaviour: yes, but the path becomes a stability hazard (a path-dep at a different version than the workspace’s other consumers). My bias: yes, with a stern warning when the documented version diverges from any sibling member’s resolved version.

  5. is_external_dep flag granularity. Single boolean is enough for the v0 surface, but future work (semver-check, doc-coverage, dep-graph analysis) may want richer metadata: source-kind, content-hash, lockfile-pinned version. Bias: ship the boolean now; widen to a struct (Option<ExternalSource>) when a second consumer needs it.

Implementation plan

PhaseWorkStatus
0External-dep visibility in workspace dep graph (the “external” row, no click target).Landed 2026-05-05
1RFD ratified + lockfile-walking helper in ox::resolve that yields the cached source path + content-hash per transitive dep.This RFD
2--document-deps flag plumbed through ox docrun_doc_workspace + run_doc_single. Per-dep build_doc_for_project invocation against the cached source.Follow-up commit
3Doc-cache at ~/.argon/cache/doc/<hash>-<schema>.json. Skip elaborate when hit.Follow-up commit
4Site.is_external_dep: bool IR field + renderer-side breadcrumb / subtitle treatment.Follow-up commit
5URL-synthesis switch for the docs.argon.dev regime — workspace [doc] config field + renderer’s qname resolver respects it.When docs.argon.dev exists
6Default flip: ox doc becomes --document-deps (or docs.argon.dev-rewriting) by default; --no-deps opts out.Coordinated with docs.argon.dev launch

Connections

  • Builds on RFD-0029 — Doc comments and ox doc. The IR contract there is the load-bearing artifact this RFD extends.
  • Coordinates with the future docs.argon.dev registry-doc host (separate effort).
  • Touches ox.lock content-hash invariants — see RFD-0024 — Diagnostic codes for the OE3xxx package-loader code namespace; new errors here register under that scheme.

RFD-0031 — Concept-reference endpoints on pub metarel

Accepted Opened 2026-05-06 · Status: Accepted

Question

How does a pub metarel declare the concepts it bridges, when the natural modeling vocabulary names concepts (e.g. UFO’s Aspect → ConcreteIndividual) but the existing constraint surface only accepts metatype keywords and axis values?

Context

pub metarel <Name> = { source: <path>, target: <path>, … } declares a relation metatype. The validator (validate_metarel_applications in oxc/src/elaborate/validate.rs) walks each pub rel … :: <metarel> direct application and confirms the relation’s endpoints satisfy the metarel’s source: / target: clauses. Before this RFD, “satisfies” had two arms:

  1. Metatype-keyword matchsource: relator accepts any concept declared with the relator metatype keyword. Last-segment string compare against concept.metatype.
  2. Axis-value matchsource: endurant accepts any concept whose metatype’s axis profile includes (nature_source, endurant). Resolution: walk engine.axes_map(), find the axis declaring endurant as a value, then check the endpoint’s metatype profile.

Neither arm covers the natural UFO case. The reference port gufo:inheresIn declares rdfs:domain gufo:Aspect; rdfs:range gufo:ConcreteIndividual — at the umbrella concept level, not at a leaf metatype level. UFO’s characterization metarel is itself “moments characterize endurants,” which a careful modeler wants to express as source: Aspect, target: ConcreteIndividual (the concepts that bound the relation), NOT source: moment, target: endurant (the axis-value substitutes).

The pre-RFD workaround forced one of three uncomfortable choices:

  • Relax constraints — drop source: / target: and lose endpoint validation entirely. Quick but throws away the semantics.
  • Mint umbrella metatypes — declare pub metatype aspect_um = { …, moment } and re-classify Aspect with it. Works but pollutes the metatype surface with bookkeeping types that exist only to mediate between concept-level relations and axis-level constraints.
  • Demand leaf metatypes everywhere — change gufo:inheresIn from Aspect → ConcreteIndividual to a fan-out of leaf-typed relations (one per moment subtype). Departs from the spec, multiplies relation declarations, and breaks downstream tooling that expects the umbrella shape.

The Sharpe internal port of UFO hit this in 2026-05; the pragmatic fix landed there used Option 2 (umbrella metatypes) with comments documenting the language gap and pointing at this RFD.

Decision

Add a third resolution arm to check_endpoint: concept-reference subtype check. Resolution order becomes:

  1. metatype-keyword match (unchanged)
  2. axis-value match (unchanged)
  3. concept-reference subtype match (new)constraint_last is the local name of a registered pub <metatype> <Concept> in the elaborated module. The endpoint satisfies the constraint iff the endpoint concept’s transitive <: closure contains the constraint concept (or equals it).
  4. fallback: emit OE0226 with the unresolved-reference subtree message.

The new arm is keyed on the same Vec<String> path the existing arms see; no grammar change is required. Resolution is by last-segment local-name match against CoreConcept.name, mirroring the metatype-keyword arm’s last-segment compare. Collisions between a concept named X and a metatype keyword named X resolve in favor of the metatype (Arm 1 wins by precedence) — preserving the legacy interpretation where it exists.

A new diagnostic helper relation_endpoint_subtype_mismatch distinguishes “concept resolved but the relation’s endpoint isn’t a subtype” from the fallback “constraint resolved to nothing.” Same OE0226 code; different message body and help so the modeler sees the subtype lattice as the actionable surface, not the metatype profile.

Consequences

For modelers. The natural UFO style works:

pub metarel characterization = {
    source: Aspect,
    target: ConcreteIndividual
}
pub rel inheres_in(s: IntrinsicMode, t: Quality) :: characterization

IntrinsicMode <: Aspect and Quality <: ConcreteIndividual (assuming the lattice declares those edges), so the application validates without anyone declaring a bookkeeping metatype.

For the existing axis-value style. No change. Modelers who prefer leaf-axis constraints (source: moment, target: endurant) keep their existing surface — Arm 2 still fires first when the constraint resolves to an axis value. Mixed-style metarels work too: a metarel can have source: Aspect (concept) and target: endurant (axis value) without the validator caring which arm catches each side.

For the umbrella-metatype workaround. Codebases that introduced umbrella metatypes (aspect_um, endurant_um, concrete_individual_um) to dance around the gap can revert to UFO-faithful declarations. Migration is mechanical: drop the umbrella metatypes, swap the metarel constraints back to concept names, and re-classify the umbrella concepts to their original metatypes (pub category Aspect, etc.).

For the LSP InfoView. Hover on a metarel endpoint that resolves as a concept reference can now show the resolved <: chain — actionable affordance for “why does my relation satisfy this constraint” introspection. Not part of this RFD’s scope, but the IR change is what enables it.

Implementation

Landed on argon/metarel-concept-endpoints (commit TBD on merge). Changes:

  • oxc/src/elaborate/validate.rs::check_endpoint — new Arm 3 between the existing axis-value and fallback arms; helpers find_concept_by_local_name and is_subtype_or_self (BFS over concept.supers with cycle guard).
  • oxc/src/diagnostics/rendering.rs — new relation_endpoint_subtype_mismatch constructor for OE0226.
  • oxc/tests/decl_forms_metarel_decorator.rs — four new tests covering subtype acceptance, self-reference, sibling-rejection, and transitive-supers walk.

No grammar change. No wire-format change. No bump to OE0226’s code or metadata — same code, expanded message surface.

Open questions

  • Multiple concepts with the same local name. When two concepts in the elaborated module share name = "Foo", find_concept_by_local_name returns the first by BTreeMap iteration order (id-sorted). Deterministic across runs but not user-controllable. A future iteration could surface a multi-match warning at metarel-declaration time, prompting the modeler to qualify (source: my_pkg::Aspect).
  • Path-qualified concept references. Today the resolution uses last-segment compare; the multi-segment path is preserved on the metarel record for diagnostics but not used during lookup. A follow-up could prefer exact-path match over last-segment match when the metarel’s constraint path has more than one segment.
  • Cross-package umbrella concepts. When the umbrella concept lives in a dependency (e.g. ufo::Aspect), the elaborated module’s concept registry already includes the dep’s concepts. Last-segment match works. Verified via the registry-dep pathway in the test corpus.

RFD-0032 — Shared-base package classification for workspace-vendored deps

Accepted Opened 2026-05-06 · Status: Accepted (Phase 1)

Question

When a customer overlay vendors a foundational package (UFO, std, …) as a workspace member instead of pulling it from the registry, how does the bundle assembler classify that member so the kernel-side compile path treats it as SharedBaseProvided rather than as a tenant-emitted overlay?

Context

The oxc_runtime::PackageRole enum has four variants — Root, WorkspaceMember, Dependency, SharedBaseProvided. The kernel-side compile pipeline (kernel/api/src/v2/schema/compile.rs) filters emit output for SharedBaseProvided packages so their declarations don’t land as tenant axioms — the kernel’s base tenant (SHARED_BASE_TENANT_ID) carries equivalent declarations already, and per-tenant duplication would shadow-collide on reload (OE7062).

The classification happens in argon/ox/src/bundle.rs::build_virtual_source_bundle. Before this RFD it consulted BundleBuildOptions::shared_base_policy only for locked external deps — packages installed into ~/.argon/packages/ and pinned in ox.lock. Workspace members got Root (for the package being compiled) or WorkspaceMember (for siblings) regardless of policy.

That left a gap for codebases that vendor foundational packages as workspace members:

  • The Sharpe internal overlay (sharpe-ontology) keeps UFO source as a sibling workspace member of the customer’s domain package. Hand-authoring UFO concepts inside the same workspace is convenient; pulling UFO from the registry would force a publish-rebuild loop on every UFO edit.
  • The Argon examples/lease-story demo vendors UFO + economic-foundation packages as workspace members for similar reasons.

Without the workspace-member policy consult, both setups end up uploading UFO twice: once via the bundle (as WorkspaceMember, emit-output stored as overlay axioms), and once into the kernel’s base tenant via the out-of-band base-loader path. The second store wins on read but pollutes the overlay’s storage and triggers shadow-collision warnings.

Workarounds before this RFD:

  • Drop the foundational package from the workspace and pin it as a registry dep instead. Defeats the “edit UFO inline” workflow.
  • Manually massage the bundle’s PackageRole after the fact. Bypasses the public API; brittle.
  • Live with the duplicate-emit cost. What the customer overlay was effectively doing.

Decision

build_virtual_source_bundle consults BundleBuildOptions::shared_base_policy for non-Root workspace members. Role precedence becomes:

  1. Root — the package the bundle is centered on (selection target). Always Root regardless of policy. A modeler editing UFO directly inside a workspace where they own UFO’s source is the package’s author — they want emit output back from the kernel.
  2. SharedBaseProvided — non-Root members whose canonical name matches policy.is_shared_base_provided(name). The kernel filters their emit; the kernel base provides equivalent declarations.
  3. WorkspaceMember — every other non-Root member.

The default BundleBuildOptions::default() constructs an empty policy, so codebases that have not opted in keep the prior WorkspaceMember classification. BundleBuildOptions::kernel_v2() (used by first-party loaders for the kernel-api v2 endpoint) seeds the policy with the kernel’s authoritative shared-base list (std, ufo, core, coex, cofris, ccf).

Locked-dep classification is unchanged — Arm 1 (workspace member root precedence) does not apply to lockfile packages, and the existing locked-dep path already consults the policy.

Consequences

For modelers. Customer overlays that vendor UFO as a workspace member now have UFO classified as SharedBaseProvided automatically, when uploading through a first-party loader. No bundle-level surgery, no manual policy override per call site.

For codebases on BundleBuildOptions::default(). No change. Empty policy preserves the prior classification.

For examples/lease-story. Loaders using BundleBuildOptions::kernel_v2() will now classify lease-story’s vendored ufo / coex / cofris / ccf as SharedBaseProvided. The kernel filter strips their emit — matches the existing locked-dep behavior. Lease-story’s local-toolchain-only tests (which don’t involve the kernel) are unaffected.

For the LSP and InfoView. The LSP doesn’t run the bundle assembler; this change is invisible to editor-side resolution. Only the kernel-upload path sees the new classification.

Implementation

Landed on kernel/base-schema-compile-resolution (Phase 1). Changes:

  • argon/ox/src/bundle.rs::build_virtual_source_bundle — workspace-member role assignment now consults options.shared_base_policy. Same precedence for Root. New SharedBaseProvided arm for non-Root members.
  • argon/ox/src/bundle.rs::tests — three new tests covering (a) workspace-member shared-base classification, (b) Root precedence over policy, (c) empty-policy backwards-compat.

No changes to public API. No changes to wire format. No changes to the policy endpoint or storage layer.

Phase 2 (planned, separate RFD)

The kernel-side base loader has loose ends that this RFD does not address:

  • Source-of-truth coupling. The hardcoded SHARED_BASE_PROVIDED_PACKAGES list in kernel/api/src/v2/schema/package_policy.rs does not consult the actual base-tenant content. A customer who renames or repartitions UFO would have to update the kernel binary’s policy list to match — coupling at compile time when the kernel and customer overlays could be on different release cadences. The fix is for the policy endpoint to read the live base-tenant package metadata.
  • Argon-source base loading. The current base loader (orca-db-resetkernel/owl-loader) loads UFO from OWL2 EL++ Turtle files. A future Phase 2 ships a canonical Argon-source UFO package, compiles it kernel-side at DB-reset time, and stores axioms under SHARED_BASE_TENANT_ID directly from CoreModule. Drops a translation layer; aligns content-hash provenance.
  • Customer override. A customer who wants a different UFO variant (or a non-default foundational stack) needs a way to pin it. The natural surface is a per-tenant config row that overrides the default base packages; the kernel’s policy endpoint then returns the per-tenant overrides instead of the global default.

These are tracked as RFD-0033 (planned).

Open questions

  • What should BundleBuildOptions::default() ship with? Today it returns an empty policy, which preserves backwards-compat. The argument for changing the default to SharedBasePolicy::kernel_v2() is that it’s the right thing for the dominant first-party-loader case. The argument against is silent-behavior-change for any third-party code consuming bundle::build_virtual_source_bundle directly. Phase 1 keeps the conservative empty default; revisit when Phase 2 lands.
  • Should SharedBasePolicy::kernel_v2() continue to be a hardcoded list, or read from a feed? Hardcoded works while the base content is stable. A feed (e.g., served by the kernel-api alongside the policy endpoint) lets the kernel evolve the base without forcing a toolchain rebuild on every consumer. Tracked alongside the policy endpoint redesign in Phase 2.

RFD-0033 — Sequenced test statements: mutate and cleanup in test blocks

Discussion Opened 2026-05-06 · Revised 2026-05-06

Question

How does a test block exercise a pub mutation end-to-end — verify its require preconditions hold, that the do { } body’s effects reach the test ABox, that emitted events surface in post-saturation queries, that retracted individuals leave — and how do tests express ordered teardown that runs regardless of mid-test assertion failures?

Context

pub mutation is a first-class declaration form (D-064) with five clauses: require { <atom> } preconditions, do { <stmt>... } field updates and locally-bound individuals, retract { <pattern>... } removals, emit <expr> event emission, return <expr>. Each mutation produces axiom-events into the kernel’s bitemporal event log at production runtime; in tests, the runner has its own forked Knowledge ABox.

The Phase-B language redesign (vault, 2026-04-24, Move 1) locks the test context as { stmts } — imperative, source-ordered — with statements drawn from let / mutate / assert / cleanup. Today the test-runner grammar admits only let and assert, flattened during elaboration into parallel individuals: Vec<CoreTestIndividual> and assertions: Vec<CoreTestAssertion> vectors. There is no source-order preservation between them: every let materializes individuals into the ABox, the runner saturates once, then every assert evaluates against the post-saturation state.

The lease-story scene-test convention works around the missing mutate form by let-binding the mutation’s would-be emitted events as if the mutation had run, then asserting the post-state shape. That convention:

  • Verifies the parameter types are constructible.
  • Verifies emitted-event shapes match downstream consumers.
  • Does not evaluate require preconditions.
  • Does not verify do { } field updates take effect.
  • Does not verify emit clauses fire and produce the events the assertions claim are present.
  • Does not distinguish “mutation failed precondition” from “mutation succeeded but assertion is wrong”.

Customers writing legal / financial domain ontologies on top of Argon want a contract test for the mutation surface they’re shipping: when a modeler hands record_rent_payment an is_timely: false payment, the test should fail with a structured “precondition violated” signal rather than appearing to succeed because no require clause ever evaluated.

The redesign also names a cleanup { } block as the fourth test-statement form. Today the test runner has no notion of teardown. For v1 isolated-ABox tests the practical role of cleanup is structural separation + ordered post-main statements that run regardless of mid-test assertion failures, so modelers can express “exercise the operation, observe state, then exercise the teardown mutation, observe again” without losing teardown coverage to a mid-test assertion drift.

Proposal

Two new TestStmt variants — Mutate and Cleanup — and a structural shift in how the test runner consumes test bodies.

Statement grammar

The test body becomes a source-ordered sequence drawn from four statement kinds:

test "rent payment passes timeliness check" {
    let p: RentPayment = {
        paid_on: 2025-03-03,
        amount: 9500,
        period_label: "2025-03",
        is_timely: true,
    }

    mutate record_rent_payment(p)

    assert RentPayment(p)
    assert tenant_balance(p.tenant) == 0

    cleanup {
        mutate retract_test_payment(p)
        assert not RentPayment(p)
    }
}

let and assert keep their current semantics. mutate and cleanup are new.

CoreTest shape change

CoreTest gains statements: Vec<CoreTestStmt> as the source-ordered statement list. The existing individuals: Vec<CoreTestIndividual> and assertions: Vec<CoreTestAssertion> vectors are derived views computed from statements for backwards compatibility; new code reads statements directly.

#![allow(unused)]
fn main() {
pub enum CoreTestStmt {
    Let(CoreTestIndividual),
    Mutate(CoreMutateCall),
    Assert(CoreTestAssertion),
    Cleanup(Vec<CoreTestStmt>),
}

pub struct CoreMutateCall {
    pub mutation_id: u64,
    pub args: Vec<CoreRuleAtom>,
    pub span: Span,
}
}

Cleanup carries its own statement list — a cleanup { } block admits three statement kinds (let, mutate, assert); cleanup blocks do not nest. A test admits at most one cleanup block; it must be the last statement. Multiple cleanup blocks fire OE0240 MultipleCleanupBlocks; cleanup at non-last position fires OE0241 CleanupNotAtEnd; nested cleanup fires OE0242 NestedCleanup.

Elaboration

For each Mutate { path, args, span }:

  1. Resolve <path> against the elaborator’s ModuleScope (scope.local + .imports + .re_exports) — the same surface that resolves any cross-package imported item — and filter the resolved SymbolInfo to SymbolKind::Mutation (already a distinct symbol-kind variant in oxc::elaborate::SymbolKind, sibling to Query / Computation). The predicate-call resolver in eval_predicate_call is not the right path — mutations aren’t predicates and aren’t reachable from that surface. The resolved SymbolInfo.id keys into CoreModule.mutations for the CoreMutation to bind on the resulting CoreMutateCall. Unknown name (or a name that resolves to a non-mutation symbol kind) fires OE0237 UnknownMutation.
  2. Validate arg arity vs. parameter count. Mismatch fires OE0238 MutationArgArityMismatch.
  3. Validate each arg against the parameter’s declared type via the existing let-binding type-resolution path. Mismatch fires OE0239 MutationArgTypeMismatch.

For Cleanup { stmts }: recursively elaborate each inner statement under the same elaboration context. Inner Cleanup is structurally rejected (OE0242 NestedCleanup); cleanup blocks don’t nest.

Runtime semantics

The test runner replaces its current “materialize-all → saturate-once → check-all” shape with a per-statement saturation loop.

Pre-loop:

  • Fork Knowledge. Materialize using_frames and fixture.resolved. Run an initial saturation so frame + fixture facts are saturated before the first user statement runs.

Main loop, for each statement in statements (excluding the trailing Cleanup):

StmtRuntime
LetMaterialize the individual into the ABox via the existing materialize_individual() path. Re-saturate.
Mutate(call)Look up the mutation by id. Evaluate each require atom against the current post-saturation ABox. If any returns false, record a MutationPreconditionFailure failure and skip the mutation’s do / retract / emit clauses for this call; continue to the next statement against the unchanged ABox. If all require atoms pass: apply retract { }, apply do { } field updates and any do { let } local bindings, evaluate and insert each emit <expr> (see §Emit semantics below). Re-saturate.
AssertEvaluate the assertion against the current post-saturation ABox. Record pass / fail. Continue the loop on failure — the runner reports every assertion’s outcome; it does not halt on the first failure.

After the main loop, run the Cleanup block (if present). Cleanup statements process the same way as main-loop statements with one difference: failures inside cleanup are tagged with a cleanup: true flag on the TestFailure so the runner output distinguishes “the operation failed” from “the teardown failed.” Cleanup runs regardless of whether main-body assertions failed. A test where main-body assertions all passed but cleanup fails is reported as failed.

Multiple mutate statements compose left-to-right with re-saturation between each, so a require in mutation B that queries a derived fact produced by A’s do update sees the post-A saturation state, not the pre-A state.

Propagation when A’s precondition fails. If A’s require returns false, A’s do / retract / emit clauses are skipped (see step 2 above) and the ABox stays unchanged. The runner does not halt the statement loop — it continues to B with the unchanged ABox. B’s require evaluates against pre-A state; if it holds, B applies normally. The modeler sees the test fail (A’s MutationPreconditionFailure is recorded) and can read every subsequent statement’s outcome. This matches the design choice for failed assertions (failed mid-test asserts also don’t halt). The cost is that a chained-mutation test where B depends on A’s effects will see B fail or behave unexpectedly when A is skipped — but the source ordering of the failures (A first, then B’s drift) makes the dependency obvious. Halting after A’s failure would hide downstream drift; running B preserves it.

The return <expr> value is discarded in v1. A future v2 may add let result = mutate <name>(<args>) to bind it.

Emit semantics

emit <expr> is the canonical event-insertion mechanism per D-064. Within a mutation, each emit clause’s expression is a value-position expression. Two shapes are common in practice:

  • Constructor formemit RentPaid { lease, amount, when: today() }. The expression is a typed record literal naming an event-class constructor.
  • Path-reference formemit p, where p was bound earlier in the mutation’s do { let p: ... = { ... } } block. The expression is a path that resolves to an existing individual.

At runtime — production or test — the semantics:

  1. Evaluate the emit expression against the post-require, post-retract, post-do ABox plus the mutation’s parameter bindings and any do { let } local bindings.
  2. Path-reference case — the expression resolves to an individual already in the ABox (the do { let } binding materialized it). The runtime treats the emit as already-realized for ABox purposes; no fresh id is minted, no duplicate insertion. Production additionally publishes the existing individual into the axiom-event log.
  3. Constructor case — the expression evaluates to a typed record value. The runtime mints a fresh individual id, inserts a new individual of the expression’s type with the record’s field values via the same materialize_individual() path the runner uses for let. Production additionally writes the corresponding axiom-event entry.
  4. In both cases, the event individual is visible to subsequent saturation and to assertions in the test that follow the mutate statement.

There is no requirement that the emit target be a previously let-bound name. The earlier “pre-bind via let” workaround was a regression from the canonical semantic and is dropped. Tests that assert on emitted events — mutate record_rent_payment(p); assert RentPaid(p.lease, p.amount) — exercise the emit clause genuinely: the assertion only holds when the runtime actually evaluated and inserted (or re-realized) that event.

Sequential assertion ordering — explicit semantic shift

Today, tests behave as let* assert* with a single saturation between the two phases. After this change, asserts evaluate at their statement position with re-saturation between mutates. For tests that contain only let and assert statements, the observable behavior is unchanged: all lets materialize before the first assert, the ABox saturates, and assertions evaluate against the saturated state. Tests that mix mutate in get the per-statement saturation semantics.

This is a deliberate semantic shift, not an accident of implementation. Modelers should be able to read the test top-down and reason about it as a temporal sequence: “after the lease starts, this should hold; after the first payment, this other thing; after termination, neither.”

Diagnostic codes

Allocated in clusters 23x–24x (extending the existing OE022x metarel/decorator range):

  • OE0237 UnknownMutation — mutation name in a mutate stmt doesn’t resolve.
  • OE0238 MutationArgArityMismatch — arg count doesn’t match <mutation> parameters.
  • OE0239 MutationArgTypeMismatch — arg’s resolved type isn’t <: the parameter’s declared type.
  • OE0240 MultipleCleanupBlocks — a test declares more than one cleanup { } block.
  • OE0241 CleanupNotAtEnd — the cleanup { } block is followed by another statement.
  • OE0242 NestedCleanup — a cleanup { } block contains another cleanup { }.

Test-runtime failures (precondition violation, cleanup-tagged failures) surface as TestFailure entries on the existing SingleTestResult.failures channel, not as new diagnostic codes. They distinguish themselves via structured kind and cleanup fields on TestFailure.

Consequences

For modelers — what v1 catches.

  • Existing-precondition violations. A test that hands record_rent_payment an is_timely: false payment now fails with MutationPreconditionFailure rather than silently passing because no require clause ever evaluated.
  • emit clauses. Asserting an emitted event after a mutate statement is now meaningful coverage of the emit clause: the runtime evaluates the emit expression, mints an event individual, inserts it into the ABox, and the assertion succeeds only if all of that happened. Previously assertions on emitted events were satisfied by the pre-binding regardless of whether the mutation ran.
  • Multi-mutation flows (composition). mutate A(...); mutate B(...); assert <post-state> proves the chained effect, with re-saturation between steps so B’s require sees A’s derived facts.
  • Teardown ordering. cleanup { ... } runs after main-body statements regardless of mid-test assertion failures, so a teardown mutation + its post-state assertion are exercised even when an earlier assertion drifted.

What v1 does NOT catch — explicit v2 scope.

The v1 runtime is honest about which mutation clauses it evaluates against the test ABox vs. which it surfaces as MutationUnsupported failures so the modeler sees the gap rather than a silent pass:

  • require clauses — fully evaluated. v1.
  • do { let } clauses — type assertion lands; field assignments inside the let’s value expression do not yet bind. The let-bound name is visible to subsequent assertions only as a typed individual.
  • do { x.field = expr } field-updates — surface as MutationUnsupported. A test that asserts post.field == ... after a field-update mutation will see the runner flag the gap explicitly. v2.
  • retract { } clauses — surface as MutationUnsupported per clause. mutate teardown(x); assert not Concept(x) reports the unsupported flag rather than silently passing or failing on the assertion. v2.
  • emit Event { ... } constructor — mints a fresh event individual with literal field values. Computed field expressions ({ when: today() }) are skipped in the field-args extraction; v1 covers literal-typed emit args only.
  • emit p path-reference — no-op (the source individual is already in the ABox via the do { let } that bound it). v1.
  • Dropped preconditions. A record_rent_payment whose require { p.is_timely } line was deleted from source still appears to pass v1 tests — there’s no atom left to evaluate. Detecting “this mutation should have a precondition but doesn’t” requires a separate mutate_fails <name>(<bad-args>) assertion form. v2.

The v2 work plan: implement field-update propagation against KnowledgeStore, implement retract (ByAssertionId at minimum), evaluate do { let } value-expression bindings, lift emit’s field-arg evaluation to the full atom-value evaluator, ship mutate_fails. Each is a clean follow-up against the v1 surface this RFD lands.

For the existing scene-test convention. Tests that don’t use mutate or cleanup continue to materialize-and-assert as before. The lease-story scene_04 / scene_06 / scene_11 / scene_13 tests can opt into mutate incrementally; they don’t need to change to keep passing.

Sequential assertion semantics. Tests that contain only let and assert retain today’s observable behavior: a single saturation, then all assertions evaluated against the same post-saturation state. Tests that introduce mutate get re-saturation between statements. A test author who introduces a mutate and observes that an assertion that previously passed now fails should investigate whether the mutation legitimately changes the asserted state — the change is signal, not noise.

For the LSP InfoView. Hover on a mutate <name>(...) site can show the mutation’s parameter list + require / emit / do / retract clauses — actionable affordance for “what is this mutation contracting?” introspection. Cleanup blocks render as a folded region by default with their own statement count.

Implementation surface

Estimated 10–14 hours across:

  • oxc/src/syntax/kind.rs — new MUTATE_STMT + CLEANUP_STMT syntax kinds; mutate and cleanup as contextual keywords in test bodies.
  • oxc/src/cst/parser.rs — recognize mutate <path>(<args>) and cleanup { stmts } inside test { } bodies; produce MUTATE_STMT / CLEANUP_STMT nodes.
  • oxc/src/cst/lower/items.rs — lower to ast::TestStmt::Mutate { path, args, span } and ast::TestStmt::Cleanup { stmts, span }.
  • oxc/src/ast.rs — new TestStmt::Mutate + TestStmt::Cleanup variants.
  • oxc/src/core_ir.rs — new CoreTestStmt enum and CoreMutateCall struct; thread statements: Vec<CoreTestStmt> onto CoreTest. Keep individuals + assertions as derived views during the migration window.
  • oxc/src/elaborate/phase_elaborate.rs::resolve_test — name resolution + arg validation; emit OE0237OE0242. Recursive elaboration into Cleanup body.
  • oxc/src/diagnostics/codes.rs + rendering.rs — six new diagnostic helpers.
  • oxc/src/test_runner.rs — replace the materialize-all → saturate-once → check-all shape with a per-statement loop. New helpers: apply_mutation (evaluates require, applies retract/do/emit, re-saturates), evaluate_emit_expr, mint_event_individual. Cleanup-tagged TestFailure flag.
  • oxc/src/reasoning/atom_eval.rs — confirm eval_atom_truth covers every atom shape that appears in require { } bodies; extend if any are missing.
  • oxc/tests/test_framework_runner.rs — exercise: passing precondition, failing precondition, multi-mutate composition, emit insertion + post-saturation visibility, retract + assert-not, cleanup runs after main-body failure, cleanup-only failures, unknown-mutation diagnostic, arg-mismatch diagnostic, multiple-cleanup diagnostic, cleanup-not-at-end diagnostic, nested-cleanup diagnostic.
  • argon/examples/lease-story/packages/story-lease/src/tests/scene_04_security_deposit.ar — opt the existing test block into mutate record_security_deposit(...) to demonstrate the surface; add a cleanup block that exercises deposit-return.
  • argon/book/src/ — section in the test-runner chapter documenting the four-statement grammar + per-statement saturation + emit semantics + cleanup ordering.

Open questions

  1. let result = mutate <name>(<args>) for return-value binding. Punting to v2 keeps v1’s surface minimum-viable. The dominant case (precondition + emit + assert) ships first; return-binding is additive.
  2. retract semantics under saturation. When a retract removes an individual, derived facts that depended on it must also be removed. v1 picks re-saturate-from-scratch for simplicity; cost is acceptable at test sizes (typically < 100 individuals). Incremental retraction is a future optimization.
  3. mutate_fails <name>(<args>) assertion form. A statement that asserts the precondition fails — useful for testing the negative case, and the path to v2 catching dropped-require regressions. Out of scope for v1.
  4. Cleanup ordering vs. test-attribute interaction. #[unproven] and #[assumed] apply to the test as a whole. Whether cleanup runs for #[assumed] tests (which the runner doesn’t actually evaluate) is a small semantic question; v1 picks “yes, cleanup runs” for symmetry with the other test-attribute paths, on the principle that cleanup should be observable side-effect-wise even when the test body’s assertions aren’t being checked.

RFD-0034 — Composition pipeline and the oxc / ox boundary

Committed Opened 2026-05-04 · Committed 2026-05-04

Question

What does it mean to take N elaborated Argon packages and produce a single closed input that a runtime can consume? Where does the work split between the per-package compiler (oxc) and the workspace orchestrator (ox)? What is the wire shape of the per-package output, and what is the wire shape of the workspace-level composed artifact?

Context

Today’s Argon pipeline stops halfway. Source elaborates through oxc into CoreIR serialised as JSON, and the toolchain ends there. There is no discrete pass that closes cross-package references, merges standpoint lattices, validates tier consistency across the workspace, or produces an artifact a runtime can load. Three implicit composition paths exist instead — the kernel loading packages into a tenant overlay, the test runner materialising using <frame> bodies, the planned ox doc cross-rendering across packages — each implementing its own version with no shared model.

The cost of this gap is structural: Argon’s runtime concerns leak into every consumer, the kernel becomes the de facto runtime by absorption, and there is no portable artifact to ship a built workspace anywhere else. Decoupling Argon from any single runtime requires a discrete composition phase with a typed wire shape between elaboration and execution.

The PL-modules literature has already converged on the answer. Backpack ’17 (Yang, Eisenberg, Yorgey, Peyton Jones et al., POPL 2017), the WebAssembly Component Model (WebAssembly Community Group), and the Engine / Module / Store factoring shared across Wasmtime, JVM HotSpot, and Truffle / GraalVM all reach the same two-phase pipeline: a workspace-level pass computes wiring (cross-component reference resolution, signature unification) and produces a composition signature; a per-component compiler instantiates against that wiring and produces a typed artifact. Three independent designs from three different lineages converging on the same shape is not accidental.

Argon’s existing oxc / ox split already implements half of this without saying so. The other half — the explicit composition pass, the typed per-package output, the wire shape — is what this RFD locks.

A research campaign preceding this RFD ([vault: research/argon-execute-layer/]) surveyed the space across six tracks (PL modules, KG persistence, standpoints, saturation/provenance, runtime/execution, artifact formats) and produced a primary-source-grounded set of recommendations. The campaign’s most striking finding is that the structural shape Argon needs is settled by literature; the genuine invention surface lives almost entirely on the artifact side (RFD-0035) rather than the composition side. RFD-0034 ratifies the convergent shape; RFD-0035 specifies the artifact.

Decision

Argon adopts a two-phase composition pipeline with a sharp oxc / ox boundary, an explicit compose pass, and typed wire shapes at every stage.

1. Pipeline shape

   per-package source
         │
         │  elaborate                ← oxc owns
         ▼
   per-package CoreIR
         │
         │  instantiate              ← oxc owns; consumes wiring
         ▼
   per-package .oxc cache
         │                          ─┐
         │                           │  ox owns
   (workspace metadata + lockfile)   │  (compute wiring +
         │                           │   composition signature
         │  compose                  │   from package graph)
         ▼                          ─┘
   workspace .oxbin
         │
         │  consume                  ← runtime backend (kernel,
         ▼                              in-process, sandboxed, …)
   answers / docs / saturated state / …

Three stages, three artifacts. Each artifact is typed; each transition is a well-specified pass.

2. ox is the workspace orchestrator

ox owns everything that requires a workspace-level view of the package graph:

  • Wiring computation. Resolve every use path across the workspace + lockfile-pinned dependency cache. Produce a wiring diagram — a typed mapping from every public-eligible symbol reference to the qualified path of its target. Identical to Backpack ’17 Cabal-side mixin linking; identical to WASM Component Model component-type signature resolution.

  • Standpoint lattice composition. Workspaces composed from N packages may declare standpoints in any of them. Standpoints from different packages identify by stable UUID, never by name. Lattice composition is union with cycle rejection. Lattice cycles surface as OE0905 StandpointLatticeCycle. Soundness invariant: the composed lattice is a DAG.

  • Tier consistency check. Every rule’s tier classification is visible at composition time. The workspace declares a max_tier cap; composition rejects rules whose classified tier exceeds the cap unless the rule sits inside an unsafe logic { } block (which lifts the cap to FOL by construction). Violations surface as OE0906 TierCapExceeded.

  • Composition signature. A SHA-256 content-hash over four pre-hashed legs:

    composition_signature = SHA-256(
        wiring_diagram_hash       ∥   // SHA-256 over the canonical-form wiring diagram
        standpoint_lattice_hash   ∥   // SHA-256 over the canonical-form standpoint lattice
        tier_ladder_version       ∥   // u32, little-endian
        kernel_api_version            // u32, little-endian
    )
    

    Two identical inputs produce the same signature; any change to any axis produces a different one. The signature is the cache key for downstream re-composition. Crucially, the wiring diagram source itself is not preserved in the artifact — it is a transient build-time input. The artifact stores wiring_diagram_hash plus the other three legs, which is sufficient for the runtime to reverify signature integrity at load time without reconstructing the wiring (RFD-0035 §6 Layer-2 sub-pass). ox compose is the build-time check that the wiring is correct; the runtime is the load-time check that the artifact’s stored sub-hashes have not been tampered with.

  • Workspace artifact emission. ox compose reads N per-package .oxc caches, validates they agree on the wiring diagram, applies the standpoint-lattice + tier-cap + reference-closure invariants, and emits a workspace .oxbin (RFD-0035).

3. oxc is the per-package compiler

oxc owns everything that is per-package and stops at the package boundary:

  • Elaboration of the package’s source files into CoreIR (every existing oxc responsibility — name resolution, type checking, refinement validation, rule classification, structural checks).
  • Instantiation of CoreIR against a wiring diagram supplied by ox. This produces the per-package .oxc cache: an instantiated, typed, lazily-deserialisable serialisation of the package’s elaborated content as it stands relative to this composition. The cache is not abstract; it is composition-specific. Yang et al. 2017 deliberately abandoned separately-compiled-uninstantiated-components for performance (“no cross-package inlining can occur”); Argon adopts the same lesson.
  • Per-package introspection commandsoxc emit core-ir | tier-classification | symbols | resolve | classify | bench-elab. These expose the elaborated form to compiler-internal work and tooling without requiring ox-level workspace context.

oxc does not see the workspace. oxc does not resolve cross-package references on its own. oxc does not merge standpoint lattices. Every cross-package concern that would otherwise live in oxc lives in ox instead.

4. Per-package .oxc cache shape

The .oxc is a per-package, instantiated, typed serialisation. The format is the per-package analogue of the workspace .oxbin defined in RFD-0035: same encoding family (CBOR + Cap’n Proto layered), same content-addressing discipline, smaller scope.

A .oxc carries:

  • Composition signature. The signature ox computed at instantiation. Re-instantiating the package against a different signature invalidates the cache.
  • Symbol table. Every public-eligible item (concept, rule, metatype, metarel, decorator, frame, query, mutation, compute, axiom) with stable_id + qualified_path + tier + visibility + doc-presence. Front-coded.
  • CoreIR serialisation. The elaborated form, sectioned by item kind. Lazy-deserialise on first lookup, mirroring Idris 2’s ContextEntry = Either Binary Defn pattern.
  • Per-package events. Axioms declared in the package, in the unified event-log shape defined by RFD-0035. Empty for packages that declare only TBox.
  • Per-package standpoint declarations. Local additions to the workspace’s standpoint lattice; merged at compose time.
  • Doc strings. Per RFD-0029, every public-eligible item carries a DocBlock. The cache serialises the parsed shape, not the raw /// strings.

Re-instantiating against a new wiring diagram (composition signature changed) re-runs oxc instantiate. Cabal-style content-hash caching mitigates the re-work cost: if the signature didn’t change and the source’s content-hash didn’t change, the existing cache is reused.

5. Workspace .oxbin artifact

The workspace-level composed artifact is specified in RFD-0035. RFD-0034 commits to:

  • .oxbin is the file extension.
  • It is produced by ox compose, which internally invokes oxc instantiate per package against the computed wiring diagram and merges the resulting .oxc caches. There is no oxc compose — composition is a workspace-level concern owned entirely by ox per §3.
  • It carries the merged events from every package + the composed standpoint lattice + the resolved tier table + the composition signature + every section RFD-0035 specifies.
  • It is content-addressed; semantically equivalent compositions produce byte-equivalent artifacts.

6. Engine / Module / Store runtime factoring

The runtime contract that consumes a .oxbin factors into three roles, mirroring the convergence across Wasmtime / JVM / Truffle:

  • Engine — the shared, immutable base. Argon’s analogue is the shared schema (UFO + Core + std + any other base packages declared by the workspace), held as Arc<SchemaStore>. Hot-path: mmap’d; shareable across processes.
  • Module — a typed loaded artifact. .oxbin is the Module. One .oxbin may be loaded into many Stores concurrently.
  • Store — an isolated execution context. Argon’s analogue is the per-tenant overlay, holding events that aren’t part of the Engine’s shared base. The kernel today implements this as a per-tenant overlay over the shared base.

The factoring is ratified, not invented — it is already implicit in Argon’s existing shared-base + per-tenant-overlay design. RFD-0034 names it explicitly so future runtime-backend work has a vocabulary to align against.

7. Module identity is a wiring-diagram content-hash

Backpack ’17 establishes that module identity in a recursive composition is a recursive term (a regular infinite tree under , Kilpatrick et al. 2014). Cyclic compositions need Huet’s regular-tree unification to compute identity; non-cyclic compositions reduce to flat content-hashes.

Argon adopts the flat content-hash form for the v1 of this RFD. Module identity is the SHA-256 over the wiring-diagram subtree rooted at the module’s top-level. The recursive form (and its unification machinery) is reserved for a future RFD if cyclic pub use chains are observed in practice.

8. Cyclic re-exports are rejected

A pub use chain that produces a cycle in the import graph surfaces as OE0907 CyclicReexportInComposition at compose time. The cycle is reported with the full chain. The current cut admits no cycles by construction.

The Huet regular-tree unification escape hatch is out of scope for this RFD. If a real use-case for cyclic re-exports surfaces — and Argon’s stable_id-as-content-hash discipline starts producing aliasing errors — a follow-up RFD admits cycles with the formal identity calculus. Reject-by-default is the conservative choice; admitting cycles later is a strict superset.

9. Diagnostic codes introduced

CodeSeverityTrigger
OE0905ErrorStandpoint lattice cycle detected at compose time.
OE0906ErrorRule’s classified tier exceeds the workspace’s max_tier cap.
OE0907ErrorCyclic pub use chain detected at compose time.
OE0908ErrorPer-package .oxc cache’s composition signature does not match ox’s recomputed signature (cache invalidation race or filesystem corruption).
OW0909WarningComposition signature is the same as a prior cached composition; ox reused the cached .oxbin.

Codes register in oxc-protocol::core::codes via the existing inventory::submit! registry. The drift gate ensures uniqueness.

10. CLI surface

  • ox compose — produce a workspace .oxbin from the package graph. Implicit in ox check, ox test, ox doc, ox run; explicit when ahead-of-time composition is desired (deployment, distribution, CI caching).
  • oxc instantiate <package> --wiring <wiring.json> — instantiate a single package against a supplied wiring diagram. Compiler-internal; primarily used by ox compose and by oxc-internal benchmarking.
  • oxc emit <shape>core-ir | symbols | tier-classification | wiring. Compiler-internal introspection. Mirrors rustc --emit.

ox runs oxc instantiate per package as part of ox compose. End users do not invoke oxc instantiate directly.

Rationale

Three independent designs converged on the two-phase pipeline. Backpack ’17 (PL modules), the WebAssembly Component Model (component composition), and Argon’s existing oxc / ox split (workspace orchestration). Convergence from three lineages on one architectural shape is the strongest available signal that the shape is correct. RFD-0034 ratifies the convergence rather than inventing.

Per-package output is instantiated, not abstract. Yang et al. 2017 explicitly abandoned separately-compiled-uninstantiated-components for performance reasons — uninstantiated components prevent cross-package optimisation and forced the Backpack team away from that model. Argon should not pay the cost. A per-package .oxc cache that is instantiated against the current composition is the right shape; Cabal-style content-hash caching covers the re-work cost when compositions don’t change.

Engine / Module / Store factoring is convergent across runtime traditions. Wasmtime, JVM HotSpot, and Truffle / GraalVM independently reach this shape. Argon’s existing shared-base + per-tenant-overlay implements it; naming it explicitly gives future runtime-backend work a stable vocabulary.

Reject cycles by default. Backpack handles cyclic compositions with Huet regular-tree unification, but the machinery is heavy and the use-case is narrow. Admitting cycles later when there is empirical evidence is a strict superset of rejecting them now. Reject-by-default is conservative; the upgrade path is open.

Composition signature ≠ lockfile hash. The lockfile pins source content. The composition signature pins the wiring diagram + standpoint lattice + tier ladder + kernel API version. Two compositions with identical sources but different standpoint lattices produce different signatures and require different .oxc caches. The two hashes serve different purposes and both belong in the cache key.

Standpoint lattice composition is novel. Every surveyed standpoint-logic implementation (Strass et al. 2023’s Standpoint EL+ Soufflé prototype; Emmrich et al. 2023’s Standpoint-OWL 2 reasoner; DDL/MCS implementations; named graphs) assumes a single monolithic lattice. Cross-package lattice composition is unaddressed in the literature. Argon’s rules (UUID-based equality, union with cycle rejection, soundness check at compose time) are small and grounded; the documentation lives here so future contributors do not re-litigate.

oxc keeps a rich CLI for compiler-internal work. The ox surface is the user-facing surface; oxc is the toolsmith’s surface. The emit family (modelled after rustc --emit) plus introspection commands give compiler-internal work a stable footing without loading user-facing semantics into oxc.

Consequences

  • New crate / module: composition pipeline implementation. oxc instantiate, oxc emit, the .oxc cache reader/writer. New ox compose subcommand. Workspace + lockfile + composition-signature integration.
  • Per-package .oxc cache format is wire-stable and lifted into oxc-protocol. The format is the per-package analogue of the workspace .oxbin (RFD-0035).
  • Composition signature joins the lockfile content-hash and constructs-hash as a third hash on workspace state. Cache invalidation walks all three.
  • Module identity = wiring-diagram content-hash. A package’s stable_id is no longer just a function of its source; it is a function of its source and the wiring it is instantiated against. Cross-composition references to the same package are stable; cross-composition references between packages can shift if the wiring changes.
  • Cyclic pub use chains rejected. Existing packages that depend on cyclic re-exports (none known at the time of this RFD) need to refactor.
  • The kernel becomes one runtime backend among several. The kernel’s storage layer continues to consume composed events as it does today; the .oxbin artifact is the formalised input to that consumption. RFD-0035 specifies the trait surface that other backends (in-process oxc-runtime, sandboxed bytecode, future oxigraph-backed embedded runtime) implement.
  • Clean introspection surface for compiler-internal work. oxc emit family and oxc resolve enable compiler-internal debugging without going through the workspace orchestrator.
  • The argon-codegen drift gate covers the new wire shapes. oxc-protocol::core::oxc_cache and oxc-protocol::core::wiring are added; ts-rs + JSON schema generation flow through the existing pipeline.
  • Diagnostic codes OE0905OE0908, OW0909 register in oxc-protocol::core::codes. The OW prefix on OW0909 is correct: per RFD-0024’s severity convention, warnings carry OW, errors carry OE. Code-uniqueness checks span both prefixes.

Historical lineage

This RFD is largely novel. It builds on three convergent prior designs:

  • Backpack ’17 (Yang, “Backpack: Towards Practical Mix-In Linking for Haskell,” 2017). The two-phase pipeline (Cabal-side mixin linking → GHC-side instantiation+typecheck) is the closest existing prior art. Argon’s ox ≈ Cabal; Argon’s oxc ≈ GHC; Argon’s .oxc ≈ instantiated component. Argon adopts the two-phase shape with the per-package output instantiated (Yang’s pragmatism — uninstantiated components prevent cross-package optimisation, and Backpack ’17 explicitly abandoned that route).
  • WebAssembly Component Model (WebAssembly Community Group, ongoing). Component-type signature resolution → core-module wiring is the artifact-side analogue of Backpack’s package-side resolution. Argon’s ox compose resolves wiring at the workspace level; the workspace .oxbin ≈ a WASM component.
  • Engine / Module / Store factoring (Wasmtime, JVM HotSpot, Truffle / GraalVM). Three independent runtime traditions converge on this shape. Argon’s Arc<SchemaStore> ≈ Engine; .oxbin ≈ Module; per-tenant overlay ≈ Store. Already implicit in Argon’s design; named explicitly here.

The standpoint lattice composition rules are Argon-specific; the surveyed literature does not address cross-package lattice composition.

The reject-cycles-by-default policy is a conservative interpretation of Backpack’s regular-tree unification. The literature supports cycles; Argon defers them. A future RFD revisits if real use-cases emerge.

RFD-0035 — Binary artifact .oxbin and the execute layer

Committed Opened 2026-05-04 · Committed 2026-05-04

Question

What is the wire shape of an Argon workspace’s composed output? How does a runtime backend consume it? What invariants does the artifact carry across schema evolution, content-addressing, hot replacement, and tier-bound execution?

Context

RFD-0034 ratifies the two-phase composition pipeline: ox computes wiring + composition signature; oxc instantiates per-package; ox compose merges into a workspace artifact. This RFD specifies that workspace artifact: its file format, its sections, its encoding, its versioning, its content-addressing, and the contract a runtime must honour to consume it.

The artifact is structurally without precedent. It carries simultaneously a typed knowledge container, a program (rules + queries + mutations + computes), a composed link target, a derived-state cache, a provenance archive, a tier-typed safety container, and a standpoint-lattice carrier. No surveyed system carries even four of these in one form. But the structural skeleton — sectioned binary with self-describing preamble, per-section sub-headers, reservation-based extensibility, three-axis versioning, Merkle content-addressing — has been settled by thirty years of compiled-artifact engineering across ELF, Erlang BEAM, WebAssembly, HDT, and Lean 4’s multi-file .olean.* split. RFD-0035 inherits that skeleton and specifies the Argon-shaped sections inside.

The genuine invention surface is six items, each forced by an existing settled property of Argon’s storage and reasoning model: tier-typed sectioning, standpoint-lattice serialisation, the saturation-cache + per-fact provenance combination shipped together, a Kleene–Belnap four-valued bridge handled in query interpretation, cross-package standpoint lattice composition (specified here, executed in RFD-0034), and a bitemporal-basis-T-preserving hot-replacement contract. Each is sketched in this RFD with grounded precedent.

The runtime concerns must factor cleanly so that the kernel becomes one backend among several. An in-process oxc-runtime library, an external embedded engine (e.g., oxigraph-backed), and a future sandboxed bytecode runtime are all anticipated consumers. The runtime contract is specified as a trait surface that any backend implements; the artifact is the contract’s data shape.

A research campaign preceding this RFD ([vault: research/argon-execute-layer/]) produced primary-source-grounded recommendations for every load-bearing decision. The campaign’s findings are summarised inline where relevant; this RFD ratifies twelve specific recommendations and documents six invention zones with sketched approaches.

Decision

1. File format

The artifact’s file extension is .oxbin. A workspace .oxbin is the output of ox compose; a per-package .oxc cache (RFD-0034) shares the same skeleton with a smaller scope.

Magic header: 0x00 0x6F 0x78 0x62 0x69 0x6E 0x00 0x01 — eight bytes — \0oxbin\0\1. Followed by three independent uint32 version numbers (§4) and a reserved uint32. Total file-magic + preamble: 24 bytes.

2. Section model

[file-magic + preamble: 24 bytes]
[global-control                  ]   CBOR-encoded; section directory; composition_signature; wiring_diagram_hash; max_tier_claimed; precomputed standpoint ancestor sets
[symbol-table                    ]   HDT-PFC front-coded string pool of qualified paths + stable_ids
[events                          ]   CBOR streaming; sorted by (tenant_id, fork_id, standpoint_id, predicate, valid_time, tx_time)
[standpoint-lattice              ]   CBOR DAG; precomputed ancestor sets in global-control
[fork-lineage                    ]   CBOR chain; copy-on-write fork structure
[tier-table                      ]   CBOR per-rule; max_tier_claimed in global-control
[projection-cache                ]   array of Cap'n Proto segments, content-keyed; one per maintained projection
[arrangement-section             ]   optional Cap'n Proto Z-set arrangements; future-compat for DBSP
[pathology-flags                 ]   CBOR list; compile-time-detected reasoning pathologies
[doc-blocks                      ]   CBOR per-item; per RFD-0029 typed DocBlock

Ten standard section types, listed in canonical order in the diagram above. Five are mandatory; five are optional. Optional ≠ LAZY: optionality controls presence (the section may be omitted from the artifact); the LAZY sub-header flag controls load discipline (the runtime mmaps it on first access rather than reading it during the synchronous prefix). The two are independent. The ten sections classify as follows:

SectionPresenceLoadWhy
global-controlMandatorySynchronousSection directory; without it, no other section can be located.
symbol-tableMandatorySynchronousEvery reference in events / rules / queries / mutations resolves through it; Layer-2 symbol resolution requires it.
eventsMandatorySynchronous (mmap’d)Source of truth for axioms + retractions + derived events; cannot be reconstructed. Mapped at startup; the body is read incrementally on query.
standpoint-latticeMandatorySynchronousEvery event row carries a standpoint_id; the lattice is needed to interpret ancestor sets. An artifact with a single default standpoint still carries a one-node lattice.
tier-tableMandatorySynchronousCarries per-rule tier annotations consumed by Layer-2’s tier-consistency sub-pass (§6). Layer 1’s check reads only the max_tier_claimed uint8 from global-control; the tier-table itself is a Layer-2 input. The section’s MANDATORY sub-header flag enforces presence at load time so Layer 2 can run without optional-section degradation (§8).
fork-lineageOptionalSynchronous when presentArtifacts without forks (most published packages) omit it. Small enough that lazy-loading is not worthwhile when present. Empty present-form is also valid.
projection-cacheOptionalLAZY (mmap on first access)A maintained derivation of the canonical events; the runtime saturates from rules + ABox if the section is absent. Re-add at any time without semantic change.
arrangement-sectionOptionalLAZY (mmap on first access)Forwards-compatibility for DBSP-shaped Z-set arrangements; absent in DRedc-only artifacts.
pathology-flagsOptionalSynchronous when presentSmall list of compile-time-detected reasoning pathologies. Read at startup so downstream behaviour can adapt; absence is equivalent to “no detected pathologies.”
doc-blocksOptionalLAZY (per-item on first deserialise)A .oxbin produced by ox build --strip-docs omits doc strings. The runtime contract works without them; tools like ox doc require them and refuse such artifacts.

Section types are numbered as a single uint8 namespace:

  • 0..99 — standard sections (the ten listed above; specific type-id assignments live in oxc-protocol::core::oxbin).
  • 100..199 — Argon-future sections (debug-info, traces, semver-check metadata).
  • 200..255 — user-custom (third-party tooling). Unknown sections in this range are ignored on load (WASM custom-section discipline) unless MANDATORY-flagged. This range is the extension hook for tools that want to attach metadata to a .oxbin without coordinating with the Argon project.

Each section has a sub-header carrying:

  • section_type: uint8 — number from the registry above.
  • flags: uint8 — bit 0 = MANDATORY (load fails if section type is unknown; mirrors Mach-O LC_REQ_DYLD); bit 1 = LAZY (mmap on first access); bit 2 = CONTENT_HASHED (content_hash field is populated); bits 3-7 reserved.
  • content_hash: [u8; 32] — SHA-256 of the section’s encoded body when CONTENT_HASHED.
  • size: uint64 — encoded body size.
  • offset: uint64 — byte offset from start of file.

The section directory in global-control indexes every section. The order in the directory is the canonical load order.

3. Encoding — layered

  • Outer container + structural sections (global-control, events, standpoint-lattice, fork-lineage, tier-table, pathology-flags, doc-blocks): CBOR per RFC 8949, with the §4.2.1 Deterministic Encoding discipline (shortest argument forms, definite-length, lex-sorted map keys, preferred float representations). Deterministic CBOR is what makes content-addressing well-defined.
  • Hot mmap’d cache + Z-set segments (projection-cache, arrangement-section): Cap’n Proto. Zero-copy load; mmap-friendly; schema-evolution by additive numeric field IDs.
  • Symbol table: bespoke HDT-PFC (Header–Dictionary–Triples Plain Front Coding). Optimised for the high-prefix-redundancy of qualified Argon paths (pkg::module::ItemA, pkg::module::ItemB, …).

CBOR tags 65536, 65537, 65538 are claimed for Argon’s PosBool(M) provenance witnesses (axiom_id, rule_id, module_id respectively). These tags fall in the First Come First Served range per RFC 8949 §9.2 — the FCFS range is contiguous from 32768 upward, with the 65536 boundary marking only an encoding-width change (4-byte argument vs 2-byte), not a registration-policy change. There is no “Private Use” range for CBOR tags. Argon’s use of these numbers is therefore a deliberately deferred FCFS application — a lightweight registration that Argon will submit to IANA before the .oxbin format graduates to a public-stable surface. Until the format is public, the small risk that another party registers these specific numbers for incompatible semantics is mitigated by the format’s internal-only deployment plus the major-version-bump migration path. Tag numbers may be reassigned only via a major-version bump of oxbin_format_version.

4. Versioning — three axes + multi-release overlay

The preamble carries three independent uint32 version axes:

  • oxbin_format_version — section model + encoding evolution. Bumped on additive section types (minor) or breaking section semantics (major).
  • tier_ladder_version — the decidability tier vocabulary itself. Bumped when the tier ladder changes (a tier renamed, added, removed). Today: the ladder defined in RFD-0004. Major bump only.
  • kernel_api_version — the kernel’s HTTP API surface (the resource-oriented v2 model the artifact is composed against). Bumped when the API contract changes. Minor on additive surface; major on breaking.

The robustness principle: producers strict, consumers liberal. A consumer accepts future minor bumps on any axis unless the new artifact carries a MANDATORY-flagged section the consumer doesn’t recognise; in that case load fails with OE1003 UnknownMandatorySection.

Within a major version on oxbin_format_version, the artifact may carry a multi-release overlay — e.g., both a projection-cache (DRedc-shaped, today’s default) and an arrangement-section (DBSP-shaped, future) in parallel. Each runtime picks the section it understands. The pattern is JEP 238 (multi-release JAR) at the section level instead of the file level.

5. Content-addressing — Merkle-tree-of-sections

Per-section content_hash: [u8; 32] (SHA-256 over the encoded body) populates the CONTENT_HASHED field for sections where it is meaningful — events, standpoint-lattice, fork-lineage, tier-table, projection-cache (per-segment), arrangement-section (per-segment), doc-blocks. The artifact-level hash is the SHA-256 over (composition_signature ∥ section_directory_canonical_form), where composition_signature is the value RFD-0034 specifies.

Two semantically-equivalent compositions produce byte-equivalent artifacts. The deterministic-CBOR discipline (§4.2.1) plus per-section canonical-form rules (lex-sorted entries within each section’s logical structure; explicitly specified per section in oxc-protocol::core::oxbin) guarantee this.

Selective cache invalidation follows: a TBox change touches only the events section’s content hash; the lattice and tier-table hashes are unchanged. Downstream caches (the kernel’s segment cache, peer caches in deployment, Bazel/Nix-style build caches) invalidate at section granularity.

6. Validation — hybrid Layer 1 + Layer 2

Layer 1 (closed-boolean, WASM-style). Computed at load time after global-control is parsed and before any other section body is read:

tier_valid(.oxbin, runtime) := max_tier_claimed ≤ runtime.max_tier_supported

max_tier_claimed is a uint8 in global-control denoting the highest tier of any rule in the artifact. If Layer 1 fails, the runtime refuses the artifact with OE1004 TierMismatch. The check is O(1).

Layer 2 (per-section verifier, JVM-style). Run after Layer 1 passes; per-section invariants:

  • Symbol resolution — every reference in events / rules / queries / mutations resolves to a symbol in symbol-table.
  • Lattice acyclicity — standpoint-lattice is a DAG.
  • Provenance well-formedness — every derivation_provenance JSONB blob is a valid PosBool(M) DNF (lex-sorted clauses; lex-sorted witnesses within each clause).
  • Composition-signature consistency — the runtime recomputes composition_signature from the four stored sub-hash legs (global-control.wiring_diagram_hash, section_directory["standpoint-lattice"].content_hash, the preamble’s tier_ladder_version, the preamble’s kernel_api_version) per the formula in RFD-0034 §2 and verifies the result matches the stored global-control.composition_signature. The wiring diagram source itself is not preserved in the artifact (it’s a transient build-time input); ox compose is the build-time check that the wiring is correct, and the runtime is the load-time check that the artifact’s stored sub-hashes have not been tampered with.
  • Tier consistency — every rule’s tier in tier-table does not exceed max_tier_claimed.
  • Doc-block well-formedness — every DocBlock per RFD-0029 has resolvable cross-links.

Layer 2 failures surface as OE1005..OE1010 (one code per invariant family).

The default lenient/strict policy is per runtime kind:

  • Sandboxed runtimes (future bytecode runtime, untrusted-content) default strict — any Layer 2 failure refuses the load.
  • In-process oxc-runtime defaults lenient — Layer 2 failures log warnings and skip the offending section. The kernel and ox check use lenient; ox build for distribution uses strict.

A --validate=strict|lenient flag on ox compose and on each runtime’s load surface overrides the default.

Strict mode forces eager validation of every Layer-2 invariant, including those for sections marked LAZY in §2. The “refuse the load on any Layer-2 failure” promise must be honourable, which means a runtime in strict mode cannot defer doc-block well-formedness checks to first deserialisation: it eagerly walks every doc-block at load time, paying the one-time cost so the contract is real. §7 specifies the lazy-by-default load order; the strict-mode override flips the lazy steps synchronous for validation purposes only (the underlying sections still mmap as before; the runtime simply runs each section’s Layer-2 sub-pass before returning success). Lenient mode keeps the lazy schedule and surfaces failures as OW1011 when they’re discovered later.

7. Streaming load order

Layer-2’s per-section invariants depend on the sections being loaded — symbol resolution needs the symbol table, lattice acyclicity needs the lattice, and so on. Layer 2 is therefore not a single pass after step 3; it splits into per-section sub-passes interleaved with loading, each running immediately after its dependent section is in memory. This matches the natural reading of “per-section verifier” (JVM-style) and avoids the load-before-validate ordering problem.

1. Synchronous: file-magic + preamble.
2. Synchronous: global-control + section directory.
3. Layer-1 validation (tier check, O(1)).                           ← fails fast on tier mismatch
4. Synchronous: symbol-table.                                       ← front-coded; fast
5. Synchronous: events (mmap'd; ready for prefix-scan).
   Layer-2 sub-pass: symbol resolution + provenance well-formedness.
6. Synchronous: standpoint-lattice + fork-lineage + tier-table + pathology-flags.
   Layer-2 sub-pass: lattice acyclicity + tier consistency + composition-signature consistency.
7. Lazy: projection-cache segments.                                 ← mmap on first miss; per-segment CONTENT_HASHED
8. Lazy: arrangement-section (if present).
9. Lazy in lenient mode / synchronous in strict mode: doc-blocks — per-item lazy-deserialise (Idris 2 `ContextEntry` pattern). Layer-2 sub-pass: doc-block well-formedness, run on first deserialisation of each block in lenient mode, run eagerly across every block before load returns in strict mode (per the §6 strict-mode override).
10. Ignored: user-custom sections in the `200..255` range (unless `MANDATORY`-flagged).

The synchronous prefix is steps 1-6 in lenient mode; strict mode extends the synchronous prefix to include the lazy Layer-2 sub-passes (step 9 in particular). Heavy payloads (projection cache, arrangements) remain mmap’d-on-first-access regardless of mode — they don’t have Layer-2 invariants the validator needs to walk eagerly. Cold-start cost in lenient mode is dominated by steps 1-6; strict mode adds the doc-block traversal cost at the front. Empirical target: sub-25 ms cold start on the lease-story workload in lenient mode; strict-mode cost is bounded by doc-block count and verified via benchmark harness.

Layer-2 failure within a sub-pass is handled per the lenient-vs-strict policy in §6. Strict runtimes refuse the load on the first sub-pass failure (including doc-blocks per the §6 override); lenient runtimes log the failure, skip the offending section’s logical content, and continue, surfacing the failure as OW1011 when it’s encountered.

8. Tier-typed sectioning

Per-rule tier annotations live in the tier-table section: tier_table[rule_id] = tier, where tier is a uint8 enum into the tier ladder defined by RFD-0004. The maximum across all rules is global-control.max_tier_claimed.

A runtime advertises its capabilities via runtime.max_tier_supported. Layer-1 validation refuses an artifact whose max_tier_claimed > max_tier_supported. Sandboxed runtimes that cannot evaluate tier:fol content thus refuse FOL artifacts at the doorstep, before any rule body is parsed.

The tier-table section itself is MANDATORY-flagged: a runtime that does not understand the section refuses the load. This is necessary because Layer 2’s tier-consistency sub-pass (§6) verifies that every rule’s per-rule tier annotation does not exceed max_tier_claimed; without the section the sub-pass cannot run and the artifact-runtime tier contract is unverifiable. (Layer 1 itself reads only the max_tier_claimed uint8 from global-control; Layer 2 is what consumes the tier-table.)

Per-section tier filtering is reserved for a future cut. Today the granularity is artifact-wide (max_tier_claimed); per-section tier (e.g., tier:closure rules in one section, tier:fol rules in another, with the runtime loading only the closure section) is a v2 extension.

9. Standpoint-lattice serialisation

The standpoint-lattice section serialises the workspace’s standpoint DAG: a flat list of standpoint records ({id: UUID, name: String, parents: [UUID]}) plus precomputed ancestor sets ({standpoint_id: UUID, ancestors: [UUID]}), the latter cached in global-control for O(1) lookup at query time.

Every event row in the events section carries a standpoint_id: UUID column. Cross-standpoint queries traverse via WHERE standpoint_id = ANY($ancestors) against the precomputed ancestor set — no recursive CTE at query time.

Cross-package lattice composition (when ox compose merges N packages each declaring standpoints) follows RFD-0034’s rules: standpoints from different packages identify only by UUID (never by name); union the lattices; reject cycles. The composed lattice is a DAG by invariant.

This is the single most novel section in the artifact. No surveyed system persists N standpoints in one binary form with efficient cross-standpoint indexing. Strass et al. 2023’s Standpoint EL+ Soufflé prototype encodes the lattice in a 5-ary gci_nested(t, b, s, c, d) Datalog form but explicitly admits scalability issues; Argon’s pre-materialised ancestor sets are exactly the optimisation that paper invites.

10. Saturation cache + per-fact provenance combination

The events section is canonical: every axiom + every retraction + every derived event carries its derivation_provenance JSONB inline (PosBool(M) DNF, per RFD-0007 and the workspace’s settled provenance convention). The events section is the source of truth for both factual content and provenance.

The projection-cache section is derived — one Cap’n Proto segment per maintained projection (subsumption closure, classification, rule-supports, etc.). Each segment is content-keyed; on TBox or rule change the affected segments invalidate; unchanged segments persist. The section is optional (per the §2 table): an .oxbin that omits it is fully functional — the runtime saturates from canonical events on first query. Production deployments typically ship the cache; development builds and minimal-footprint distributions may omit it.

This combination is novel as a shipped artifact. RDFox and VLog persist saturation but not per-fact provenance. Provenance-aware reasoners (Bourgaux/Ozaki/Peñaloza 2023) prove polynomial-time guarantees but do not ship persisted artifacts. Argon ships both. Bourgaux 2023’s idempotent-semiring tractability theorem ratifies the polynomial cost of carrying PosBool(M) DNF alongside the cache.

11. PosBool(M) provenance — value field, lex-sorted DNF

Per the workspace’s settled conventions: PosBool(M) DNF is a value field on every event row, encoded as JSONB-shape inside CBOR. The DNF carries lex-sorted clauses; within each clause, lex-sorted witnesses; within each witness, the CBOR tag (65536 / 65537 / 65538) discriminates axiom_id / rule_id / module_id.

PosBool(M) is not a semiring weight on a Z-set algebra. It is a value field. The reason is algebraic: PosBool is a distributive lattice without additive inverses; a Z-set group requires inverses; so direct PosBool-weight lift is unsound. The settled approach (provenance updates as side-effect via the Green/Karvounarakis/Tannen 2007 homomorphism) is the production form.

Bourgaux/Ozaki/Peñaloza 2023’s main theorem: under the multiplicative-idempotent-semiring restriction (which PosBool(M) satisfies), provenance-aware ontology completion is polynomial. The same algebraic property that ruled out the DBSP weight lift gives Argon polynomial provenance-aware reasoning. One constraint, two unrelated benefits.

12. Four-valued (Kleene–Belnap) refinement — query-side, not storage-side

Argon’s refinement semantics under open-world assumption are four-valued (Kleene–Belnap: T / F / U / B). Kleene’s three-valued logic supplies the {T, F, U} fragment (Unknown for missing support); Belnap’s 1977 FOUR extends this with B (Both / Paradox) for facts supported in conflicting directions. The combined Kleene–Belnap system is four-valued; calling it “three-valued” elides the B case and would mislead implementors. No surveyed work bridges Standpoint Logic with four-valued semantics; the bridge is an Argon invention.

Argon’s bridge: storage stays two-valued; four-valuedness lives in query interpretation. The events section records each source’s claim (T or F) per row. Query semantics aggregate to {T, F, U, B} per Kleene–Belnap by interpreting the PosBool(M) DNF: a fact with only T-supports renders as T; a fact with only F-supports renders as F; a fact with both T and F-supports under conflicting standpoint perspectives renders as B; a fact with no supports under the queried standpoint renders as U.

The schema does not change. The runtime contract specifies the query-side interpretation. Empirical validation against the lease-story workload is required during implementation; if real workloads need cell-level four-valued storage the approach revisits.

13. Hot replacement — bitemporal basis-T preservation contract

The artifact-runtime contract for hot replacement is novel as a contract specification. The technical capability already exists (the bitemporal valid_time × tx_time event log + the kernel’s query-at-basis API). What this RFD adds is the explicit specification.

Contract (the runtime backend implements this):

  1. On artifact replacement (a .oxbin swapped in at tx_time T₂), in-flight queries that began at basis T₁ continue to see the artifact’s state at T₁. The runtime keeps the old artifact in memory until those queries complete.
  2. New queries initiated after T₂ see the new basis.
  3. Two-version invariant (BEAM lesson). At most “current” and “old” coexist in the runtime simultaneously. Loading a third version waits for queries on the old to complete (or aborts them per a runtime policy flag, with the cost surfacing as a metric).
  4. The runtime advertises the basis-T it serves on every response. Consumers see which basis their answer was computed against.

Hot replacement is therefore a property of the runtime’s discipline, not of the artifact’s structure. The artifact carries the bitemporal columns; the runtime preserves the basis-T invariant.

14. Runtime backend trait surface

A backend that consumes a .oxbin implements the trait. Module handles are shared via Arc<Module> so in-flight queries can keep their handle alive while the runtime atomically swaps in a new one (the two-version invariant in §13). Every method takes &self — implementors use interior mutability (ArcSwap<Module> for the current-module pointer, plus a per-Store RwLock or per-overlay Mutex for append’s write path) so concurrent queries and a hot replacement never compete for &mut self. This is the standard Rust pattern for the “old code keeps running” invariant.

#![allow(unused)]
fn main() {
pub trait OxbinRuntime: Send + Sync {
    /// Validate + load an artifact. Honours Layer 1 + Layer 2.
    /// Sets the runtime's "current" handle to the returned `Arc<Module>`
    /// and returns it for the caller's use.
    fn load(&self, artifact: &Oxbin) -> Result<Arc<Module>, LoadError>;

    /// Hot-replace the loaded artifact. Honours the §13 basis-T
    /// preservation contract: the runtime atomically installs the new
    /// module as "current" while keeping `old` reachable through every
    /// existing `Arc<Module>` clone. In-flight queries that already
    /// hold the old handle complete against it; new `current()` calls
    /// return the new handle. Returns the new handle.
    fn replace(&self, old: Arc<Module>, new: &Oxbin) -> Result<Arc<Module>, LoadError>;

    /// Cheap snapshot of the runtime's current module handle. Callers
    /// initiating a new query / mutation grab a fresh `Arc<Module>`
    /// here; they retain it for the duration of their work.
    fn current(&self) -> Arc<Module>;

    /// Query at a specified bitemporal basis. Returns rows with
    /// provenance. The caller passes the `Arc<Module>` they want to
    /// query against — typically the result of a recent `current()`.
    fn query(&self, module: &Arc<Module>, q: Query, basis: Basis) -> QueryResult;

    /// Apply a mutation as a dry-run (no persistence). Returns the
    /// would-emit + would-retract delta with provenance.
    fn dry_run(&self, module: &Arc<Module>, m: Mutation, basis: Basis) -> MutationDryRun;

    /// Append axioms to the runtime's per-tenant overlay (not the
    /// artifact's events). The artifact remains immutable; the
    /// overlay is the per-tenant Store. Implementations serialise
    /// concurrent writers via per-overlay locking.
    fn append(&self, module: &Arc<Module>, events: &[Event]) -> Result<(), AppendError>;

    /// Capabilities advertisement. Runtime reports max_tier_supported,
    /// resource bounds, and feature flags.
    fn capabilities(&self) -> RuntimeCapabilities;
}
}

The trait lives in oxc-protocol (or its companion crate) so that any backend — kernel, in-process oxc-runtime, sandboxed bytecode runtime, oxigraph-backed embedded engine — implements the same contract. Argon stays kernel-independent: the kernel becomes one implementation of OxbinRuntime; alternative backends are first-class.

The Send + Sync super-trait bound is required so a single runtime instance can be shared across a thread pool. The contract is that all methods are concurrency-safe; the implementor decides how (mutexes, atomic pointers, lock-free structures).

15. Resource bounds — three orthogonal axes

A runtime advertises resource bounds along three orthogonal axes (per the Wasmtime-derived two-axis pattern, with the third axis being Argon-specific):

  • Memory — heap / table / instance count limits (Wasmtime ResourceLimiter-style).
  • CPU — fuel-bounded (deterministic, ~3× slowdown) or epoch-bounded (non-deterministic, ~10% slowdown). Wasmtime consume_fuel / epoch_interruption are the precedents.
  • Rule-fire count — Argon-specific; bounds saturation work. A tier:closure runtime advertises a finite rule-fire bound; a tier:fol artifact section declines this bound (FOL has no termination guarantee in general).

Resource limits are part of the runtime’s capability advertisement. Artifacts may declare resource requirements in global-control; the runtime refuses an artifact whose declared requirements exceed its advertised bounds.

16. Diagnostic codes introduced

CodeSeverityTrigger
OE1001ErrorArtifact magic / preamble invalid.
OE1002ErrorSection directory references a section beyond the file size.
OE1003ErrorMANDATORY-flagged section with unknown type.
OE1004ErrorLayer 1: max_tier_claimed > max_tier_supported.
OE1005ErrorLayer 2: symbol resolution failed.
OE1006ErrorLayer 2: lattice has a cycle.
OE1007ErrorLayer 2: provenance JSONB malformed.
OE1008ErrorLayer 2: composition signature mismatch (cache invalid or filesystem corruption).
OE1009ErrorLayer 2: tier-table inconsistent with max_tier_claimed.
OE1010ErrorLayer 2: doc-block has unresolvable cross-link.
OW1011WarningLenient validation: a Layer-2 failure was tolerated; section skipped.
OE1012ErrorResource requirement declared by artifact exceeds runtime’s advertised bound.
OE1013ErrorHot-replace aborted: two-version invariant would be violated and runtime policy is abort; in-flight queries on the prior artifact were terminated.
OW1014WarningHot-replace queued: two-version invariant would be violated; replacement deferred until in-flight queries on the prior artifact complete. Normal degraded-performance path; not a failure.

Codes register in oxc-protocol::core::codes.

17. CLI surface

  • ox compose — produce a workspace .oxbin. Per RFD-0034.
  • ox run <oxbin> [--query <q>] [--mutate <m>] [--basis <T>] — execute against an artifact via the in-process runtime. Returns answers + provenance.
  • ox inspect <oxbin> — surface the artifact’s structure: section directory, content hashes, max_tier, standpoint count, event count, projection-cache state, doc-block coverage, declared resource requirements.
  • ox bench <oxbin> [--cold | --warm] — run the benchmark harness against an artifact. Cold-start, query latency, mutation dry-run latency.
  • oxc instantiate <package> --wiring <wiring.json> — emit a per-package .oxc cache, instantiated against the supplied wiring diagram. Defined in RFD-0034 §10. Driven by ox compose; rarely invoked directly.
  • oxc dump <oxbin-or-oxc> <section> — dump a section’s CBOR (or Cap’n Proto) decoded to JSON for compiler-internal debugging. Accepts either a workspace .oxbin or a per-package .oxc cache (the section model is the same skeleton at different scopes per §1).

ox run is the user-facing surface. oxc dump is the toolsmith surface. The kernel does not invoke either; it consumes .oxbin directly via the OxbinRuntime trait.

18. Backwards compatibility

This RFD lands at oxbin_format_version = 1. Earlier versions did not exist; there is no migration burden.

Future minor bumps add sections (additive only). Future major bumps may rearrange the section model; consumers detect via oxbin_format_version and refuse-or-upgrade. Multi-release overlays carry both old + new sections in parallel during the migration window.

The tier_ladder_version is initially 1; the existing tier ladder (RFD-0004) is the v1 vocabulary. The kernel_api_version is initially aligned with the current kernel HTTP API.

Rationale

The structural skeleton is settled by literature. ELF (1995), Erlang BEAM (1986), WebAssembly (2017), HDT (2013), and Lean 4.22 multi-file .olean.* (2025) independently arrived at the same shape: file-magic + global-control + section directory with content hashes + per-section sub-headers + reservation-based extensibility. Five mature systems across thirty years and four lineages converging on the same skeleton is a strong signal. RFD-0035 inherits the skeleton.

Layered encoding is the production discipline. CBOR (RFC 8949 deterministic) is the right choice for structural sections because the encoding is well-specified, the deterministic discipline gives content-addressing for free, and the workspace already uses CBOR for axiom-event bodies. Cap’n Proto is the right choice for hot mmap’d cache because zero-copy load is what makes the cache pay off; an entirely-CBOR cache would defeat its purpose. HDT-PFC for the symbol table is the targeted choice for high-prefix-redundancy qualified-path strings.

Three-axis versioning matches ONNX’s lesson. ONNX’s IR / opset / model split is the cleanest precedent for independent evolution of artifact format, vocabulary, and consumer-interface versions. JEP 238 (multi-release JAR) is the lesson for in-major migration. Argon’s three axes (oxbin_format_version, tier_ladder_version, kernel_api_version) inherit both lessons.

Merkle-tree-of-sections content-addressing is the modern build-system discipline. Bazel CAS, Nix store, Datomic immutable segments, and RFC 8949 deterministic CBOR all push toward content-keyed substructure with selective invalidation. Per-section content hashing makes a TBox change touch only the events section; rules + lattice + tier-table caches survive. The discipline is uniform across modern build systems.

Hybrid validation matches the production-grade pattern. WASM’s closed-boolean validate + JVM’s per-class verifier are both load-time disciplines, applied at different granularities for different reasons. WASM’s check is O(1) and refuses large categories of invalid programs cheaply; JVM’s verifier handles fine-grained per-section invariants. Argon needs both: tier mismatch is O(1) closed-boolean (Layer 1); symbol resolution and lattice acyclicity are per-section (Layer 2).

Streaming load order with lazy heavy payloads is the cold-start discipline. Lean 4’s compactedRegion + Idris 2’s ContextEntry lazy-deserialise pattern + HDT’s wavelet-tree-on-load all converge on the same approach: load the structural prefix synchronously, mmap the rest, deserialise on first access. Cold-start cost is dominated by the synchronous prefix; the empirical target (sub-25 ms on lease-story) is the engineering goal.

Tier-typed sectioning is Argon invention forced by the tier ladder (RFD-0004). eBPF’s load-time-static-analysis-then-execute discipline applies; eBPF’s single-envelope type system (accept/reject) does not. Argon’s ladder needs ladder-typed loading. The sketched approach (closed-boolean Layer 1 against max_tier_claimed) is the smallest invention that delivers the discipline; per-section tier filtering is a v2 extension when granularity matters.

Standpoint-lattice serialisation is novel. No surveyed system carries N standpoints in one persistent artifact with efficient cross-standpoint indexing. Argon’s column-discriminator + lattice DAG section + precomputed ancestor sets is the smallest design that delivers the capability. Strass et al. 2023’s prototype admits scalability issues; Argon’s optimisation is the answer that paper invites.

Saturation cache + per-fact provenance combination is novel as a shipped artifact form. No surveyed system ships both. Bourgaux 2023’s polynomial-time guarantee under idempotent semirings ratifies the cost. The combination is forced by the workspace’s settled provenance + IVM model.

Four-valued bridge handled in query interpretation, not storage. No published bridge between Standpoint Logic and Kleene–Belnap exists. Argon’s choice — keep storage two-valued, render four-valuedness ({T, F, U, B}) in query semantics via PosBool DNF interpretation — is the smallest invention; it does not change the schema; it admits empirical revision if real workloads need cell-level four-valuedness.

Hot replacement as a contract, not a mechanism. BEAM’s lesson: hot module replacement works because the language is functional and processes are isolated, not because the runtime has a clever swap mechanism. Argon’s analogue is bitemporal basis-T preservation: a query at basis T₁ continues against the old artifact even if a new artifact arrives at T₂. The technical capability already exists (bitemporal valid_time × tx_time + query-at-basis); RFD-0035 adds the contract specification.

Runtime-backend trait surface decouples Argon from any single runtime. The kernel is one implementation; an in-process oxc-runtime library is another; future sandboxed bytecode and external embedded engines (e.g., oxigraph-backed) are anticipated. Argon stays kernel-independent because the kernel is a backend, not the privileged consumer.

Three-axis resource bounds extend Wasmtime’s two axes. Memory + CPU is Wasmtime’s factoring; Argon adds rule-fire-count as the saturation analogue. The tier:closure runtime offers a finite bound; the tier:fol artifact section declines it; the type system propagates correctly through the validation layer.

Consequences

  • New crate oxc-oxbin (or equivalent) implements the format. Reader, writer, validation. Includes the deterministic CBOR helpers + Cap’n Proto schema + HDT-PFC symbol-table encoder/decoder.
  • oxc-protocol::core::oxbin carries the typed wire shapes for every section (events, lattice, tier-table, projection-cache via Cap’n Proto FFI types, doc-blocks). The codegen drift gate covers them.
  • oxc-protocol::core::oxbin_runtime carries the OxbinRuntime trait and supporting types (Module, Basis, QueryResult, MutationDryRun, RuntimeCapabilities, LoadError, AppendError).
  • Kernel storage layer becomes a OxbinRuntime implementation. Today’s per-tenant overlay becomes the Store; the kernel’s reasoner becomes the runtime; the artifact is the formalised input.
  • In-process oxc-runtime library becomes a OxbinRuntime implementation. What ox check / ox test / ox doc use today becomes named explicitly.
  • Future runtime backends (sandboxed bytecode for agent-side execution, oxigraph-backed embedded engine, distributed peer/transactor split per Datomic) are first-class — implement the trait and they’re a runtime choice for ox run.
  • Diagnostic codes OE1001OE1010, OW1011, OE1012OE1013, OW1014 register in oxc-protocol::core::codes. The OW prefix on OW1011 and OW1014 is correct: per RFD-0024’s severity convention, warnings carry OW, errors carry OE. Code-uniqueness checks span both prefixes.
  • Empirical questions land as benchmark targets in the implementation:
    • CBOR vs Cap’n Proto byte-size delta on the lease-story projection cache.
    • Cold-start cost on lease-story (target: sub-25 ms for the synchronous prefix).
    • Symbol-table front-coding compression ratio on lease-story qualified-path namespace.
    • DRedc determinism in parallel rule-firing — does the cache content-hash stay stable across multiple ox compose invocations?
    • Standpoint-lattice ancestor-set cache size at lease-story scale.
    • Provenance JSONB DNF growth under heavy rule-firing — does it dominate artifact size?
  • Specific ratification questions for the implementation phase:
    • CBOR tag numbers 65536-65538 for PosBool witnesses — final. Argon will submit a lightweight FCFS application to IANA before the .oxbin format graduates to a public-stable surface; until then deployment is internal-only and the major-version-bump migration path mitigates collision risk.
    • Bump-trigger policy for the three version axes.
    • Cap’n Proto canonical-form rules per segment for content-addressing.
    • Lenient/strict default per runtime kind.
    • Specific list of MANDATORY-flagged sections in v0.0.1 (tier-table is mandatory; others TBD).
    • Specific section type assignments in 100..199.
    • Cyclic pub use chain handling — confirmed in RFD-0034 as reject-by-default; recheck against real Argon use-cases.
  • The benchmark harness (ox bench) lands with the implementation. Lease-story serves as the canonical workload; UFO and the full BFO smoke test extend it as those packages stabilise.

This RFD’s scope is the artifact format and the runtime contract. The composition pipeline that produces the artifact is RFD-0034. The doc-rendering layer that emits HTML from doc-blocks is RFD-0029 (Phase 3). Future RFDs will cover specific runtime backends (bytecode runtime; embedded engine), the saturation cache invalidation semantics during multi-package re-composition, and any subsequent extensions.

Historical lineage

This RFD is largely novel as a combined artifact form. The structural shape is grounded in convergent prior art across compiled-artifact engineering and knowledge-graph persistence:

Sectioned binary skeleton: ELF (System V ABI gabi ch. 4); Erlang BEAM IFF (Armstrong, Programming Erlang ch. 8); WebAssembly Core Specification §5 (Haas et al., “Bringing the Web up to Speed with WebAssembly,” PLDI 2017); HDT (Fernández et al., “Binary RDF Representation for Publication and Exchange (HDT),” J. Web Semantics 2013); Lean 4.22 multi-file .olean.* (lean-lang.org/doc/reference/4.22.0).

Encoding choices: CBOR per RFC 8949 (Bormann & Hoffman 2020); Cap’n Proto (capnproto.org); HDT-PFC (Fernández et al. 2013 §3.1).

Versioning model: ONNX three-axis (IR / opset / model — onnx.ai/onnx/repo-docs/IR.html); JEP 238 (multi-release JAR — openjdk.org/jeps/238); BEAM chunk-rename-on-encoding-change (J. Högberg, erlang/otp#9336).

Content-addressing: Bazel CAS; Nix store; Datomic immutable segments (Hickey, Datomic Cloud Architecture docs); RFC 8949 §4.2.1 deterministic encoding (Bormann & Hoffman 2020).

Validation: WASM Core Specification §5.5 (validation as typed property); JVM class file verifier (Lindholm, Yellin, Bracha, Buckley, The Java Virtual Machine Specification ch. 4.10).

Streaming load + mmap: Smalltalk-80 image (Goldberg & Robson, Smalltalk-80: The Language and Its Implementation 1983); SBCL save-lisp-and-die; Lean 4 compactedRegion (de Moura & Ullrich, “The Lean 4 Theorem Prover and Programming Language”); Idris 2 ContextEntry (Brady, “Idris 2: Quantitative Type Theory in Practice,” ECOOP 2021).

Hot replacement contract: Erlang OTP “Code and Code Loading” guide; Armstrong, Programming Erlang (2007) ch. 8 — the “two-version invariant + voluntary migration + functional language preconditions” pattern.

Engine / Module / Store factoring: Wasmtime (Engine/Module/Store); JVM HotSpot (compilation cache / Class<T> per loader / VM state per process); Truffle / GraalVM (engine / language context / polyglot instance).

Resource bounds: Wasmtime ResourceLimiter + consume_fuel + epoch_interruption (wasmtime.dev docs examples-interrupting-wasm.html).

Argon-specific (invention surface):

  • Tier-typed sectioning — eBPF discipline applies; eBPF type system does not. Argon’s invention forced by RFD-0004’s tier ladder.
  • Standpoint-lattice serialisation in one binary artifact with cross-standpoint indexing — no surveyed system. Sketched approach inherits the encoding precedent from Strass, Gómez Álvarez & Rudolph, “Standpoint EL+: A Family of Logic-Based Reasoning Procedures” (KR 2023), with the precomputed ancestor sets being the optimisation that paper’s prototype admits is needed.
  • Saturation cache + per-fact PosBool(M) provenance shipped together — no surveyed system. Bourgaux, Ozaki & Peñaloza, “Semiring Provenance for Lightweight Description Logics” (2023, arXiv:2310.16472) ratifies polynomial cost under PosBool(M)’s idempotency.
  • Kleene–Belnap query-side four-valued interpretation — no published bridge between Standpoint Logic and four-valued semantics. Argon’s interpretation lives in query semantics, not storage. Belnap, “A Useful Four-Valued Logic,” 1977 (in Modern Uses of Multiple-Valued Logic) supplies the {T, F, U, B} truth-value set; Kleene’s strong three-valued logic supplies the {T, F, U} fragment.
  • Cross-package standpoint lattice composition — RFD-0034. No surveyed system addresses cross-package lattice merging.
  • Bitemporal basis-T preservation hot-replacement contract — BEAM’s hot-replacement pattern lifted to the bitemporal basis. Technical capability is from Snodgrass-style bitemporal databases + the workspace’s existing query-at-basis API; the contract specification is new.

The sum of these is unprecedented. Argon will be the first system to ship a typed knowledge container + program + composed link target + derived-state cache + provenance archive + tier-typed safety container + standpoint-lattice carrier in one binary artifact. The literature demonstrates that each ingredient is tractable; the combination is what this RFD locks.

RFD-0036 — Generic declarations for derive, mutation, and compute

Discussion Opened 2026-05-11

Question

Argon today admits generic type parameters on exactly one declaration form: pub constraint. The other rule-shaped declarations — pub derive, pub mutation, pub compute — are syntactically monomorphic. RFD-0009 committed to generic computes but the grammar surface never landed; generic derives and mutations were never specified.

What is the minimal surface that lifts the existing <T: Bound> pattern to derive, mutation, and compute — without committing to dispatch, typeclass, or coherence machinery?

Context

Three pieces of background.

Today’s generic surface is narrow. The grammar parses <Ax: ordered_axis> on pub constraint (argon/oxc/src/cst/grammar/items.rs:954), and the elaborator monomorphizes the resulting GenericConstraint against the axis database (argon/oxc/src/meta/generics.rs). UFO ships its R-rules as generic constraints — necessary_subsumes_contingent<rigidity> etc. — and they specialize per concrete axis at elaboration time. The machinery exists and works.

Outside pub constraint, every rule-shaped declaration is monomorphic. pub derive name(params) :- body, pub mutation name(params) { … }, pub compute name(params) -> T = expr — no generic parameter slot. Real models duplicate identical bodies across concept-typed parameters: argon/examples/lease-story/packages/cofris/prelude.ar:603 declares pub compute total_asset_value(e: Enterprise) -> Money = 0 and packages/story-lease/src/computes.ar:91 re-declares pub compute total_asset_value(e: TenantLLCBooks) -> Money = …. The pattern repeats for equity_value, total_liability_value, total_expense_value, total_revenue_value. Each is a copy of the same shape parameterized over the enterprise concept.

RFD-0009 promised the compute side but didn’t ship. The committed RFD names compute f<T: HasValue>(x: T) -> Money as the canonical surface for generic computations. The grammar slot never landed. Derives and mutations were never specified at all.

The motivating ask (May 2026 design conversation): declare an abstract rule shape once, let multiple concrete cases fit it. The example:

pub predicate holds<S: Situation>(s: S)

pub derive holds(s: RentDue) :- s.amount > 0
pub derive holds(s: RentPaid) :- exists(p: Payment, p.lease = s.lease)

This RFD addresses the body-of-the-rule half of that ask — generics on rule declarations. The abstract-signature-without-body half (pure schema declarations that constrain conforming concrete rules) is reserved for a follow-up RFD; see Future work below.

Decision

Lift <T: Bound> from pub constraint to pub derive, pub mutation, pub compute. Semantics: monomorphizing expansion at composition time, following the existing GenericConstraint precedent and Rust’s compile-time monomorphization model. No dispatch, no typeclass, no coherence requirement.

1. Surface

pub derive name<T: Bound>(x: T, ...) :- body [=> consequence]

pub mutation name<T: Bound>(x: T, ...) { require? do emit* return? }

pub compute name<T: Bound>(x: T, ...) -> R = expr
pub compute name<T: Bound>(x: T, ...) -> R { body { … } | in { … } out { … } require? ensure? }
pub compute name<T: Bound>(x: T, ...) -> R impl rust("…")

Multiple parameters: <T: Foo, U: Bar>. Unconstrained: <T> (bound is the universal top type ). Parameter names follow the existing pub constraint convention — single-letter or short PascalCase identifiers.

Type parameters scope over the entire declaration. They may appear in parameter types, return types, rule-body atoms, mutation do { let x: T = … } bindings, and compute body { … } expressions.

2. Bound language

For v1, bounds are concept subtypes only. <T: Bound> means: at instantiation time, T is some concrete concept satisfying T <: Bound in the workspace’s concept lattice, where Bound is any concept reachable via the import graph.

The substrate makes no assumption about which metatype Bound’s instances belong to. Foundational-ontology packages classify concepts as kind, role, phase, mixin, etc. according to their own discipline — the substrate doesn’t see those distinctions. A <T: HasAmount> bound where HasAmount is UFO-classified as a mixin works exactly like <T: Lease> where Lease is UFO-classified as a relator. The bound resolves to a concept; the concept lattice supplies subtypes; monomorphization proceeds.

This RFD does not add field-shape bounds (<T: { amount: Money }>), where-clause bounds, or any inline structural specification. A package that wants structural typing declares a concept whose fields name the structural shape, and uses that concept as the bound. UFO’s existing mixin metatype is the prototypical example of how a foundational-ontology package wraps the substrate-level mechanism in its own vocabulary.

Inline field-shape bounds, multi-bound conjunctions, and where-clauses are future work; see below.

3. Monomorphization semantics

Generic rule declarations are stored abstractly in the per-package .oxc cache (RFD-0034) and instantiated at composition (ox build/ox compose, RFD-0034 §3).

For each generic declaration pub <kind> name<T: Bound>(...):

  1. The composer walks the workspace’s concept lattice and finds every concrete concept C such that C <: Bound and C is reachable via the workspace’s import graph.
  2. For each such C, the composer emits a monomorphized declaration pub <kind> name(x: C, ...) with every occurrence of T substituted by C in parameters, return type, and body.
  3. The monomorphized declarations enter the fixpoint engine alongside hand-written declarations. The engine never sees the abstract form.

The walk is transitive. Given pub kind Lease, pub subkind ResidentialLease : Lease, pub subkind CommercialLease : Lease, and pub phase ActiveLease : ResidentialLease, a generic pub derive holds<T: Lease>(l: T) :- l.active produces four monomorphizations: one for Lease, one for ResidentialLease, one for CommercialLease, one for ActiveLease.

Stable IDs include the resolved type (Cargo-style mangled names): stable_id("holds<ResidentialLease>"). Composition signatures (RFD-0030 §2) cover monomorphization sets — adding a new subtype of Bound to the workspace changes the signature and re-monomorphizes.

4. Composition with Datalog alternatives

Argon’s derive semantics is already multi-rule: multiple pub derive rules with the same head form a multi-headed Datalog program in which every body that matches contributes a derivation. Monomorphization preserves this exactly.

Given:

pub derive holds<S: Situation>(s: S) :- s.active

pub derive holds(s: RentDue) :- s.amount > 0

the composer produces one monomorphization of the generic per concrete subtype of Situation, then admits the hand-written holds(s: RentDue) as an additional rule with the same head. The fixpoint engine fires whichever bodies match. The union semantics matches the rest of the language.

If a modeler wants a specific case to override the generic default rather than add to it, they reach for defeasibility — pub derive holds(s: RentDue) [defeats: holds_generic_default] :- s.amount > 0 — using existing machinery (RFD-0005, RFD-0007). The generic surface itself does not introduce dispatch or specialization semantics; it produces additional alternatives.

5. Composition with package boundaries

Per RFD-0034 §3, wiring resolution and monomorphization are workspace-level operations. A generic declaration lives in its declaring package’s .oxc cache abstractly. When the workspace adds a new package that contributes subtypes of Bound, the composer regenerates monomorphizations to cover them.

Consequence: a downstream package that declares pub kind NewSituation : Situation automatically gains a holds(s: NewSituation) :- s.active derivation from any in-scope generic declaration over Situation. This matches the spirit of Datalog open-world composition and avoids the typeclass coherence problem (the new package isn’t picking a specific implementation — every generic just fires for every new subtype).

If a downstream package wants the generic NOT to fire for a particular subtype, the same defeasibility surface in §4 applies, scoped to the downstream package.

6. Generic computes and the FFI surface

For Form 1 (= expr) and Form 2 ({ body { … } | in { … } out { … } require? ensure? }) computes, monomorphization is straightforward — each clause is re-instantiated per concrete T, exactly as for derives.

For Form 3 (impl rust("…")), the FFI bridge must dispatch on concrete T. Two viable approaches:

(a) Generate one bridge function per monomorphization. impl rust("total<ResidentialLease>") resolves to a Rust function total_residential_lease(…) registered in the RustComputeRegistry. The codegen pipeline produces the per-T entry points.

(b) Single bridge function with runtime dispatch. impl rust("total") resolves to a single Rust function that branches on the concrete T at runtime via the schema.

Approach (a) is consistent with Rust’s monomorphization model and avoids runtime dispatch cost. Approach (b) is consistent with the kernel’s existing RustComputeRegistry shape (one named compute, one Rust function). The implementation chooses one (recommended: (a), with a compile-time error if any monomorphization’s bridge function is missing — OE0XXX MissingFFIBridge).

7. Substrate neutrality

Three invariants the implementation must honour:

  1. No metatype assumption. <T: Bound> says T is a subtype of Bound. The substrate does not require T to be classified by any particular metatype (kind, mixin, role, etc.). Foundational-ontology packages may impose metatype constraints on T via their own structural rules; the substrate does not.
  2. No field-shape assumption. The bound is the concept, not a field signature. The substrate does not inspect the bound concept’s fields to derive an implicit structural constraint. Field access on a T-typed binding fails the same way it would on a non-generic Bound-typed binding: the field must be declared on the bound concept itself or on a known subtype.
  3. No new vocabulary. This RFD introduces no new keywords. <T: Bound> reuses existing tokens (<, >, identifier, :). The semantics piggybacks on existing concept-lattice machinery in oxc::meta and the GenericConstraint precedent.

8. Diagnostics

Five diagnostic codes register in oxc-protocol::core::codes:

CodeSeverityTrigger
OE02XX GenericBoundNotConceptError<T: SomeBound> where SomeBound doesn’t resolve to a declared concept.
OE02XX GenericArityMismatchErrorGeneric parameter count differs between declaration and a reference site.
OE02XX MonomorphizationFailedErrorA monomorphization candidate failed type-checking (e.g. field access on T that resolves to a concrete type lacking that field). The diagnostic carries the concrete T that triggered the failure.
OE02XX MissingFFIBridgeErrorA Form-3 compute’s impl rust("…") resolves to a name with no corresponding bridge function for some concrete monomorphization.
OW02XX EmptyMonomorphizationWarningA generic declaration’s Bound has zero concrete subtypes in the workspace. The declaration parses but produces no concrete rules.

Exact code numbers reserved at implementation time per the RFD-0024 namespace.

9. Composition-signature interaction

Monomorphization sets are part of the workspace’s composition signature (RFD-0030 §2). The signature input includes:

  • For each generic declaration: its abstract IR + the set of concrete subtype-of-Bound concepts in scope at compose time
  • For each monomorphization: its concrete IR

A new subtype of any in-scope Bound changes the composition signature and triggers re-monomorphization. The .oxc cache invariant from RFD-0034 §4 (“cache is composition-specific”) holds without modification.

10. CLI surface

No new commands. ox build and ox compose (per RFD-0034 §10) cover monomorphization implicitly. oxc emit core-ir per RFD-0034 §10 shows both abstract generic declarations and their monomorphizations. oxc emit monomorphizations is a new diagnostic emit shape, scoped to this RFD’s introspection needs.

Rationale

Three independent design pressures converge on monomorphization. Rust’s <T: Trait> model (compile-time monomorphization, no runtime dispatch), Argon’s existing pub constraint <Ax: ordered_axis> machinery (axis-database walk + per-axis instantiation), and Lean 4’s elaboration-time type-class resolution all produce concrete declarations at compile time and leave the runtime to consume monomorphic code. Argon already pays the monomorphization cost on the constraint side; lifting it to derives, mutations, and computes reuses the same engine.

Datalog alternatives subsume what typeclasses would otherwise need to do. In a typeclass system, one declares an abstract method, multiple instances supply implementations, and dispatch picks one at the call site. In Argon’s Datalog-alternatives world, multiple rules with the same head all contribute; no dispatch is needed because the semantics is union. Monomorphization produces additional alternatives, not competing implementations. This is what makes Argon’s generic surface simpler than Rust’s or Haskell’s: the language doesn’t have a coherence problem because it doesn’t have a dispatch problem.

Substrate neutrality is preserved by construction. Bounds are concepts; concepts are user-declared via pub metatype-classified declarations; the substrate doesn’t know what kind of concept any particular bound is. UFO packages can call something a mixin and use it as a structural bound; BFO packages can use a continuant similarly; hand-rolled vocabularies can declare anything they want. The substrate provides the mechanism; foundational ontologies supply the vocabulary.

The minimal surface is also the most extensible. Closing on monomorphizing-generics-only leaves abstract signatures (RFD-0037+), inline field-shape bounds, and full traits as independent follow-on RFDs. Each can be evaluated on its own merits without committing to it now. Conversely, shipping a more ambitious surface here would force premature design decisions about coherence, default-method specialization, and dispatch semantics — all of which Argon may never need.

Closing the RFD-0009 promise. RFD-0009 named the compute generics surface as committed but never delivered the grammar. This RFD delivers it and extends to derives + mutations under the same machinery. The “no parametric concepts” position of RFD-0009 holds without amendment.

Consequences

  • Grammar: the existing <Ax: bound> parse logic in argon/oxc/src/cst/grammar/items.rs:954 (currently inside parse_package_constraint) factors into a reusable parse_generic_param_list. parse_derive_decl (rules.rs), parse_mutation (modules.rs), and parse_compute (modules.rs) each gain an optional generic-param-list slot after the keyword + name.

  • AST: DeriveDecl, MutationDecl, ComputeDecl each carry an Option<Vec<GenericTypeParam>>. The shape mirrors ConstraintDecl.

  • IR: CoreDerive, CoreMutation, CoreComputation each grow an optional type_params: Vec<TypeParam> field. Abstract IR is what the .oxc cache stores; the workspace composer produces concrete monomorphizations alongside.

  • Elaborator: a new pass in argon/oxc/src/elaborate/ walks generic declarations, computes their subtype-of-Bound sets, and emits monomorphized declarations. The pass runs after standard concept-lattice elaboration and before lowering. The existing GenericConstraint monomorphization in oxc::meta is the implementation template.

  • Stable IDs: stable_id includes resolved type parameters when present (name<ResolvedT>::field). The FNV-1a content-hash on .oxc cache entries covers the monomorphization set so the lockfile (RFD-0013) tracks changes.

  • Composition signature (RFD-0030 §2): monomorphization sets are signature inputs. Subtype additions invalidate caches.

  • Kernel API (RFD-0023): query name <ResolvedT> ( … ) becomes valid; the kernel resolves generic queries against the monomorphized set. v2 endpoint validators learn the new shape.

  • Documentation: argon/book/src/ch02-05-computations-and-mutations.md and ch02-04-rules-and-reasoning.md each get a “generic declarations” section. The for-agents glossary entry on generics (per the open-questions doc-tracker, #396 D.5) extends to cover rule-side generics.

  • Examples: argon/examples/lease-story/packages/cofris/prelude.ar and packages/story-lease/src/computes.ar can collapse the per-Enterprise-subtype compute duplication into a single generic pub compute total_asset_value<T: Enterprise>(e: T) -> Money declaration. Worth doing in the same PR as a demonstration; otherwise the deduplication is opportunistic.

  • Diagnostic registry: five new codes per §8.

  • Migration: none. Existing monomorphic declarations continue to parse and elaborate unchanged.

Open questions

  1. FFI bridge approach for Form-3 generic computes. §6 lists two options ((a) per-monomorphization bridge fns, (b) single bridge with runtime dispatch). Recommendation: ship (a) by default; allow impl rust("…", dispatch = "runtime") opt-in for cases where the Rust side prefers a single entry point. To be finalized during implementation.

  2. Empty-bound semantics. Should EmptyMonomorphization be a warning or an error? Argument for warning (current §8): the generic might gain monomorphizations later as the workspace grows. Argument for error: zero concrete subtypes likely indicates a misnamed bound. Recommendation: warning, with an #[allow(empty_monomorphization)] style opt-in if a package legitimately ships a generic ahead of any subtypes.

  3. Subtype walk scope on private concepts. Should monomorphization extend over pub-visible subtypes only, or include package-private subtypes? Recommendation: pub-visible only — pub <kind> name<T: Bound> is visible across packages; package-private subtypes within the declaring package should not leak their monomorphizations to consumers. (A package-private generic + package-private subtype would still monomorphize within the package, just not visibly.)

  4. Where-clause refinement on type parameters. <T: Bound where T.foo == bar> is a natural extension once refinement-by-meta-property (per RFD-0016) is fully wired. Out of scope for v1.

Future work

This RFD ships the minimum useful surface. Subsequent RFDs may build on it:

  • Abstract rule signatures (RFD-0037+ candidate). Declarations of the shape pub signature name<T: Bound>(x: T) (no body) — pure schemas that constrain conforming concrete derives/mutations/computes. Henrique’s pub predicate proposal, in substrate-neutral terms.

  • Inline field-shape bounds. <T: { amount: Money }> for ad-hoc structural constraints without declaring a named concept. Useful when the only intent is “T has these fields” and naming a concept would be ceremony.

  • Multi-bound conjunctions. <T: Foo + Bar> for cases where T must specialize multiple bounds simultaneously.

  • Where-clause refinement on type parameters (per Open Question 4 above).

  • Trait/typeclass dispatch. Reserved for when (and if) real workloads need pick-one-implementation semantics that doesn’t compose with Datalog alternatives. Likely a separate pub trait keyword if it ever lands; not in scope for this RFD and not a commitment.

  • Generic relations and metatypes. pub rel name<T>(...) would re-open the parametric-concepts problem RFD-0009 closes. Out of scope; closed by reference to RFD-0002 + RFD-0009.

Relationship to other RFDs

  • RFD-0009 (Generics are functor modules and generic computations). This RFD delivers the compute-generics surface RFD-0009 promised, and extends to derives + mutations under the same machinery. RFD-0009’s “no parametric concepts” position holds.

  • RFD-0034 (Composition pipeline). Monomorphization runs at compose time. Abstract generics live in per-package .oxc caches; concrete monomorphizations land in the workspace .oxbin artifact (RFD-0035).

  • RFD-0035 (Binary artifact + execute layer). .oxbin carries monomorphized declarations in the symbol table + rule sections. The abstract generic form does not appear in .oxbin — it’s a per-package cache concept, materialized away by compose time.

  • RFD-0024 (Diagnostic codes). Five new codes register per §8.

  • RFD-0030 (Composition signature). Monomorphization sets are signature inputs.

  • RFD-0013 (Bivalent lockfile). Lockfile content-hashes include monomorphization sets through the composition signature.

  • RFD-0005 (One grammar, multiple contexts). Generic parameter syntax is identical across derive, mutation, and compute — the existing “context determines semantics, not syntax” discipline applies.

  • RFD-0019 (Patterns). Patterns emit declarations on explicit instantiation; generics monomorphize implicitly by subtype walk. Both mechanisms can coexist; they cover different reuse shapes.

Historical lineage

The two-phase abstract-IR + monomorphize-at-compose pattern is the convergent shape across:

  • Rust (Hume 2019, “A Tour of Metaprogramming Models for Generics”) — <T: Trait> monomorphizes at the mono-collector pass before MIR lowering.
  • Lean 4 (de Moura et al. 2015, “Elaboration in Dependent Type Theory”) — instance synthesis at elaboration; instances fully resolved before code generation.
  • GenericConstraint in oxc::meta::generics.rs (Argon, in-tree) — UFO R-rule schemas parameterize over axes; the elaborator monomorphizes against the axis database. Three years of internal use; the pattern is proven on the constraint side.

The Datalog-alternatives composition (§4) draws on the modular Datalog literature, particularly the perspectival-Datalog work that grounds RFD-0030. Multiple rules with the same head are the union of bodies; monomorphization produces additional union members rather than competing implementations. No coherence machinery needed.

The decision not to ship trait/typeclass dispatch in v1 follows the discipline RFD-0009 set: the simplest mechanism that addresses the immediate need, with extensibility reserved. Jones’s qualified types (Jones 1994, ESOP) provide the formal foundation if dispatch ever lands; the concept-lattice-as-bound mechanism is already a qualified-types restriction (one predicate symbol: subtype-of).

RFD-0037 — AFT-grounded truth-value semantics — bilattice substrate, K3/FDE/Boolean lattice contexts, fail-closed projection, per-standpoint consistency policy

Committed Opened 2026-05-08 · Committed 2026-05-08 · Supersedes RFD-0016

Question

What is the truth-value semantics under which Argon evaluates refinement membership, query results, rule bodies, and cross-standpoint federated queries? When does the inconsistent value both arise; when does it not; and how does the language guarantee that “K3 contexts cannot accidentally observe both” without losing the ability to express genuine cross-source disagreement?

Context

RFD-0016 ratified that refinement membership under OWA is three-valued (Kleene-Belnap), with success requiring true. The MetaValue (IS/CAN/NOT) implementation in meta-property-fixpoint realizes this directly. The Lean 4 mechanical proof at argon-formal/ (804 lines, zero sorrys) establishes flow-typing soundness over the K3 fragment.

Two pressures push beyond the original D-113 framing:

  1. Standpoint federation. RFD-0034 and RFD-0035 commit to per-standpoint composition with cross-standpoint queries as a first-class operation. When a federated query aggregates results from multiple standpoints that genuinely disagree (e.g., federal-irs says T while state-ca-ftb says F), the K3 truth-value space has no value to record the disagreement — it can only collapse to unknown, losing the structural information that distinguishes “no source had evidence” (Belnap N) from “sources disagreed” (Belnap B).
  2. Terminology rigor. The phrase “Kleene-Belnap three-valued” is informal: K3 (Kleene strong, 1952) is three-valued; Belnap-Dunn FDE (1977/1976) is four-valued. The literature converges on the synthesis that the natural restriction of Belnap’s logic to an OWA single-source setting is K3 — but the original RFD-0016 didn’t pin this down, leaving room for future inconsistency (e.g., RFD-0035 §12 originally claimed “four-valued Kleene-Belnap” before this RFD’s correction).

Approximation Fixpoint Theory (Denecker, Marek & Truszczynski 2000; refined 2004) is the published, peer-reviewed framework that subsumes K3, FDE, Boolean, and every CWA variant Argon could plausibly want — under one bilattice algebra. AFT has been proven to capture: logic programming with negation-as-failure, default logic, autoepistemic logic, abstract dialectical frameworks, hybrid MKNF (Liu & You 2019, the most direct match for our setting), active integrity constraints, distributed AEL, higher-order LP via weak bilattices, and fuzzy LP. Stratification across modules (Vennekens, Gilis & Denecker 2006) handles cross-standpoint composition by construction.

This RFD adopts AFT as Argon’s truth-value semantic foundation, ratifies the bilattice substrate, and specifies how the K3/FDE/Boolean lattice contexts emerge as type-level constraints over a single uniform storage layer.

Decision

Argon adopts a bilattice-parametric truth-value semantics grounded in Approximation Fixpoint Theory. Storage is the Belnap-Dunn FDE four-valued bilattice; observable values are determined by lattice context, a type-level constraint that restricts which Truth4 values an expression may carry. Three named contexts:

1. The Truth4 lattice (FDE)

The substrate is Truth4, the four-valued Belnap-Dunn bilattice:

ValueMeaningAFT pair (lower, upper)
isTold only true(T, T)
notTold only false(F, F)
canTold nothing yet (Belnap N / “neither”)(F, T) (consistent: F ≤ T)
bothTold both T and F by distinct sources (Belnap B)(T, F) (inconsistent: T ≰ F)

Two orderings (Fitting 1991):

  • Truth ordering ≤_t: not ≤_t (can | both) ≤_t is. can and both are incomparable in truth.
  • Information ordering ≤_k: can ≤_k (is | not) ≤_k both. is and not are incomparable in information.

Four operations: truth meet ∧_t, truth join ∨_t, negation ¬, information meet (consensus, drops disagreement to can), information join (gullibility, accumulates conflict to both). The bilattice algebra is settled by Fitting 1991.

2. Lattice contexts as type-level constraints

Three named contexts restrict observable values:

ContextConstraintObservableUse
Booleancomplete pairs (x, x){is, not}CWA-mode rules; predicates with stratified negation (Horn-restricted refinement bodies)
K3consistent pairs (x, y) with x ≤ y{is, not, can}Refinement membership; per-standpoint queries; flow-typing narrowing. Default for OWA modules.
FDEunconstrained{is, not, can, both}Cross-standpoint federated queries; explicit multi-source aggregation

Subtyping is information-precision: Boolean ⊑ K3 ⊑ FDE. The K3-context typecheck guarantees that both is statically unreachable — not silently hidden behind a default — because the consistent-pair constraint excludes (T, F) by construction.

3. K3 closure under the K3 operations

The K3 fragment is closed under {∧_t, ∨_t, ¬, ⊗} and not closed under . The escape witness is is ⊕ not = both — exactly the federation-across-disagreeing-sources case. This is the formal characterization of “K3 contexts can use the truth-side connectives and consensus, but federation/gullibility produces FDE.”

(Mechanical proof in ArgonFormal/Foundation/LatticeContext.lean: theorems truthMeet_inK3, truthJoin_inK3, neg_inK3, infoMeet_inK3, infoJoin_escapes_K3.)

4. K3 ↔ MetaValue isomorphism

The existing MetaValue (IS/CAN/NOT) is exactly the K3 fragment of Truth4. The bidirectional embedding MetaValue ↔ {v : Truth4 // v.inK3} is constructive and an information-ordering isomorphism. Every existing MetaValue theorem lifts to a K3-fragment Truth4 theorem; every Truth4 theorem on the K3 fragment specializes to a MetaValue theorem. The existing flow-typing soundness proof transfers verbatim.

(Mechanical proof: ArgonFormal/Foundation/LatticeContext.lean, ofMetaValue_infoLe_iff, ofMetaValue_toMetaValue, toMetaValue_ofMetaValue.)

5. World-assumption modes are AFT operators

Each per-standpoint world-assumption mode maps to a specific AFT approximator construction:

ModeAFT operator
OWAIdentity approximator (no closure).
CWA (Reiter 1978)Stratified-negation closure; well-founded fixpoint = unique stable model when stratified. Negation-as-failure (Clark 1978) is the procedural form.
LCWA (Denecker et al. 2010)Parameterized window operator. Closed predicates are the binary-window special case.
Hybrid MKNF + WFS (Knorr-Alferes-Hitzler 2011)Liu & You 2019’s alternating-fixpoint AFT approximator.
CircumscriptionVia AEL or stable-model bridge (DMT 2003).

These are operator choices, not lattice choices. The lattice context (Boolean/K3/FDE) determines what’s observable; the AFT operator determines how knowledge propagates.

6. Per-standpoint stratified composition

Each standpoint declares its world-assumption operator. Cross-standpoint composition is stratified AFT (Vennekens-Gilis-Denecker 2006): when the standpoint dependency graph is a DAG, the global well-founded fixpoint computes standpoint-by-standpoint. Inflationarity of each per-standpoint operator implies inflationarity of the cross-standpoint fold; narrowings established at any earlier standpoint persist through the composition.

(Mechanical proof: ArgonFormal/Standpoint/Stratification.lean, theorems crossStandpointFold_inflationary, narrowing_preserved_across_standpoints, stratified_cross_standpoint_soundness.)

7. Cross-standpoint federation = AFT info-join

A federated query aggregates per-standpoint results via AFT information join (gullibility). The federated result reads:

ResultCause
isAt least one source said T; none said F or both.
notAt least one source said F; none said T or both.
canNo source had evidence.
bothAt least one source said T and at least one said F (or any source already said both).

The federation operator’s signature is Federate<FDE> — federated results live in the FDE context. K3 consumers must explicitly project (see §8).

(Mechanical proof: ArgonFormal/Standpoint/Federation.lean, theorems federate_eq_classify, federate_eq_both_iff, federate_inK3_of_agreement.)

8. Fail-closed projection FDE → K3 with explicit policy

When an FDE value flows into a K3-typed consumer, the user must specify a projection policy. The language does not silently collapse both to can. Four built-in policies:

PolicyEffect on both
collapseToUnknownboth → can. Most common; treats source disagreement as “we don’t know.”
treatAsErrorboth → fail. Surfaces the disagreement as a downstream diagnostic.
preferIsboth → is. Priority resolution: T-side wins.
preferNotboth → not. Priority resolution: F-side wins.

The treatAsError projection succeeds iff the input is in the K3 fragment — this is the formal “fail-closed” property that the type system enforces on top of the policy decision.

(Mechanical proof: ArgonFormal/Foundation/Projection.lean, theorems project_inK3, project_treatAsError_iff, roundtrip_K3_FDE_K3.)

A future cut may add a syntax-level shorthand (analogous to Rust’s ?) for the most common policy, declared at the module level. The shorthand is purely additive and does not change the type rule; the projection remains visible at the source in some form.

9. Per-standpoint consistency policy

Each standpoint declares a ConsistencyPolicy controlling within-standpoint append-time behaviour:

PolicyAppend behaviour
strict (default)Reject writes that would create an inconsistent pair within the standpoint. The standpoint’s cells stay in the K3 fragment. Sharpe’s standard mode.
paraconsistentAccept all writes; inconsistencies persist as both-valued cells. Surface in queries via FDE projection. Useful for global-KG-mirror-style systems.

Cross-standpoint disagreement is always preserved regardless of policy because it manifests at federation join time, not at within-standpoint append time. Different standpoints with different policies coexist in one workspace without cross-contamination.

(Mechanical proof: ArgonFormal/Standpoint/Consistency.lean, theorems append_strict_inK3, append_strict_fails_iff, strictFold_from_can_inK3.)

10. Refinement membership in K3 with Pietz-Rivieccio designation

Refinement membership under OWA defaults to the K3 lattice context with the Exactly True Logic designation (Pietz & Rivieccio 2013): only is is designated. can and both (the latter is statically unreachable in K3 contexts) both fail the membership check; the call site sees a refinement-violation diagnostic.

This is the rigorous restatement of D-113’s “success requires true, not ‘anything but false’” rule. Pietz-Rivieccio is the published anchor for the designation choice; Belnap (1977) supplies the philosophical motivation; Kleene (1952) supplies the value space and connective truth tables.

11. K3 vs Łukasiewicz Ł3 — K3 chosen

The two classical three-valued logics differ on the conditional: K3 maps U → U to U; Ł3 maps U → U to T. K3 is chosen because Ł3’s rule makes p → p a tautology even when p is unknown — silently passing checks the system cannot verify. K3’s pessimistic propagation is the fail-closed move that matches the rest of the design.

K3 truth tables (strong Kleene; the propagation rule for the Truth4 truth meet/join restricted to K3):

∧ | T  U  F        ∨ | T  U  F        ¬ |
T | T  U  F        T | T  T  T        T | F
U | U  U  F        U | T  U  U        U | U
F | F  F  F        F | T  U  F        F | T

The conditional is currently not a primitive in Argon’s refinement-body surface; if/when it lands, it inherits K3’s semantics by default.

12. CWA collapse soundness witness

D-113 states that under CWA modules, unknown collapses to false. The soundness of this collapse rests on a syntactic restriction: refinement bodies in the current implementation admit only meta-property predicates over a finite type graph (the Horn-restricted fragment per book/src/ch02-06-the-type-system.md:129). Reiter (1978)’s warning that CWA can introduce inconsistency in non-Horn theories does not bite because the refinement language is Horn. Lutz et al. 2013’s coNP-hardness result for closed-predicate DL-Lite is the formal warning: any future extension that admits non-Horn refinement bodies must re-argue soundness.

The Lean 4 proof at ArgonFormal/TypeSystem/Soundness/CwaOwa.lean covers the existing K3 case (cwa_narrowing_into_owa); the new framework’s per-standpoint policies preserve this property by construction (strict-policy K3 standpoints contain only K3-fragment cells; the existing soundness theorem applies).

13. Forward compatibility

The framework is naturally extensible without breaking the K3/FDE/Boolean contracts:

  • Refined approximation spaces (Vanbesien et al. 2025) — beyond intervals; relevant if confidence-weighted reasoning lands.
  • Non-deterministic AFT (Heyninck et al. 2022) — disjunctive extension; relevant for explicit choice-point reasoning.
  • Higher-order LP via weak bilattices (Charalambidis et al. 2024) — for nested-rule contexts.
  • Continuous truth-value lattices for soft constraints / probabilistic reasoning.

None of these require breaking the K3/FDE/Boolean lattice contexts; they add new contexts on top of the same AFT substrate.

14. Diagnostic codes introduced

CodeSeverityTrigger
OE0610ErrorStrict-policy append rejected: would create inconsistent pair within standpoint.
OE0611ErrorFDE → K3 projection with treatAsError policy received both-valued input.
OE0612ErrorLattice-context type mismatch: K3-typed expression observes both-valued value (typecheck rule violated).
OW0613WarningFederation produced both value; K3 consumer projected with non-error policy.

Codes register in oxc-protocol::core::codes via the existing inventory::submit! registry.

15. CLI surface

This RFD does not introduce new top-level commands. The lattice-context type parameter shows up in:

  • ox query NAME — defaults to --lattice K3. --lattice fde opts into federation behavior. --lattice boolean is the CWA-only path (Horn-restricted bodies only).
  • ox query NAME across [s1, s2, ...] — federated query, returns FDE by default.
  • ox check and ox test — typecheck the lattice-context constraints.
  • ox doc and ox inspect — surface the lattice context of each declared query/mutation/rule.

Per-standpoint consistency policy is declared in the standpoint’s manifest entry (e.g., [standpoints.federal-irs] consistency = "strict").

Rationale

Three reasons AFT is the right substrate:

  1. Convergent literature. AFT (Denecker, Marek, Truszczynski 2000; refined 2004) is the published peer-reviewed framework that subsumes every CWA variant Argon could plausibly want. The connections are well-traced in the literature: LP/NAF, default logic, AEL, MKNF+WFS, ADFs, AICs, fuzzy LP, distributed AEL — all are AFT instances. Stratification (Vennekens-Gilis-Denecker 2006) handles cross-standpoint composition by construction.

  2. The “carry B as None when it can’t arise” intuition is literal. The AFT pair (x, y) with consistency constraint x ≤ y is exactly what the K3 lattice context enforces. The both value (T, F) is statically unreachable, not silently hidden. This is the cleanest possible realization of the design intuition: type-level capability restrictions on a uniform storage layer.

  3. The per-context lattice = the right value space for the right job. Refinement membership wants K3 (success requires positive evidence; fail-closed). CWA modules want Boolean (Horn-restricted; can statically eliminable). Cross-standpoint federation wants FDE (both is meaningful; provenance witnesses source disagreement). Forcing one truth-value space everywhere either weakens the system (just K3, lose federation expressiveness) or burdens single-source contexts with a value they can’t produce (just FDE, type-pollution). Lattice-parametric is the right shape.

Why supersede RFD-0016 rather than amend. D-113 / RFD-0016’s decision — refinement membership under OWA is three-valued, success requires T — is unchanged. What changes is the framing: the rigorous published name (K3, motivated by Belnap’s question-answering argument), the Pietz-Rivieccio designation, the CWA-collapse soundness witness (Horn), the K3-vs-Ł3 conditional choice, and the placement of the K3 case as one fragment of the broader bilattice. This is more than a clarification edit. Per rfd/README.md’s lifecycle, supersession is the honest move when the new RFD names the prior position as a special case of a broader framework.

Why fail-closed projection. Argon follows Rust’s “simple, powerful rules, and a compiler that helps you” convention. Silent collapse from FDE to K3 (B → U with no syntactic mark) is exactly the silent-error-handling pattern Rust rejects via Result / ?. A K3 consumer of an FDE value must declare its policy; the call site is visible; reviewers can ask “should this be BothToUnknown or BothToError?” without remembering a global default. The ergonomic cost is one method call (or future shorthand operator) at the federation→K3 boundary, paid only by users who actually run federated queries.

Why per-standpoint consistency policy. The “Argon rejects invalid writes” instinct (Sharpe’s preferred mode) and the “persist source disagreement for repair” instinct (multi-source-KG mode) are both legitimate. Forcing one would either burden Sharpe with paraconsistent semantics it doesn’t want or prevent multi-source-KG users from declaring a paraconsistent standpoint. Per-standpoint declaration scopes the choice to the natural unit (the standpoint is the “single consistent perspective” by definition); cross-standpoint disagreement is preserved regardless of policy because it manifests at federation, not at append.

Why mechanical verification before commit. The new framework subsumes a settled RFD’s behaviour; getting that subsumption wrong is a class of bug that’s hard to catch by inspection. The Lean 4 proof at argon-formal/ (now extended by 1528 lines, zero sorrys, eight new theorems) pins the structural soundness — particularly that the K3 fragment is closed under the K3 operations, that strict-policy folds preserve K3, and that the new framework reproduces D-113’s K3 result exactly when restricted to single-standpoint strict-Horn settings. The proof IS the soundness witness; it’s not aspirational.

Consequences

  • oxc-protocol::core::truth_value — new module carrying the typed Truth4 enum, lattice-context predicates (inK3, inBool, inFDE), ProjectionPolicy, ConsistencyPolicy, and the wire-shapes for federation results. Drift-gated through oxc-codegen.
  • crates/nousExprResult (currently {Satisfied, NotSatisfied, Incomplete}) becomes a parametric EvalResult<T, L> where L : LatticeContext. The K3 case (L = K3) reduces to the existing three-valued shape; the FDE case adds Inconsistent { conflict_witness }. This is a structural change with codegen ripple.
  • MetaValue — unchanged at the implementation level; positioned as the K3 fragment of the new Truth4. The Truth4.ofMetaValue embedding bridges the two without breaking any existing code path.
  • Per-standpoint manifest[standpoints.<name>] consistency = "strict" | "paraconsistent" is added to the standpoint declaration grammar. Default strict; existing standpoints inherit the default with no migration needed.
  • Federation operator — new query primitive query NAME across [s1, s2, ...] returns EvalResult<T, FDE>. K3 consumers pipe through explicit .project(<policy>).
  • Diagnostic codesOE0610, OE0611, OE0612, OW0613 register in oxc-protocol::core::codes.
  • RFD-0016 supersessionrfd/0016-refinement-under-owa-is-three-valued.md flips to state: superseded, with superseded_by: 0036. Body is preserved verbatim per the lifecycle rule.
  • Book chapter sweepch02-06-the-type-system.md (refinement types), ch02-04-rules-and-reasoning.md (occurrence typing), ch05-01-metatype-calculus.md, ch05-02-defeasibility.md, ch05-04-standpoints-bitemporal.md, book/src/for-agents/glossary.md, appendix-c-diagnostics.md — update to cite RFD-0037 and the K3 / FDE / Boolean lattice-context terminology. Pin “K3” and “FDE” as glossary terms.
  • AGENTS.mdargon/AGENTS.md:57 line on three-valued semantics tightens to “K3 (Kleene strong three-valued) refinement membership; cross-standpoint federation in FDE; see RFD-0037.”
  • CHANGELOG entry — under the next argon release, document the framework adoption + the parametric EvalResult type.
  • Diagnostic-message stringsoxc/src/diagnostics/codes.rs:579, 1369, oxc/src/diagnostics/rendering.rs:513, oxc/src/elaborate/validate.rs:974 — replace “Kleene-Belnap three-valued semantics” with “K3 (Kleene strong three-valued) semantics; see RFD-0037.” (Per RFD-0024’s anti-pattern rule against citing decision IDs in user-facing diagnostic strings, the RFD reference goes in the explain-text, not the message body.)
  • Lean 4 proof obligation discharged. argon-formal/ extended by Truth4.lean, LatticeContext.lean, Projection.lean, StandpointStratification.lean, Federation.lean, Consistency.lean, BackwardCompat.lean. 1528 lines added, zero sorrys. The mechanical-proof reference goes into the new RFD plus the D-113 frontmatter.
  • Forward-compatibility statement on file. Refined approximation spaces (Vanbesien 2025), non-deterministic AFT (Heyninck 2022), higher-order via weak bilattices (Charalambidis 2024), and continuous-confidence lattices all extend the framework additively. Not committed today; not closed off.
  • RFD-0038 (discussion-shape) opened in parallel. Runtime capability advertisement — how OxbinRuntime::capabilities() (RFD-0035 §14) reports supported AFT operators per the framework above. Open questions, not commitments. Dedicated design session before drafting any answers.

Historical lineage

  • Approximation Fixpoint Theory — Denecker, Marek, Truszczynski (2000). Approximations, Stable Operators, Well-Founded Fixpoints, and Applications in Nonmonotonic Reasoning. Refined in (2004) Ultimate Approximation. The substrate.
  • Stratification of approximators — Vennekens, Gilis, Denecker (2006). Splitting an Operator: Algebraic Modularity Results for Logics with Fixpoint Semantics. The cross-standpoint composition tool.
  • Hybrid MKNF as AFT approximator — Liu & You (2019). The bridge to OWA + rule-CWA hybrids. Argon’s per-standpoint world-assumption mode generalizes this.
  • Bilattice algebra — Fitting (1991). Bilattices and the Semantics of Logic Programming. The two-orderings algebra Argon’s Truth4 realizes.
  • Belnap-Dunn FDE four-valued logic — Belnap (1977) A Useful Four-Valued Logic; Dunn (1976) Intuitive Semantics for First-Degree Entailments and ‘Coupled Trees’. The FDE value space; motivation for needing N vs B distinction in multi-source settings.
  • Kleene strong three-valued K3 — Kleene (1952) Introduction to Metamathematics §64. The K3 fragment Argon uses for refinement membership and per-standpoint queries.
  • Exactly True Logic / “only T designated” — Pietz & Rivieccio (2013) Nothing but the Truth. The rigorous published anchor for the fail-closed designation.
  • CWA / NAF foundations — Reiter (1978) On Closed World Data Bases; Clark (1978) Negation as Failure. The classical CWA framework, captured by AFT’s stratified-negation operator.
  • LCWA — Denecker et al. (2010). The parameterized-window CWA generalization, captured as an AFT instance.
  • Closed predicates in DL-Lite — Lutz et al. (2013). The forward-compatibility warning: non-Horn extensions to refinement bodies require re-argument.
  • Forward-compatibility extensions — Vanbesien et al. (2025) refined approximation spaces; Heyninck et al. (2022) non-deterministic AFT; Charalambidis et al. (2024) higher-order via weak bilattices.

The K3-as-restriction-of-FDE synthesis is not invented here — it’s the consensus reading the literature converges on (Fitting 1991 and Belnap 1977 are the canonical statements; the relationship is flagged explicitly across the survey literature). Argon’s contribution is the design realization: making the lattice-context constraint a first-class type-level discipline, with explicit fail-closed projection and per-standpoint consistency policy as the architectural surfaces.

The mechanical Lean 4 proof at argon-formal/ is the soundness witness. Eight new theorems extend the existing 804-line proof by 1528 lines with zero sorrys. The proof is the literal verification that this RFD’s commitments are internally consistent and that the new framework reproduces RFD-0016’s K3 case exactly under the stated restriction.

D-113 (the prior decision identifier) remains the historical anchor; its body is preserved verbatim. The new RFD is its formalization, not its replacement.

RFD-0038 — Runtime capability advertisement — what OxbinRuntime::capabilities() reports about supported AFT operators and lattice contexts

Discussion Opened 2026-05-08

Question

RFD-0035 §14 specifies the OxbinRuntime trait surface, including fn capabilities(&self) -> RuntimeCapabilities. RFD-0037 commits to AFT as the truth-value semantic foundation, with per-standpoint world-assumption operators (OWA, CWA, LCWA, MKNF+WFS, …) and three lattice contexts (Boolean, K3, FDE). What should RuntimeCapabilities advertise, and how does artifact-runtime tier matching incorporate the truth-value framework?

A runtime that does not implement the well-founded fixpoint cannot load an artifact whose composition signature requires it. A runtime that supports only K3 refinement membership cannot load an artifact that uses cross-standpoint federation (FDE-typed queries). The Layer-1 / Layer-2 validation hooks in RFD-0035 §6–§7 need to incorporate operator-and-lattice-context capability matching alongside the existing tier-table check.

This RFD opens the design space; it does not commit answers. A dedicated design session is needed before this RFD goes to committed.

Context

The relevant surfaces:

  • OxbinRuntime::capabilities() (RFD-0035 §14) — currently advertises max_tier_supported, resource bounds, feature flags. Loosely typed.
  • Layer-1 validation (RFD-0035 §6) — closed-boolean, O(1): refuses artifact when max_tier_claimed > max_tier_supported.
  • Layer-2 validation (RFD-0035 §6, §7) — per-section verifier; runs after Layer-1 passes. Currently covers symbol resolution, lattice acyclicity, provenance well-formedness, composition-signature consistency, tier-table consistency, doc-block well-formedness.
  • AFT operators (RFD-0037 §5) — per-standpoint mode declared in the standpoint manifest entry. Maps to a specific approximator construction: OWA (identity), CWA (stratified-negation closure), LCWA (window-parameterized), MKNF+WFS (Liu-You alternating fixpoint), circumscription (via stable-model bridge).
  • Lattice contexts (RFD-0037 §2) — Boolean / K3 / FDE. Each typed query, mutation, and rule has a declared context.
  • Per-standpoint consistency policy (RFD-0037 §9) — strict or paraconsistent. Affects append-time invariant checking.
  • Artifact metadata — the .oxbin carries per-standpoint operator declarations + per-query lattice contexts in the global-control section’s serialized manifest.

The capability-mismatch question has at least four sub-questions. Each is open.

Open questions

Q-37.1: What’s in RuntimeCapabilities?

Plausible shape (not committed):

#![allow(unused)]
fn main() {
pub struct RuntimeCapabilities {
    pub max_tier_supported: TierLevel,
    pub supported_aft_operators: BTreeSet<AftOperatorClass>,
    pub supported_lattice_contexts: BTreeSet<LatticeContext>,
    pub supports_paraconsistent_standpoints: bool,
    pub supports_federation: bool,
    pub resource_limits: ResourceLimits,
    pub feature_flags: BTreeSet<FeatureFlag>,
}

pub enum AftOperatorClass {
    Identity,             // OWA
    StratifiedNegation,   // CWA / Reiter+Clark
    WindowedClosure,      // LCWA
    AlternatingFixpoint,  // MKNF+WFS / Liu-You 2019
    Circumscription,      // via AEL or stable models
}

pub enum LatticeContext { Boolean, K3, FDE }
}

Open: is BTreeSet<AftOperatorClass> the right granularity, or should we report individual operator implementations with versions? Should supports_federation be a derived flag from supported_lattice_contexts.contains(FDE), or independent?

Q-37.2: What does artifact-runtime capability matching look like?

The artifact’s global-control section carries required_aft_operators (the union over all standpoints’ declared modes) and required_lattice_contexts (the union over all queries/rules). Layer-1 needs to check:

artifact.required_aft_operators ⊆ runtime.supported_aft_operators
artifact.required_lattice_contexts ⊆ runtime.supported_lattice_contexts

Open: is this a Layer-1 check (fast, O(1) — set inclusion is constant-time over a small fixed alphabet) or a Layer-2 check? Where does it sit in the §7 streaming load order? My intuition says Layer-1 because the alphabet is small and fixed; this also matches the pattern of max_tier_claimed ≤ max_tier_supported.

Q-37.3: How does this interact with multi-release overlay?

RFD-0035 §4 specifies multi-release overlay (JEP-238 at the section level). An artifact may carry both a projection-cache (DRedc-shaped) and an arrangement-section (DBSP-shaped) in parallel; each runtime picks the section it understands.

Open: does the same overlay pattern apply at the operator level? An artifact could carry both an LCWA-shaped event log section and an MKNF-WFS-shaped section, and each runtime picks the one it can evaluate. Or is operator-level overlay too granular? My intuition is that operator selection is a property of the standpoint declaration, not of artifact storage — the operator is fixed at compose time, and a runtime that doesn’t support that operator simply refuses the artifact. But there might be a use case for “this artifact can be evaluated under EITHER LCWA or full CWA, depending on the runtime.”

Q-37.4: Diagnostic codes for capability mismatch

RFD-0035 §16 already has OE1004 TierMismatch for tier-level mismatch. Capability mismatch suggests parallel codes:

Candidate codeTrigger
OE1015Required AFT operator not supported by runtime.
OE1016Required lattice context not supported by runtime.
OE1017Artifact contains paraconsistent standpoint; runtime does not advertise supports_paraconsistent_standpoints.

Open: are these all distinct, or does OE1017 collapse into OE1015 (paraconsistent support is a kind of operator-level capability)? Should the runtime also advertise consistency policy support per standpoint, or is this implicit in supporting the FDE lattice context?

Q-37.5: How do alternative runtime backends advertise?

RFD-0035 §14 anticipates multiple backends: kernel, in-process oxc-runtime, sandboxed bytecode, oxigraph-backed embedded engine. Each implements OxbinRuntime and advertises capabilities. Sandboxed runtimes likely advertise a smaller capability set (no MKNF+WFS, no paraconsistent, only K3).

Open: is the capability set monotone in implementation richness — i.e., a strictly-richer runtime advertises a strictly-larger set — or are there genuinely incomparable runtimes? An oxigraph backend might support a different set of operators than the kernel for performance-or-architecture reasons. This affects whether we can have a useful “runtime-A is a strict subset of runtime-B” relation, which would let oxup recommend the right backend per artifact.

Q-37.6: How does this interact with ox bench and ox inspect?

RFD-0035 §17 specifies ox inspect <oxbin> (surface artifact structure) and ox bench <oxbin> (benchmark harness). Both should incorporate the capability story:

  • ox inspect should report which AFT operators the artifact requires + which lattice contexts.
  • ox bench should refuse to bench an artifact a runtime can’t load.

Open: does ox inspect benefit from a --runtime <kernel|in-process|...> flag that scopes the report to “what’s required for this runtime to load this artifact”? Or is the unfiltered report always the right thing?

Q-37.7: Forward-compatibility for refined approximation spaces

RFD-0037 §13 lists Vanbesien 2025 refined approximation spaces, Heyninck 2022 non-deterministic AFT, Charalambidis 2024 higher-order weak bilattices, and continuous-confidence lattices as forward-compatible extensions. Each adds new AFT operator classes and possibly new lattice contexts.

Open: should RuntimeCapabilities use AftOperatorClass as a closed enum (today’s classes only) or an open string-tagged set (String for future operator names)? Closed enum gives us drift-gate coverage but blocks unsanctioned extensions; open set is forwards-compatible but loses the type-safety story. The Argon convention so far (per RFD-0024’s closed Severity enum) is closed — but capability surfaces are different from diagnostic surfaces in that capabilities want to grow naturally over time.

Non-questions (locked by upstream RFDs)

The following are settled and do not reopen here:

  • The truth-value bilattice substrate (FDE / Truth4) — RFD-0037 §1.
  • The three lattice contexts (Boolean, K3, FDE) — RFD-0037 §2.
  • Fail-closed projection from FDE to K3 — RFD-0037 §8.
  • Per-standpoint consistency policy at the language level — RFD-0037 §9.
  • The OxbinRuntime trait shape — RFD-0035 §14.
  • The Layer-1 / Layer-2 validation discipline — RFD-0035 §6–§7.

This RFD is purely about the capability advertisement surface on top of those settled commitments.

Anticipated direction

Given the discussion above, my anticipated commitment shape (subject to the design session) is:

  • RuntimeCapabilities carries closed enums for AftOperatorClass and LatticeContext, expanding additively when new operators land.
  • Capability matching is Layer-1, alongside the existing tier check, because the alphabets are small and fixed.
  • Operator-level multi-release overlay is rejected (Q-37.3). Operator selection is a compose-time decision; the artifact requires the operator chosen at compose; runtimes either support it or refuse.
  • Diagnostic codes are distinct (OE1015 operator, OE1016 lattice context, OE1017 paraconsistent), since each represents a different mismatch class with different remediation paths.
  • ox inspect --runtime <name> is added as a filter, defaulting to “all required” when omitted.

But these are anticipations, not commitments. The design session should validate or reject each.

Out of scope

  • The implementation of any specific AFT operator. RFD-0037 names them; the implementations live in the per-standpoint operator modules and are covered by the soundness proof at argon-formal/.
  • The wire-format of global-control’s required_aft_operators field. That belongs in oxc-protocol::core::oxbin per RFD-0035 §17 once this RFD commits.
  • How alternative runtime backends (kernel, oxigraph, …) actually compute their capability set. That’s an implementation detail per backend.

Diagnostic codes (anticipated, not registered yet)

Pending the design session and this RFD’s promotion to committed:

  • OE1015 RuntimeOperatorMismatch — required AFT operator not supported.
  • OE1016 RuntimeLatticeMismatch — required lattice context not supported.
  • OE1017 RuntimeParaconsistentUnsupported — artifact has paraconsistent standpoints; runtime does not support them.

Codes are NOT reserved while this RFD is in discussion per the lifecycle rule.

Historical lineage

  • RFD-0035OxbinRuntime trait + Layer-1/Layer-2 validation. The base this RFD extends.
  • RFD-0037 — AFT-grounded truth-value semantics. The framework this RFD reports on.
  • WebAssembly Component Model — capability-set advertisement for component composition is the closest published precedent.
  • ONNX three-axis versioning (RFD-0035 §4 historical lineage) — lesson for evolving capability surfaces additively.

RFD-0039 — Collections and collection operators

Committed Opened 2026-05-13 · Committed 2026-05-15

Implementation status (2026-05-14)

The substrate ships in two phases. Phase 1 lands the parts that are correct, testable, and self-contained without a multi-pass type- inference layer; Phase 2 lands the elaboration desugaring and runtime evaluators that depend on receiver type inference.

PhaseTracksWhat lands
Phase 1 (this RFD’s initial PR)A, B, C, D, E, J, K, partial NRFD ratified · grammar surface for all five RFD-0039 expression forms · std::collection::{Set,List,Map,Optional} + std::math::Range symbol registration with v1 op surface populated as Computation symbols · DataValue / DataValueType carry Set / Map / Optional / Range runtime variants · TypeExpr::Generic / Collection / OptionalCollection resolve to the matching DataValueType · OE2401, OE2402, OE2403–OE2408, OW2401–OW2402 reserved with explanations · 22-case substrate integration harness
Phase 2 (follow-up)F, G, H, I, L, MUFCS method-call desugaring with receiver type inference · per-context tier enforcement · crates/nous/src/runtime/std_collection_eval.rs Rust evaluators backing every v1 op · book chapter ch02-08-collections.md · examples/collections-tour/ end-to-end · lease-story Building/Unit/tenants extension

Phase 2 is tracked at issue #391 (E3 epic) with the deferred-op catalog at issue #409. Code written today using the RFD-0039 expression surface parses cleanly and surfaces OE2402 from the elaborator — the diagnostic carries the workaround (qualified std::collection::List::size(xs) form) and the tracking link.

Question

Argon today admits collection-shaped type expressions (Set[T], List[T], Map[K, V], Optional[T], [T; bound]) at parse time, and DataValueType already carries List, Set, Map at the runtime level. But the symbols those type expressions resolve against are not registered, the runtime has no operations on them, and no expression-level surface exists for indexing, slicing, membership testing, or higher-order transformations. Modelers cannot write the conditions and transformations real ontologies require.

What surface — substrate primitives, expression operators, and standard-library operations — closes the collection story without violating foundation-neutrality (RFD-0002) and the no-parametric-concepts position (RFD-0009)?

Context

Three feedback items from the May 2026 Argon Open Questions exchange are addressed here:

  • #26 — Collection operations (priority:blocker). Modelers cannot write is_member, append, remove, union, intersect at the expression level.
  • #20 — Ordered collections. Today’s [T; bound] admits cardinality constraints but doesn’t distinguish ordered from unordered semantics.
  • #21 — Optional syntax. spouse: [Person; <=1] reads as a singleton-bounded set instead of an optional value.

The current state is well-scoped:

  • The CST parser produces TypeExpr::Generic { head, args } for Set[T] / List[T] / Map[K, V], TypeExpr::Optional for T?, TypeExpr::Collection for [T; bound], and Expr::List for list literals. None of these are blocked in the grammar.
  • phase_elaborate.rs already computes stable_id("std::collection::Set") / List / Map and dispatches TypeExpr::Generic to DataValueType::Set / List / Map — but no symbols are registered against those stable ids, so the resolution silently degrades to concept refs.
  • DataValueType in nous already has List(Box<Self>), Set(Box<Self>), Map(Box<Self>, Box<Self>). DataValue has List(Vec<Self>) but lacks Set, Map, Optional, Range.
  • Aggregate forms (sum(expr for v in path where pred)) work in query heads and compute bodies; they are explicitly disallowed in rule bodies via OE0403.
  • No expression-level method-call syntax exists today. x.foo(bar) fails to parse (is_call_ahead only fires for bare identifiers, not post-dot).

The remaining work is connecting the parser surface to a populated std::collection module, adding the missing expression forms (method-call, indexing, slicing, membership, comprehension), and wiring the runtime to evaluate them.

RFD-0009 forbids user-declared parametric concepts. The implication: Set, List, Map, Optional, and Range cannot be user-declared and cannot be modeled as ordinary concepts with type parameters. They must live as a closed set of substrate-level type constructors. User code consumes them; user code does not extend them. Future user-declarable parametric types live behind a functor-module path that this RFD does not commit to.

Decision

1. Five built-in type constructors

std::collection exposes four type constructors; std::math exposes a fifth:

ConstructorArityModule pathID range
Set[T]1std::collection::SetTYPE_CTOR_SET_ID = 0x0003_0000_0000_0000
List[T]1std::collection::ListTYPE_CTOR_LIST_ID = 0x0003_0000_0000_0001
Map[K, V]2std::collection::MapTYPE_CTOR_MAP_ID = 0x0003_0000_0000_0002
Optional[T]1std::collection::OptionalTYPE_CTOR_OPTIONAL_ID = 0x0003_0000_0000_0003
Range[T]1std::math::RangeTYPE_CTOR_RANGE_ID = 0x0003_0000_0000_0004

The id range 0x0003_0000_0000_0000..0x0003_0000_0000_00FF is reserved for built-in type constructors, parallel to the 0x0002_* primitive-sentinel range. Type constructors are parametric and therefore have no DataValueType round-trip through primitive_sentinel_for; they resolve through a separate type_ctor_from_id function.

A new SymbolKind::TypeConstructor variant labels these symbols in the symbol table. The substrate cannot ship a new type constructor without a code change; the closed set is the price of foundation-neutrality (no metatype machinery decides what List means).

2. Surface — generic types use brackets, T? is sugar

Argon’s existing convention reserves < and > for comparison operators. Generic types therefore use brackets:

items:    List[Item]
tags:     Set[Text]
owners:   Map[Tenant, Lease]
discount: Optional[Money]
band:     Range[Money]

T? is admitted as user-facing sugar for Optional[T] and lowers identically at the AST→CoreIR boundary. [T] and [T; bound] continue to serve relation-shaped multi-valued field declarations; both lower to Set[T] by default, and an explicit ordered flag ([T; bound, ordered]) selects List[T]. [T; <=1] triggers OW2402 suggesting rewrite to T?.

3. Expression-level surface

Six new expression forms, all desugaring to ordinary calls at elaboration time:

  • Method callxs.m(a, b) desugars to <TypeOf(xs)>::m(xs, a, b). Dispatch is at elaboration time; no traits, no runtime dispatch, no method tables.
  • Indexingxs[i] desugars to <TypeOf(xs)>::at(xs, i) returning Optional[T]. Receivers typed as Set[T] reject with OE2406.
  • Slicingxs[i..j], xs[i..], xs[..j] desugar to <TypeOf(xs)>::slice(xs, range). Returns List[T].
  • Range literali..j constructs Range[T]::new(i, j) (half-open); i..=j constructs Range[T]::inclusive(i, j).
  • Membership operatorx in xs desugars to <TypeOf(xs)>::contains(xs, x); x not in xs negates.
  • Comprehension[expr for x in xs where pred] desugars to xs.filter(<pred>).map(<expr>) inline. The binding subgrammar reuses the existing AGGREGATE_BINDING + AGGREGATE_WHERE productions.

Higher-order arguments accept two forms:

  • Compute references — a bare pub compute name in an argument position is a value of compute-reference type. Elaboration validates the reference’s signature against the higher-order parameter’s expected shape; mismatch fires OE2407. CoreValue::ComputeRef(compute_id) carries the value.
  • Comprehensions — inline transformations that don’t need a named function.

First-class lambdas (|x| x + 1) and explicit function-type syntax (T -> U) are deferred. They become necessary only when compositional combinators (compose, curry, partial) enter the language; for now, comprehensions plus compute references cover the common cases.

4. Operation catalog

Each type constructor’s submodule exposes a fixed set of built-in computes. Names follow PL-mainstream conventions (Rust/Scala/Kotlin reach for these names).

Set[T] — unordered, distinct, no indexing.

Construction: Set::of(args...), Set::empty(), Set::singleton(x), Set::from_list(xs). Queries: size, is_empty, contains. Set algebra: union, intersect, difference, symmetric_difference. Subset relations: is_subset_of, is_superset_of, is_disjoint_from. Higher-order: map, filter, any, all, find, count, fold, reduce.

List[T] — ordered, allows duplicates, indexable.

Construction: List::of, List::empty, List::singleton, List::from_set (deterministic ordering). Queries: size, is_empty, contains, index_of. Access: at, head, tail, first, last. Manipulation (return new list): append, prepend, insert_at, remove_at, replace, reverse, sort, sort_by, zip, concat, flatten. Slicing: slice, take, drop, take_while, drop_while. Higher-order: same as Set plus flat_map, scan.

Map[K, V] — key-value, K must be orderable.

Construction: Map::of((k, v), ...), Map::empty. Queries: size, is_empty, contains_key, contains_value. Access: get, get_or, keys, values, entries. Manipulation: put, remove, merge, map_values, filter_keys, filter_values.

Optional[T] — 0-or-1 value.

Construction: Some(x), None(). Queries: is_some, is_none. Access: unwrap_or, unwrap_or_else. Mapping: map, flat_map, filter, and, or.

Range[T] (in std::math) — over any orderable type.

Construction: Range::new (half-open), Range::inclusive, Range::from, Range::to. Queries: contains, start, end, is_empty. Iteration: usable in comprehensions over numeric/date ranges.

5. Tier classification per operation

Every operation carries an explicit tier per RFD-0004’s seven-tier ladder. Categories:

TierOperations
tier:structuralis_some, is_none, Range::contains / start / end / is_empty
tier:closuresize, is_empty, contains, union, intersect, difference, subset checks, indexing, slicing, head/tail, append/prepend, list manipulation without HOFs, get / keys / values
tier:expressivefind, any, all, count when predicate is non-trivial
propagatesmap, filter, flat_map, fold, reduce, scan, sort_by, predicate-taking ops — tier(op) = max(tier:closure, tier(f))

The propagation rule is consistent with how recognized-shape tier reduction already works for rule bodies. A modeler writing xs.map(complex_compute) where complex_compute is tier:fol gets a tier:fol-classified result and must either accept the tier or refactor.

6. Graduated-context admission

Following RFD-0005, different evaluation contexts admit different operation sets:

ContextMethod callsComprehensionsCompute references
pub compute bodyall opsyesyes
pub mutation do { }all opsyesyes
pub derive bodytier:closure onlyyestier:closure only
query bodytier:closure onlyyestier:closure only
Refinement where { }rejected (OE2408)rejectedrejected
test blockall opsyesyes

Rule-body tier violations fire OE2408 HOFInRuleBodyTierViolation. Refinement-body violations route through the existing refinement-restriction code (ch02-06).

7. Functional semantics; mutation is rebuild-and-assign

All collection operations are pure-functional: they return new collections and never mutate in place. xs.append(x) returns a new List[T] with x appended; xs is unchanged.

In pub mutation do { } bodies, the idiom is rebuild-and-assign:

do { l.parents = l.parents.append(parent) }

True in-place mutators (l.parents.insert!(x)) are deferred. Functional semantics keep the kernel’s bitemporal event-log model clean: every collection-valued property change is a new GroundAssertion, not an in-place edit.

8. Field cardinality vs collection types

Three field shapes carry related but distinct readings:

  • field: List[T] — a single collection-valued property whose value is a List object.
  • field: [T; bound] — a binary relation pattern with cardinality bound (default Set semantics).
  • field: [T; bound, ordered] — same relation pattern but ordered (List semantics).

The cardinality bounds >= N / <= N / == N / >= N, <= M apply only to relation-shaped declarations. Collection-typed properties carry their cardinality through the collection’s runtime size; explicit bounds use where-clause refinement predicates over .size().

[T; <=1] is admitted but triggers OW2402 suggesting T?. The semantics are equivalent at the data layer; the readings differ.

9. Diagnostics

Ten new codes register in oxc-protocol::core::codes:

CodeSeverityTrigger
OE2401ErrorType-constructor arity mismatch (Set[T, U], Map[T]).
OE2402ErrorUnknown collection method (xs.bogus()). Hint lists valid methods on the receiver’s type.
OE2403ErrorCollection element-type mismatch (List[Nat].append("foo")).
OE2404ErrorIndex expression’s type isn’t Nat.
OE2405ErrorSlice bounds invalid (xs[5..2], compile-time when literal).
OE2406ErrorUnordered collection indexed (s[i] where s: Set[T]).
OE2407ErrorCompute reference signature mismatch.
OE2408ErrorHOF in rule body violates context tier ceiling.
OW2401Warning.unwrap() without fallback; suggests unwrap_or or match.
OW2402Warning[T; <=1] field; suggests T?.

10. CLI surface

No new CLI commands. ox check, ox build, ox test, ox doc all interpret the new surface through existing dispatch. oxc emit core-ir shows the desugared form for any expression with collection operators.

Rationale

Closed type-constructor set preserves RFD-0009. Allowing user-declared parametric concepts would commit Argon to a position about how foundational ontologies handle parametric type families — exactly the foundational coupling RFD-0002 forbids. A closed set of substrate primitives keeps the door shut without sacrificing user productivity; the everyday collection-modeling cases fit inside Set / List / Map / Optional / Range.

UFCS dispatch keeps the substrate small. Method-call syntax is a sugar; the desugar produces an ordinary Expr::Call against a qualified path. No method tables, no trait-resolution machinery, no runtime dispatch overhead. Receiver type inference at elaboration time picks the right submodule. The book reads naturally (xs.filter(p).map(f).count()); the IR stays uniform.

Comprehensions + compute references defer first-class functions. First-class lambdas are a real feature with real costs — function-type syntax, closure capture, type inference over function values. The team’s pain is concrete: comprehensions cover the inline-transformation case; compute references cover the named-transformation case. Lambdas can land later if combinator-style composition becomes desirable; the surface for that addition stays open.

Functional semantics align with the bitemporal event log. Every collection-valued property mutation is a new GroundAssertion in the kernel’s append-only event log per RFD-0021. In-place mutation would require the kernel to materialize partial-update events, which contradicts the existing storage discipline. Rebuild-and-assign matches the storage model byte-for-byte.

Graduated-context tiering reuses existing machinery. Rule bodies are tier-classified per RFD-0004; refinement bodies admit a restricted sub-grammar per ch02-06; compute bodies admit the full surface. Collection operations slot into the existing classification: xs.size() is tier:closure; xs.find(complex_pred) propagates from the predicate. No new tier infrastructure needed.

Bracket syntax avoids the < / > ambiguity that bedevils C++. Angle-bracket generic syntax requires lookahead disambiguation (is a < b a comparison or the start of a<b>?). Argon’s bracket syntax for type parameters sidesteps the problem entirely.

Consequences

  • New module: argon/oxc/src/std_collection.rs modeled on std_math.rs. Registers the four collection type constructors plus their submodule operation surfaces. Range registers in std_math.rs (extends, doesn’t replace).
  • core_ir.rs allocates the 0x0003_* id range for type constructors plus type_ctor_from_id mirror function.
  • SymbolKind::TypeConstructor new variant; every exhaustive match across oxc updates.
  • DataValue in nous grows Set(BTreeSet<Self>), Map(BTreeMap<Self, Self>), Optional(Option<Box<Self>>), Range { ... } variants. Additive serde change; no kernel-storage migration.
  • DataValueType in nous grows Optional(Box<Self>) and Range(Box<Self>).
  • AST in oxc/src/ast.rs grows Expr::MethodCall, Expr::Index, Expr::Slice, Expr::Range, Expr::Comprehension, Expr::Membership variants.
  • CST grammar in oxc/src/cst/grammar/exprs.rs grows a postfix-chain parser handling method-call, index, and slice forms after primary expressions. in / not in join the binary-operator precedence table. Comprehensions admit inside list literals via the for keyword.
  • Tree-sitter grammar mirrors the new productions; the codegen audit gate (oxc-codegen audit) catches drift.
  • Elaborator grows elaborate_method_call and a sibling family that desugar postfix forms into qualified calls. resolve_type_expr_to_id learns Generic / Collection / OptionalCollection arms.
  • Interpreter in nous grows ~80 evaluator functions covering every op in §4. Registered as ComputeDef::Rust bodies in the existing ComputeRegistry; no new KernelExpr variants needed.
  • Tier classifier in meta/classify.rs reads per-op tier annotations from a static table built at registration time.
  • Diagnostics in oxc/src/diagnostics/codes.rs add OE2401–OE2408 + OW2401–OW2402.
  • Book chapter ch02-08-collections.md ships with the operator catalog, tier matrix, and worked example. ch02-04, ch02-05, ch02-06 and the appendices extend.
  • Example examples/collections-tour/ ships as a minimal package exercising every operator. The lease-story example gains Building with units: List[Unit], tenants: Set[Person], rent_band: Optional[Range[Money]].
  • Open follow-ups (not in this RFD):
    • Interaction with RFD-0036’s user-level generic bounds — specifically, whether <T: Bound> admits type-constructor bounds (<T: List>).
    • User-declarable parametric type constructors via a functor-module path.
    • First-class lambdas and explicit function-type syntax.
    • In-place mutation operators (!-suffixed methods) in do { } contexts.

Historical lineage

The surface borrows directly from PL mainstream:

  • UFCS as desugar — Rust’s xs.iter().map(f).collect() reads as method calls but compiles to free-function calls in trait-impl scope. Argon’s UFCS is the same shape without traits: type-directed dispatch at elaboration time.
  • Comprehensions — Python and Haskell list comprehensions; Argon’s [e for x in xs where p] reuses the aggregate-binding grammar that already exists in Argon for sum(e for v in p where q).
  • Optional/Maybe — Haskell Maybe a, Rust Option<T>, Scala Option[T]. Argon picks Rust’s vocabulary (Some / None / unwrap_or) for PL familiarity.
  • Closed type-constructor set — Erlang’s built-in list, tuple, map types are similarly closed at the language level; user code consumes them but doesn’t declare new ones. Argon’s discipline is the same.

The functional-only semantics with rebuild-and-assign for mutation mirrors Clojure’s persistent data structures (returning new collections from “mutation” operations); the bitemporal-event-log alignment (every assignment is a new GroundAssertion) is Datomic’s transactor pattern adapted to Argon’s per-tenant overlay model per RFD-0020 + RFD-0021.

11. Addenda — decisions locked during Phase 2 implementation

Four design questions were resolved during Phase 2 build-out. Each is committed here so the RFD reflects the shipped surface.

11.1 Optional under the open-world assumption — K3-faithful

Optional[T] semantics under OWA split presence from inner-value truth state:

  • is_some(o) is classical on presence. Some(_) yields true; None() yields false. The inner value’s truth status does not enter the determination — is_some(Some(Undefined)) is true.
  • unwrap_or(o, d) is classical on absence. Some(v) yields v; None() yields d. unwrap_or(Some(Undefined), d) yields the inner Undefined, not d. The fallback applies to absence, not to the inner element’s truth status.

The reason: K3 (the three-valued logic the language uses for refinement under OWA) distinguishes “no value here” (a structural absence) from “value present but its truth is unknown” (an epistemic gap on the value itself). An unwrap_or that collapsed both cases into the fallback would erase the distinction, and downstream truth-value propagation relies on the distinction surviving.

When a modeler wants “either way, substitute a default,” the idiom is to chain a separate operation that operates on the inner value’s truth state — .or(d) (deferred) or a match over the value’s truth status. This RFD does not commit to the chained-operation surface; it commits to the rule that unwrap_or does not perform that combined collapse.

Cross-link: RFD-0037 §6 — bilattice substrate and K3/FDE projection.

11.2 Set and Map equality + normalization invariant

Set[T] and Map[K, V] evaluators normalize their backing storage so equality is Vec equality:

  • Set[T] is stored sort-deduplicated by T’s canonical ordering.
  • Map[K, V] is stored sort-by-key then dedup-keys (last write wins within a single construction call).

Every evaluator that produces a Set or Map runs the normalization step before returning. == on two Set / Map values is straight Vec equality after normalization. The invariant is asserted via debug_assert! round-trips in each evaluator — produce, normalize, re-normalize, compare; the second normalization must be a no-op.

The shipped surface does not expose the underlying Vec representation, so the discipline is internal to the evaluator implementation. The user-facing reading is “two sets are equal iff they contain the same elements” and “two maps are equal iff they contain the same key-value pairs,” which is what the normalization preserves.

11.3 Comprehension binder scoping — shadow with warning

A comprehension’s binder name shadows any in-scope let binding or parameter within the comprehension body and fires OW2403 ComprehensionBinderShadowsOuter. The warning is informational; the shadow takes effect.

pub compute weird(units: List[Unit], unit: Unit) -> List[Text] =
    [unit.number for unit in units]   // OW2403 — outer `unit` shadowed

The convention matches what Datalog rule bodies already do — every body introduces fresh binders, and modeler intent typically reads as “iterate the source,” not “shadow the outer name.” Erroring on collision would be too strict (renaming a parameter to fix a comprehension is annoying); silently shadowing would be too permissive. The warning surfaces the collision so the modeler chooses.

Rename the binder to silence:

pub compute fine(units: List[Unit], unit: Unit) -> List[Text] =
    [u.number for u in units]

11.4 Range materialization — predicate-only in v1

Range[T] does not materialize an element sequence in the v1 surface. Range::contains(r, x) is predicate-shaped: it answers “is x in the interval r?” without iterating. Range values participate in slicing (xs[i..j] constructs a Range[Nat] and passes it to List::slice) and in membership tests at expression position and rule-atom position.

Range::collect(r) -> List[T] (which would materialize the element sequence for numeric / date / money ranges) is tracked as follow-up work. The v1 surface does not iterate over ranges anywhere; the future collect addition will be a pure extension because no v1 caller exists to break.

The reading commits us to: ranges are intervals first and iterators second. The interval reading covers the common rent-band, age-band, date-band cases; the iterator reading is a convenience the language can add without disturbing the interval semantics.

RFD-0040 — Substrate atoms and the explicit-writes principle

Discussion Opened 2026-05-17

Question

What are Argon’s irreducible atoms, and what rules govern their composition? This RFD names the substrate, commits to a categorical purity ladder for the four operator surfaces, and commits to the no-implicit-writes rule. Three companion RFDs ship grammar against this substrate: RFD-0041 (traits), RFD-0042 (macros), RFD-0043 (query/mutation surface), RFD-0044 (MLT cross-level instantiation).

Context

Argon’s surface has accreted distinctions through incremental implementation that the substrate does not support:

  • Clause-structured mutation bodies (require / do / retract / emit / return) that a team contributor compared to COBOL.
  • A let x: T = expr form inside do { } that silently persists when the type annotation is present and binds-locally when it isn’t — the syntax is otherwise identical.
  • An emit Event { … } form that routes invisibly to one of three sinks (audit log, HITL queue, notification dispatcher); the sink is not visible at the call site.
  • An Instantiates core-IR atom locked to order-0 → order-1, with no variant supporting concept-to-concept instantiation (the MLT case).
  • A :: operator that lowers to meta-property assignment, not instantiation, despite reading as instance-of.
  • The : token carrying five distinct meanings disambiguated by lookahead (RFD-0015).
  • No trait or interface surface for declaring behavioral contracts the team has been asking for.
  • No macro surface, forcing higher-order patterns (state machines, derived boilerplate) into either compiler-built-in keywords or hand-written rule scaffolding.

The predicate language has always been one grammar with graduated admission (RFD-0005: rule-body, constraint, diagram contexts admit different tiers of the same predicate surface). The accretion is at the operator and writes layer, not the predicate layer.

Three inputs ground this RFD beyond the codebase itself:

  • The Lean mechanization (argon-formal/). It shows that beneath the four operator surfaces sits one stratified-fixpoint construct with three rule categories (monotone / NAF / constraint-check). The four operators are projections, not separate constructs at the formal level. The Lean is one input among several — the surface design is not obligated to match it line for line — but the collapse to one underlying mechanism is real and worth surfacing.
  • The Epilog technical reference. Three lexically-distinct rule arrows (:- derivation, := function, ==> transition). Reads vs writes are categorical at the syntax level, not contextual. The discipline is proven.
  • The sharpe-ontology/ story branches. Team members hit the current limits and wrote in imaginary syntax. Their wish list — relation axioms, cardinality constraints, higher-order type fields, behavioral contracts on type fields — resolves to atoms-becoming-more-expressive, not to new atoms beyond the ones this RFD names.

Decision

D1 — Six atoms

Argon’s irreducible primitives are exactly six.

1. Metatype. A sealed-at-top declaration of a class of entities. pub metatype kind = { rigidity: rigid, sortality: sortal, order: 1 }. The primitive Metatype self-instantiates; every user-declared metatype is an instance of Metatype. Metatypes carry typed axes that constrain the entities they classify. Vocabularies (UFO, BFO, GFO, DOLCE) declare metatypes; the language ships no built-in vocabulary content (RFD-0002).

2. Concept. An instance of a metatype. The “type” in the ordinary type-system sense. Concepts can specialize other concepts (<:) and instantiate other concepts (:). A relation is a concept whose metatype declares arity ≥ 2; there is no separate relation atom.

3. Rule. The unified computational primitive. Has a head, a body, and a mode. The mode picks one of four surface keywords (fn, derive, query, mutation) which control purity, persistence, and call discipline. Each rule projects to one of three formal categories (monotone / NAF / constraint-check) at lowering. The Lean mechanization treats these as one construct; the surface keeps four projections for ergonomic clarity. See D2.

4. Trait. A named behavioral contract. A trait declares required fn / derive / query / mutation items (zero or more of each), optional associated types, optional associated constants, and optional default implementations for any required item. Traits can extend other traits.

pub trait Identifiable {
    fn id(self) -> Text
}

pub trait Comparable <: Identifiable {
    fn compare(a: Self, b: Self) -> Ordering
    fn equal(a: Self, b: Self) -> Bool = compare(a, b) == Ordering::Equal   // default impl
}

Implementation is a separate declaration:

impl Identifiable for Person {
    fn id(self) -> Text = self.email
}

Concepts and traits are orthogonal axes. Concepts answer “what is this entity?” (ontological commitment); traits answer “what can this entity do?” (behavioral commitment). A concept implements zero or more traits; a trait is implemented by zero or more concepts.

Dispatch is static. Generic parameters bound by traits — pub fn sort<T: Comparable>(xs: List[T]) -> List[T] — monomorphize at ox compose time using RFD-0034’s pipeline and RFD-0036’s bounded-generic machinery. The kernel sees fully-specialized rule sets. No runtime trait objects in v1; no dynamic dispatch.

Traits compose with the purity ladder transparently: a trait method declared as fn is fn-pure for any caller bounded by the trait; a trait method declared as mutation makes the bounded site impure. The purity rules in D3 apply unchanged.

RFD-0041 specifies grammar, trait inheritance semantics, conflict resolution for diamond impls, coherence rules, and the interaction with RFD-0036 generic bounds.

5. Decorator. A named, parameterized shape annotation attachable to metatypes, concepts, relations, rules, or traits. Decorators carry semantics (FOL formulas or constraint rules in their bodies); the compiler’s shape recognizer fast-paths declared shapes when it can match them.

@[transitive, asymmetric, irreflexive]
pub rel parent_of: Animal -> Animal

Decorators are user-declarable in packages via pub decorator. They do not extend the type system or the rule system; they annotate. The recognizer’s job is to take FOL bodies and identify when they match a known shape (transitivity, qualified cardinality, etc.) so the reasoner can fast-path. Decorators name those shapes.

6. Macro. A compile-time AST → AST transformation. Two flavors:

  • Declarative macrosmacro_rules-style pattern matching on Argon syntax. Pure-syntactic; no type access. For ergonomic sugar.
  • Procedural macros — compiled Rust code invoked at oxc elaboration time. Procedural macros receive a resolved AST: type assignments, metatype assignments, decorator annotations, tier classifications. They emit any of the six atoms.

Macros expand within the per-package composition boundary established by RFD-0034. Cross-package macro use rides the existing composition wiring.

Two consequences for the surface language:

  1. State machines move out of the language. The lifecycle { … } / statemachine { … } construct (issue #321) becomes a stdlib procedural macro. The macro reads a state-machine declaration, walks phases and transitions, and emits: phase concepts with the right metatype-calculus axes, transition derives, reachability constraint rules. The kernel handles bitemporal phase tracking unchanged — it already understands phase concepts via the metatype calculus.

  2. Derive macros for boilerplate. @[derive(Eq, Hash, Json)] lets concepts get standard trait impls for free. The macro reads the concept’s fields through the resolved AST and emits the appropriate impl blocks.

Macros do not have runtime access, do not see database state, and do not bypass the type system. Emitted AST re-elaborates through the normal pipeline.

RFD-0042 specifies the macro grammar, procedural-macro Rust API surface, resolved-AST format, expansion phase placement in the elaboration pipeline, and hygiene rules.

Everything else in the surface — patterns (RFD-0019), living diagrams (RFD-0026), state machines, refinement types, collection operations (RFD-0039) — is a composition of these six. Patterns are macros that emit parameterized rule scaffolds. Living diagrams are macros that emit visual-tier annotations. State machines, as noted, are macros that emit phase concepts and transition derives. Refinement types are decorators on existing concept declarations. Collections (RFD-0039) are substrate primitives at the type-system level; their methods are fn declarations like any other.

D2 — Four surface keywords, one rule primitive

The four keywords surface different modes of the same underlying rule construct. They share grammar, body shape (modulo the :- marker on derive), and elaboration path. Mode determines: purity, write-scope, which tier of the decidability ladder the body admits, and how callers invoke the rule.

KeywordPurityWrite scopeBody shapeCall style
fnpure value, deterministic given (args, state-version)noneexpression-shapedinvoked by name; returns a value
derivepure entailmentderived facts in the saturation closure (transient)rule-atom-shaped (:- body)fires automatically during saturation; not invoked by name
queryobservation of saturated statenoneshape-projection over rule atomsinvoked by name; returns a set or value
mutationimpuretransactional + persisted to the event logshape-projection + write verbsinvoked by name; returns a value with side effects

derive and query walk the same rule machinery; the difference is direction. derive writes into the saturation closure; query reads from it.

The compute keyword is renamed to fn. Migration is mechanical.

D3 — The call-purity ladder

A caller can only call callees at equal-or-lower impurity.

fn        — pure value (depends on args + state-version)
query     — reads saturated state
derive    — modifies saturation closure (deterministic given inputs)
mutation  — modifies persisted state (transaction-scoped)
Caller \ Calleefnqueryderivemutation
fn
query
derive
mutation✓ (composes into one atomic transaction)

Three semantic clarifications:

  • fn calling query is allowed. A fn is pure with respect to its arguments and the world-state version at call time. Cacheability is keyed by (args, state-version). The alternative — fn must take all state via arguments — is unworkable for a knowledge-base language.
  • derive cannot call mutation. Every ✗ entry in the matrix is forbidden, but derive → mutation is the load-bearing case: it’s what makes saturation termination and determinism possible. Other forbidden directions (fn → derive, fn → mutation, query → derive, query → mutation) follow from the same purity invariants but are usually caught earlier — a fn body that calls a derive is more obviously a category error than a derive body that calls a mutation.
  • mutation → mutation composes into a single atomic transaction. Commit or rollback is at the outermost call site.

Trait methods inherit the purity of their declared keyword. A <T: Trait> bound carries the most-impure method through; calling code is restricted accordingly.

D4 — The explicit-writes principle

Persistent state changes happen only through four explicit verbs: insert, update, delete, emit. emit must declare its sink.

TodayReplacement
let x: T = expr (persists)insert x: T { … }
let x = expr (local)let x = expr (unchanged; never persists)
path = value in do { }update p set { field = value }
retract { path: value where … }delete p where …
emit Event { … }emit (audit: Event { … }) / emit (hitl: Event { … }) / emit (notify: Event { … })
derive head :- bodyunchanged; book makes “derived facts are transient and saturation-regenerated” explicit

A reader grepping for state changes finds them by searching for four keywords. emit carries its sink as a syntactically required prefix; there is no destination-by-runtime-routing.

D5 — Syntactic separation of instantiation and specialization

: and <: take exactly one meaning each, in all positions:

  • : means instantiation. a : A reads as “a is an instance of A”. Applies to individuals-of-concepts, concepts-of-concepts (D12), and statements-of-statements.
  • <: means specialization. A <: B reads as “A is a subtype of B”. Also used for trait inheritance: trait Ord <: Eq.

The five overloaded meanings RFD-0015 allowed for : are reassigned:

TodayTomorrow
pub kind Dog : Animal (specialization sugar)pub kind Dog <: Animal
field: T (field typing)field: T (kept; instantiation at the term level)
arg: T (function parameter typing)arg: T (kept; instantiation at the term level)
pub rel parent_of: A -> B (relation signature)pub rel parent_of: A -> B (kept; the LHS is what’s being typed)
pub kind Dog : AnimalSpecies (genuinely instantiation)pub kind Dog : AnimalSpecies (now means what it always read as)

RFD-0015’s disambiguation rule disappears. The auto-migration tool rewrites kind A : B to kind A <: B where B is a sibling concept (not a higher-order concept).

D6 — Block syntax: all bodies use { }

Multi-statement and multi-clause bodies use braces:

  • Concept, relation, metatype, and trait bodies — braces.
  • fn bodies with multi-statement or non-trivial expression content — braces.
  • derive bodies — :- followed by braces. The :- carries the semantic claim (“this is an entailment rule”); the braces carry the visual structure.
  • query, mutation, and impl bodies — braces.
  • Decorator bodies (when they carry semantics) — braces.

One exception: trivial single-expression fn bodies admit the = expr shorthand.

pub fn rent_per_day(l: Lease) -> Money = l.monthly_rent / 30

Single-expression derive / query / mutation bodies do not get the shorthand.

D7 — Decorator conventions

One @[ … ] block immediately preceding the declaration, with multiple shape claims comma-separated inside.

@[transitive, asymmetric, irreflexive]
pub rel parent_of: Animal -> Animal

Not after the declaration. Not split into multiple @[…] blocks. Decorators with arguments use parentheses: @[axiom("a3"), bounded_order(3)].

D8 — Statement separators

Inside { } bodies:

  • ; separates statements in fn, query, mutation, impl, and trait-method bodies.
  • , separates atoms inside :- { … } derive bodies. The comma is logical conjunction.

Mixing separators is a syntax error.

D9 — Multi-supertype and multi-instantiation: line-broken

Single specialization, single instantiation, or one each — inline:

pub kind Husky <: Dog
pub kind Dog : AnimalSpecies
pub kind Husky <: Dog : DogBreed

Multiple supertypes or multiple instantiations — line-broken, one clause per line:

pub kind Husky
    <: Dog, WorkingAnimal
    : DogBreed, RegistrationCategory

Inline comma-lists with both <: and : clauses on one line are a syntax error.

D10 — Query syntax: select without from

The target concept is the first non-keyword token after select. There is no from keyword.

pub query active_leases(t: Person) -> [Lease] {
    select Lease { id, monthly_rent, end_date }
    where .tenant = t and .status = Active
    order by .end_date
}

Aggregates over derived sets use a with-binding:

pub fn active_monthly_total() -> Money {
    with active := select Lease where .status = Active;
    select sum(active.monthly_rent)
}

D11 — Scenario blocks for hypothetical writes

query bodies admit no top-level write verbs. The escape hatch is the scenario { } block:

pub query rent_under_proposed_increase(percent: Real) -> Money {
    scenario {
        update Lease set { monthly_rent = .monthly_rent * (1 + percent / 100) }
        where .status = Active;
    };
    with active := select Lease where .status = Active;
    select sum(active.monthly_rent)
}

scenario { … } creates a kernel fork (RFD-0023) at the moment of entry, with the query’s current state as the fork’s base. Writes inside the block apply to the fork. When the block closes (};), the fork’s state replaces the query’s “current state” for the remainder of the query body — subsequent statements (including any final select) read against the forked state. The fork is discarded when the query body completes (the query returns or implicit-returns); writes never propagate to persistent storage.

Multiple scenario blocks in a single query body are chained: each scenario forks from the state including all previous scenarios’ writes. To branch from a known earlier point, factor the shared setup into a with-binding before any scenario begins. Nested scenarios are a syntax error (OE0836). scenario does not appear in mutation bodies.

D12 — Cross-level instantiation (MLT) via a single IR atom

The Core IR atom Instantiates { individual: String, concept_id: u64 } is replaced by:

#![allow(unused)]
fn main() {
InstanceOf { instance: EntityRef, type_concept: u64 }
}

where EntityRef = Individual(u64) | Concept(u64) | Statement(u64). One atom for all instantiation, parameterized by what kind of entity is being instantiated. Tonto’s kind Dog (instanceOf AnimalSpecies) and Argon’s pub kind Dog : AnimalSpecies both lower to one InstanceOf atom with instance: Concept(dog_id).

Order is derived from the instantiation closure, not stamped at declaration: order(c) = 1 + max(order(x) for x in instances_of(c)), where instances_of(c) = { x | x : c } (the direct instances of c), starting from individuals at order 0. Types are strictly higher-order than their instances (Carvalho & Almeida 2017); see RFD-0044 D3 for the full derivation. Vocabularies can decorate metatypes with @[bounded_order(n)] or @[order(=2)] to assert a level; the elaborator validates against the derived value.

RFD-0044 covers grammar, the new elaboration phase, and the Lean extension.

Rationale

Six atoms, not five. The previous draft folded traits into “future work.” Traits are not a composition of the other five: not metatypes (a trait is not a category of entities), not concepts (a trait carries no ontological commitment), not rules (a trait declares signatures, not bodies), not decorators (decorators annotate; traits contract), not macros (macros generate code; traits constrain it). They are a distinct atom. The team’s wish list — behavioral contracts on type fields, polymorphic generic bounds, derive-style boilerplate — requires the atom.

Concepts and traits are orthogonal axes. This is the load-bearing claim that keeps Argon distinct from OO inheritance. Concepts answer “what is this” — UFO/BFO-classified ontological entities. Traits answer “what can this do” — Rust-style behavioral contracts. A Person concept can implement Identifiable, Comparable, Json, without those traits leaking into Person’s ontological commitments. The metatype calculus governs concepts; trait bounds govern generic-parameter resolution.

Macros need metatype-calculus visibility. State machines today require compiler-owned guarantees (phase disjointness, reachability checking, bitemporal tracking) because the metatype calculus is only available to the compiler. The right fix is to expose the metatype calculus to procedural macros so user-defined patterns can require the same guarantees. State machines then move to a stdlib macro — the compiler no longer has privileged knowledge of state machines, but the kernel still recognizes phase concepts because the metatype calculus does.

The four keywords stay despite collapsing to one rule. The Lean shows one underlying construct; the surface keeps four projections because the keyword IS the load-bearing reader signal. query foo() declares no side effects to a reader before they read the body; mutation bar() declares them. Collapsing the keywords erases this signal. The four-keyword surface preserves it while honoring the single-construct substrate.

computefn. RFD-0015 chose PL idiom; fn is more PL-idiomatic than compute. Every PL written in the last twenty years uses fn or a near-analog. Semantic content is unchanged; only spelling.

Explicit writes via four verbs. The implicit-writes audit found four mechanisms by which today’s surface persists state without visible signal at the call site. Categorical fix: every persistent write uses one of four keywords; every external effect uses emit with explicit sink. The Epilog discipline (lexical mark distinguishes pure from effectful at every site) is the proven model.

: / <: separation. The five overloads of : produced five places where readers had to slow down. Math notation is unambiguous. The cleanup unblocks MLT (D12), which uses : for cross-level instantiation in the same syntactic role.

:- and braces both. :- is semantic (this is an entailment rule); { } is visual structure. Keeping both costs one extra character per derive vs the bare-body alternative and gives readers two signals where one might be ambiguous.

Consequences

This RFD is the philosophical commitment. Grammar is in the companion RFDs:

  • RFD-0041 — Traits and behavioral polymorphism. Grammar for pub trait declarations, impl Trait for Concept blocks, trait inheritance (<:), associated types, generic bounds (<T: Trait>), multiple bounds, default-method overriding, coherence rules. Closes the team’s “implement trait feature” TODOs in the workflow ontology.
  • RFD-0042 — Macros with metatype-calculus visibility. Grammar for declarative macros (macro_rules-style), procedural macros (compiled Rust API surface), macro invocation syntax, hygiene rules, expansion-phase placement, the resolved-AST format procedural macros receive. State machines move to a stdlib macro; pattern declarations (RFD-0019) become macros.
  • RFD-0043 — Query and mutation surface. select / insert / update / delete / with / emit / scenario grammar replacing require / do / retract / emit / return. Purity-violation diagnostic codes. The full syntax of scenario blocks.
  • RFD-0044 — Cross-level instantiation (MLT). Generalizes Instantiates to InstanceOf, adds the : declaration form for concept-of-concept, derives order, extends TypeGraphMLT.lean.

Breaking changes. Adoption is a major version bump — argon 1.0. Migration includes:

  1. computefn.
  2. pub kind A : Bpub kind A <: B.
  3. let x: T = expr inside do { }insert x: T { … }.
  4. path = value inside do { }update p set { field = value }.
  5. retract { … }delete p where ….
  6. emit Event { … }emit (audit|hitl|notify: Event { … }).
  7. require / do / retract / emit / return clause structure → { … } body with statements (RFD-0043).
  8. Decorator-after-declaration → decorator-before-declaration single block.
  9. lifecycle { … } / statemachine { … } → stdlib macro (no language change required after RFD-0042 lands).

The compiler ships ox migrate for mechanical rewrites (items 1–6, 8). Item 7 requires manual review; the tool flags every site. Item 9 lands when the stdlib macro ships. Old syntax remains parseable under --edition=0.4 for one minor version; argon 1.1 removes it.

Documentation. Book chapters covering operator distinctions, clause structure, and the : / <: disambiguation are restructured around the six atoms. The “For Agents” reference is rewritten against the new surface.

Predicate grammar consolidation. RFD-0005’s “one grammar, multiple contexts” stays the design. RFD-0043 confirms the rule-body, constraint, and diagram contexts admit the same predicate grammar with graduated tier ceilings. Any surface-level fragmentation that has crept in gets cleaned up there.

Wire types. oxc-protocol bumps in two places: InstantiatesInstanceOf (additive enum variant), and mode tags added to rule declarations. SDK regeneration via oxc-codegen is mechanical.

The Lean is extended with the instantiates relation and order theorem (per D12 and RFD-0044). The rule machinery, fixpoint, and decidability theorems are reused unchanged.

Historical lineage

  • RFD-0002 (foundational-ontology neutrality) — committed. Prerequisite for the six-atom model.
  • RFD-0005 (one grammar, multiple contexts) — committed. The predicate-graduation design this RFD respects.
  • RFD-0006 (three compute forms) — committed. Survives as fn body shapes after the rename.
  • RFD-0007 (first-class queries, mutations, provenance) — committed. Queries and mutations remain first-class items; provenance preserved.
  • RFD-0015 (PL idiom over proof-assistant idiom) — committed. Extended: : overload replaced by : / <: separation. PL-idiom principle reinforced by compute → fn.
  • RFD-0019 (patterns are first-class) — committed. Reframed: patterns become a macro library.
  • RFD-0023 (kernel API v2) — committed. Fork machinery used by scenario blocks is already shipped.
  • RFD-0033 (test mutate statements) — discussion. Compatible; mutate in test blocks is a mutation call.
  • RFD-0034 (composition pipeline) — committed. Macros expand within its boundary.
  • RFD-0036 (generic declarations) — discussion. Extended by RFD-0041 (trait bounds).
  • RFD-0037 (AFT truth-value semantics) — committed. Unchanged.
  • RFD-0039 (collections and collection operators) — committed. Unchanged.

This RFD extends, reframes, or refines each cited RFD. It does not supersede any committed RFD wholesale.

RFD-0041 — Traits and behavioral polymorphism

Discussion Opened 2026-05-17

Question

How does Argon express behavioral polymorphism — generic algorithms that work across multiple concept hierarchies — without introducing OO-style inheritance or runtime dispatch? This RFD specifies the trait system named as atom #4 in RFD-0040.

Context

Argon’s current generic surface lifts <T: Bound> from pub constraint to pub derive / pub mutation / pub compute (RFD-0036, discussion). The bound today is a concept subtype — T must be a subtype of some concept. This is sufficient for monomorphizing over a domain hierarchy but does not express the common case of “T must provide a compare method” or “T must be hashable.”

The sharpe-ontology/ workflow branch reaches for behavioral contracts repeatedly. From workflow.ar:

// TODO: Implement the trait feature for this.
// Every instance must define its own function to define its successor
hasAlternativeSuccessors: [WorkflowActionType]

The team contributor who raised this in standup framed it as needing the “trait/macro/fn stuff” — three substrate primitives missing from the current language. This RFD addresses the trait component; RFD-0042 the macro component; RFD-0040 the rule/fn unification.

The OO trap is real. Multiple inheritance via concept hierarchies — pub kind Manager <: Person, Employee, Authority — invites diamond-inheritance pathologies the metatype calculus does not handle. Rust’s trait model handles this by separating what a thing IS (its type / concept) from what it can DO (its traits), with static dispatch resolving method conflicts at compose time.

Decision

D1 — Trait declaration

A trait declares required behavior. Syntax:

pub trait Identifiable {
    fn id(self) -> Text
}

A trait has zero or more required items. Items can be:

  • fn — pure functions.
  • derive — derivation rules (head bound by the trait, body provided by impls).
  • query — read-only observations.
  • mutation — state-changing operations.
  • Associated types: type Item;
  • Associated constants: const N: Nat;

Items declared in a trait body have signatures only; impls provide bodies.

pub trait Comparable {
    fn compare(a: Self, b: Self) -> Ordering
    type Key                                       // associated type
    const Identity: Self                           // associated constant
}

Self is the implementing type. It is in scope inside the trait declaration as a placeholder.

D2 — Default implementations

Required items can carry default bodies:

pub trait Comparable {
    fn compare(a: Self, b: Self) -> Ordering
    fn equal(a: Self, b: Self) -> Bool = compare(a, b) == Ordering::Equal
    fn less(a: Self, b: Self) -> Bool = compare(a, b) == Ordering::Less
}

An impl can override any default. Items without defaults must be implemented.

D3 — Trait inheritance via <:

A trait can extend other traits. The supertype operator <: is reused; inheritance means “implementing the subtrait requires also implementing the supertrait.”

pub trait Ord <: Eq {
    fn compare(a: Self, b: Self) -> Ordering
}

impl Ord for Person requires impl Eq for Person to exist. Multiple supertraits are line-broken per RFD-0040 D9:

pub trait Comparable
    <: Eq, Hashable
{
    fn compare(a: Self, b: Self) -> Ordering
}

Trait inheritance is a contract relationship, not a method-override relationship. Subtrait default methods cannot override supertrait default methods; the implementer chooses.

D4 — impl blocks

A concept implements a trait via a separate impl declaration:

pub kind Person {
    name: Text,
    email: Text,
}

impl Identifiable for Person {
    fn id(self) -> Text = self.email
}

impl Comparable for Person {
    fn compare(a: Self, b: Self) -> Ordering = Ordering::cmp(a.email, b.email)
}

impl is not pub — visibility follows the more restrictive of the trait’s and the concept’s visibility. Cross-package impls require both the trait and the concept to be pub in their respective packages, and they must satisfy the orphan rule (D8): at most one of the trait or concept may be external to the impl’s home package.

D5 — Generic bounds

Generic parameters can be bounded by traits:

pub fn sort<T: Comparable>(xs: List[T]) -> List[T] {
    // body uses T::compare
}

Multiple bounds compose with +:

pub fn dedup<T: Eq + Hashable>(xs: List[T]) -> List[T] {
    // body uses T::equal and T::hash
}

Bounds at declaration time make T’s trait methods callable inside the body. Outside the bounded scope, T is untyped.

A bound can require multiple traits via the + separator. It cannot require a concept-subtype bound and a trait bound mixed: <T: Animal + Comparable> is a syntax error. Use where-clauses for mixed bounds:

pub fn race<T>(animals: List[T]) -> T
where
    T <: Animal,
    T: Comparable,
{
    // T must be a subtype of Animal AND implement Comparable
}

D6 — Static dispatch via monomorphization

Trait bounds resolve at ox compose time using RFD-0034’s pipeline and RFD-0036’s bounded-generic machinery. Every site where sort<T: Comparable> is called with a concrete T produces a specialized rule set with the appropriate T::compare body inlined.

No runtime trait objects in v1. There is no dyn Comparable or analog. A function bounded by a trait sees the trait methods as named items; the compiler monomorphizes one specialization per concrete type used at a call site.

Cross-package trait method invocation goes through the same compose-time machinery: a downstream package using sort<T: Comparable> with its own T triggers monomorphization at the consuming ox compose. Stable IDs include the resolved T (per RFD-0036).

D7 — Concept / trait orthogonality

Concepts and traits live on orthogonal axes. The type system distinguishes them by declaration form:

Declaration formAtomPurpose
pub kind X { … }concept“X is an entity of metatype kind” (ontological)
pub trait X { … }trait“X is a behavioral contract” (no ontological commitment)
pub kind X <: Yconcept-to-concept specialization
pub trait X <: Ytrait-to-trait inheritance
pub kind X : Yconcept-to-concept instantiation (MLT — RFD-0044)
impl T for X { … }concept X implements trait T

A concept implements zero or more traits. A trait is implemented by zero or more concepts. Neither carries the other.

The Rust analogy holds with one adjustment: Rust’s struct is closest to Argon’s kind. Argon does not have struct separately — structures-without-ontological-commitment (like Result[T, E] or Pagination) are substrate primitives (the collections layer, RFD-0039) or library-defined kinds with a neutral metatype.

D8 — Coherence

The standard trait-system coherence problem: if both crate A and crate B can write impl Trait for Concept, which wins? Argon’s rule is the orphan rule: at least one of Trait or Concept must be in the impl’s own package. Equivalently, at most one of the two may be external — impls where both are external are rejected with OE0860 OrphanImpl.

// In package my_lib: implementing my own trait on someone else's concept — OK.
impl my_lib::Sortable for std::collection::List { … }

// In package my_lib: implementing someone else's trait on my own concept — OK.
impl std::traits::Eq for my_lib::Lease { … }

// In package my_lib: implementing someone else's trait on someone else's concept — ERROR.
impl std::traits::Eq for std::collection::List { … }   // OE0860 OrphanImpl

Diamond inheritance through traits is resolved by explicit disambiguation at the impl site. If Person implements Ord (which extends Eq) and PartialEq (which also extends Eq), the Person impl provides one equal method; the compiler errors with OE0862 ConflictingDefaultImplementation if the trait defaults disagree and the impl doesn’t pick one.

D9 — Trait methods inherit purity

A trait method declared as fn is fn-pure for callers; declared as mutation, the bounded site is impure.

pub trait Sink {
    mutation send(self, msg: Message)         // mutation, not fn
}

pub fn broadcast<T: Sink>(sinks: List[T], msg: Message) {
    // ERROR: fn cannot call mutation (RFD-0040 D3 purity ladder)
    for s in sinks { s.send(msg) }
}

pub mutation broadcast<T: Sink>(sinks: List[T], msg: Message) {
    // OK: mutation can call mutation
    for s in sinks { s.send(msg) }
}

The purity ladder applies through trait bounds transparently. There is no “pure trait” / “impure trait” distinction at the trait level; the purity is per-method.

D10 — Self-bounded recursion

A trait method can take Self as a parameter or return Self:

pub trait Monoid {
    fn empty() -> Self
    fn combine(a: Self, b: Self) -> Self
}

Recursive bounds — a trait that requires its associated type to also implement the trait — are admitted with a depth limit at monomorphization (RFD-0036 limits apply).

D11 — Diagnostic codes

OE0860 OrphanImplimpl violates the orphan rule. OE0861 MissingTraitItemimpl does not provide a required item without a default. OE0862 ConflictingDefaultImplementation — diamond inheritance leaves an ambiguous default. OE0863 TraitBoundUnsatisfied — call site uses a type that does not implement the required trait. OE0864 MixedBoundKinds<T: Concept + Trait> mixes concept-subtype and trait bounds (use where-clause). OE0865 SelfOutsideTraitSelf appears outside a trait declaration or impl block. OE0866 SupertraitNotImplementedimpl Subtrait for X without impl Supertrait for X.

D12 — Trait-bound derive and mutation

The same generic-bounds machinery applies to derive and mutation, not just fn. RFD-0036 already lifts <T: Bound> to all four operator surfaces; this RFD adds trait bounds as a permitted kind of bound.

pub derive ordered_pair<T: Comparable>(a: T, b: T) :- {
    less(a, b)
}

pub mutation persist_sorted<T: Comparable>(xs: List[T]) {
    with sorted := sort(xs);
    for x in sorted { insert StoredItem { value: x }; };
}

Rationale

Traits exist as a distinct atom because none of the other five expresses behavioral contracts. A concept is ontology; a trait is capability. Folding them produces either OO inheritance (which Argon explicitly rejects) or a metatype-as-trait conflation (which breaks foundation-neutrality).

Static dispatch only in v1. Runtime trait objects (dyn Trait) require a vtable and runtime polymorphism, both at odds with the monomorphization-at-compose model RFD-0034 established. The need for dynamic dispatch in Argon is hypothetical until a real workload demands it. Defer.

<: for trait inheritance reuses the specialization token because the relation is structurally the same — a subtrait is a refinement of a supertrait. The same token in concept declarations means concept specialization; in trait declarations means trait inheritance. Position disambiguates.

The orphan rule is the standard solution to coherence. Without it, two unrelated packages can write conflicting impls, and the compiler cannot decide which wins. With it, the impl lives with one of the two parties — predictable and reviewable.

Mixed concept-subtype and trait bounds via where-clauses. The <T: ...> form should mean one kind of bound. Mixing them inline (<T: Animal + Comparable>) ambiguates whether Animal is a concept or a trait. where-clauses make the kinds explicit.

Trait methods inherit purity from their declared keyword. This is the cleanest interaction with RFD-0040’s purity ladder. A Sink trait with a mutation send method makes any bounded site impure; a Comparable trait with fn compare keeps callers pure. No new purity machinery needed for traits.

Consequences

RFD-0036 is extended, not superseded. RFD-0036’s bounded-generic machinery handles trait bounds; the changes are limited to admitting trait names as bound targets and emitting the appropriate trait-method-resolution calls during monomorphization.

The stdlib ships a small trait vocabulary. Core traits: Eq, Ord, Hashable, Json, Display, Default, Sortable, possibly Iter. RFD-0042 (macros) makes @[derive(Eq, Hashable)] ergonomic; without macros, every impl is hand-written.

Coherence checks land in ox compose. The orphan-rule check runs at the workspace level (the only level at which all impls are visible). Per-package elaboration enforces the local invariants (impl provides all required items, doesn’t conflict with itself); cross-package conflicts surface at compose.

Wire types. oxc-protocol gains:

  • A Trait declaration kind alongside Concept, Relation.
  • An Impl declaration kind binding a (Trait, Concept) pair with a method body table.
  • Trait-bound generic-parameter metadata on fn / derive / query / mutation declarations.

Wire compatibility. oxbin artifacts gain a trait section. Older runtimes that don’t recognize trait sections must fail-fast (RFD-0038 capability advertisement covers this).

Book chapter. ch02-09-traits.md covers trait declarations, impl blocks, generic bounds, the orphan rule, and the concept/trait orthogonality principle.

Migration. Existing patterns that simulate traits (decorator-driven shape recognition for Sortable-like classes, hand-written gen_X_invalid_Y rules in the ArgUFO branch) can migrate to real trait declarations + impl blocks. The migration is not mechanical; trait extraction requires human design judgment.

Historical lineage

  • RFD-0036 (generic declarations) — discussion. Extended here to admit trait bounds.
  • RFD-0034 (composition pipeline) — committed. Provides the monomorphization-at-compose machinery traits depend on.
  • RFD-0019 (patterns are first-class) — committed. Patterns are reframed as macros (RFD-0042); some pattern use cases become trait + impl combinations.
  • RFD-0040 (substrate atoms and the explicit-writes principle) — discussion. Names traits as atom #4; this RFD specifies the grammar and semantics.

This RFD does not supersede any committed RFD.

RFD-0042 — Macros and compile-time code generation

Discussion Opened 2026-05-17

Question

How does Argon expose user-extensible compile-time code generation? This RFD specifies the macro system named as atom #6 in RFD-0040. State machines, derive-style boilerplate, and parameterized rule scaffolds all flow through it.

Context

Today’s surface bakes higher-order patterns into the compiler. State machines have a dedicated lifecycle { … } syntax that desugars in compiler-owned code to phase concepts + transition derives + reachability constraints. Patterns (RFD-0019) have their own first-class declaration form. Living diagrams (RFD-0026) sit beside both.

The compiler-owned approach has two costs:

  • Every higher-order pattern requires compiler changes.
  • The set of available patterns is closed.

The first cost has been paid repeatedly (lifecycle/statemachine, patterns, diagrams). The second cost shows up in the sharpe-ontology/ branches: the team reaches for behavioral contracts on type fields, for declarative ordering lattices in metaxis declarations, for change-modeling templates — each of which would be a new keyword today.

Macros invert this. The compiler ships the six substrate atoms (RFD-0040); package authors write macros that compose them into higher-order constructs. State machines move to a stdlib macro. Pattern scaffolds become macros. @[derive(Eq, Hashable)] becomes a macro.

The Rust analogy is direct. Rust’s #[derive(...)] and macro_rules! cover the common cases; procedural macros handle the rest. The two-flavor design (declarative + procedural) is well-trodden. Argon adopts it, with one significant addition: procedural macros see the resolved AST, including metatype-calculus annotations, not just token streams.

Decision

D1 — Two flavors

Argon ships two macro forms.

Declarative macros (macro!-style):

pub macro vec {
    () => { Vec::empty() };
    ($head:expr, $($tail:expr),* $(,)?) => {
        Vec::cons($head, vec!($($tail),*))
    };
}

let xs = vec!(1, 2, 3, 4);

Pattern-matching on token sequences. No type information; no metatype-calculus access. Used for syntactic sugar.

Procedural macros (#[macro]-style):

#![allow(unused)]
fn main() {
// In a Rust crate inside the package.
#[argon_macro]
pub fn expand(input: ResolvedAst) -> Result<TokenStream, MacroError> {
    let fields = input.fields();
    let body = fields.iter().map(|f| quote! {
        a.#f == b.#f
    }).reduce(|x, y| quote! { #x && #y });
    Ok(quote! {
        impl Eq for #input.name {
            fn equal(a: Self, b: Self) -> Bool = #body
        }
    })
}
}

Compiled Rust code. Receives a resolved AST input: type assignments, metatype annotations, decorator annotations, tier classifications. Emits Argon AST via a TokenStream-equivalent that re-elaborates through the normal pipeline. The canonical signature (ResolvedAst in, Result<TokenStream, MacroError> out, #[argon_macro] attribute) is spelled out in D4.

D2 — Invocation

Declarative macros invoke with !:

let xs = vec!(1, 2, 3);
println!("hello {}", name);

Procedural macros invoke as decorators (single block, before declaration, per RFD-0040 D7):

@[derive(Eq, Hashable, Json)]
pub kind Person {
    name: Text,
    email: Text,
}

The two surfaces are distinct because the cost is different: declarative-macro expansion is purely syntactic and cheap; procedural-macro expansion runs Rust code at compile time and is opt-in. The ! syntax flags an expression-level expansion; the @[…] syntax flags a declaration-level transformation.

Procedural macros can also be invoked at expression position with @[…]! syntax for inline cases:

let report = @[generate_audit_summary(period="Q3")]!;

This is the same procedural-macro mechanism; only the invocation position differs.

D3 — Expansion phase

Macros expand at oxc elaboration time. The pipeline iterates: an initial pass converges the metatype calculus on user-written declarations, macros then expand with full metatype-calculus visibility, and emitted declarations re-enter the pipeline for their own name-resolution + metatype-calculus pass. The phase order:

                ┌───────────────────────────────────────────┐
                │  parse → name resolution → metatype       │
                │  calculus (converge on user-written)      │
                └──────────────────┬────────────────────────┘
                                   │
                                   ▼
                       ┌───────────────────────┐
                       │  MACRO EXPANSION      │  ← procedural macros
                       │  (resolved AST in,    │    see resolved AST
                       │   AST tokens out)     │    with metatype info
                       └──────────┬────────────┘
                                  │
                  ┌───────────────┴─────────────┐
                  │ any emitted declarations?    │
                  └───┬──────────────────────┬──┘
                      │ yes                  │ no
                      ▼                      ▼
            re-enter name resolution +    type check → lowering
            metatype calculus, then
            re-run macro expansion
            (up to recursion limit)

Declarative macros expand purely syntactically (no resolved AST input; output is raw tokens) and so can run in the initial pass before the metatype calculus converges if no procedural macro is attached to the surrounding declaration. Procedural macros always run after the metatype calculus.

Procedural macros receive the resolved-AST input for the declaration they’re attached to and the surrounding scope (visible declarations, in-package metatypes with computed axes, in-package trait impls, recognized shapes). They emit AST that re-enters the pipeline at the name-resolution phase — emitted declarations go through name resolution, metatype calculus, and subsequent macro expansion.

The expansion is hermetic within the per-package boundary established by RFD-0034. Cross-package macros work because the consuming package’s ox compose walks the dependency graph and runs each package’s macros at its oxc step. Termination is guaranteed by the macro recursion limit (D8) and by the requirement that emitted AST not re-attach the same procedural macro to the same declaration.

D4 — Procedural-macro API surface

Procedural macros are Rust crates inside the Argon package, marked in ox.toml:

[macros]
derive_eq = { path = "macros/derive_eq", kind = "procedural" }
statemachine = { path = "macros/statemachine", kind = "procedural" }

The macro crate depends on oxc-macro-api:

#![allow(unused)]
fn main() {
use oxc_macro_api::{ResolvedAst, TokenStream, MetatypeRef, ConceptRef};

#[argon_macro]
pub fn expand(input: ResolvedAst) -> Result<TokenStream, MacroError> {
    // Inspect input.metatype(), input.fields(), input.decorators()
    // Build output via the quote! macro
    Ok(quote! { … })
}
}

The ResolvedAst type carries:

  • The declaration the macro is attached to (with its resolved metatype, decorators, fields).
  • Read-only access to in-scope declarations (other concepts, traits, fns, metatypes).
  • Read-only access to the metatype calculus (axis values, computed orders, recognized shapes).
  • A diagnostic emitter (input.emit_error(span, code, message)).

TokenStream is the same shape as Rust’s, scoped to Argon’s syntax. The quote! macro inside the Rust crate lets authors write Argon-source-shaped templates with #identifier interpolation.

D5 — Hygiene

Macros are hygienic by default. Identifiers introduced by a macro do not collide with identifiers in the surrounding scope. A macro that expands to:

let x = compute_thing();
return x + outer_var;

binds a fresh x that does not shadow any caller-scope x. outer_var resolves in the macro-call site’s scope (where the macro was invoked), not in the macro-definition site (where the macro was written).

Authors can opt out of hygiene with unhygienic! blocks when intentional capture is needed (rare; mostly for debugging).

D6 — What macros emit

A macro can emit any of the six substrate atoms (RFD-0040 D1):

  • New metatypes (rare; for vocabulary extensions).
  • New concepts.
  • New rules (fn, derive, query, mutation).
  • New traits.
  • New decorators.
  • New macro invocations (recursive expansion).

Emitted AST is re-elaborated through the normal pipeline. A macro that emits a pub kind declaration produces a concept that the metatype calculus sees, that downstream rules can reference, that other macros can introspect.

D7 — What macros do not do

  • No runtime access. Macros run at compile time; they cannot read the live database or query the deployed kernel.
  • No bypass of the type system. Emitted AST goes through full elaboration. A macro that emits ill-typed code produces compile errors at the emit site.
  • No bypass of the metatype calculus. A macro that emits a concept with bogus metatype assignments fails metatype validation like any hand-written concept.
  • No bypass of the purity ladder. A macro that emits a fn body calling a mutation produces an OE0840 like any other purity violation.
  • No filesystem access by default. Procedural macros run in a sandboxed Rust environment. File reads require an explicit ox.toml capability grant; build reproducibility requires the read paths to be content-addressed inputs.

D8 — Recursion and termination

Declarative macros are bounded by a recursion depth (default 128). Procedural macros are bounded by wall-clock time per invocation (default 30 seconds). Both limits are configurable per package via ox.toml.

Mutual recursion between macros is admitted within the depth limits. The compiler emits OE0871 MacroRecursionLimit if exhausted.

D9 — Spans and diagnostics

Every token emitted by a macro carries a span that points back to the macro invocation site. Diagnostics on macro-emitted code report:

  1. The error at the emitted location.
  2. The macro invocation that produced the location.
  3. The macro definition site.

The diagnostic format:

error[OE0123]: type mismatch
  --> src/leases.ar:42:5
   |
42 |     @[derive(Eq)]
   |     ^^^^^^^^^^^^ macro invocation
   |
   = note: emitted by macro `derive_eq` at lib/std/derive.ar:18:4
   = note: expected `Text`, found `Nat` (in synthesized impl body)

D10 — Raw-token attribute macros (the DSL-in-body case)

Some procedural macros embed a domain-specific language whose syntax is not valid Argon. The canonical case is @[statemachine] (D11) which wants phases { … } and transitions { … } blocks inside the annotated declaration’s body — neither is admitted by RFD-0040 D6’s kind-body grammar.

For these cases, procedural macros declare a raw-token mode in their Rust definition:

#![allow(unused)]
fn main() {
#[argon_macro(raw_tokens)]
pub fn expand(item: RawItemTokens) -> Result<TokenStream, MacroError> {
    // item.attributes() — decorators on the declaration (resolved)
    // item.head()       — the kind/category/relation/etc. header (resolved)
    // item.body()       — the body as a TokenStream, NOT parsed as Argon
    // ...
}
}

The parser’s behavior changes when a raw-token-mode macro is attached to a declaration:

  1. Parse the declaration’s head (pub kind Lease, pub category Foo, etc.) as usual.
  2. Tokenize the body without attempting to parse it as Argon. Deliver as a TokenStream.
  3. Skip name resolution and the metatype calculus for the declaration until the macro emits its expansion.
  4. After the macro emits, the emitted AST goes through the normal pipeline (parse → name resolution → metatype calculus → recognize).

This mirrors Rust’s #[proc_macro_attribute]: the macro receives a raw token stream of the item, parses it itself, and emits valid Rust. Raw-token mode is opt-in per macro; default procedural macros (without raw_tokens) receive the fully resolved AST per D4.

The macro author is responsible for parsing the DSL with their own parser (e.g., using syn and quote adapted for Argon tokens). Diagnostics emitted from the macro’s parsing carry spans pointing at the original source locations.

D11 — State machines as a macro (using raw-token mode)

The statemachine macro is the canonical demonstration of D10. It declares raw_tokens mode and reads a declaration like:

@[statemachine]
pub kind Lease {
    phases {
        Pending,
        Active,
        Expired,
        Terminated,
    }

    transitions {
        Pending -> Active   { on: signed },
        Active  -> Expired  { on: term_ended },
        Active  -> Terminated { on: terminated },
    }
}

The kind body contains phases { … } and transitions { … } blocks — not valid Argon kind-body syntax under RFD-0040 D6. Raw-token mode (D10) lets the macro receive the body as a TokenStream and parse it with the macro’s own parser. The macro then emits valid Argon:

  1. A phase concept for each phase (Pending, Active, Expired, Terminated) — each with pub phase metatype assignment.
  2. A derive rule for each transition, firing on the event.
  3. Reachability constraint rules from the transition graph.
  4. A current_phase(l: Lease) -> Phase fn returning the current phase from bitemporal kernel state.

The kernel — which already understands the phase metatype via the metatype calculus — handles bitemporal phase tracking unchanged. The compiler has no special knowledge of state machines. Issue #321 closes as “no language change required; package implementation.”

D12 — Patterns as macros

RFD-0019 patterns become a stdlib macro family. A pattern declaration:

@[pattern]
pub kind Subscription[Subject, Object] {
    holder: Subject,
    target: Object,
    started_at: DateTime,
    canceled_at: DateTime?,
}

expands to a generic concept declaration parameterized by Subject and Object. Use sites:

type GymMembership = Subscription[Person, Gym]

are macro invocations that produce concrete concept declarations.

The pattern surface that RFD-0019 specified continues to read the same; the implementation is library code in std::patterns.

D13 — Macros and visibility

Macros declared pub in a package are usable by dependents. Non-pub macros are package-local.

Declarative macros across packages are imported alongside other items:

use std::collection::vec

Procedural macros are imported via decorator path:

@[std::derive::Eq]
pub kind Person { … }

@[my_lib::statemachine]
pub kind Workflow { … }

Common stdlib macros (derive, statemachine, pattern) ship under unqualified names by convention.

D14 — Diagnostic codes

  • OE0870 MacroNotFound — invocation references a macro that doesn’t exist or isn’t in scope.
  • OE0871 MacroRecursionLimit — depth limit exhausted.
  • OE0872 MacroTimeout — procedural macro exceeded its wall-clock limit.
  • OE0873 MacroEmitInvalid — emitted AST fails parse or elaboration; nested diagnostic chain points to the failure.
  • OE0874 MacroCapabilityDenied — procedural macro attempted filesystem/network access without the ox.toml capability grant.
  • OE0875 HygieneViolationunhygienic! block leaks an identifier in a way that produces ambiguous resolution.
  • OE0876 MacroArgumentMismatch — declarative macro invocation does not match any case.

Rationale

Two flavors are necessary. Declarative macros cover the cases where syntactic pattern-matching is sufficient (vec!, println!); procedural macros cover everything else (derive_eq needs to inspect fields, statemachine needs to walk a transition graph). Either alone is insufficient — declarative-only can’t see types; procedural-only is overkill for trivia.

Procedural macros need resolved-AST access. The Rust analog stops at token streams plus syntactic inspection; that’s enough for #[derive(Eq)] because Rust’s tooling crates (syn, quote) re-parse the tokens with type-aware heuristics. Argon’s compile-time model already runs name resolution and metatype-calculus convergence before macro expansion in the proposed pipeline, so passing the resolved AST directly is cheaper and more correct. The macro author doesn’t re-implement the elaborator.

Hermetic per-package expansion. The composition pipeline (RFD-0034) compiles per-package then composes; macros run within the per-package step. Cross-package macro invocations work because the consuming package’s oxc step runs each macro the dependency declares. No global macro registry; no compose-time-only macros.

The decorator-shape invocation for procedural macros (vs !-suffix for declarative). A procedural macro is a declaration-level transformation: it takes a declaration as input, emits declarations. A declarative macro is an expression-level transformation: it takes tokens, emits tokens. The two invocation surfaces match the two scopes. @[derive(Eq)] reads as “this declaration gets the Eq impl appended”; vec!(1, 2, 3) reads as “this expression is a vec literal.”

The @[…]! form for procedural macros at expression position handles the edge case where a procedural macro should produce an expression (e.g., @[generate_audit_summary]! returning a structured value). The ! suffix flags expression-position; the @[…] flags the macro identity.

No runtime dispatch, no bytecode interpretation. Procedural macros are compiled Rust. They execute deterministically given identical inputs (subject to wall-clock limits and the sandbox). This matches the rest of Argon’s static-only-at-compose discipline.

State machines as the proving example. If the macro system can express state machines with the same guarantees the compiler-built-in lifecycle provides today, it can express anything the team currently asks for. The metatype-calculus visibility is the load-bearing capability; without it, macros could emit phase concepts but couldn’t validate disjointness or reachability without compiler help.

Filesystem capability gating. Procedural macros run untrusted code at compile time. The capability grant model (ox.toml declares “this package’s macros need read access to X”) is the standard solution; the alternative (full filesystem access) makes build reproducibility nearly impossible.

Consequences

Compiler. New iterative macro-expansion phase that runs after the metatype calculus converges on user-written declarations and re-enters the pipeline on emitted AST (per D3). New oxc-macro-api crate published alongside oxc-protocol. Procedural macros build as Rust dynamic libraries; the elaborator loads them in a sandboxed process.

Sandbox. The procedural-macro execution sandbox restricts: no network access, filesystem reads gated by ox.toml, no environment variable access, wall-clock time limit, memory limit. The implementation reuses existing Rust sandboxing primitives (e.g., wasmtime or seccomp + namespaces); selection deferred to implementation.

Stdlib macros. Initial stdlib macro set:

  • derive(Eq, Hashable, Json, Default, Display, Ord) for trait-impl generation.
  • statemachine for the lifecycle pattern.
  • pattern for RFD-0019 patterns.
  • vec!, println!, format! for declarative ergonomics.

Patterns RFD reframed. RFD-0019 stays committed; its first-class status is preserved but the implementation is a macro. The book chapter on patterns documents the macro and the recommended use cases.

State machine RFD closes. Issue #321 (lifecycle → statemachine rename) closes when the statemachine macro ships. The compiler removes the lifecycle { … } keyword in argon 1.1.

ox.toml extensions. New [macros] section listing declarative and procedural macro entries. Capability grants under [macros.<name>.capabilities].

Wire types. oxbin artifacts include a macro_expansion_provenance section recording which macros expanded which declarations. RFD-0028 diagnostics extend to carry macro-expansion chains.

Book chapter. ch04-04-macros.md covers the two flavors, invocation syntax, hygiene rules, and authoring procedural macros. ch03-05-state-machines-via-macro.md replaces the existing state-machine chapter.

Migration. No source migration required for the macro system itself — it’s additive. The lifecycle removal in argon 1.1 is mechanical (rewrite to @[statemachine]).

Historical lineage

  • RFD-0019 (patterns are first-class) — committed. Patterns become a macro library; the declaration surface continues to read the same.
  • RFD-0026 (living diagrams) — committed. Diagrams can be authored either as compiler-built-in or as a macro. Reassessed in a follow-up RFD.
  • RFD-0028 (diagnostics schema 1.0) — committed. Extended for macro-expansion provenance.
  • RFD-0034 (composition pipeline) — committed. Macros expand within its per-package boundary.
  • RFD-0040 (substrate atoms and the explicit-writes principle) — discussion. Names macros as atom #6; this RFD specifies the grammar and semantics.
  • Issue #321 (lifecycle → statemachine) — closes when the statemachine macro ships.

This RFD does not supersede any committed RFD.

RFD-0043 — Query and mutation surface

Discussion Opened 2026-05-17

Question

How do query and mutation bodies read and write state? This RFD specifies the grammar RFD-0040 commits to: the select / insert / update / delete / with / emit / scenario verbs, the shape grammar shared between reads and writes, the body shapes for the four operator surfaces, and the migration path from today’s require / do / retract / emit / return clause structure.

Context

Today’s mutation bodies use five clause names (require, do, retract, emit, return). The audit (RFD-0040 Context) showed four implicit-writes footguns the surface admits — let x: T = expr silently persisting, path = value reading like assignment, emit Event { … } routing invisibly to one of three sinks, derive-vs-persisted ambiguity. A team contributor compared the result to COBOL.

The fix is not patching the existing clause structure; it is replacing it. RFD-0040 D4 commits to four explicit write verbs (insert, update, delete, emit) and a no-implicit-writes rule. RFD-0040 D2 commits to four surface keywords (fn, derive, query, mutation) projecting one rule primitive. This RFD specifies the body grammar that operationalizes both.

The query syntax is informed by three external languages:

  • EdgeQL (EdgeDB) for the shape grammar — Lease { id, monthly_rent, tenant: { name } } reused identically on reads, writes, and computed projections.
  • Cypher / GQL (ISO/IEC 39075:2024) for graph-pattern traversal in the rare case where multi-hop patterns are the clearest reading.
  • Datomic / Datalog for with-bindings as the universal procedural-composition mechanism.

The result is closer to EdgeQL than to any of the others, with two divergences: Argon has concepts and traits (not object types with links), and Argon’s tier system constrains which forms admit where. This RFD makes both divergences explicit.

Decision

D1 — Four body shapes, one grammar surface

The four operator keywords each get a body shape:

KeywordBody shape
fn= expr (single-expression shorthand) or { stmt; … [trailing-expr] } — trailing expression is the implicit return value
derive:- { atom, atom, … }
query{ stmt; … [trailing-expr] } — same implicit-return shape as fn
mutation{ stmt; … [return expr]? } — explicit return only; bodies without return have unit return type

Statements (stmt) inside { } bodies are one of:

  • with name := expr; — let-binding, transaction-local, never persists.
  • let name = expr; — equivalent to with name := expr;. let is admitted as a synonym for readability when the binding is a single line.
  • select <shape> [from <source>] [where <pred>] [order by …] [group by …] [limit N]; — read; returns a set or scalar. from is permitted only when the projection shape introduces local bindings that need disambiguation (the group by case, see D5); otherwise the target concept is the first non-keyword token after select (D3).
  • insert <Concept> { <field> = <expr>, … }; — persistent write (mutation only).
  • update <pattern> set { <field> <op> <expr>, … } [where <pred>]; — persistent write (mutation only), where <op> is one of = (replace), += (add to a multi-cardinality field), -= (remove from a multi-cardinality field), or := (replace a multi-cardinality field wholesale). See D7 for the field-cardinality interaction (RFD-0039 collection semantics). The where clause is optional when the target is a bound instance variable (a single specific instance); it is required when the target is a concept name (to avoid unintentional bulk update — see D7).
  • delete <pattern> [where <pred>]; — persistent retract (mutation only). Same optionality rule as update.
  • emit (<sink>: <Event> { <field> = <expr>, … }); — externally-effecting write (mutation only).
  • scenario { stmt; … }; — hypothetical write block (query only).
  • if <expr> { stmt; … } else { stmt; … }; — conditional execution.
  • for <name> in <expr> { stmt; … }; — iteration over a set.
  • return <expr>; — explicit value-returning exit. May appear in fn, query, and mutation bodies. In fn and query bodies, return is optional: a body’s trailing non-;-terminated expression (typically a select) is the implicit return value. OE0839 fires only when a fn body has neither an implicit-return trailing expression nor an explicit return statement. mutation bodies without return have unit return type.

derive bodies are different: a comma-separated list of rule atoms following :- { … }. They do not admit let, insert, select, etc.

D2 — The shape grammar (read = write = compute)

The same shape syntax projects on reads, payloads on writes, and field declarations on computed values.

// Read projection
select Lease {
    id,
    monthly_rent,
    end_date,
    tenant: { name, email },              // nested projection
    rent_per_day := .monthly_rent / 30,   // computed field
}
where .status = Active

// Write payload — same shape
insert Lease {
    id = next_lease_id(),
    monthly_rent = 2400,
    end_date = 2027-12-31,
    tenant = (select Person where .email = "alice@example.com" limit 1),
    property = (insert Property { … }),   // subquery / nested insert
}

Shape rules:

  • field (bare name) — project the field by name; the result has a field of the same name.
  • field: { … } — nested projection; the result includes a sub-shape.
  • name := expr — computed field; expr evaluates in the scope of the parent record (.foo refers to the parent’s fields).
  • field = expr (in insert/update payloads) — assignment.

.field resolves to the field of the record currently in scope (EdgeQL convention). Inside select Lease { … }, .monthly_rent is the lease’s monthly_rent. Inside nested tenant: { … }, .name is the tenant’s name.

D3 — select without from

The target concept is the first non-keyword token after select. There is no from.

select Lease where .status = Active           // returns [Lease]
select count(Lease) where .status = Active    // returns Nat
select Lease.monthly_rent                     // returns [Money] (all leases' rents)
select sum(Lease.monthly_rent)                // returns Money

Aggregates over derived sets use with-binding:

with active := select Lease where .status = Active;
select sum(active.monthly_rent)

A query body’s final select is the return value. Multiple select statements before the final one are admitted but bound (use with to name them).

D4 — Where clauses

where <pred> filters. The predicate language is the same predicate language used in rule bodies (RFD-0005 graduated admission) — boolean expressions over field paths, equality / comparison operators, in / not in for membership, and / or / not for composition, exists(...) for existential bounds.

select Lease
where .status = Active
  and .monthly_rent > 2000
  and .tenant in (select Person where .priority = High)

Predicates compose with rule atoms via the existing graduated admission (RFD-0005) — query and derive bodies admit tier:closure by default; method calls on collections (RFD-0039) and higher-tier expressions are admitted in query bodies, not derive bodies.

D5 — order by, group by, limit

SQL-shaped, applied after where:

select Lease { id, monthly_rent }
where .status = Active
order by .monthly_rent desc
limit 10

group by:

select {
    tenant := .tenant,
    total := sum(.monthly_rent),
    count := count(*),
}
from Lease                                    // exception: group-by needs explicit source
where .status = Active
group by .tenant

(The from keyword reappears only in group by clauses where the source needs to be named explicitly because the projection shape has its own bindings. This is the one exception to D3’s no-from rule.)

D6 — Insert

insert Lease {
    id = next_lease_id(),
    monthly_rent = 2400,
    tenant = t,
    property = p,
    start_date = now(),
    status = Pending,
}

The shape lists field-value pairs. Field types must match the concept’s declared field types; type errors fire OE0830.

insert returns the freshly-created instance. Capture with with:

with l := insert Lease { … };
// l is the new lease; chained statements can reference it
emit (audit: LeaseSignedEvent { lease = l, at = now() });
return l;

insert admits subqueries in any value position:

insert Lease {
    tenant = (select Person where .email = email_address limit 1),
    property = (insert Property { … }),   // nested insert composes
    ...
}

Bulk insert from a set:

for record in incoming_batch {
    insert Lease {
        tenant = record.tenant,
        monthly_rent = record.amount,
        ...
    };
}

D7 — Update

update <pattern> set { <field> = <expr>, … } [where <pred>]

The pattern is the concept (or a with-bound name); set { … } carries the assignments; where filters which instances to update.

The where clause is optional when the target is a bound instance variable (a single specific instance — there’s nothing to filter), and required when the target is a concept name (to avoid unintentional bulk update across every instance of the concept). The grammar in D1 admits this with [where <pred>]; the elaborator enforces the requirement on concept-name targets and emits a diagnostic (TBD) if a bare update Lease set { … } appears without a where clause. The pay_rent example in D12 uses a bound variable (update p set { processed = true }) so the where clause is omitted by design.

update Lease
set { monthly_rent = .monthly_rent * 1.05 }
where .status = Active and .start_date < (now() - 365.days)

.field on the right-hand side refers to the current row’s pre-update value. The post-update value is what’s set.

Updating fields of cardinality > 1 uses += / -= / := (RFD-0039 collection semantics):

update Building
set {
    units += new_unit,                        // add to set
    tenants -= former_tenant,                 // remove from set
    rent_band := Range::new(1000, 2500),      // replace
}
where .id = building_id

Updates return the count of affected instances. Capture with with if needed; otherwise the value is discarded.

D8 — Delete

delete <pattern> [where <pred>]

Retracts every matching instance. The where clause follows the same optionality rule as update (D7): optional when the target is a bound instance variable (a single specific instance), required when the target is a bare concept name (to prevent unintentional bulk deletion). Cascading deletion follows the cardinality declarations on relations: if Lease.tenant : Person is mandatory, deleting a Person referenced by a Lease fires OE0832 DanglingReference (the user must explicitly delete the lease first, or the cascade rule must be declared via decorator).

delete Lease where .status = Terminated and .end_date < (now() - 7.years)

Like update, delete returns a count of affected instances.

D9 — Emit

emit writes to an external sink. The sink is a syntactically required prefix.

emit (audit: LeaseSignedEvent { lease = l, at = now() });
emit (hitl: ReviewRequest { document = d, reviewer = next_available() });
emit (notify: SubscriptionRenewed { user = u, plan = p });

Sinks (audit, hitl, notify) are declared by the kernel-runtime configuration. The compiler validates the event type against the sink’s declared schema (e.g., audit accepts AuditRecord subtypes; hitl accepts HitlTask subtypes; notify accepts Notification subtypes).

User-extensible sinks are admitted via pub sink <name>: <EventConcept> declarations at module level; the kernel routes them through configured channels (typed pubsub, external queue, etc.).

emit does not return a value. Emissions happen at transaction commit; if the transaction rolls back, the emission is suppressed.

D10 — Scenario blocks

scenario { … } introduces a hypothetical-write block inside a query body. On entry to the block, a kernel fork (RFD-0023) is created with the query’s current state as its base. Writes inside the block apply to the fork. On block close (};), the fork’s state replaces the query’s current state for the remainder of the query body — subsequent with-bindings and select statements read from the fork. The fork is discarded when the query body completes (the query returns or implicit-returns); writes never propagate to persistent storage.

pub query rent_after_proposed_increase(percent: Real) -> Money {
    scenario {
        update Lease
        set { monthly_rent = .monthly_rent * (1 + percent / 100) }
        where .status = Active;
    };
    with active := select Lease where .status = Active;
    select sum(active.monthly_rent)
}

Multiple scenario blocks in the same query body are chained: each scenario forks from the state including all previous scenarios’ writes (the writes accumulate sequentially within the query’s transient state). To branch from a known earlier point, factor the shared setup into a with-binding before any scenario begins. Nested scenarios are a syntax error (OE0836).

Scenario blocks do not appear in mutation bodies. A real transaction is already a fork that commits or rolls back; nested hypotheticals inside a real transaction have no clear use case in v1.

D11 — Pattern syntax for multi-hop traversal

Most queries traverse one hop and project. Multi-hop patterns admit Cypher-style syntax when clearest:

select p1
where match (p1: Person)-[:married_to]->(p2: Person)-[:works_at]->(c: Company)
  and c.name = "Acme"

The match form admits:

  • Node patterns (name: ConceptType) or (name) (untyped).
  • Edge patterns -[:relation_name]-> (directed) or -[:relation_name]- (undirected if the relation declares symmetry).
  • Variable-length edges -[:related_to*1..5]->.
  • In-pattern predicates (p: Person where .age > 18).

The match clause lowers to a join over the relation graph. It is not a separate grammar — it’s one form of predicate inside where, sharing the underlying machinery with the existing rule-body atom grammar.

For most domain code, plain select Concept where ... is clearer than match. The match form is reserved for the cases where the multi-hop structure is the question being asked.

D12 — Return

return <expr> is the final statement in query and (optionally) mutation bodies. In query, the return expression is the result of the query; if omitted, the final select statement’s result is returned (implicit return). In mutation, return is optional; if omitted, the mutation returns Unit (no value).

pub query active_count() -> Nat {
    select count(Lease) where .status = Active     // implicit return
}

pub query named_leases(name: Text) -> [Lease] {
    with t := (select Person where .name = name limit 1);
    return select Lease where .tenant = t           // explicit return
}

pub mutation create_lease(...) -> Lease {
    with l := insert Lease { ... };
    emit (audit: ...);
    return l;
}

pub mutation pay_rent(p: Payment) {
    update p set { processed = true };
    emit (notify: ...);
    // no return; returns Unit
}

D13 — Diagnostic codes

  • OE0830 FieldTypeMismatch — insert/update field-value type doesn’t match the concept’s declared field type.
  • OE0831 RequiredFieldOmitted — insert misses a required field.
  • OE0832 DanglingReference — delete leaves a mandatory reference unsatisfied; cascade decorator required or explicit delete chain.
  • OE0833 UpdateOfReadOnlyField — update targets a field declared @[readonly].
  • OE0834 WriteVerbInQueryBodyinsert / update / delete / emit outside a scenario block in a query body.
  • OE0835 ScenarioInMutationscenario { } appears in a mutation body.
  • OE0836 NestedScenarioscenario { scenario { … } }.
  • OE0837 UnknownEmitSinkemit (foo: ...) where foo is not a declared sink.
  • OE0838 EmitTypeMismatch — event payload type doesn’t match the sink’s accepted concept.
  • OE0839 MissingReturnfn body without an implicit-return final expression and without an explicit return statement.
  • OE0840 ImpureCallFromPureContext — purity-ladder violation (per RFD-0040 D3); e.g., fn calling mutation.
  • OE0841 SelectInDeriveBodyselect statement in a derive body (use rule atoms instead).
  • OE0842 GroupByWithoutFromgroup by clause without explicit from source.
  • OE0843 OrderByOnUnorderedorder by on a field whose type lacks Ord.

D14 — Migration mapping

The mechanical rewrites the ox migrate tool performs:

TodayTomorrow
mutation foo() { require { body } do { … } emit Event { … } return expr }mutation foo() { /* require body lowers to where + early return */; … emit (sink: Event { … }); return expr; }
let x: T = expr inside do { }with x := insert T { … } (or let x = expr if T was a type annotation on a local)
let x = expr inside do { } (local)let x = expr or with x := expr
path = value inside do { }update <root> set { <subpath> = value } where ...
retract { path: value where … }delete <root> where ... and <subpath> = value
emit Event { … } (unknown sink)emit (audit: Event { … }) for AuditRecord subtypes; emit (hitl: Event { … }) for HitlTask subtypes; emit (notify: Event { … }) for Notification subtypes. The tool inspects the event’s metatype to pick the sink.
query foo(...) :- body => headquery foo(...) -> T { select <head> where <body> }
compute foo(...) = exprfn foo(...) = expr
compute foo(...) { body { ... } }fn foo(...) { ... }
compute foo(...) { impl rust("...") }fn foo(...) impl rust("...")

Items with multiple plausible rewrites (e.g., the require clause may be a precondition, a constraint, or a guard depending on use) require manual review. The migration tool flags every site without a unique-rewrite and provides a // MANUAL REVIEW: ... comment with the alternatives.

D15 — Worked migration: lease-story

argon/examples/lease-story/packages/story-lease/src/mutations.ar today contains the record_security_deposit mutation. Old form (illustrative):

pub mutation record_security_deposit(
    deposit: Payment,
    lease: Lease,
) {
    require { deposit.amount > 0 and deposit.purpose = SecurityDeposit }
    do {
        let receipt: DepositReceipt = make_receipt(deposit, lease);
        lease.deposit_held = deposit.amount;
        lease.deposit_status = Held;
    }
    emit DepositReceived { lease: lease, receipt: receipt }
    return receipt
}

New form:

pub mutation record_security_deposit(
    deposit: Payment,
    lease: Lease,
) -> DepositReceipt? {
    // Precondition becomes an early-return guard. The mutation returns
    // Optional[DepositReceipt] (RFD-0039); precondition failure yields None,
    // captured in the audit log for diagnosis.
    if not (deposit.amount > 0 and deposit.purpose = SecurityDeposit) {
        emit (audit: DepositRejected {
            deposit = deposit,
            reason = "invalid amount or purpose",
            at = now(),
        });
        return None;
    };

    with receipt := insert DepositReceipt {
        deposit_id = deposit.id,
        lease_id = lease.id,
        amount = deposit.amount,
        received_at = now(),
    };

    update lease
    set {
        deposit_held = deposit.amount,
        deposit_status = Held,
    }
    where .id = lease.id;

    emit (audit: DepositReceived {
        lease = lease,
        receipt = receipt,
        at = now(),
    });

    return Some(receipt);
}

What changed:

  • require becomes an if-guard with explicit early return. The return type uses Optional[DepositReceipt] (sugar DepositReceipt?) from RFD-0039 — a substrate primitive already shipped. Precondition failure returns None; the audit emit captures the failure reason for diagnosis. A richer Result[T, E] substrate is future work (separate RFD); Optional is sufficient for the canonical migration pattern.
  • do { let x: T = expr } becomes with x := insert T { … }.
  • path = value becomes update set { field = value } where ….
  • emit Event { … } becomes emit (audit: Event { … }).
  • return expr becomes return expr; (statement-terminated).
  • The mutation’s return type is now explicit (-> DepositReceipt?).

D16 — Tier ceilings per body

Per RFD-0040 D2 and RFD-0004 (decidability ladder):

BodyTier ceilingNotes
fntier:expressivehigher-order operations admitted (collection methods, comprehensions)
derivetier:closurestructural + closure admitted; expressive operations rejected in rule atoms (OE2408)
querytier:expressiveshape grammar + aggregations admitted; cross-tier closures via with-bindings
mutationtier:expressivesame as query, plus write verbs

Bodies can opt up via #dec(<tier>) directives (existing RFD-0004 surface). Bodies that exceed the directive’s tier produce diagnostic OE0850 TierCeilingViolation.

Rationale

EdgeQL’s shape grammar is reused identically on reads and writes. This is the highest-leverage decision in the grammar — one shape grammar for projection, payload, and computed fields. Three operations for the price of one syntax. Modelers learn shapes once, use them everywhere.

select without from. The target concept is the first non-keyword token after select. SQL’s from adds noise without information; the target’s name already names the source. The only exception is group by, where the projection shape’s bindings need disambiguation from the source.

Statements over clauses. The current surface’s require / do / retract / emit / return clauses are positional and named — a reader has to scan for the clause, find its braces, and then read the contents. Statements with explicit verbs are linear: read top to bottom, every statement is self-describing.

The four write verbs cover everything. insert, update, delete, emit. Every persistent state change uses one. A reader greps a body for these four words and finds every write. The audit’s three implicit-write footguns disappear by construction.

with is the only let-binding form for procedural composition. let name = expr is admitted as a synonym for single-line readability, but conceptually the binding mechanism is one: introduce a name, bind it to a value, use it in subsequent statements. The proliferation of clause-named bindings in the current surface (do { let … }, require { let … }, emit { let … }) collapses.

Scenario blocks for hypotheticals, not query-can-write. A query declaration without scenario is statically guaranteed read-only. With scenario { … }, the hypothetical writes are visible at the call site. The graduation matches the implicit-writes principle: writes are visible everywhere they happen.

Cypher-style match reserved for multi-hop, not promoted to the default. Most domain queries traverse one hop. EdgeQL-style select Concept where .field = … reads clearer for those cases. The match form is a graduated admission within the existing rule-body atom language — same machinery, more verbose syntax for when multi-hop is the question.

Group-by needs from; otherwise no. The exception preserves consistency where the projection shape has bindings (tenant := .tenant, total := sum(.monthly_rent)) that need to disambiguate which concept they project from. Everywhere else, the first token after select is the source.

Bulk operations via for-loops. The alternative — SQL-style insert ... select ... syntax — adds a special form. A for loop with insert inside is more uniform: same syntax for “do this one thing for each of these things” everywhere. The kernel’s transaction semantics fold the loop’s inserts into one atomic commit.

Returns are statements, not clauses. return expr; at the end of a body, terminated by ;. Implicit return (final select statement) is admitted for single-statement query bodies to reduce ceremony.

Aggregations are functions, not keywords. count(...), sum(...), min(...), max(...), avg(...) are stdlib fn declarations. The grammar admits them as ordinary function calls. The compiler can recognize the canonical aggregation forms for query-plan optimization, but the surface keeps them as functions.

Consequences

Compiler. New parser entry points for select, insert, update, delete, emit, scenario, match, for, if, with. New elaboration phase phase_mutation_lowering that lowers the verbs to CoreMutationOp IR atoms (extending the existing Let / FieldUpdate atoms with Insert, UpdateSet, Delete, Emit variants).

Kernel. The MutationEvaluator in crates/nous/src/mutation/evaluator.rs reorganizes: the per-clause sequence (require → retract → do → emit → return) becomes per-statement, with each statement lowered to one or more AssertionRequest / EventRequest calls in declaration order. The atomic-commit semantics (all-or-nothing per mutation) are preserved.

Sink declarations. New module-level surface: pub sink <name>: <EventConcept>. The stdlib ships three default sinks (audit, hitl, notify); kernels can register more via the existing emit-routing API.

The wire types. oxc-protocol extends CoreMutationOp with the new variants. SDK regeneration via oxc-codegen is mechanical.

Predicate-grammar consolidation. RFD-0005’s “one grammar, multiple contexts” architecture stays. The predicate language used in where clauses, :- { … } derive bodies, match patterns, and constraint refinements is the same — three sub-grammars get unified at the implementation level (the framing-issue the team contributor flagged). The same lexer and parser entry points serve all four positions; the tier ceilings differ per context.

Migration tool. ox migrate performs the mechanical rewrites in D14. Items requiring manual review get // MANUAL REVIEW: ... comments with alternatives. The tool runs in two modes: --check (lists sites without modifying) and --apply (rewrites in place).

Book chapter. ch02-08-queries-and-mutations.md replaces the current chapter on compute/mutation. Covers select, the shape grammar, insert/update/delete, with-bindings, emit with sinks, scenario blocks, the match form for multi-hop, the four-body tier matrix.

Examples. All .ar files using the old clause grammar migrate via ox migrate. The lease-story is the canonical worked migration (D15); the sharpe-ontology/ story branches follow.

Old syntax compatibility. The old grammar parses under --edition=0.4 for one minor version after argon 1.0 lands. argon 1.1 removes it. The two grammars share the same AST after lowering, so cross-edition packages can compose without recompilation of the dependency. This matches the deprecation timeline in RFD-0040.

Historical lineage

  • RFD-0005 (one grammar, multiple contexts) — committed. The predicate-graduation architecture this RFD operationalizes. where, :- {…}, match, refinement bodies share the same predicate grammar with different tier ceilings.
  • RFD-0007 (first-class queries, mutations, provenance) — committed. Queries and mutations remain first-class items; the clause structure changes but the provenance machinery is preserved.
  • RFD-0023 (kernel API v2) — committed. The fork machinery used by scenario blocks is shipped.
  • RFD-0033 (test mutate statements) — discussion. Compatible; mutate in test blocks is a call to a mutation declaration; the test admits the new body grammar.
  • RFD-0039 (collections and collection operators) — committed. The shape grammar uses RFD-0039’s collection methods (+=, -=, :=) for cardinality > 1 fields.
  • RFD-0040 (substrate atoms and the explicit-writes principle) — discussion. Commits to the four explicit verbs (D4), the four operator surfaces (D2), the call-purity ladder (D3); this RFD specifies the body grammar.
  • RFD-0041 (traits) — discussion. Trait methods declared as mutation admit the new body grammar; trait bounds compose with the purity ladder per RFD-0040 D3.

This RFD supersedes RFD-0007’s body-clause structure (require / do / retract / emit / return) while preserving RFD-0007’s first-class-item commitment and the provenance machinery. The clause names are replaced by statements; the first-class status is not.

References

  • EdgeQL — https://docs.geldata.com/reference/edgeql
  • GQL / ISO/IEC 39075:2024 — https://www.iso.org/standard/76120.html
  • Cypher 25 — https://neo4j.com/docs/cypher-manual/current/
  • Datomic Datalog — https://docs.datomic.com/query/query-data-reference.html

RFD-0044 — Cross-level instantiation (MLT)

Discussion Opened 2026-05-17

Question

How does Argon express concept-to-concept instantiation — the powertype pattern at the heart of multi-level theory — while preserving foundational-ontology neutrality? This RFD specifies the substrate change RFD-0040 D12 commits to.

Context

Argon’s Core IR has one atom for the instance-of relation:

#![allow(unused)]
fn main() {
// argon/oxc/src/core_ir.rs:771
Instantiates { individual: String, concept_id: u64 }
}

The first parameter is locked to String (an individual identifier). The atom expresses order-0 → order-1 instantiation: alice : Person. It cannot express Person : AnimalSpecies — concept-to-concept, the case multi-level theory (MLT, Carvalho & Almeida 2017) and Tonto (the OntoUML DSL) handle natively.

The :: operator (Person :: Kind) does not fill the gap. It lowers to CoreRuleAtom::Meta { subject, expected: Some("kind") } — meta-property assignment, asserting that Person has the meta-property kind. That is classification by metatype, not instantiation of another concept.

A team contributor’s diagnosis after auditing the IR:

Argon cannot represent concept-to-concept instantiation anywhere in Core IR. Not just on declarations — anywhere. The relevant Core IR atom is locked to order-0 → order-1 instantiation (an individual is an instance of a concept). There is no Core IR variant for order-1 → order-2 (a concept is an instance of a higher-order concept). So Tonto’s kind Dog (instanceOf AnimalSpecies), encoded in OntoUML as an «instantiation» Relation between two Class nodes, has no current Core IR analog in Argon.

The audit also surfaced the half-built infrastructure that already exists:

  • crates/nous/src/multilevel.rs:14–81 enforces UFO axiom A-108 (categorization order constraints) when concepts declare order and categorizes axes. The order-arithmetic works; it has no IR or surface to drive it.
  • argon/examples/lease-story/packages/ufo/src/mlt.ar documents the Carvalho & Almeida axioms (a105–a108) as relational facts but is orphaned — no story module uses it.

This RFD closes the gap with one IR generalization plus a new elaboration phase that derives order. The substrate stays vocabulary-neutral; UFO, BFO, or any other vocabulary can ride on top via decorators.

Decision

D1 — Generalize the IR atom

Replace Instantiates with InstanceOf. The new atom carries an enum on the left-hand side that distinguishes which kind of entity is being instantiated.

#![allow(unused)]
fn main() {
pub enum EntityRef {
    Individual(u64),  // an instance assertion in the ABox
    Concept(u64),     // a concept in the TBox (MLT case)
    Statement(u64),   // a reified statement (future; for higher-order reflection)
}

pub struct InstanceOfAtom {
    pub instance: EntityRef,
    pub type_concept: u64,
}
}

One atom expresses every instantiation. Existing emit sites pass EntityRef::Individual(id) and continue to work; new emit sites for concept-to-concept pass EntityRef::Concept(id).

Statement(_) is reserved for higher-order reflection (reified statements as entities). No surface admits it in v1; the variant exists so the IR doesn’t need a second generalization when that day comes.

D2 — Surface syntax admission

RFD-0040 D5 commits to : always meaning instantiation. This RFD extends the parser to admit : in concept declaration heads where today only individual-of-concept is recognized.

pub kind AnimalSpecies { order: 2 }
pub kind Dog : AnimalSpecies                    // Dog is an instance of AnimalSpecies
pub kind Husky <: Dog : DogBreed                // specialization + instantiation

Multi-instantiation follows RFD-0040 D9 (line-broken for multi):

pub kind Dog
    : AnimalSpecies, RegistrationCategory       // two instantiation targets

Mixed multi-specialization + multi-instantiation:

pub kind Husky
    <: Dog, WorkingAnimal
    : DogBreed, RegistrationCategory

The grammar admission is local — the parser already accepts : in declaration heads under RFD-0015’s specialization-sugar interpretation. RFD-0040 D5 replaces that interpretation with the correct one (instantiation); this RFD’s surface change is the lowering: when the right-hand side resolves to a concept (not a metatype primitive), emit InstanceOf instead of the old Meta atom.

Substrate / vocabulary boundary. Three layers:

LayerModuleContentsUsed by
Universal substratestd::metaMetatype sealed top; the : and <: operators; reflection (meta(), ancestors(), order()); the InstanceOf IR atomevery vocabulary
MLT primitivesstd::mlt@[categorizes], @[partitions], @[subordinate_to] decorators; Categorizes / Partitions / SubordinateTo IR atoms; the four CL inference rules (D13)MLT-adopting vocabularies (UFO)
Foundational ontologyufo, bfo, dolce, …specific metatypes — UFO’s kind, subkind, phase, role, relator, category, mixin; BFO’s universals; etc.individual ontology modelers

A modeler writes:

use std::mlt::*        // MLT primitives (decorators + reflection on them)
use ufo::{kind, category}   // UFO metatypes

@[categorizes(Currency)]
pub category CurrencyType { }

pub kind USD : CurrencyType { … }

BFO, DOLCE, and any future ontology that does not adopt MLT skip std::mlt. Their vocabularies declare their own metatypes against std::meta directly. MLT is one standard-library module among many — opt-in.

RFD-0002 (foundational-ontology neutrality) holds at every layer: the substrate ships no UFO-specific content; MLT is its own discrete bundle; UFO is one foundational ontology among several. The categorization/partitioning relations originated in MLT (Carvalho & Almeida 2017), not UFO — UFO adopted them. std::mlt honors that lineage by keeping them separate from std::meta (universal) and from ufo (vocabulary-specific).

D3 — Order is derived

Order is not stamped at declaration. The elaborator computes order as a fixpoint over the instantiation closure. The direction follows Carvalho & Almeida (2017): types are strictly higher-order than their instances.

order(c) = 1 + max(order(x) for x in instances_of(c))

where instances_of(c) = { x | x : c }   (i.e., x where InstanceOf(x, c) holds)

Concretely: Dog : AnimalSpecies gives instances_of(AnimalSpecies) ⊇ { Dog, Cat, … }, so order(AnimalSpecies) = 1 + max(order(Dog), order(Cat)) = 1 + 1 = 2. AnimalSpecies is higher-order than Dog, which is higher-order than the individuals (fido, rex).

Base case: individuals are order 0; types with no instances yet are order 1 (the sup over an empty set contributes 0). The fixpoint terminates in at most |concepts| iterations (the longest possible instantiation chain).

Vocabularies can assert an order via metatype decorators:

@[order(=2)]
pub metatype higher_order_type = { … }

The elaborator validates the asserted order against the derived value. If they disagree, OE0890 fires.

For convenience, the metatype can also declare a bound:

@[bounded_order(2)]
pub metatype species = { … }

This caps the order of any concept of this metatype at 2; deeper instantiation chains through such a concept produce OE0891.

D4 — Elaboration phase

A new phase phase_mlt runs after phase_resolve (name resolution) and before phase_recognize (shape recognition). The phase:

  1. Walks all emitted InstanceOf atoms and builds the instantiation graph.
  2. Detects cycles. Reports OE0892 InstantiationCycle for each cycle.
  3. Computes order via the fixpoint above.
  4. Stores derived order in CoreConcept.derived_order: Option<u32>.
  5. Validates user-asserted orders (@[order(=N)], @[bounded_order(N)]) against derived values.

The phase is per-package per RFD-0034’s composition boundary. Cross-package instantiation chains resolve at ox compose time when the workspace-level instantiation graph is assembled; the same fixpoint and acyclicity check run against the global graph.

D5 — Lean extension

A new file argon-formal/ArgonFormal/Schema/MLT.lean:

import ArgonFormal.Schema.TypeGraph
import Mathlib.Data.Finset.Lattice

namespace ArgonFormal.MLT

variable {C A : Type} [Fintype C] [DecidableEq C] [Fintype A] [DecidableEq A]

/-- The instances of `d` under an instantiation relation — concepts that instantiate `d`. -/
def instancesOf (instantiates : C → C → Prop) [DecidableRel instantiates] (d : C) : Finset C :=
  Finset.univ.filter (fun c => instantiates c d)

structure TypeGraphMLT extends TypeGraph C A where
  -- `instantiates c d` reads as "c is an instance of d" (c : d in the surface).
  instantiates : C → C → Prop
  instantiates_dec : DecidableRel instantiates
  instantiates_acyclic : Acyclic instantiates
  order : C → ℕ
  -- Types are strictly higher-order than their instances
  -- (Carvalho & Almeida 2017 axiom A-3):
  order_increases : ∀ c d, instantiates c d → order d > order c
  -- `order(d)` equals one plus the supremum of the orders of `d`'s instances.
  -- For types with no instances, `Finset.sup` on an empty set returns `⊥ = 0`
  -- in ℕ (via `OrderBot ℕ`), giving `order = 1` for leaves.
  -- This axiom is what makes `order` the canonical MLT order assignment rather
  -- than an arbitrary `ℕ`-valued function satisfying `order_increases`.
  order_minimal : ∀ d, order d
                       = 1 + ((@instancesOf C _ instantiates instantiates_dec d).image order).sup id

end ArgonFormal.MLT

The empty-Finset.sup base case uses Mathlib’s OrderBot ℕ instance (⊥ = 0), not a default := named argument. instancesOf is defined outside the structure (parametric over the instantiation relation) and referenced inside order_minimal via the structure’s own instantiates and instantiates_dec fields. Promoting order_minimal to a structural axiom (rather than an external theorem) is necessary: without it, order would be a free -valued function constrained only by strict monotonicity over instantiates, which permits arbitrary inflations like order c = 42 for all c. The axiom binds order to the canonical fixpoint.

The Lean PR that mechanizes this will need to discharge a non-emptiness or termination obligation — the well-foundedness of instantiates (guaranteed by instantiates_acyclic) ensures the fixpoint exists.

Theorems to prove:

  • order_well_defined: order is the least fixpoint of the closure starting from order 1.
  • order_bounded: ∀ c, order c ≤ |C| (instantiation chains are bounded by concept count).
  • order_consistent_with_specialization: c <: d → order c = order d (specialization preserves order).
  • instantiates_specializes_independence: instantiation and specialization are independent relations; a concept’s specializations and instantiations can be reasoned about separately.

The Rule machinery, fixpoint algorithm, and decidability theorems in the existing Lean files are reused unchanged. Rules can reference instantiates as just another relation; no rule-category extension is needed.

D6 — Diagnostic codes

  • OE0890 OrderMismatch — user-asserted order via @[order(=N)] disagrees with derived order.
  • OE0891 InstantiationChainTooLong — concept’s derived order exceeds a metatype’s @[bounded_order(N)] cap.
  • OE0892 InstantiationCycle — concept-to-concept instantiation graph contains a cycle.
  • OE0893 InstantiationOfPrimitiveMetatypepub kind Foo : kind (instantiating the kind primitive metatype). The metatype calculus handles classification; instantiation of metatype primitives is Meta not InstanceOf.
  • OE0894 InconsistentMixedDeclaration — declaration mixes inline <: and : clauses without line-break per RFD-0040 D9.
  • OE0895 InstantiationFieldNotInTarget — body of pub <metatype> X : Y { … } contains field = value where field is not declared on Y and not reachable via Y’s categorization chain (D12). See D11.
  • OE0896 InstantiationFieldShadows — body of pub <metatype> X : Y { … } contains field: Type that shadows a field declared on Y or on any concept in Y’s categorization chain. See D11.
  • OE0897 CategorizationViolation — CL-1 fired: an instance of a higher-order type is explicitly declared disjoint from the categorized base type, contradicting CL-4’s derived specialization. Reserved in v1 (no explicit-disjointness surface yet); fires once explicit disjointness ships. See D13.
  • OE0898 PartitionDisjointnessViolation — CL-2 fired: an individual is classified by two distinct instances of a partition. See D13.
  • OE0899 OrderInconsistency — CL-3 fired: a concept of order N is instantiated by a concept of order ≥ N. See D13.

D7 — Migration of existing emit sites

InstanceOf replaces Instantiates. The emit sites enumerated in the IR audit:

  • phase_elaborate.rs:3970 — bare variable in rule body → InstanceOf { instance: Individual(local_id), type_concept }.
  • lower.rs:648, 1035, 1963 — pattern matches replaced with InstanceOf pattern matches; the EntityRef::Individual(_) arm preserves existing behavior.
  • stratify.rs:113 — stratum classification reads type_concept directly.
  • test_runner.rs:885, 914 — test execution dispatches on the new variant.

The old Meta lowering for Person :: Kind is kept (it’s still meta-property classification, not concept-to-concept instantiation). The :: operator continues to lower to Meta; the : operator in concept declaration heads lowers to InstanceOf. The two operators now carry distinct semantics, as RFD-0040 D5 commits.

D8 — Tonto compatibility

Tonto’s kind Dog (instanceOf AnimalSpecies) and Argon’s pub kind Dog : AnimalSpecies are semantically identical. Both lower to one InstanceOf atom with instance: Concept(dog_id), type_concept: animal_species_id.

A future ox import tonto tool can mechanically translate Tonto source into Argon source by rewriting kind X (instanceOf Y) to pub kind X : Y and similar for other Tonto syntactic forms.

D9 — @[bounded_order(n)] and unbounded chains

Practical ontologies use bounded-order MLT: order 0 (individuals), 1 (types), 2 (types of types), occasionally 3 (DogBreed : Species : Kingdom). RFD-0040 calls out tier:mlt as a reserved decidability tier; bounded-order MLT is decidable (Carvalho & Almeida 2017 prove this), and the elaborator stays within the bound for any vocabulary that uses @[bounded_order(n)].

Unbounded MLT is not decidable in general; the elaborator emits OW0420 UnboundedOrderChain as a warning when a chain exceeds a heuristic depth (default: 8) without a bound annotation. Vocabularies that want unbounded MLT can suppress the warning via @[allow_unbounded_order] on the relevant metatype.

D10 — meta() intrinsic still works

The meta(c) intrinsic — which lifts a concept’s metatype name to a queryable value — is unchanged. It returns the primitive metatype of the concept (kind, phase, relator, etc.), not the concept-of-which-c-is-an-instance. The two questions are different:

  • meta(Dog) == "kind" — Dog is classified by the kind metatype primitive.
  • Dog : AnimalSpecies — Dog is an instance of the concept AnimalSpecies.

A meta_concept(c) intrinsic that returns the concept(s) c instantiates is a small additional surface; it can ship in this RFD or be deferred. Deferring.

D11 — Body semantics for cross-level instantiation

The body of pub <metatype> X : Y { … } admits two statement kinds, distinguished syntactically by = vs ::

  • field = value — X provides a kind-level value for field. The field must be declared on Y or on any base type Y categorizes (transitively via the @[categorizes] / @[partitions] chain — see D12). The type of value must match the declaring type’s field type. Fires OE0895 InstantiationFieldNotInTarget if the field is not declared on Y or any concept reachable through Y’s categorization chain.
  • field: Type — X declares a new field on its own instances. Fires OE0896 InstantiationFieldShadows if Y already declares field, or if any concept in Y’s categorization chain declares field (no implicit shadowing through the chain either).

The two forms can mix freely in a single body. The parser distinguishes them on the = / : token. The elaborator’s field-lookup pass walks the categorization closure once before checking each assignment (the closure is finite and cached per type).

Concretely: pub kind USD : CurrencyType { iso_code = "USD", … } succeeds because CurrencyType @[categorizes(Currency)] and Currency declares iso_code. The kind-level value is recorded on USD; access via USD.iso_code (kind-level lookup) yields "USD". Without the categorization-chain lookup, this assignment would be rejected since CurrencyType itself declares no fields — that’s exactly the OE0895 false-positive D11 must avoid.

pub category Currency {
    iso_code: Text,
    decimal_places: Nat,
    symbol: Text,
}

@[categorizes(Currency)]
pub category CurrencyType { }

pub kind USD : CurrencyType {
    // kind-level values (provide values for Currency's fields, via CL-4 specialization)
    iso_code = "USD",
    decimal_places = 2,
    symbol = "$",

    // instance-level field declarations (each USD bill carries these)
    serial_number: Text,
    denomination: Nat,
    series_year: Nat,
}

Fields do not propagate downward through instantiation: an individual x : X has only the fields X declared via : (plus the fields X’s specializations carry). Kind-level fields are accessed through the type, not through the individual:

  • USD.iso_code returns "USD" (kind-level lookup).
  • my_twenty.serial_number returns "L12345678A" (instance-level field).
  • my_twenty.iso_code is a type error — bills do not have an iso_code field at the individual level.

Kind-level field access USD.iso_code resolves directly from the kind_level_values map on CoreConcept (see Consequences for the wire-type extension). The meta() intrinsic — which returns the primitive metatype (kind, category, etc.) per D10 — does not expose kind-level field values; it answers a different question. Programmatic kind-level reflection uses a kind_level_field(c, name) operation (or the deferred meta_concept(c) intrinsic, also D10, for navigating the instantiation chain). The categorization-chain lookup ensures USD.iso_code resolves even though iso_code is declared on Currency, not on USD directly (D11 field-lookup rule).

D12 — Categorization and partitioning as substrate relations

MLT’s cross-level relations beyond instantiation are categorization (A-108) and partitioning. Both are substrate-level — they’re MLT primitives, not vocabulary-specific concepts. Add two new IR atoms:

#![allow(unused)]
fn main() {
Categorizes { higher_order: u64, base: u64 }     // A-108
Partitions  { higher_order: u64, base: u64 }     // categorizes + complete + disjoint
}

Surface admission via decorators on the higher-order type’s declaration:

@[categorizes(Animal)]
pub category AnimalSpecies { }

@[partitions(Person)]
pub category LifePhasePartition { }

Decorator form aligns with RFD-0040 D7 conventions: one block, before, comma-separated args. The decorator takes one or more concept identifiers naming the base type(s) that the declared higher-order type categorizes or partitions.

The decorators ship in std::mlt — the standard library’s MLT module. Modelers use std::mlt::* to access them. UFO and any other MLT-adopting vocabulary depends on std::mlt; vocabularies that don’t adopt MLT (BFO, DOLCE) don’t import it. The metatype assignments (category, kind, subkind, etc.) remain vocabulary-provided (ufo, bfo, etc.).

The compiler validates categorization at elaboration time:

  • The categorizing type must be of strictly higher order than the categorized type (after D3 order derivation).
  • Each instance of the categorizing type must specialize the categorized type (enforced by CL-1, see D13).

Partitioning extends categorization with two additional semantic claims:

  • Disjointness: no individual is simultaneously an instance of two distinct subtypes that participate in the partition (enforced by CL-2).
  • Completeness: every individual that’s an instance of the base type must be an instance of some subtype in the partition. Completeness is the only MLT primitive that interacts with the closed-world / open-world assumption; this RFD defers completeness enforcement to RFD-0038 (runtime capability advertisement) — runtimes that advertise CWA enforce completeness; OWA runtimes treat the partition as a witness without completeness obligation.

D13 — Cross-level enforcement rules

Four Datalog-style rules ship in the kernel as built-in cross-level inference. They fire automatically when concepts declare Categorizes or Partitions IR atoms. The rules are stratified below the user-rule fixpoint per RFD-0034 composition order.

Predicate split. The rules use distinct predicates for declared vs. derived specialization to avoid stratified-NAF deadlock between CL-4 (constructive) and CL-1 (negative check):

  • declared_subclass(X, Y) — populated only from explicit X <: Y declarations.
  • derived_subclass(X, Y) — populated by CL-4 from categorization.
  • subclass(X, Y) :- declared_subclass(X, Y). and subclass(X, Y) :- derived_subclass(X, Y). — the union, used by queries.

The vault’s original CL-1/CL-4 pair shared a single subclass predicate, which would have made CL-1 vacuous (CL-4 fires first, CL-1’s NAF antecedent is always false). The split fixes this.

CL-4 — Categorization propagation (positive inference):

derived_subclass(?X, ?T1) :-
    categorizes(?T2, ?T1),
    has_type(?X, ?T2).

When X is an instance of T2 and T2 categorizes T1, X specializes T1 (via the derived relation). This is the rule that makes the <: Currency clause unnecessary when USD : CurrencyType and CurrencyType categorizes Currency — CL-4 derives derived_subclass(USD, Currency) automatically. CL-4 runs in the constructive stratum, before any NAF rule.

CL-1 — Categorization-disjointness conflict (A-108 violation form):

violation_categorization(?X, ?T2, ?T1) :-
    categorizes(?T2, ?T1),
    has_type(?X, ?T2),
    declared_disjoint(?X, ?T1).

Fires when CL-4 would derive a specialization but the user has explicitly declared X disjoint from T1 via an explicit disjointness declaration. Reports OE0897 CategorizationViolation as a genuine contradiction (not a missing inference). The declared_disjoint predicate is populated from explicit disjointness declarations.

Note: explicit disjointness syntax is not in the v1 surface — it’s reserved for a future RFD. Until that syntax ships, CL-1 has no firing trigger in v1; the rule structure is present so the diagnostic and rule machinery don’t need to be re-wired when disjointness lands. The NAF-vs-derivation tension is resolved by construction (CL-1 reads declared_disjoint, not subclass).

CL-2 — Partition disjointness:

violation_partition_disjoint(?Ind, ?T3a, ?T3b) :-
    partitions(?T2, ?T1),
    has_type(?T3a, ?T2),
    has_type(?T3b, ?T2),
    ?T3a != ?T3b,
    has_type(?Ind, ?T3a),
    has_type(?Ind, ?T3b).

Reports a OE0898 PartitionDisjointnessViolation when a single individual is classified by two distinct instances of a partition.

CL-3 — Order consistency:

violation_order(?X, ?T) :-
    concept_order(?T, ?N),
    has_type(?X, ?T),
    concept_order(?X, ?M),
    ?M >= ?N.

Reports a OE0899 OrderInconsistency when a concept of order N is instantiated by a concept of order ≥ N.

Wiring (per the vault’s MLT design options evaluation):

  • crates/nous/src/reasoning/encoding.rs — at schema load, emit ground facts:
    • has_type(X, T) for every InstanceOf { instance: X, type_concept: T } IR atom. Both EntityRef::Individual(_) and EntityRef::Concept(_) variants produce has_type facts — this is what lets CL-rules fire on both the ABox (individual-of-concept) and the TBox (concept-of-concept) cases.
    • declared_subclass(X, Y) for every explicit Specializes { sub: X, sup: Y } IR atom (from <: declarations).
    • declared_disjoint(X, Y) for every explicit disjointness declaration (reserved for the future disjointness surface; emits nothing in v1).
    • categorizes(T2, T1) and partitions(T2, T1) for every Categorizes / Partitions IR atom (D12).
    • concept_order(T, N) for every concept whose derived order is N (D3 fixpoint).
  • crates/nous/src/reasoning/rule_gen.rs — generate CL-1 through CL-4 plus the subclass := declared_subclass ∪ derived_subclass union rule.
  • crates/nous/src/reasoning/profile.rs — new CompletionPhase::CrossLevelInference stratum, ordered after schema-fact emission and before user-rule fixpoint.

The existing crates/nous/src/multilevel.rs A-108 enforcement is the prototype that informed this design; it gets retired in favor of the rule-based version which composes with the existing Datalog pipeline.

D14 — Subordination reserved

MLT’s third type-of-type relation is subordination: instances of t1 specialize instances of t2, where t1 and t2 are both higher-order (same order). Formally:

The decorator surface:

@[subordinate_to(VehicleType)]
pub category SportsCarType { }

Reserves the IR atom and decorator name, with no enforcement rule shipped in v1. The domain-pattern census (vault: Orca Multi-Level Domain Requirements) found zero use cases across the six order-2 patterns Orca needs. Future vocabularies that need subordination can introduce the enforcement rule incrementally — the substrate slot is held now to prevent a future syntax conflict.

Rationale

One atom, parameterized. The alternative — adding a sibling InstantiatesConcept atom alongside Instantiates — duplicates pattern-matching everywhere Instantiates is used. Generalizing the LHS to an EntityRef enum makes every pattern-match exhaustive over the cases and lets new variants (statements, reified facts) join later without further duplication.

Order derived, not stamped. Stamping order at declaration creates a maintenance burden: every concept needs an explicit order, every instantiation chain edit risks order drift. Deriving from the graph means correctness by construction. Vocabularies can still assert constraints via @[order(=N)] / @[bounded_order(N)] when they need to lock the level.

Vocabulary-neutral substrate. UFO, BFO, GFO, DOLCE have different opinions about how many levels are admitted, what powertype semantics mean, what the categorization vs instantiation distinction looks like. The substrate provides the relation and the derivation; the vocabularies layer their axioms on top via decorators (@[bounded_order(2)], @[categorizes(...)], etc.). This matches RFD-0002’s foundational-ontology-neutrality principle.

Statement(_) reserved. Higher-order reflection (querying statements about statements, e.g., RDF reification) is a real future case. Reserving the variant now means the IR doesn’t need a second generalization when that surface ships.

Cycle detection lives in the elaboration phase. Lifting it to lowering or kernel execution costs nothing in the common case (no cycles) and avoids a class of footguns. The check runs whether or not bounded-order is asserted.

Categorization and partitioning live in std::mlt, not std::meta or ufo. They are MLT primitives (Carvalho & Almeida 2017) — they originated in MLT and UFO adopted them. Putting them in std::meta would ship unused machinery for non-MLT vocabularies (BFO, DOLCE, GFO don’t have powertypes). Putting them in ufo would force any future MLT-aware vocabulary to depend on UFO or re-declare them. std::mlt honors the actual lineage: MLT is the theory, UFO is one of several vocabularies that adopt it, the stdlib bundles the MLT primitives as their own opt-in module.

The four CL rules ship in the kernel because they’re MLT theorems, not vocabulary choices. A-108 (categorization → specialization) is a theorem of MLT (Carvalho & Almeida 2017); it holds independent of UFO, BFO, DOLCE. Same for partition disjointness, order consistency, and categorization propagation. The kernel applies them universally. Vocabularies that disagree with MLT can opt out per concept via @[mlt_off] (not in v1; reserved if a real use case emerges).

Body semantics make the powertype pattern usable. The field = value form at the kind level + field: Type form at the instance level is what makes the type itself a bearer of data at the kind level — Currency’s iso_code is a property of the kind USD, not of any individual bill. Without this, MLT collapses to “instantiation as a typed relation” without the powertype-bearer structure that UFO ontologies actually use.

Subordination reserved. No domain pattern requires it. Reserving the slot now (decorator name, IR atom) prevents a future syntax conflict and signals to readers of the RFD that the MLT primitive set is complete: instantiation + specialization + categorization + partitioning + subordination, with their respective relations. Future use is additive.

Consequences

Wire types. oxc-protocol adds EntityRef, replaces Instantiates with InstanceOf, and adds the Categorizes, Partitions, and SubordinateTo IR atoms (per D12 and D14). The dual-body content for cross-level instantiation (D11) extends CoreConcept with kind_level_values: BTreeMap<FieldId, Value> alongside the existing fields: BTreeMap<FieldId, FieldDecl>. SDK regeneration via oxc-codegen is mechanical.

Storage. kernel-storage schemas referring to Instantiates in axiom-event bodies migrate. CBOR-encoded bodies for existing events continue to deserialize via a compatibility shim that wraps the old Instantiates shape in InstanceOf { instance: Individual(id), … }. No data migration required.

Lean. New file Schema/MLT.lean. No changes to existing Lean files except adding instantiates : C → C → Prop to the TypeGraph extension in MLT.lean.

crates/nous/src/multilevel.rs retires; cross-level reasoning moves to Datalog. The hand-coded A-108 enforcement is replaced by the four CL rules (D13) emitted into the standard reasoning pipeline. encoding.rs emits the categorizes, partitions, and concept_order facts; rule_gen.rs produces CL-1 through CL-4; profile.rs adds CompletionPhase::CrossLevelInference. Test cases that previously simulated cross-level via the metaxis-only path migrate to using : declarations + @[categorizes(...)] decorators.

Three-layer stdlib split. std::meta ships only the universal substrate primitives (Metatype sealed top, reflection operators meta() / ancestors() / order(), the : and <: operators). std::mlt ships the MLT primitives — the @[categorizes], @[partitions], @[subordinate_to] decorators, the cross-level IR atoms, and the four CL inference rules (D13). Vocabulary-specific metatypes (UFO’s kind, subkind, phase, role, relator, category, mixin, roleMixin; BFO’s universals; DOLCE’s perdurants/endurants) live in their respective packages. Modelers import: use std::mlt::*; use ufo::{kind, category}. Non-MLT vocabularies (BFO, DOLCE) skip std::mlt.

Book. New chapter ch02-10-multi-level-types.md. Covers the powertype pattern, the : operator in declaration heads, derived order, vocabulary constraints (@[bounded_order], @[order(=N)]), the worked example Dog : AnimalSpecies and Husky : DogBreed, and the tier:mlt decidability ladder reservation. The orphaned mlt.ar example becomes a tested showcase.

Examples. argon/examples/lease-story/packages/ufo/src/mlt.ar extends to use the new surface — pub kind GoldenRetriever <: Dog : DogBreed etc. The argufo-consolidation-fixed-kernel/v1 branch can migrate its hand-written ordering rules to derived-order constraints.

Issue tracking. A new issue tracks the Tonto import path; another tracks meta_concept(c) intrinsic if/when needed. Henrique’s MLT diagnosis (the IR audit) is the canonical closure for this work.

Tier ceiling. tier:mlt (per RFD-0040 D-references) is the reserved tier for bounded-order MLT reasoning. The decidability ladder gains this as a separate stratum from tier:fol (full first-order logic) — bounded MLT is decidable; unbounded is not.

Lean extension grows. Beyond the instantiates relation added in D5, the Lean adds categorizes, partitions, and subordinate_to as relations on the TypeGraphMLT structure, along with theorems for the four CL rules: CL-1 soundness (categorization implies specialization), CL-2 soundness (partition disjointness), CL-3 soundness (order monotonicity), CL-4 termination (the propagation rule reaches fixpoint in iterations). These are MLT theorems proper; the proofs follow Carvalho & Almeida (2017) directly.

Historical lineage

  • RFD-0002 (foundational-ontology neutrality) — committed. MLT support stays vocabulary-neutral; no UFO axioms baked in.
  • RFD-0015 (PL idiom over proof-assistant idiom) — committed. The : overload removed by RFD-0040 D5 is what unblocks the : admission this RFD specifies in concept declaration heads.
  • RFD-0034 (composition pipeline) — committed. The new phase_mlt elaboration phase fits within the per-package boundary; cross-package instantiation resolves at ox compose.
  • RFD-0040 (substrate atoms and the explicit-writes principle) — discussion. Commits to the IR atom generalization (D12); this RFD specifies grammar, elaboration, Lean, diagnostics, migration.

This RFD does not supersede any committed RFD.

References

  • Carvalho, V. A., & Almeida, J. P. A. (2017). Multi-level ontology-based conceptual modeling. Data & Knowledge Engineering. Defines the formal MLT primitives (instantiation, specialization, categorization, partitioning, subordination, order).
  • Fonseca, C. M., et al. (2021). Multi-level conceptual modeling: theory, language and application. DKE. Extends MLT with MLT* and orderless types. Orca needs only the stratified MLT fragment (max order = 2).
  • Jeusfeld, M. A., & Neumayr, B. (2016). DeepTelos: multi-level modeling with most general instances. The DeepTelos approach: MLT realized as 5 Datalog rules + 1 integrity constraint (Most General Instance pattern).
  • Jeusfeld, M. A., & Almeida, J. P. A. (2020). Deductive reconstruction of MLT for multi-level modeling. Proves the bridge from DeepTelos’s Datalog primitives to MLT*’s richer vocabulary.
  • Brasileiro, F., et al. (2016). Applying multi-level modeling theory to assess Wikidata. Empirical evidence: 14,320+ anti-pattern instances from level confusion in a major knowledge graph; demonstrates the cost of leaving cross-level enforcement to honor-system.
  • Tonto language (OntoUML DSL): https://github.com/matheuslenke/Tonto
  • Issue #390 (higher-order types epic, priority:medium) — adjacent but orthogonal; this RFD addresses the foundational MLT gap.

Appendix A — Keyword Reference

This appendix enumerates Argon’s keywords. The authoritative source is KEYWORD_TABLE in argon/oxc/src/syntax/kind.rs; everything below is derived from it.

Hard-reserved keywords

These tokens are reserved by the lexer. They cannot appear as plain identifiers anywhere in source. Twenty-seven entries:

enum    rel      let       derive    assert     query    match    mod
use     compute  mutation  if        else       hypothetical with  extend
powertype compose not       unsafe    test       fixture  expect   frame
using   true     false

A few notes:

  • frame and rel are hard-reserved as item declarators (frame Name { … }, rel name(s, t) { … }). They also appear as values of for <kind> / decorator on <Target> clauses (for frame, on rel); the parser remaps the keyword token to an identifier in those positions.
  • with and compose are hard-reserved even though they only appear in narrow contexts (with { … } inside hypothetical, compose <pat> inside structural patterns).
  • powertype is currently hard-reserved with a PowertypeDecl AST node and a parser test. The team is exploring a redesign that lets powertype be modelled with the metatype mechanism + a user package; until that lands the keyword stays reserved (see Appendix D).

pub is not in KEYWORD_TABLE — it lexes as an identifier and the parser recognizes it in declaration-head position as the visibility prefix. Treat it as contextual.

Contextual keywords

These names lex as identifiers; their meaning is determined by parse position. Vocabulary packages are free to declare metatypes whose names collide with most of these (the parser disambiguates by context). The list is large; what follows groups them by where they appear.

Substrate-form heads

  • metaxis, metatype, metarel, decorator — declaration heads for the four substrate forms covered in Chapter 2.1.
  • pub — visibility prefix (described above).

Metatype-keyword identifiers (defined by vocabulary packages, not the language)

These are not built-in. UFO’s prelude.ar declares them as pub metatypes:

kind  subkind  role  phase  category  mixin  rolemixin  phasemixin  relator

BFO’s prelude declares a different set (continuant, occurrent, independent_continuant, …). The token type is not reserved either — a vocabulary package may declare pub metatype type = { … } if it chooses.

for <kind> / decorator-on <Target> qualifiers

type  rel  dec  field  individual  frame

rel and frame are hard-reserved tokens elsewhere; in this position the parser accepts them as kind / target identifiers.

Concept-decl clauses

  • where — refinement predicate clause.
  • as — alias / rename binding.

Metaxis clauses

order  condition

Metarel clauses

source  target  cardinality_default  properties

Decorator clauses

on  semantics  lowers_to  fragment

Powertype clauses

categorizes  partitions

Mutation clauses (inside mutation Name { … })

require  do  retract  emit  return

do is the in-mutation block keyword; it is contextual, not hard-reserved.

Compute clauses (inside compute Name { … })

input  out  ensure  body  impl

Query clauses

group  by  order  limit  asc  desc  from  to

Lifecycle clauses (inside lifecycle { … } blocks on a concept)

lifecycle  phases  transitions  initial  terminal  on  when  brings_about

Diagram clauses (inside diagram "name" { … })

selection  include  exclude  set  show  hide
style  color  label  highlight  group  layer  collapse
layout  direction  align  ensure  encourage
title  description  format  size  box  diamond  connected_by

Reasoning operators

forall  exists  is  unknown  ambiguous  timeout

Allen-interval relations

The thirteen Allen relations are admitted as binary operators in rule bodies and queries:

before        after        meets         met_by
overlaps      overlapped_by  starts        started_by
during        contains     finishes      finished_by
equals

Subtype-test predicates

specializes  generalizes

(Trajectory: these may be renamed is_subtype / is_supertype — see Appendix D.)

Severity-constraint heads

strict  error  warning  info

Used in the [pub] [strict] error|warning|info <Name>(<params>) :- <body> constraint form.

Compiler-built-in attributes

The parser admits attribute applications in two surface forms — #[…] (Rust-style, canonical for test-block annotations) and @[…] (alternate, canonical for rule / derive annotations). Both lower to the same AST shape. The list below is the COMPILER_BUILTIN_ATTRIBUTES set in argon/oxc/src/elaborate/validate.rs; everything else is dispatched through the user-declarable pub decorator registry and emits OE0229 UnknownDecorator if the name does not resolve.

Test-block proof status

#[unproven]   #[assumed]

Both @[…] and bare @<name> / #<name> forms are accepted as alternates. Applied to a test block, they lower to CoreTest.proof_status.

Rule annotations

@[scope(N)]                @[scope(max: N)]
@[budget(N)]               @[budget(heartbeats: N)]
@[complexity(<class>)]
@[monotone]
@[cache]

@[scope(...)] bounds the Kodkod search space for tier:fol rules; @[budget(...)] caps heartbeats for full-FOL execution; @[monotone] asserts monotonicity (the elaborator verifies); @[complexity] is an optional explicit complexity-class annotation; @[cache] requests memoization.

Defeasibility / superiority

@[default]   @[override(<rule>)]   @[defeat(<rule>)]   @[chain(<rule>)]

Used in defeasible-rule networks; see Chapter 5.2.

Theorem marker

@[theorem]

@[theorem] is dispatched through the user-decorator path. The vendored UFO package ships pub decorator theorem() on derive. Application is restricted to derive items; OW0821 fires on misuse.

Directives

Argon’s only top-level directive is #dec(<tier>):

#dec(structural) | #dec(closure) | #dec(expressive) | #dec(recursive)
#dec(fol)        | #dec(modal)   | #dec(mlt)

Both bare-name and tier:-prefixed forms (#dec(tier:closure)) are accepted. Scope: module-head, block, or per-declaration. The strictest tier on the ambient stack wins; rules whose effective tier exceeds the ambient ceiling produce OE0604 TierViolation. unsafe logic { … } injects whole-block tier:fol ambient — the FOL escape hatch.

Reserved alternates

  • <: is the canonical specialization operator. The Unicode (U+2291) is accepted as a typeset alternate.
  • The legacy file extension .ol is accepted with an OW0813 deprecation warning. Use .ar.

Appendix B — Operator Reference

Reserved for the second edition. The exhaustive operator + precedence reference will land here once the upcoming language audit completes.

Until then, a quick overview of what the chapters have introduced.

Arithmetic

+, -, *, /, % — addition, subtraction, multiplication, division, modulo. Bind in the conventional order; parentheses disambiguate.

Comparison

==, !=, <, <=, >, >= — equality, inequality, ordering. Both sides can be expressions over bound variables and constants.

Logical / set membership

not <atom> — negation as failure. ?x in <collection> — set membership at rule-atom position. At expression position, x in xs and x not in xs desugar to <TypeOf(xs)>::contains(xs, x) (and its negation).

Collection operators

The collection-op surface is grounded in Chapter 2.8. All forms desugar at elaboration time to qualified calls into the receiver’s std::collection::* (or std::math::Range) submodule.

TokenFormFixityPrecedenceDesugarExample
.xs.m(args)postfixtightest (binds before any binary op)<TypeOf(xs)>::m(xs, args)b.units.size()
[i]xs[i]postfixtightest<TypeOf(xs)>::at(xs, i) (or Map::get)b.units[0]
[i..j] / [i..] / [..j]slicingpostfixtightest<TypeOf(xs)>::slice(xs, Range::new(i, j))b.units[0..3]
..range literalbinary infixbelow + / -, above comparisonRange::new(lo, hi) (half-open)0..10
..=inclusive range literalbinary infixsame as ..Range::inclusive(lo, hi)0..=9
inmembershipbinary infixsame precedence as comparison (==, <, …)<TypeOf(xs)>::contains(xs, x)p in b.tenants
not innon-membershipbinary infixsame as innot <TypeOf(xs)>::contains(xs, x)p not in b.tenants
[expr for x in xs where pred]comprehensionbracket formbracket-boundedxs.filter(<pred>).map(<expr-as-closure>)[u.number for u in b.units]

Comprehensions reuse the aggregate-binding subgrammar. The where clause is optional. Binder shadowing emits OW2403.

Path

a.b.c — field-access path on a value. ?x.b.c — Datalog-binding-position field-access path on a variable.

Cardinality (inside collection types)

>= N, == N, <= N — at-least / exactly / at-most cardinality clauses. Distinct from comparison operators despite sharing tokens.

Type tests / subtype tests

v: T — match-arm type test (also a rule-atom shape). ?x specializes T — subtype predicate (rule-body only). ?x : T — rule-atom typed-binding form.

box(...) — necessity. diamond(...) — possibility. Rule-atom position.

Allen-interval operators

before, after, meets, met_by, overlaps, overlapped_by, starts, started_by, during, contains, finishes, finished_by, equals — interval-relations algebra binary infix operators.

Quantifiers

forall <var>: <T> [where <body>], exists <var>: <T> [where <body>] — restriction and binding forms; tier-classified.

Aggregation

sum, count, min, max, avg — fold-style aggregates over for var in path [where atom].

Module path

:: — module-qualified path separator. pkg::mod::Item.

Rule arrows

:- — body separator. => — consequence (rules) and arm body (match arms). Context disambiguates.

Membership / subtype-by-pattern

The full set is documented in the per-crate API documentation.

Appendix C — Diagnostic Codes

Argon diagnostics use a structured naming scheme: OE / OW / OI plus four digits. Run ox explain <code> for the long-form explanation of any diagnostic.

The naming scheme:

  • OE (Oxide Error) — the build fails. Codes in the four-digit range; the leading two digits categorise the source area (compiler / runtime / package / kernel / network / …).
  • OW (Oxide Warning) — the build proceeds; the issue is flagged.
  • OI (Oxide Info) — informational hint; nothing is wrong.

ox explain <code> prints the canonical long-form explanation and a fix suggestion. The reference below lists every code shipped today, organised by category.

Parser (OE0001OE0004)

CodeMeaning
OE0001Unexpected token
OE0002Unclosed block
OE0003Invalid decorator syntax
OE0004Invalid character in identifier

Resolution (OE0101OE0103)

CodeMeaning
OE0101Unresolved name
OE0102Ambiguous import
OE0103Cyclic module dependency

Type system + meta-property structural rules (OE/OW/OI 02xx)

CodeMeaning
OE0201Concept used as relation, or vice versa
OE0202Property type mismatch
OE0203Non-exhaustive match or cardinality violation
OE0204Rigid type specializing anti-rigid type
OE0205Role used as concept
OE0206Duplicate declaration
OE0207Mixed-strength rules on same head
OE0208compute call arg-count mismatch
OE0210Recursive compute-call cycle
OE0211derive head’s consequence atoms cannot lower (silent rule drop refused)
OE0212meta(X) argument is not ground at rule time
OE0213expect { diagnostic <code> } references an unknown diagnostic code
OE0214using <name> clause on test references an unresolved frame
OE0215Two composed frames declare the same-named member
OE0216Inline fixture redeclares a frame member
OE0218Frame include chain forms a cycle
OE0219Rule-body aggregate cannot be lowered into the Datalog body
OE0221Relation cardinality has min > max (unsatisfiable range)
OE0222let _ as <handle>: T shadows a binding in the enclosing scope
OE0223Anonymous-type content-hash collision (defensive; astronomically rare)
OW0207Sortal missing Kind ancestor (UFO structural R28)
OW0208Anti-rigid under Category without Kind in path (R24)
OW0209Mixin lacks rigid or anti-rigid subtypes (R25)
OW0210Non-sortal aggregates only one Kind (R30–R31)
OW0211Role has no mediation path to relator (R34)
OW0212Phase has no sibling phase (R35)
OW0213Phase / phasemixin cross-specialization (R32–R33)
OW0214Phasemixin has no Category ancestor (R36)
OW0215@requires / @requires_all deprecated; use where clause
OI0216box() tautology — rigid type necessarily satisfies
OE0217box() impossible — anti-rigid cannot satisfy necessity
OI0218diamond() tautology — anti-rigid trivially possible
OW0219kind specializes kind — consider subkind
OE0220Inner doc comment not at module scope
OE0224Test assertion arity mismatch (assert Concept(individual) vs assert role(subj, obj))

Metatype substrate (OE 022x023x)

User-declared metaxis / metatype / metarel / decorator declarations and their application sites.

CodeMeaning
OE0225Unknown metarel — application or path resolution to a nonexistent metarel
OE0226pub rel endpoints don’t satisfy the bridging metarel’s source / target constraints
OE0227metaxis for <kind> qualifier names an unknown declaration kind, or a metaxis is used in a metarel context (or vice versa)
OE0228metaxis <Name> { … } lacks the required for <kind> qualifier
OE0229@[<name>] does not resolve to a pub decorator
OE0230Decorator application argument count doesn’t match declared parameter count
OE0231Decorator’s on <Target> doesn’t match the application site
OE0232lowers_to: <hint> clause names a recognizer shape that doesn’t pattern-match the body
OE0233Decorator’s explicit fragment: <tier> contradicts the inferred body tier
OE0234Decorator argument value doesn’t satisfy the parameter type
OE0235compute has no body and no impl rust(...) binding

Collections (OE/OW 24xx)

Diagnostics for the collection substrate covered in Chapter 2.8.

CodeMeaning
OE2401Type-constructor arity mismatch — Set[T, U], Map[T], Optional[T, U]. Set / List / Optional / Range take 1 type argument; Map takes 2.
OE2402Collection-op expression form parsed cleanly but full UFCS desugaring and runtime evaluation are not yet wired (tracked by the E3 epic). Workaround: write the qualified-call form (std::collection::List::size(xs)) or rebuild via direct field assignments.
OE2403Collection element-type mismatch — List[Nat].append("foo"), [1, "two", true]. Argon collections are homogeneous; the expected element type comes from the receiver’s declared type.
OE2404Index expression’s type isn’t Nat (for List) or doesn’t match the declared key type (for Map::get).
OE2405Slice bounds invalid — xs[5..2], negative literal bound. Checked at elaboration for literal bounds; dynamic bounds are checked at runtime.
OE2406Unordered collection indexed — s[i] where s: Set[T]. Set elements have no positional identity; use Set::contains for membership tests or convert to a List.
OE2407Higher-order argument’s signature does not match the operation’s expected closure shape (xs.map(f) where f has the wrong arity or wrong argument types).
OE2408Higher-tier collection op used inside a context that admits only tier:closure or below — typically xs.map(f) inside a derive rule body. Lift the transformation into a pub compute or rewrite using closure-tier ops.
OW2401Optional unwrapped without a clear fallback — bare .unwrap() or .unwrap_or whose fallback is itself Optional[T]. Provide a concrete T-typed fallback or guard with is_some.
OW2402[T; <= 1] is a singleton-bounded set; Optional[T] (sugared T?) expresses the same intent more directly and integrates with the Optional op surface. Quickfix suggestion, not a hard error.
OW2403Comprehension binder shadows a let binding or parameter in the enclosing scope. Rename the binder to silence. Matches the fresh-binder convention Datalog rule bodies already use.

Reasoning / DL (OE/OW 03xx)

CodeMeaning
OE0301Unsatisfiable concept (TBox-level: contradictory axioms make the concept impossible to instantiate)
OW0302Redundant axiom
OW0303Decidability warning
OE0304Incoherent TBox
OW0305Unsatisfiable concept (warning-severity twin of OE0301; what ox check --reason actually emits when reasoning over imported / under-specified ontologies, so contradictions in the source don’t gate the check)
OE0306Disjointness violation — ABox contradiction at evaluation time (an individual is asserted as instance of two disjoint concepts; distinct from OE0301 which is a TBox contradiction)
OW0307Hypothetical block evaluation error — override target shape, missing individual, missing property, or unsupported value type

Defeasibility (OE 04xx)

CodeMeaning
OE0401Ambiguous defeat
OE0402Unreachable defeater
OE0403Circular superiority relation

Stratification (OE 05xx)

CodeMeaning
OE0501Circular negation
OE0502Unstratifiable program

Constraint (OE 06xx)

CodeMeaning
OE0601Unsatisfiable constraint
OE0603metatype literal type doesn’t match the axis’s declared value_type
OE0604#dec(<tier>) enforcement: declaration’s required tier exceeds ambient
OE0605Unknown metatype name (four-variant hint: not-a-metatype-but-found / available-not-imported / multi-package-available / not-declared)
OE0606metaxis <name> : <T> where { <pred> } refinement violated by a typed-axis literal

Package / module (OE 07xx)

CodeMeaning
OE0701Private concept in public context
OE0702Orphan axiom without extend block
OE0703Private item imported across module boundary
OE0704Conflicting explicit imports of the same name
OE0705Ambiguous glob imports at use site
OE0706Import conflicts with local declaration
OE0707Package is not a direct dependency

Decidability tiers (OE/OW/OI 08xx)

CodeMeaning
OI0801Non-polynomial reasoning (informational)
OE0802Variable-binding quantifier outside unsafe block
OW0803Bounded FOL rule without scope annotation
OI0804Derive-rule decidability classification
OE0805Asserted complexity contradicts classified tier
OW0806Tier-6 rule without heartbeat budget
OE0807Monotone assertion violated by non-monotone atoms
OI0808unsafe logic rule gated
OE0809Tier-6 rule outside unsafe logic block
OE0810Invalid key in @[budget] annotation
OE0811Invalid key in @[scope] annotation
OE0812Unknown complexity class
OW0813Deprecated .ol extension; rename to .ar
OW0821@[theorem] applied to a non-derive item
OW0822@[theorem] declared multiple times on the same derive
OW0823#[unproven] / #[assumed] applied to a non-test item

External format warnings (XW 08xx)

CodeMeaning
XW0841Unknown OntoUML stereotype preserved as metadata
XW0842OntoUML element name disambiguated on import
XW0843OntoUML aggregation preserved as metadata
XW0844OntoUML relation arity reduced to binary
XW0845OntoUML cardinality letter-shorthand (n/m) preserved as marker for byte-stable round-trip (Argon’s grammar fully expresses the semantics; the marker only pins the source’s lexical form)
XW0846OntoUML GeneralizationSet emitted as Argon structural block (partition / disjoint / complete)
XW0847OntoUML restrictedTo natures preserved as metadata
XW0850OntoUML class resolved against registry foundation package (informational; local declaration dropped, use <pkg>::* emitted)
XW0851OntoUML source-package collapsed to root because slug matches registry

Export / re-export (OE 09xx)

CodeMeaning
OE0901Cannot export private symbol
OE0902Cannot re-export private item
OE0903Re-export cycle detected
OE0904Implicit-head derive shadows a declared concept

Reasoning runtime (RW 00xx)

Runtime-side diagnostics emitted by the reasoning engine when atom evaluation cannot proceed.

CodeMeaning
RW0001Atom references an unbound variable
RW0002Predicate name resolves to neither a concept, registered compute, nor a derive-rule head
RW0003Atom argument type doesn’t match the resolved predicate’s declared arity / type

Perspectival composition — modules, standpoints, defeat order (OE/OW 10xx)

CodeMeaning
OE1001Cyclic standpoint dependency
OE1002Unknown defeat-ordering strategy
OE1003Unknown configuration strategy
OE1004Module references undeclared standpoint
OE1005Duplicate module name
OW1006extend block breaks conservative extension
OE1007Cross-module superiority cycle
OW1008CWA concept in OWA module
OW1009Standpoint DAG has no root
OE1010Bridge rule references private concept
OE1011Module discriminator collision
OW1012Tree-shaking excluded a required module
OE1013Unresolvable cross-module defeat target

Diagrams (OE/OW 11xx)

CodeMeaning
OE1101Unknown concept in diagram
OE1102Unknown relation in diagram
OE1103Unknown module in diagram
OE1104Concept not accessible in diagram (missing use)
OE1110Unknown filter property in diagram predicate
OE1111Type mismatch in diagram predicate
OE1112Non-enumerable property in color by
OE1120Concept not in diagram selection
OE1121Contradictory layout constraints
OE1122Concept is both included and excluded in the same diagram
OE1123Concept belongs to two named groups in the same diagram
OE1124collapse target is excluded from the diagram
OW1130Empty diagram
OE1131Unknown base diagram in from clause
OE1132Unknown named selection

Narrowing / refinement (OI 12xx)

CodeMeaning
OI1201Redundant check — already narrowed by prior atom

Query / mutation / compute (OE 14xx)

CodeMeaning
OE1401Mutation require clause contains a side-effect expression
OE1402retract pattern statically matches multiple assertions
OE1403Mutation return type does not match declared return type
OE1404Query output shape does not match return-type annotation
OE1405Construct exceeds module max_tier cap
OE1406Inline-compute body references an unresolved name
OE1407Mutation precondition evaluates to non-Bool
OE1408Invalid @[cached] placement
OE1409Query head references an unbound variable
OE1410Object literal missing required field
OE1411Object literal has unknown field
OE1412emit event type is not a sortal
OE1413Inline-compute result type does not match out { T }
OE1414let type is non-instantiable (non-sortal mixin)
OE1415List literal in non-collection-field position

Metatype (OE 15xx)

CodeMeaning
OE1501Unresolved metatype name

Inline-compute runtime (OE 17xx)

CodeMeaning
OE1700Type mismatch in compute body
OE1701Arithmetic error in compute body
OE1702Missing required compute input
OE1703Compute budget exhausted
OE1704Compute scope exceeded
OE1705Body max_tier exceeds module cap
OE1706Compute path could not be resolved
OE1707Compute variable not bound
OE1708Unknown predicate in compute body
OE1709Postcondition failed

Lifecycle (OE/OW 18xx)

CodeMeaning
OE1801Lifecycle declares transitions but has no initial phase
OE1802Lifecycle declares more than one initial phase
OW1803Lifecycle has no terminal phase
OW1804Lifecycle phase is unreachable from any initial phase
OW1805Lifecycle phase has no path to any terminal phase
OE1806Duplicate phase name in a lifecycle { … } block
OE1807Transition references an unknown phase name
OW1808Lifecycle phase data fields not yet elaborated (placeholder)

Looking up a code

$ ox explain OE0204

The full explanation includes the rule violated, a typical example that triggers it, and the canonical fix. ox explain is the authoritative surface — this appendix is a navigation aid, not a replacement.

Appendix D — Migration Notes

Argon is in the middle of a measured surface redesign. The body of the book teaches today’s argon-v0.4.x syntax, which is what the toolchain accepts and what the UFO team needs to ship. This appendix is the lever that keeps the book honest as the redesign rolls out: every chapter that flags a migration points here, and every row here carries the trajectory from “what you write today” to “what the redesigned form will look like.”

The redesign is captured-direction, not yet ratified. The team is exploring the moves below; today’s syntax remains supported, and migrations will land additively with a window during which both forms are accepted.

How to read this appendix

Each row has three columns:

  • Today — what the toolchain accepts at argon-v0.4.x. This is what the chapters teach.
  • Trajectory — the shape the team is exploring. Captured-direction, subject to refinement.
  • Where discussed — the chapter that flags the migration in passing.

Where a column reads “open”, the design is in active discussion and the chapter does not commit to a target form.

The migration map

TodayTrajectoryWhere discussed
lifecycle { phases, transitions } inside a concept bodystatemachine X { states { … } transitions { … } } as a top-level item, desugaring to enum + rule + Transition eventCh3.2
Source -> Target { brings_about { Event } } (lifecycle effect)Source -> Target :- guard => Transition(from, to) (rule with explicit event consequence)Ch3.2
phase metatype with implicit sibling-disjointness via partitionphase metatype with explicit axioms { siblings_disjoint, siblings_complete } blockCh2.2, Ch3.2
partition Animal { Mammal, Bird, Fish } (item-decl axiom)Implicit on the subkind metatype via axioms { siblings_disjoint }; explicit-decl form retained for subset-partitionsCh2.2
specializes(?x, T) (rule-body subtype predicate)is_subtype(?x, T)Ch2.6
generalizes(T, ?x)is_supertype(T, ?x)Ch2.6
assert P(...) :- body => error("…")[pub] rule P(...) :- body => Error("…") (rule-shape with explicit Error event)Ch2.4
derive H :- body => escalate("...", args)[pub] rule H :- body => Escalate("...", args) (rule-shape with user-defined event)Ch2.4
severity-constraint declarationsFolded into rule-shape with Error / Warn / Info event consequencesCh2.4
package-constraint declarationsFolded into rule-shape with Error event consequenceCh2.4
mutation X(...) { do { stmts } emit Event return v }mutation X(...) { stmts; emit Event; return v } (raw statements; do keyword removed)Ch2.5
Money (language-core primitive)std::finance::Money (in the std::finance package, language-core stays domain-neutral)Ch2.3, Ch2.6
correlative_pair / part_whole / power_type (declarable item-forms)std::patterns::* (user-defined patterns, not language-core declarables)n/a (deferred to Part 5)
powertype (hard-reserved keyword) — still parsed and reserved today, with a PowertypeDecl AST and parser surfaceModelled via the metatype mechanism + user package; the keyword reservation comes off once the user-package replacement landsn/a (deferred to Part 5)
hypothetical { … } (block)hypo { … } (alias added; both spellings accepted)Ch5 (later)
@[ontouml_id] / @[layout] / @[ontouml_stereotype] (compiler attributes)Moved out of the compiler — handled by the OntoUML package and the diagram-DSL respectivelyCh1.1, Ch3.4
mediates(...) / relator(...) / has_mediation_path(...) (constraint atoms)UFO-specific — moved to ufo::* package definitionsn/a (deferred to Part 5)
no_kind_in_path(...)Generalized to no_metatype_in_path(M, ...); UFO sugar no_kind_in_path becomes a one-line user aliasn/a (deferred to Part 5)

Five locked design moves

The redesign organizes around five themes. Each row above belongs to one or more of them.

  1. Three deliberate body shapes. Argon ends up with three principled body shapes: :- pred-body for declarative predicate items (rules, asserts, queries, transitions); { stmts } for imperative blocks (mutations, compute Form-2, tests, hypothetical, unsafe); = expr for single-expression definitions (compute Form-1, FFI bindings, let-bindings).
  2. Rule-shape unification. derive, assert, lifecycle transitions, severity/package constraints — all reduce to [pub] rule X :- body [=> consequence] with implicit consequences for the common cases. derive keeps its sugar for the most-common implicit => FactDerived(H); assert keeps its sugar for implicit => Error("…").
  3. Events as the universal effect type. Consequences become events; events are the universal effect mechanism across all three computational modes. Mutations emit events imperatively; rules emit events when their bodies fire; computes do not emit events at all (preserving referential transparency).
  4. State-machine sugar. lifecycle collapses into statemachine X { states { … } transitions { … } }, which is sugar over enum + rule + Transition event. The four lifecycle-related grammar productions fold into one sugar wrapper.
  5. Group axioms via metatype-axiom integration. Sibling-disjointness, sibling-completeness, and partition constraints move from declaration-site keywords to metatype-level axioms { … } blocks. The metatype is the constraint handle.

What is not changing

For clarity:

  • The 6-layer architecture (lexical / syntactic / semantic / evaluation / package / toolchain).
  • The Cargo-shape package system (ox.toml, lockfile, registry, prelude pattern, direct-deps rule).
  • The toolchain layer (ox, oxup, ox-lsp, ~/.argon/ state directory).
  • The three-engine reasoning pipeline.
  • The decidability tier ladder.
  • Bitemporal axes, standpoints, fork semantics.
  • The mechanically-verified core (refinement-fragment decidability, occurrence-typing soundness, meta-property fixpoint termination).
  • Three-valued semantics under OWA.
  • Module structure and two-tier visibility.
  • Doc-comment conventions (/// outer, //! inner, Markdown content).

These are the stable structural commitments. The redesign is surface-level and item-system-level; the underlying architecture stays.

Reading the rest of the book

Where a chapter says “the team is exploring …” in a callout, the corresponding row above is what they are exploring. The body of each chapter teaches today’s syntax and shows code that compiles and runs at argon-v0.4.x. When migrations land, this appendix flips: each row’s “today” column moves to a “former” column, the “trajectory” column becomes the canonical syntax, and the chapter’s callout becomes a “historical note” pointing back at what changed.

That is the Book’s discipline. The model on the page works today. The trajectory is visible. The migrations land additively, with a window. No reader has to chase a moving target.