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 — what to do instead.
- Decision guides — picking between alternatives.
- Error recovery — diagnostics that catch these mistakes.