Progressive Enhancement — one surface, additive features¶
This is the conceptual overview of the single most important architectural decision in Trails. The normative text lives in ADR-0021; this page is the short, prose-shaped explanation that the vision, the README, and the architecture and design-spec documents point at.
The one-line version¶
Trails exposes one surface. Start with nodes and edges; add types, shapes, and OWL when you need them. Code written on day one keeps working on day 365 — new features are never rewrites.
Why "progressive" and not "tiered"¶
An earlier draft (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. Writing the supporting amendments surfaced that the framing
added more complexity than it removed:
- Users had to pick a tier at project start.
- Cedar needed three entity-type prefixes with "deliberately verbose" cross-tier policies.
- The provenance graph was always RDF anyway, so T1 was a veneer.
- Three namespaces to remember and bridge.
Rails solved this the other way: one ActiveRecord surface, with validations, associations, and callbacks as additive features. Users never pick a "Rails tier." Trails does the same.
The four rungs¶
Every Trails app sits on some subset of the same ladder. All four
rungs use the same @capability decorator, the same ctx, the same
invoke():
Rung 1 — labels only¶
from trails import capability
@capability
def create_note(ctx, title: str, body: str) -> dict:
iri = ctx.kg.add({"title": title, "body": body})
return {"id": iri}
No IRI design, no shape, no ontology. The framework still emits PROV-O provenance, enforces cost envelopes, and logs Cedar policy decisions — it just does so against a label-only node. Works on a Tuesday afternoon; works forever.
Rung 2 — @node_type for JSON-Schema validation¶
from trails import capability, node_type
@node_type("Note", fields={"title": str, "body": str})
class Note:
pass
@capability
def create_note(ctx, title: str, body: str) -> dict:
n = Note(title=title, body=body)
ctx.kg.add(n)
return {"id": n.id}
The Rung-1 capability above is still valid in the same app. The only
change is that writes to Note-labelled nodes now get JSON-Schema
validation on the field types.
Rung 3 — @shape for closed-world SHACL¶
from trails import capability, shape, predicate
@shape
class Note:
title: str = predicate("schema:name", min_length=1)
body: str = predicate("schema:text")
Now Cedar, SHACL, and PROV-O can all reason about the shape IRI and
its predicates. trails onto export materialises the shape into
ontology/generated.ttl. Existing Rung-1 and Rung-2 code is
untouched.
Rung 4 — OWL for reasoning and cross-system interop¶
# ontology/vocab.ttl
@prefix : <https://myapp.example/ns/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
:PublishedNote rdfs:subClassOf :Note .
:Note a owl:Class .
No handler change. The reasoner feature-detects owl:Class +
rdfs:subClassOf in the loaded ontology and begins materialising
entailments on the affected named graphs (see
ADR-0004). Query with
SPARQL; publish the ontology for other systems to consume.
Strongest-available-type matching¶
Under the hood, Cedar, PROV-O, and SHACL all inspect each entity and act on whatever typing is present: RDF class > SHACL shape > JSON-Schema type > label. One code path, feature-detected at dispatch time. This is the rule formalised by ADR-0022.
A single Cedar policy can write resource is Note and match on all
four rungs without three separate entity-type prefixes:
- Rung 1:
resource.labels contains "Note"matches. - Rung 2:
resource.type == "Note"matches on the node-type name. - Rung 3:
resource.shape == <myapp:Note>matches on the SHACL IRI. - Rung 4:
resource.rdf_type == <myapp:Note>matches on the RDF class.
Policy authors write one rule; the framework resolves whichever
typing the resource actually carries. The same principle applies to
SHACL (it only runs where a shape is declared) and to PROV-O (it
emits trails:typing hints based on what's present).
What this buys you¶
- No decision at project start. Write the handler; feature-decide
later. The demo loop (
trails new blog && trails server) drops you straight onto Rung 1. - Additions, not rewrites. Adding
@node_typedoes not change storage. Adding@shapedoes not change storage. Adding OWL does not change storage. Existing data keeps working. - Opt-in trust stack. Cedar policy, DIDs + VCs, PROV-O always on, SHACL, OWL reasoning, consent receipts, ACT/biscuit capability tokens, replayable traces, cost envelopes — all first-class, all opt-in. You don't pay for them until you reach for them, and you don't re-architect when you do.
- One module to learn.
trails.capability,trails.node_type,trails.shape,trails.policy,trails.kgonctx. No tier-split namespaces, no "choose your tier" doc.
What it costs¶
- Harder to explain in a tweet. "Pick T1 for speed, T3 for compliance" is catchy. "One surface, opt into what you need" is slightly less so — and significantly easier to use.
- Feature-detection everywhere. Cedar, SHACL, and PROV-O each carry a strongest-available-type resolver. The kernel pays the branch cost; the author never sees it.
- No "freeze this capability at labels-only" declaration. If you want to guarantee a capability never grows past labels, you rely on conventional review (and on Cedar policies that reject writes carrying shape IRIs you didn't expect). There is no descriptor flag for it.
Where to read next¶
- Vision — the narrative framing of the same ladder, written for a first-time reader.
docs/tutorials/growing-your-kg-app.md— the hands-on walkthrough that takes you rung-by-rung from an empty project to a reasoning-enabled KG app.- ADR-0021 — the normative decision record this page summarises, with its full consequences, alternatives considered, and migration plan.
- ADR-0022 — the strongest-available-type rule at the policy layer.
- 02-architecture.md — where each rung is implemented in the kernel + surface split.
- 03-design-spec.md §3.3.1 — the four rungs side by side in API form.