Skip to content

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 --raw punctures 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:

  1. Start with nodes, edges, properties. trails.kg.node(labels=["Evidence"], properties={...}) and trails.kg.edge(...) are the entry points. No RDF concepts visible. Works immediately.
  2. 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.
  3. Add SHACL when you want closed-world validation. @shape (existing decorator) works unchanged. Additive.
  4. Add OWL when you want reasoning or interop. Declare owl:Class and rdfs:subClassOf triples in your ontology. Additive.
  5. 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 trails module 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_type doesn't change storage; adding @shape doesn'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 --raw sees 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

  1. 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.
  2. 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.
  3. Split into two frameworks. Rejected (same reason as in ADR-0020).
  4. 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:

  1. 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.
  2. @node_type decorator — JSON-Schema typing, additive to @shape.
  3. Feature-detection in Cedar, PROV-O, SHACL layers: read entity typing at dispatch time, choose strongest-available matcher.
  4. 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.
  5. 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_type and @shape be 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 @capability dispatch? Recommendation: per ADR-0009, only at capability boundaries — raw trails.kg calls outside a capability context are treated as seed / REPL usage, no provenance unless the caller opts in.
  • What about the CapabilityDescriptor strongest-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 @shape in favour of a unified typing decorator? No. @shape stays. New @node_type is additive. Deprecation is out of scope.