ADR-0021: Progressive enhancement, not tiered surfaces¶
- Status: Accepted (2026-04-14)
- Date: 2026-04-14
- Supersedes: ADR-0020 (Tiered KG surface)
- Superseded by: —
Context¶
ADR-0020 proposed three explicit tiers (T1 plain KG, T2 typed KG, T3
full semweb) with separate module namespaces (trails.kg,
trails.kg.types, trails) and a tier field on every capability.
Drafting the supporting amendments surfaced that this framing adds more complexity than it removes:
- ADR-0005a (tier field) forces users to pick a tier at authoring time.
- ADR-0006a introduces three Cedar entity-type prefixes
(
Trails::Label::,Trails::Type::,Trails::Class::) and declares cross-tier policies "deliberately verbose." - ADR-0009a admits the tier is a veneer: the provenance graph is
always T3 RDF regardless of surface tier, and
trails trace --rawpunctures the abstraction. - Three module namespaces to remember and bridge.
- Decision friction at project start: users must read a "choose your tier" guide before writing their first capability.
The stated goal for Trails was "Rails for agentic KG apps — create apps fast, with or without semweb." Rails achieves that by avoiding the kind of upfront decision that tiered surfaces require. ActiveRecord has one surface; validations, associations, callbacks are additive. Users never pick a "Rails tier."
Tiered surfaces invert the Rails analogy at the exact point where it matters most.
Decision¶
Trails exposes one surface. Features are additive. Users never pick a tier.
Concrete surface:
- Start with nodes, edges, properties.
trails.kg.node(labels=["Evidence"], properties={...})andtrails.kg.edge(...)are the entry points. No RDF concepts visible. Works immediately. - Add typing when you want validation.
@node_type("Evidence", fields={"source": str, "confidence": float})enables JSON-Schema validation on writes to that label. Additive — existing code untouched. - Add SHACL when you want closed-world validation.
@shape(existing decorator) works unchanged. Additive. - Add OWL when you want reasoning or interop. Declare
owl:Classandrdfs:subClassOftriples in your ontology. Additive. - Every layer uses "the strongest typing available." Cedar, PROV-O, SHACL inspect each entity and act on whatever it has — labels only, or JSON-Schema types, or RDF classes. One code path, feature-detected.
No tier field. No three Cedar prefixes. No tier-specific provenance
rules. No "choose your tier" doc. Code written on day one works on day
365 — features you add are strict additions, not mode switches.
Consequences¶
Positive
- No decision at project start. Users write code, add features when they need them.
- Same
trailsmodule surface whether the app uses labels-only or full OWL. No mental context switch between tiers. - Cedar / PROV-O / SHACL all unified behind "strongest available type" matching. One code path.
- Feature addition IS the migration. Adding
@node_typedoesn't change storage; adding@shapedoesn't change storage; adding OWL doesn't change storage. Existing data keeps working. - Docs story: "Start here. Grow your KG app by adding features."
Negative
- Users who want to guarantee "this capability stays simple" cannot
declare it at the manifest level. Mitigation: opt-out flags already
exist on
@capability(prov_level,policy_required=false). - The addressable-market framing from ADR-0020 is still true, but can no longer be pitched as "pick T1 for speed, T3 for compliance" — the framing becomes "one surface, opt into what you need." Slightly harder to explain in a tweet, significantly easier to use.
Neutral
- Storage model unchanged: one Oxigraph store, feature-detection per entity. Same as ADR-0020's substrate, without the tier veneer.
- Provenance remains always-on per ADR-0009. Provenance graph uses
PROV-O IRIs. A user who runs
trails trace --rawsees RDF — same as today, same as under ADR-0020. This ADR does not promise to hide RDF; it promises that users don't have to opt into RDF to use the framework.
Relationship to other ADRs¶
| ADR | Impact |
|---|---|
| ADR-0001 (Rust kernel + Python surface) | Unchanged. |
| ADR-0002 (Python-first shapes) | Unchanged. @shape is one of the additive features. |
| ADR-0004 (Query-time reasoning) | Unchanged. Opt-in remains opt-in. |
| ADR-0005 (Rich capability manifest) | Unchanged. No tier field added. |
| ADR-0005a (Proposed amendment: tier field) | Withdrawn. Not applicable under progressive enhancement. |
| ADR-0006 (Cedar policy) | Unchanged contract. Extend Cedar entity typing to also match on labels and JSON-Schema types as "strongest available" — single Trails::Resource::* prefix, multi-typed matching. Documented in follow-on ADR if needed. |
| ADR-0006a (Proposed amendment: three Cedar prefixes) | Withdrawn. |
| ADR-0009 (Provenance always on) | Unchanged. PROV-O emits label / json-schema-type / rdf-type hints based on what's present. No tier-specific rules. |
| ADR-0009a (Proposed amendment: non-RDF provenance) | Withdrawn. The substrate-is-always-RDF position in 0009a was correct; the tier-specific typing hints it introduced are unnecessary — feature detection handles it at one call site. |
| ADR-0017 (ActiveGraph ORM) | Unchanged. The ORM IS the progressive surface. |
| ADR-0020 (Tiered KG surface) | Superseded by this ADR. |
Alternatives considered¶
- Ship ADR-0020 tiers as proposed. Rejected. Adds a decision that Rails explicitly avoided; requires three amendments (0005a/0006a/0009a) that together re-complicate what the tiers were supposed to simplify.
- Status quo before ADR-0020 (semweb-only). Rejected. The goal of serving "KG apps with or without semweb" still stands; progressive enhancement achieves that goal without the tier framing.
- Split into two frameworks. Rejected (same reason as in ADR-0020).
- Progressive enhancement with a runtime "strictness level" knob. Rejected. That is a tier by another name; adds the same decision friction.
Migration plan (roadmap impact)¶
M11 reframes from "tiered KG surface" to "Progressive KG surface and ergonomic defaults." Concrete work items:
trails.kg.node()/trails.kg.edge()/trails.kg.match()helpers — label-first authoring, no RDF type required. Lowers to triples via ADR-0003 IRI minting.@node_typedecorator — JSON-Schema typing, additive to@shape.- Feature-detection in Cedar, PROV-O, SHACL layers: read entity typing at dispatch time, choose strongest-available matcher.
- Docs: replace the tier-chooser narrative with a "Growing your KG
app" tutorial. Start with
trails.kg.node. Add types. Add shapes. Add OWL. One path. - No Cedar prefix migration. No provenance-rule migration. No module renames.
ADR-0005a, ADR-0006a, ADR-0009a are Withdrawn (never merged). Prior drafts exist in git reflog for prose mining; do not revive as amendments.
Open questions¶
- Should
@node_typeand@shapebe separate decorators or one unified decorator that dispatches on whether the argument is a JSON Schema or a SHACL shape? Recommendation: separate. SHACL implies RDF vocabulary commitment; JSON Schema does not. Keeping them distinct preserves the "you only see RDF if you opt in" property. - Does
trails.kg.node()emit PROV-O on creation, or only when reached via@capabilitydispatch? Recommendation: per ADR-0009, only at capability boundaries — rawtrails.kgcalls outside a capability context are treated as seed / REPL usage, no provenance unless the caller opts in. - What about the
CapabilityDescriptorstrongest-type declaration — should the descriptor surface "this capability consumes labels only" vs "consumes typed nodes" as metadata for admin UI / docs generation? Recommendation: derive from the registered handler's inputs at registration time; do not require authors to declare. - Does this ADR's "one surface" imply we ever deprecate
@shapein favour of a unified typing decorator? No.@shapestays. New@node_typeis additive. Deprecation is out of scope.