RFD-0031 — Concept-reference endpoints on pub metarel
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:
- Metatype-keyword match —
source: relatoraccepts any concept declared with therelatormetatype keyword. Last-segment string compare againstconcept.metatype. - Axis-value match —
source: endurantaccepts any concept whose metatype’s axis profile includes(nature_source, endurant). Resolution: walkengine.axes_map(), find the axis declaringendurantas 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-classifyAspectwith 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:inheresInfromAspect → ConcreteIndividualto 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:
- metatype-keyword match (unchanged)
- axis-value match (unchanged)
- concept-reference subtype match (new) —
constraint_lastis the local name of a registeredpub <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). - fallback: emit
OE0226with 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; helpersfind_concept_by_local_nameandis_subtype_or_self(BFS overconcept.superswith cycle guard).oxc/src/diagnostics/rendering.rs— newrelation_endpoint_subtype_mismatchconstructor forOE0226.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_namereturns the first byBTreeMapiteration 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.