RFD 0027 — The meta-property plane: axis bindings, catalog tiers, value-position resolution, and substrate-neutral modifiers
- State: committed — implemented end-to-end (S0 #276, S1+S2 #283, S3 #287, S4 = this arc’s final PR; bugs #242/#243/#229 closed)
- Opened: 2026-06-11
- Decides: how metaxis declarations and metatype axis bindings become real — validated at
elaboration, persisted on the wire, materialized as catalog relations, and queryable in rule
bodies (
meta(t).rigidity == rigidity::anti_rigid); one unified name-resolution pass for value positions in rule bodies (axis values, enum constants, individuals, qualified names — closing a family of silent mis-evaluations); metatype-tier$meta/$iofrows (settles #229); aValue::Symbolcarrier shared with enum constants (fixing enum type-erasure); and the substrate-neutral modifiersabstractandfixedthat replace every compiler read of user axis vocabulary (the OE1327"sortality"/"sortal"magic strings, the §7.5 dynamic-iof gate, §10.2 static-discharge rigidity, RP-004 VT-persistence defaults). Settles audit findings ufo-01/ufo-02/ufo-03/ufo-04 and issue #148’s constants half; motivated by the first external user collision (Tiago’s ArgUFO overlay, 2026-06-11). PR #209 parked citing a planned “RFD 0027 ambient-vocabulary cleanup” that was never written; this RFD takes the number and completes that program.
Prior state (verified at aa7a7d0e): metatype axis bindings parse and are then dropped —
lower_metatype_decl emits axes: Vec::new() (oxc-instantiate/src/lower.rs:2870; same for
metarels at :2897); the metaxis value set {anti_rigid < semi_rigid < rigid} is persisted
nowhere (MetaxisDeclBody.value_type hardcoded Null, lower.rs:2844-2845); there is zero
validation — pub metatype weird = { nonexistent_axis::bogus_value }; passes ox check and
ox build clean. $meta/$iof materialize individual-tier rows only
(oxc-runtime/src/lib.rs:3923-4040, gap note at :4012-4014); meta(Person) == kind silently
derives nothing (#229). In rule bodies, any multi-segment path lowers to a variable named by the
joined path (atom_lower.rs:1649-1655 path_term), so rigidity::anti_rigid trips OE1303 as an
“unbound variable” — and the same resolver hole makes enum constants work on comparison-RHS only,
individuals in predicate-argument position silently match everything, and qualified predicate
atoms silently derive zero rows. The trait coverage gate decides instantiability by token-scraping
the literal strings "sortality"/"sortal" from metatype declarations
(oxc-driver/src/trait_conformance.rs:398-399) — an unqualified match any module’s axis can
satisfy, and a one-letter typo silently disables the gate. The book Note (04-meta-calculus.md:44)
claims the language “ships” the rigidity/sortality/identity_provision axes.
Question
pub metaxis and the metatype binding form exist so that vocabularies can classify their own
types along their own dimensions — the meta-calculus is atom 1 of the language. The book already
commits to the semantics: axis values “are not labels — the reasoner reads them”
(crash-course §1), and the storage chapter commits axis values as (axis, target, value)
meta_property events (“No UFO axis is a column”, 18-storage.md:7). The Lean storage model
carries them (Storage/AxiomBody.lean:96-132). The implementation honors none of it.
The first external user hit the gap within a day of the trait arc shipping. Tiago’s ArgUFO overlay — UFO built as user-space vocabulary, exactly what the no-ambient-vocabulary doctrine (#208, #213) prescribes — writes:
pub derive isAntiRigid(t: TypeRef) :-
meta(t).rigidity == rigidity::anti_rigid
and gets OE1303 naming rigidity::anti_rigid as an unbound variable. No phrasing works: the
constant doesn’t resolve, the metatype tier isn’t materialized, and the binding isn’t even in the
artifact. Meanwhile the compiler itself does read axis vocabulary — by magic string — in the one
place it needs an answer (OE1327 instantiability), which is the same bug class the §3.4 gate
(#213) eliminated one level down.
Two questions, then. (1) What is the correct end-to-end design for declared meta-properties — declaration, validation, persistence, materialization, query surface? (2) Where is the boundary between user axis vocabulary and substrate semantics — what may the compiler read?
Design lineage
The vault records both questions being answered before, halfway:
argon-ontology-neutrality/rigidity-removal-storage(sub-RFC, accepted 2026-04-30): tore the built-inRigidity/SortalDependenceenums out of the legacy kernel — “the largest remaining UFO leak in nous” — replacing them with a generic per-targetmeta_properties: BTreeMap<AxisId, Vec<MetaValue>>, withMetaValue::Symbolinterned per(axis, value-name)pair. The(axis, target, value)event shape descends from this. But the cluster kept legacy helpers (is_rigid()/is_sortal()/provides_identity()) that read the axes by string — the storage went neutral, the semantics did not. The OE1327 magic strings are that compromise carried forward.- Beyond-OntoClean research (vault scratch, 2026-04-18): the useful meta-property space is open-ended — Fine’s essence-vs-modality refinement of rigidity, unity as a parameterized family (topological/morphological/functional/intentional), Mizoguchi role-ness and context-dependence, Lowe individuation-dependence, BFO’s dependence typology. No finite axis list a substrate ships can be right. Rigidity/sortality are formal-ontology-neutral (UFO/BFO/DOLCE all use them) but they are still ontological commitments — and the substrate does not need them; it needs their operational shadows.
This RFD completes the neutrality program: the 04-30 sub-RFC neutralized the data; this RFD neutralizes the semantics.
D1 — One value-position name-resolution pass
Every term position in a rule body (predicate arguments, comparison operands both sides, compute operands, head arguments) resolves names through a single pass, in priority order:
- Bound rule variable (appears in a positive body atom or is a head parameter).
- Enum constant —
Status::Active, qualified or imported. - Axis value —
rigidity::anti_rigid, where the first segment resolves to a visiblepub metaxisand the second to a value in its declared domain. - Type reference — single-segment and qualified type names (
TypeRefconstant). - Declared individual — bare or qualified (
alice,people::alice).
Refusals are loud:
- A multi-segment path that resolves to nothing is a hard error (new OE), never a variable.
- A bare identifier used both as a rule variable and resolving to a declared constant is a hard
ambiguity error naming both candidates (the lesson of Rust’s
bindings_with_variant_name, promoted from lint to refusal): rename the variable or qualify the constant. - The existing
path :: IdentmetaEq sugar (07-rules.md:128) applies only when the left side is a bound term, after constant resolution fails to claim the path — the resolution order above is the documented disambiguation forA::B(already flagged ambiguous at07-rules.md:155).
This single pass closes five verified symptoms at once: axis values as OE1303 “variables”
(Tiago); enum constants failing on comparison-LHS while working on RHS; x == alice refused
(#148); knows(x, carol) silently treating carol as a wildcard and over-deriving (the worst
member — silent wrong results, zero diagnostics); and qualified individuals refused. Qualified
predicate atoms deriving zero rows (rule compile keys joined paths, catalog keys short names) is
the predicate-position sibling fixed in the same slice.
D2 — Declarations become real: wire + validation
MetaxisDeclBodypersists the domain:unordered(values),chain(values)(declaration order is the order; the book’sa < bchains), ortyped(TypeExpr, refinement?). The LeanAxisDomainshape (MetaCalculus/Axis.lean:38-48) already models this; the Lean storage mirror does not and gains it.MetatypeDeclBody.axes/MetarelDeclBody.axesare populated with resolved bindings: axis = qualifiedNameRef(post-#213 discipline — the wire does not carry bare text), value = symbol (enumerated domains, bound with::) or literal (typed domains, bound with=, e.g.weight = 1.0;::vs=follows the existing grammar and the RFD 0017 note that=belongs to typed domains).- Validation at elaboration (new diagnostics, meta-calculus OE19xx range): unknown axis;
value not in the axis’s declared domain (or literal fails the typed domain’s refinement —
the OE0606 precedent); axis tier mismatch (
for typeaxes bind only on metatypes,for relonly on metarels); duplicate axis in one declaration (OE1903, already specified inWellformed.lean). - Per-target assertions ride the existing
MetaPropertyevent ((axis, target, value)— the channel MLT’s#[order(N)]already emits), now actually consumed (D3). - Lean storage mirrors for
MetaxisDeclBody/MetatypeDeclBody/AxisBindingare added; the Rust doc-comment claiming “MirrorsArgon.Storage.AxiomBody.AxisBinding” currently cites a declaration that has never existed and becomes true.
D3 — Catalog tiers: metatype-tier $meta/$iof and the $axis relation (settles #229)
- The runtime
Moduleindexesconcept_id → metatype_idat load (ConceptDeclBody.metatype_idis already resolved on the wire since #213; it currently has zero non-test readers). $metaand$iofgain metatype-tier rows:(type, metatype)for every declared type, plus the tower the book’s worked examples promise (meta(Person) == kind,meta(kind) == Metatype,04-meta-calculus.md:95-99). Catalog-closed ⇒ NAF-safe, the same justification as$implements. This also unblocks MLT’s CL-rules (iofover types) as a side effect.- A new catalog relation
$axis(target, axis, value)materializes the effective axis assignments: metatype-level bindings (D2) unioned with per-targetMetaPropertyassertions, per-target assertions taking precedence, functional per(target, axis)— a conflicting pair is a load-time error, not a silent choice. - Valence: single-valued per
(target, axis). The legacy design admittedVec<MetaValue>(the vault’smodemetatype binding twonaturevalues); in the IS/CAN/NOT calculus that case is correctly “the metatype does not determine the value” — a CAN, i.e. no binding — rather than two simultaneous ISes. Multi-valued axes are a recorded non-decision: revisit with a concrete consumer, against the vault lineage. Cross-axisconditionclauses from the legacy engine likewise stay out until someone needs them.
D4 — The query surface
-
meta(t)over a type evaluates via the metatype-tier$metajoin (it already lowers correctly; the rows now exist). -
Sort-directed projection.
base.namewherebaseis reflective: aTypeRef-sorted base resolvesnameas a declared field on the type (the documentedmeta(bar).xwalk-up,05-constructs.md:158-173— unchanged); aMetatype-sorted base resolvesnameas a visible metaxis and lowers to a$axisjoin. No overload collision: fields live on types, axes live on metatypes. Tiago’s rule works as written:pub derive isAntiRigid(t: TypeRef) :- meta(t).rigidity == rigidity::anti_rigidtbinds via the catalog$metajoin;.rigidityjoins$axis; the right side is a constant (D1).meta(t).rigidity == rwithrfree bindsr. An unbound axis (the metatype binds nothing for it) yields no row — ordinary Datalog absence. -
Chain comparisons. For
chaindomains,</<=/>/>=compare by declared position (crash-course §1 promises exactly this). Comparing symbols of different owners, or of anunordereddomain, is a loud compile-time error where statically known and a loud runtime refusal otherwise — never an enum-variant-order fallback. -
Type-tests over reflective sorts are catalog atoms.
t : TypeRefranges over all declared types;m : Metatypeover all declared metatypes (the predicate forms of the promisedextent(TypeRef)/extent(Metatype),04-meta-calculus.md:109). Both bind positively, catalog-closed. This legalizes Tiago’s second attempt rather than refusing it.x : Entitystays refused for now, with a diagnostic that names the supported sorts and the catalog-atom binding idiom (its extent spans individuals ∪ types and needs its own design).
D5 — Value::Symbol: one carrier for axis values and enum constants
A new runtime value sort:
#![allow(unused)]
fn main() {
Value::Symbol { owner: NameRef, name: SmolStr, ord: Option<u32> }
}
- Identity is
(owner, name)— the interned-(axis, value-name)design from the 04-30 sub-RFC.ordis populated fromchaindomains at build (None for unordered/enum owners); equality ignores it; ordered comparison requiresSomeon both sides and equal owners, else refuses loudly. - Enum constants migrate to the same carrier. Today’s enum runtime carrier is a type-erased
CBOR tag (
{tag: "<variant>"}) under which two enums sharing a variant name compare equal — a live defect this RFD fixes rather than duplicates. Pre-release, no artifact compatibility is owed; the corpus regenerates. - Full sort cost, paid in full (no hollow rendering): CBOR encoding in the event log; CLI
render_valuerendersowner::name; serve JSON renders a typed envelope with the qualified owner path (the #182 pattern);oxc-genemits branded TS types per owner with exact (de)serialization; Lean wire mirror + drift coverage.
D6 — Substrate-neutral modifiers: abstract and fixed
The compiler never reads a user axis name. Anywhere. The substrate’s three genuine needs reduce to two ontology-neutral, PL-precedented bits, declared as modifiers:
| Substrate consumer | Today | Becomes |
|---|---|---|
| OE1327 trait-coverage instantiability | "sortality"=="sortal" token scrape | abstract — no direct instances; abstract types are exempt from impl-coverage obligations |
§7.5 runtime re-classification gate (insert/delete iof on an existing individual) | rigidity::anti_rigid per the book Note; unenforced | fixed — classification decided at construction; insert/delete iof against a fixed-introduced type refuses loudly |
| §10.2 static check discharge; RP-004 valid-time iof persistence defaults | rigidity (Lean Rigidity inductive; design docs) | the same fixed bit — fixed ⇒ membership constant ⇒ static discharge sound, VT-persistent |
- Placement. Both modifiers attach to metatype declarations (
pub abstract fixed metatype category = { … };) — the metatype is the behavior bundle — andabstractis additionally allowed per-type (pub abstract type Vehicle { … }, the UML/PL convention). No override semantics: a type introduced by an abstract metatype is abstract, full stop. - Polarity: dynamic by default,
fixedis the opt-in restriction. The §7.5 gate governs re-classification of existing individuals, not construction; for a data-systems language, membership churn is the normal case and is today’s behavior throughout the examples. The restriction is the constraint you declare — the same posture ascheck. (The keyword isfixed;sealedwas rejected as colliding with the closed-hierarchy meaning in Kotlin/Scala/C#.) - Consequences for the book. The Note at
04-meta-calculus.md:44(“the language ships therigidity/sortality/identity_provisionaxes”) is deleted. Anti-rigid admission “through the axis assignment” is rewritten: admission comes from the absence offixed;identity_provisionhas zero substrate consumers and becomes pure user vocabulary like everything else. ArgUFO writespub abstract fixed metatype category = { sortality::non_sortal, rigidity::rigid };— the modifiers carry the behavior, the bindings carry their ontology, and importing a package can never change mutation semantics because its author named an axisrigidity. StaticDischarge.lean’sRigidityinductive (:38-50) is re-grounded as the Kripke-semantics justification offixed(rigid designation = fixed classification), not as a privileged axis.
D7 — Lean obligations
Surface settled (this RFD); per the workflow table the substrate semantics go Lean-first:
- Canonical model = the ternary axis relation
(target, axis, value)with per-(target, axis)functionality — the(axis,value)-point instantiation ofState C Acannot express chains or typed domains; the relation reading covers all three domains uniformly. This resolves the deferred wiring note inIsCanNot.lean:48-50(the A-instantiation becomes a derived view of the relation). - Chain-order semantics for symbol comparison; refusal semantics for cross-owner/unordered comparison.
- Catalog-closure of
$meta(both tiers),$axis, and the reflective-sort extents, with the NAF-safety corollary — the #221 (Catalog.conformant_iff) pattern, including a decidable checker and non-vacuity witnesses on a concrete catalog. - Decidability: axis atoms, metatype-tier reflection atoms, and reflective-sort type-tests are D1-tier (catalog joins); the classifier admits them.
- Modifier semantics:
fixed⇒ the mutation-boundary refusal theorem (noiofdelta on fixed-introduced types in any successful mutation — extends the RFD 0015/0019 mutation semantics);abstract⇒ the OE1327 oracle reads a wire flag (decidable, no string predicate); the static-discharge soundness hypothesis re-stated overfixed. - Wire mirrors (D2) under
@[language_interface], acknowledging the known drift-gate limitation that structures are prose-aligned — the mirrors are still written, and the lying comment dies.
D8 — Diagnostics inventory
New codes in the meta-calculus range (numbers assigned at implementation against the live
catalog; the trait arc’s OE1322→OE1326 renumbering is the cautionary precedent): unknown axis in
binding; axis value not in domain / literal fails typed-domain refinement; axis tier mismatch;
unresolvable multi-segment path in rule value position; variable/constant ambiguity; cross-owner
or unordered symbol comparison; insert/delete iof against fixed; instantiating an abstract
type; Entity type-test refusal (help names the supported idioms). Amended help texts: OE1303’s
“variable(s)” wording when the offender is an unresolved path (point at the path, not at range
restriction); OE0223 on reflective sorts (now legal per D4 — the remaining refusal case is
Entity).
Alternatives considered
- std-shipped well-known axes (
std::meta::rigidity, …): rejected. Re-creates the #208/#213 leak one level up — a blessed package whose names carry compiler semantics — and the beyond-OntoClean record shows no finite list is right. The book Note that implied this dies. - Per-value marker attributes on axis declarations (
#[dynamic_classification]onanti_rigid): rejected — the substrate bit is per-value in that encoding, the declaration site becomes clutter, and importing a vocabulary still changes substrate behavior. - Keep the magic strings: rejected; it is the audited bug (ufo-01), behaviorally confirmed — a typo silently disables a soundness gate.
- Multi-valued axes (
Vec<MetaValue>): deferred with the K3 ‘can’ reading as the principled alternative; see D3. - Static-by-default polarity for classification mutability: rejected for data-systems
pragmatics — it makes the common case ceremonial and breaks every existing example; the
ontological reading is recovered exactly by declaring
fixed. - A separate axis-value sort distinct from enum constants: rejected; two symbol-like sorts with different equality and rendering rules is incoherence by construction, and the enum carrier needed the fix anyway.
Sequencing
- S0 — D1 resolution pass + the two filed silent bugs (individual-as-wildcard, qualified predicates). Independent of everything else; converts silent-wrong to loud immediately.
- S1 — D2 wire + validation + Lean storage mirrors.
- S2 — D3 catalog tiers (
$meta/$iofmetatype tier,$axis), closing #229. - S3 — D4 query surface + D5
Value::Symbol(including the enum migration), full rendering chain. - S4 — D6 modifiers + consumer migration (OE1327, §7.5 gate, static discharge, VT defaults)
- the book rewrite (Note deletion, §7.5, §12.4, crash-course).
- Lean (D7) leads each slice’s semantics per the workflow table; the mutation-gate theorem lands with S4.
Each slice is loud-complete on its own: no slice ships a parsed-but-inert surface.
Relationship to existing issues
- #229 — settled by D3 (metatype-tier rows) + D4 (the classifier admits catalog-level bodies).
- #148 — the constants half is settled by D1; the equality-binding half stands.
- #209’s “RFD 0027” parking reference — this document.
- Audit register: ufo-01 (magic strings → D6), ufo-02 (hollow wire → D2/D3), ufo-03 (phantom Lean mirrors → D2), ufo-04 (the boundary ruling → D6), gs-09 (axis sugar diagnostics → D1/D8), plus the enum type-erasure finding (→ D5).
- #150 — the new codes land catalog-first; no raw-string emissions.