# USL — Unified Speccing Language, v0.9 (draft)

**Status:** Draft proposal.
**Date:** 2026-05-03
**Audience:** Implementers, tooling teams, and AI-agent runtimes that read and update USL specs.

---

## What USL is

USL is a graph data model and file format for software specifications. A USL repository is a collection of typed **nodes** (a Decision, a Component, a Schema) connected by typed **edges** (`depends-on`, `implements`, `governs`). Every commit updates the graph; every release publishes a snapshot.

The design optimizes, in priority order, for AI-agent comprehension, AI-agent updates, human comprehension, scoping, version control, performance, and automated graph analyses (§1.1). Core is deliberately small — 18 node kinds, 9 edge kinds, 8 invariants. Everything narrower (threat models, BPMN processes, FHIR resources) lives in extension vocabularies.

A complete worked example covering Core + Governance + Embeddings ships at the end of this document.

---

## Terms used throughout

These terms are load-bearing across the spec. One-sentence definitions; full treatment is in the linked section. The Glossary at the end is the canonical reference.

| Term | One-line definition | Where defined |
|---|---|---|
| **Node** | A single typed specification with a uniform shape. | §2, §4 |
| **Edge** | A typed, attributed relation between two nodes. | §2, §5 |
| **Vocabulary** | A versioned bundle that registers kinds, edges, predicates, and schemas. | §2, §6 |
| **URI** | A node's stable identity: `usl://<vocab>/<namespace>/<name>`. | §3 |
| **Kind** | The node's type, e.g. `core:Decision`, `acme:CustomThing`. | §4, §7 |
| **Scope** | The containing node (BoundedContext or root) that bounds visibility. | §8 |
| **Lifecycle** | Derived governance state: `proposed → accepted → deprecated → retired → tombstoned`. | §2.2, §9 |
| **Realization** | Derived operational state: `none/planned/built/running/decommissioned/unknown`. Not monotone. | §2.2, §9.4 |
| **Attestation** | A signed claim about a node, carrying a DSSE (Dead Simple Signing Envelope) payload. | §G.2 |
| **Predicate** | A node defining the body schema for a class of Attestations. | §G.2 |
| **Principal** | Any actor — human, agent, group, or service-account. | §7 |
| **BoundedContext** | DDD (Domain-Driven Design) primary scoping unit; a region of consistent terminology. | §7, §8 |
| **ChangeSet** | An optional atomic-approval bundle. Most repos use a substrate adapter (e.g., git PRs) instead. | §G.4 |
| **Module** | A conformance unit: Core (mandatory) plus optional Governance, Federation, Embeddings. | §0 |

---

## 0. Module structure and conformance

USL v0.9 is defined in four modules. Implementations declare which modules they support; validators run only invariants belonging to active modules.

| Module | Required? | What it adds |
|---|---|---|
| **Core** (§1–§17) | Yes | Nodes, edges, identity, lifecycle, realization, schema validation, structural invariants, query, CLI/MCP (Model Context Protocol) baseline. The minimum to call something a USL repo. |
| **Governance** (§G) | Optional | Approval workflows, signed Attestations, Predicates, Roles, ChangeSets. Enable when you need auditable change control or multi-party sign-off. |
| **Federation** (§F) | Optional | Cross-repo references, content-hash pinning, lockfiles, sovereignty, trust. Enable when one repo needs to import nodes from another. |
| **Embeddings** (§E) | Optional | The `usl.search` procedure contract and a normative text projection for vector retrieval. Enable when you want semantic search over the graph. |

A repo's `usl.yaml` declares the active modules:

```yaml
usl_version: "0.9"
modules: [core]                              # plus governance, federation, embeddings as needed
```

Two registries live outside this spec:

- **Projection Registry** at `https://w3id.org/usl/v0.9/projections/registry.md` — bidirectional adapters between USL kinds and external native formats (Mermaid, OpenAPI, BPMN, etc.).
- **Standard Extensions Bundle** at `https://w3id.org/usl/v0.9/extensions/standard.md` — common-but-not-universal kinds (`Threat`, `Control`, `Process`, `Workflow`, `SLO`, `Persona`, etc.). Opt in via `usl.yaml`.

---

## Module 1 — Core

## 1. Priorities

USL Core defines 18 kinds, 9 edges, and 8 invariants. Domain-specific vocabularies — UI design systems, AI agent platforms, healthcare, finance, manufacturing — are extension vocabularies, registered in a Vocabulary node and namespaced. Core never grows to accommodate them.

USL optimizes, in strict priority order:

1. **AI agent comprehension.** A small core surface so an agent encountering a new repository can hold the type system in working memory.
2. **AI agent updates.** Patch-friendly file format; mechanical conflict resolution; structural invariants are O(N+E).
3. **Human comprehension.** Markdown bodies for prose-heavy kinds; YAML for machine kinds; one file per node.
4. **Scoping.** Bounded contexts and tenants are first-class. Visibility is explicit.
5. **Version control.** Files in a versioned content store; the substrate (git, etc.) is the source of truth.
6. **Performance.** In-memory at small scale; graph-DB delegation at large scale; file format unchanged.
7. **Automated graph analyses.** Orphan, dead-end, cycle, gateway-exhaustiveness, and monotone-derived-state invariants ship in the Core validator pack.

These priorities are normative (binding on conformant implementations). Any change that improves a lower priority at the expense of a higher one requires explicit justification.

**Use case.** USL is a native authoring format. Users (and AI agents acting on their behalf) author USL nodes directly, query them, evolve them. Projection to existing standards (Mermaid, OpenAPI, BPMN, etc.) exists for handoff to tooling that doesn't yet speak USL — typically one-way export — and lives in the Projection Registry, not this spec.

**Humans and agents are first-class peers.** All actors are `core:Principal` nodes with a `type` discriminator (`human | agent | group | service-account`). Core treats all four uniformly: any authorized Principal can perform any action. Repositories needing human-only enforcement for sensitive operations enable Governance (§G), which adds the policy keys for that.

## 2. Core concepts

A USL graph is composed of three things and three only.

**Node.** A single, identifiable specification — a Decision, a Component, a Schema. Every node has the same uniform shape (§4) regardless of `kind`. No fields are conditionally permitted by kind on the base; sub-types add fields under a typed `spec` block only.

**Edge.** A typed, attributed relation between two nodes. The edge `kind` is drawn from the well-known taxonomy (§5) or a registered extension. Edges carry their own metadata (confidence, validity window, attribution).

**Vocabulary.** A versioned, owned bundle that registers node kinds, edge kinds, predicate types, and field schemas. The core vocabulary is implicitly loaded; extension vocabularies are explicit nodes (`kind: Vocabulary`) that any repository can author or import.

Everything else — files, directories, indices, query results, embeddings, attestations, predicate definitions — is either a node, an edge, a vocabulary, or a derivation of one of those.

### 2.1 The two universal axes

Two derived state values appear on nearly every node and drive most queries. They are introduced here so later sections can refer to them freely.

**`lifecycle`** — governance state. Values: `proposed`, `accepted`, `deprecated`, `retired`, `tombstoned`. Monotone forward (no backward transitions). Computed from signed Attestations (§G.2). When Governance is not active, every node is `proposed` by default.

**`realization`** — observed operational state. Values: `none`, `planned`, `built`, `running`, `decommissioned`, `unknown`. **Not monotone** — a service may go `running → decommissioned → built → running` over its life. Computed from `RealizationUpdate` Attestations.

The two are independent. The audit-critical case is `lifecycle = retired AND realization = running` — code still alive after its spec was retired. Full treatment of derivation, value semantics, and time-travel is in §9.

## 3. Identity

USL uses a single, stable URI form.

```
uri := "usl://" <vocab> "/" <namespace> "/" <name>
```

- `<vocab>` — vocabulary the node belongs to (`core`, `fhir`, `acme`). For nodes imported from another repo via Federation (§F), this is the parent repo's identifier.
- `<namespace>` — organizational scope (`checkout`, `billing`).
- `<name>` — kebab-case slug (`0042-payment-strategy`).

Examples:

- `usl://core/checkout/0042-payment-strategy`
- `usl://core/billing/charge-engine`
- `usl://fhir/clinic-x/patient-123`
- `usl://acme.platform/billing/charge-policy`  (Federation-imported from `acme.platform`)

**Kind is not part of the URI.** A node's kind is read from its `kind` field. A node's kind may change between versions through supersession without changing identity.

A node's URI is its identity. Once `accepted`, the URI never changes. A node that needs a different name is *superseded* by a new node with the new name; both nodes coexist in history; references migrate through the `supersedes` edge (§5).

**Aliases.** Nodes MAY declare additional URIs in the `aliases` field. Aliases resolve through the same resolver and resolve to the same node. Aliases never cross repositories.

**Versions.** A version is part of the node's `version` field, not the URI. References that need to pin a version use the form `<uri>@<version>`:

```
usl://core/auth/user-credential
usl://core/auth/user-credential@2.1.0
usl://core/auth/user-credential@sha256:abc123...
```

Omit `@<version>` to resolve to the latest non-retired version.

**`$ref` resolution inside embedded JSON Schema or OpenAPI bodies follows the same scheme.** Within a single file, fragment references (`#/components/schemas/X`) work as in JSON Schema; they are local and do not cross node boundaries.

**Tombstones.** A node may be tombstoned by a `Tombstone` Attestation (§G.2). A tombstoned node's URI resolves to a tombstone record (kind, version, hash of last content, tombstone reason); the body and structured fields are not returned. References to a tombstoned node produce a validation error (I-08, §13.2). Tombstoning is the only mechanism that removes content from the resolver — `retired` keeps content visible.

## 4. The canonical node shape

Every USL node, regardless of `kind`, carries the same base. **No base fields are conditionally permitted by kind.** Sub-types add fields under a typed `spec` block; they do not change anything below.

### 4.1 Required fields

| Field | Type | Description |
|---|---|---|
| `uri` | URI | The node's identity (§3). |
| `kind` | string | Vocabulary-qualified kind (`core:Decision`, `fhir:Patient`, `acme:CustomThing`). |
| `version` | string | Version string. One format per kind (§9.2). |
| `scope` | URI | URI of the containing scope node — a `BoundedContext`, `Tenant` (extension), or the global root (§8). |
| `created_at` | RFC 3339 | Creation timestamp. |
| `updated_at` | RFC 3339 | Last update timestamp. |

### 4.2 Optional fields

| Field | Type | Description |
|---|---|---|
| `description` | string | Short prose summary. Required when `lifecycle = accepted` (validator I-01, §13.2). |
| `tags` | string[] | Free-form labels. The reserved tag `experimental` excludes the node from the `--canonical` query view. |
| `body` | string | Long-form content. Markdown by default; YAML/JSON for machine-only kinds. |
| `spec` | object | Kind-specific structured payload. |
| `provenance` | Provenance | Origin/build/source trail (§10). |
| `aliases` | URI[] | Additional URIs that resolve to this node. |
| `extensions` | object | Namespace-prefixed extension fields (`acme:priority`). |
| `visibility` | enum | `public | internal | private`. Used by Federation (§F); defaults to `public`. |

### 4.3 Derived fields

The following are NOT authored. They are computed from signed Attestations (§G.2) and surfaced on read by `usl read` and `usl query`. Implementations MAY persist them as denormalized cache so long as cache and Attestations agree (validator I-04).

**Core-only repos.** Without the Governance module loaded, no Attestations exist; every node's derived `lifecycle` returns `proposed`, `realization` returns `unknown`, and `owners` returns the empty set (or whatever a `references {owned-by}` edge declares — that mechanism remains available without Governance). Invariant I-01 (description-on-accepted) is dormant in such repos because no node ever reaches `accepted`. A repo that wants `accepted` lifecycle, audited approvals, or non-trivial quorum MUST enable Governance. This is intentional: Core is the data model; the workflow that advances state is Governance's job.

| Field | Type | Computed from |
|---|---|---|
| `lifecycle` | enum | Latest of `Approval`, `Withdrawal`, or `Tombstone` Attestations on this node. Default `proposed`. |
| `realization` | enum | Latest `RealizationUpdate` Attestation on this node. Default `unknown`. |
| `owners` | URI[] | All `Principal` URIs reached by outbound `references` edges from this node with `attributes.relationship: owned-by`. |

Querying current state always returns the derived value. Writing to a derived field is an error.

The full table of "what does this combination mean?" — including the audit-critical `retired-but-running` case — lives in §9 with the rest of the lifecycle/realization treatment.

### 4.4 Localization

Localization is out of scope for Core. The `description` and `body` fields hold text in the repository's `primary_lang` (declared in `usl.yaml`, default `en`). Repositories needing per-locale variants store sibling files (`name.es.md`, `name.ja.md`); the resolver returns the file matching the consumer's locale, falling back to `primary_lang`.

### 4.5 The `description` field is normative for retrieval

`description` is the primary input to vector retrieval (§E). Authors should write descriptions that are dense, unambiguous, and complete on their own — not "see body for details." Invariant I-01 (§13.2) requires `description` on every `accepted` node; the predicate is unconditional but never matches in Core-only repos because no node reaches `accepted` without Governance. The base JSON Schema (§17.1) does not encode the conditional because `lifecycle` is derived, not authored.

### 4.6 Contracts vs. artifacts

`API`, `Endpoint`, and `Schema` describe contracts, not artifacts; they don't run themselves. Their realization is the realization of the producing or implementing Component. The derived predicate `running(api)` (§15.1) walks to the implementing Component and reads its realization.

## 5. Edge taxonomy

USL Core ships **9 well-known edge kinds**. Anything outside this set goes in extension vocabularies under a namespace prefix (`acme:cross-team-dependency`).

| Edge | Direction | Purpose |
|---|---|---|
| `contains` | parent → child | Containment. Inverse `part-of` is computed, not authored. |
| `depends-on` | consumer → producer | Hard dependency. Carries `attributes.dependency_type`. |
| `implements` | implementation → contract | Strong claim: source IS the realization of target. |
| `traces-to` | source → target | Loose traceability. "This exists because of that." |
| `supersedes` | new → old | Replacement. The successor's lifecycle moving to `accepted` automatically marks the old as superseded. |
| `references` | A → B | Loose pointer. Carries `attributes.relationship` for typed-but-not-canonical cases. |
| `extends` | A → B | Inheritance. Carries `attributes.merge: deep | reference`. |
| `governs` | constraint → constrained | Carries `attributes.relationship: enforces | mitigates | constrains`. |
| `evidence-for` | source → subject | Signed claims. Carries `attributes.evidence_kind`. |

### 5.1 Choosing the right edge

A short decision guide for authors. When in doubt, prefer the weaker edge.

- **Is A the running implementation of B?** Use `implements`. A Component implements an API; a Component emits an Event-spec.
- **Does A merely point at B's existence (audit, citation, "this exists because of that")?** Use `traces-to`.
- **Does A constrain B?** Use `governs` with the appropriate `relationship` (`enforces`, `mitigates`, `constrains`). Direction is constraint → constrained.
- **Is A owned by B (declared inline on A, the owned thing)?** Use `references` with `attributes.relationship: owned-by`. Direction is owned → owner. The derived `owners(n)` predicate (§15.1) walks these edges.
- **Is A a typed-but-not-canonical pointer (DDD context-map values, citation kinds, `must-pass-in`)?** Use `references` with `attributes.relationship`.
- **Is A a subtype, lineage parent, or instance of B?** Use `extends`. Set `attributes.merge: deep` for effective-view computation; `merge: reference` for lineage-only.
- **Is A a signed claim about B?** Use `evidence-for` with `attributes.evidence_kind`.

**Lineage and instance-of relationships** use `extends`.

**Deployment** is recorded as evidence: a `RealizationUpdate` Attestation carries the Environment URI and validity window, and the resolver follows its `evidence-for` edge with `evidence_kind: deployment`.

**Process-modeling edges** (`compensates`, `triggers`) are BPMN/CMMN-specific (Business Process Model and Notation; Case Management Model and Notation) and live in the Standard Extensions Bundle's process-modeling vocabulary.

**Owners and aliases.** Aliases are a field, queried directly. Ownership is an edge (`references` with `attributes.relationship: owned-by`, authored on the owned node, pointing at the owning Principal); the `owners` field on a node is a derived projection (§4.3) over those edges. Authoring inline on the owned thing matches how reviewers read ownership.

**DDD context-map relationships** (shared-kernel-with, customer-of, supplier-of, conformist-to, anti-corruption-from, open-host-of, published-language-of) are expressed as `references` edges with `attributes.relationship` carrying the DDD term.

### 5.2 Edge attributes

Every edge carries the following canonical fields. The set is closed; extension vocabularies place additional keys under `attributes`.

| Field | Type | Required | Notes |
|---|---|---|---|
| `kind` | string | yes | One of the 9 well-known edges or a vocabulary-prefixed extension. |
| `target` | URI | yes | The target node's URI. |
| `confidence` | number | no | 0..1; default 1.0. |
| `valid_from` | datetime | no | When this edge becomes effective. Default: source node's `created_at`. |
| `valid_to` | datetime or null | no | When this edge ceases. Null = open-ended. |
| `source` | URI | no | Where the assertion came from (Document, RFC, audit). |
| `attributes` | object | no | Edge-kind-specific or extension-specific keys (see §5.3). |

To sign an edge, attach an Attestation node whose `evidence-for` edge points at the edge's source-target pair. There is no per-edge `signed_by` field.

### 5.3 Edge attribute keys

Each edge defines its own attribute schema. Bare unknown attribute keys on an edge are validation errors per §6.3.

| Edge | Attribute | Type | Notes |
|---|---|---|---|
| `depends-on` | `dependency_type` | enum | `runtime | build | dev | optional | peer` |
| `implements` | (none required) | — | Semantics are uniform; no sub-discriminator. |
| `traces-to` | (none required) | — | Semantics are uniform; no sub-discriminator. |
| `references` | `relationship` | string | Closed per-vocabulary. Core defines: `must-pass-in`, `owned-by` (direction owned → owner; powers the derived `owners(n)` predicate), plus DDD context-map values. |
| `extends` | `merge` | enum | `deep | reference` (default `reference`) |
| `governs` | `relationship` | enum | `enforces | mitigates | constrains` |
| `evidence-for` | `evidence_kind` | enum | `attests | evaluates | validates | observes | deployment | revocation` |
| `evidence-for` | `effective_at` | datetime | When the asserted fact became active (distinct from `valid_from`, which scopes the edge itself). |

Extension vocabularies place additional keys under `attributes` with their vocabulary prefix (e.g., `attributes.acme:cost_center`).

**Canonical edge example:**

```yaml
- kind: governs
  target: usl://core/checkout/payment-service
  attributes:
    relationship: enforces
  confidence: 0.95
  valid_from: 2026-01-01T00:00:00Z
  valid_to: null
  source: usl://core/audit/2026-q1-review
```

### 5.4 Inline vs. hoisted edges

Edges are inline by default on the source node:

```yaml
relations:
  - kind: depends-on
    target: usl://core/billing/charge-engine
  - kind: implements
    target: usl://core/analytics/order-events-api
```

For high-fan-out cases (a Vocabulary with thousands of `extends` instances, a compliance crosswalk with thousands of `references`), edges may be hoisted into separate files under `edges/<source-uri-hash>.yaml`:

```yaml
# edges/sha256-abc123.yaml
source: usl://core/iso/iso-4217-currencies
relations:
  - kind: extends
    target: usl://core/iso/usd
    attributes: { merge: reference }
  # ... thousands more
```

Tools normalize at load: when both a node file and a corresponding hoisted file exist, edges from both are merged. Round-trip writes preserve whichever representation the author chose unless `usl normalize` is run.

### 5.5 The default-canonical query view

A query returns nodes where `lifecycle ≠ tombstoned` by default. To restrict further, callers pass explicit modifiers:

- `--canonical` returns nodes where `lifecycle ∈ {accepted, deprecated}` AND `tags` does not include `experimental`.
- `--accepted-only` returns nodes where `lifecycle = accepted`.
- `--include-tombstones` includes tombstoned URIs as their tombstone records.

The default returns everything except tombstoned nodes — including `proposed`, `accepted`, `deprecated`, and `retired`. This means agents working on in-progress drafts see them without remembering a flag, alongside the rest of the live graph. Agents asking for a "trusted view" pass `--canonical` explicitly.

Edges follow their endpoints: an edge appears in a filtered view iff both endpoints appear. The CLI flag has an MCP equivalent: `usl.query` accepts a `view` parameter taking the same values.

## 6. Vocabularies and extensions

A vocabulary is declared as a node of `kind: core:Vocabulary`. The core vocabulary `usl://core/vocab/core@0.9` is implicitly loaded by every USL toolchain. Other vocabularies are loaded by referencing them in the repository's manifest:

```yaml
# usl.yaml
usl_version: "0.9"
modules: [core]
primary_lang: en
vocabularies:
  - usl://core/vocab/core@0.9
  - usl://core/vocab/standard-extensions@0.9     # threat modeling, BPMN, SLOs, etc.
  - usl://fhir/vocab/fhir@1.0
  - usl://acme/vocab/acme@2.3
```

A `Vocabulary` node declares the set of node kinds it introduces, the set of edge kinds (with allowed source/target kinds), the set of `Predicate` URIs (§G.2), the JSON Schema for each kind's `spec` block, a JSON-LD (JSON for Linking Data) `@context` for type-resolution interoperability, and owner/lifecycle (vocabularies version like any other node).

### 6.1 Schema dispatch

Every `Vocabulary` node carries a `schema_url_template` field declaring how per-kind schemas are located:

```yaml
spec:
  schema_url_template: "https://w3id.org/usl/v0.9/kinds/{kind_suffix}.schema.json"
  predicate_schema_url_template: "https://w3id.org/usl/v0.9/predicates/{predicate_suffix}.schema.json"
```

For any node with `kind: <vocab>:<KindName>`, the validator substitutes `{kind_suffix}` with `KindName` and fetches the schema. Extension vocabularies pin their schemas at any stable URL; `usl validate` resolves the template at startup and caches.

### 6.2 Bootstrap

USL is **not** self-bootstrapping. The core node schema and the core vocabulary's per-kind schemas live as out-of-band JSON Schema files at `https://w3id.org/usl/v0.9/`. Validators load them at startup before parsing any repository. The Vocabulary node mechanism is for *user* vocabularies, not for self-description of the core.

### 6.3 Collision policy

Within a vocabulary, names are exclusive. Extension vocabularies MUST prefix all kinds, edges, predicate suffixes, and `extensions` field keys with their vocabulary's namespace.

- `kind: Decision` — shorthand for `core:Decision` (unprefixed names resolve to the core vocab).
- `kind: acme:CustomThing` — ACME extension. Allowed.
- `kind: CustomThing` (no prefix, not in core) — validation error.
- `extensions.priority` (no prefix) — validation error.
- `extensions.acme:priority` — allowed.

There is no "last-loaded wins": all collisions are explicit errors. Core USL fields cannot be overridden or shadowed by extensions; doing so is rejected at load time.

### 6.4 Bringing your own vocabulary

A team that wants to add new kinds, edges, predicates, or fields:

1. Author a Vocabulary node.
2. Register it in `usl.yaml`.
3. Use it via the namespace prefix.

That is the entire extension mechanism.

### 6.5 Namespace duality

USL's `namespace` field organizes nodes within a repository (a path-style identifier like `checkout` or `clinic-x`). Many vocabularies have their *own* namespace concept — Avro `namespace`, FHIR base URLs, Cedar namespaces. These are not the same field. Vocabulary-internal namespaces live inside the node's `spec` block (e.g., `spec.avro_namespace`). USL's `namespace` field identifies the file location and URI segment.

## 7. The canonical core kinds

USL Core defines **18 kinds**. Each is universal (≥80% of repos want it as-is), stable, and tooling-payoff-positive. Anything narrower lives in an extension vocabulary; the Standard Extensions Bundle catalogs the most-loaded ones.

| # | Kind | Layer | Purpose | Discriminator (`spec` field) |
|---|---|---|---|---|
| 1 | `Vocabulary` | infrastructure | Type registry. | — |
| 2 | `Predicate` | infrastructure | Definition of an Attestation predicate (§G.2). | — |
| 3 | `Invariant` | infrastructure | Validation rule. | — |
| 4 | `BoundedContext` | organization | DDD primary scoping unit. | — |
| 5 | `Principal` | organization | Actor. | `type`: human / agent / group / service-account |
| 6 | `Role` | organization | Capability bundle. | — |
| 7 | `Environment` | organization | Where things run. | `type`: dev / staging / prod / custom |
| 8 | `Goal` | intent | What we want. | `type`: outcome / objective / key-result |
| 9 | `Decision` | records | Reasoned commitment to a course of action. Subsumes the ADR (Architecture Decision Record) pattern. | `type`: architecture / product / security / vendor / process / other |
| 10 | `Document` | records | Explanatory prose. | `type`: tutorial / howto / reference / explanation |
| 11 | `Release` | records | Version snapshot. | — |
| 12 | `Attestation` | records | Signed claim (used by Governance, §G). Variant determined by `spec.predicate_uri`. | (see §G.2) |
| 13 | `API` | runtime | Interface. | `protocol`: rest / graphql / grpc / asyncapi / mcp / webhook / sse / websocket |
| 14 | `Endpoint` | runtime | Single operation. | `style`: sync / async / tool / webhook / streaming |
| 15 | `Schema` | runtime | Data shape. | `format`: json-schema / avro / proto3 / linkml |
| 16 | `Component` | runtime | Deployable unit. | `type`: service / library / function / job / mobile-app |
| 17 | `Test` | runtime | Verification. Anchors `tested(n)` and G-04 (release gating). | `style`: unit / integration / e2e / contract / property |
| 18 | `Policy` | constraints | Authorization rule. | `engine`: usl-native / cedar / opa-rego / zanzibar / custom |

Realization (§2.1, §9.4) is computed from `RealizationUpdate` Attestations on any kind that has them; it is not gated by kind in the schema. In practice, build pipelines and deployment automation produce these Attestations only against deployable artifacts: `Component`, `Release`, `Environment` in Core, plus `Workflow` and `Resource` when the Standard Extensions Bundle is loaded.

**On the ADR archetype.** The Architecture Decision Record pattern lives as `core:Decision` with `spec.type: architecture`. The kind subsumes ADR's content shape; tooling that surfaces "ADRs" filters by this discriminator value.

### 7.1 Discriminators

Several kinds use a `spec.<discriminator>` field to admit multiple shapes under one identity. The discriminator picks the per-kind schema variant. This keeps the kind count small without losing precision: a `core:API` with `protocol: rest` validates against a different `spec` schema than the same kind with `protocol: graphql`, but both are the same `kind` and answer the same edges.

A kind merits a separate top-level identity (rather than collapsing under another's discriminator) when it has structural fields that don't fit a discriminator under another kind, or when it is referenced as a distinct subject by edges from other kinds.

### 7.2 Kinds in the Standard Extensions Bundle

Many specialized kinds live in `usl://core/vocab/standard-extensions@0.9`, loaded by repos that need them. The clearest cases:

- **Process modeling:** `Process`, `Workflow`, `Saga`, `SagaStep`, `StateMachine`, `DecisionTable` (with the `compensates` and `triggers` edges this vocabulary adds).
- **Security & compliance:** `Threat`, `Control`, `ControlBaseline`, `LegalContract`, `PrivacyProcess`, `BOM`.
- **Reliability & observability:** `SLO`, `SLA`, `AlertPolicy`, `TelemetrySignal`, `Runbook`, `Incident`, `Postmortem`.
- **Operational artifacts:** `Resource`, `OCIArtifact`, `Tenant`, `Cell`, `Region`.
- **UX & journey:** `Persona`, `JTBD`.
- **Data:** `Dataset`, `DataProduct`, `DataContract`.
- **AI:** `Model`, `Skill`, `Agent`, `Prompt`, `Eval`.
- **UI design system:** `UIComponent`, `DesignSystem`, `Theme`, `IconSet`, `Icon`, `DesignToken`.
- **Modeling:** `ModelElement`, `ModelRelationship`, `Diagram`, `Aggregate`.
- **Platform:** `Gateway`, `Route`, `Flag`, `ExtensionManifest`, `CLI`, `LegalEntity`.

**`core:ChangeSet` is module-gated.** ChangeSet uses the `core:` prefix because the Governance module ships it under the core vocabulary, but it is only validatable when the Governance module is active. The §7 18-kinds list intentionally omits it: a Core-only repo cannot author or interpret ChangeSets. When Governance is enabled, ChangeSet behaves as a 19th `core:` kind. The full per-kind schema lives at `https://w3id.org/usl/v0.9/kinds/ChangeSet.schema.json`.

Repos that need any of the kinds above load the relevant vocabulary in `usl.yaml`; repos that don't get a smaller core to learn.

## 8. Scope

### 8.1 The `scope` field

Every node carries a single `scope` field whose value is the URI of a containing scope node:

```yaml
scope: usl://core/checkout/main                  # a BoundedContext
```

Scope nodes form a tree rooted at the global scope `usl://core/scope/global` (a built-in `BoundedContext` with name `global`). Common scope URIs:

- `usl://core/scope/global` — global scope; visible everywhere.
- `usl://core/<context-namespace>/<context-name>` — a `BoundedContext` node.
- `usl://core/<tenant-namespace>/<tenant-name>` — a `Tenant` node from the Standard Extensions Bundle (when multi-tenancy applies).

A node's effective visibility is determined by walking up the scope tree.

**Cells, regions, and services are attributes, not scope dimensions.** They are deployment topology, not specification scope. A Component scoped to a BoundedContext runs in zero or more Environments; the deployment relationship is recorded by `RealizationUpdate` Attestations carrying the Environment URI. Cells and regions are attributes on Environment and `Resource` (extension) nodes.

### 8.2 Visibility rules

USL queries are scope-aware: the result set is filtered by the asking scope.

- A node with `scope = usl://core/scope/global` is visible to all queries.
- A node scoped to a `BoundedContext` X is visible by default to queries originating in X. Visible to other contexts iff a `references` edge with a context-map relationship attribute crosses (`shared-kernel`, `published-language-of`, `open-host-of`, etc.).
- A node scoped to a `Tenant` X is invisible to queries from any other tenant. Always.

When a node references another by URI, the resolver loads the target without scope filtering — that is, you can always *resolve* by URI. But scope filtering applies to *enumeration* (querying for nodes of a kind).

### 8.3 Bounded context as the primary scoping unit

Every non-global node should ideally live in exactly one BoundedContext. Cross-context coordination uses the `references` edge with the context-map relationship attributes.

## 9. Lifecycle, realization, and version

### 9.1 The five lifecycle states

```
proposed → accepted → deprecated → retired
    │         │            │          │
    └─────────┴────────────┴──────────┴──→ tombstoned   (reachable from any state)
```

`lifecycle` is derived from the Attestation history (§G.2). The values:

- `proposed`: under review; mutable; not authoritative. The starting state for any new node (no Attestations).
- `accepted`: production; authoritative; immutable except via supersession. Reached by an `Approval` Attestation with `to_lifecycle: accepted`.
- `deprecated`: still works, replaced soon; new work should not reference. Reached by `Approval` with `to_lifecycle: deprecated`.
- `retired`: removed; references are validation errors (invariant I-07). Reached by `Approval` with `to_lifecycle: retired` — OR by a `Withdrawal` Attestation against a `proposed` node.
- `tombstoned`: removed from the resolver entirely. Reached by a `Tombstone` Attestation. References produce I-08.

Validators ensure transitions are monotone forward: an Attestation that would move the lifecycle backward is rejected at write time. A `proposed` node can be edited freely; once `accepted`, edits go through supersession (a new node with `supersedes` edge to the old one).

Withdrawal and retirement both arrive at `lifecycle = retired`. The distinction is recoverable from Attestation history: a node whose most recent Attestation is a `Withdrawal` was declined before reaching production. The `withdrawn(n)` derived predicate (§15.1) returns this answer in one call.

**Stability** is a tag (`tags: [experimental]`), not a separate field. The default-canonical view (§5.5) excludes nodes with this tag.

**Supersession is an edge.** A new node A with `supersedes` edge to old node B has the effect of marking B as superseded. Tools display B with a "superseded by A" annotation. There is no separate `superseded` lifecycle state — supersession is derived from the edge.

### 9.2 Lifecycle and realization are orthogonal

The two derived axes (introduced in §2.1) answer different questions and are computed independently:

| Question | Answer |
|---|---|
| Is this approved? | `lifecycle = accepted` |
| Was this proposal rejected without ever being accepted? | `lifecycle = retired` AND latest Attestation is a Withdrawal |
| Is this spec'd but not yet built? | `lifecycle = accepted AND realization IN ['none', 'planned', 'unknown']` |
| Is this built but not yet running? | `realization = built` |
| Is this retired in spec but still running in production? | `lifecycle = retired AND realization = running` (the audit-critical case) |
| Has this been decommissioned? | `realization = decommissioned` |
| Has this node been removed from history? | `lifecycle = tombstoned` |

Approval gates apply to `lifecycle` only. Realization is observed, not approved.

### 9.3 Version strings

`version` accepts three formats. The kind's schema declares the *default* `version_strategy` for new nodes of that kind; a node's history MAY mix strategies across versions (real-world artifacts migrate, e.g., a Component starting with SemVer and later moving to content-hash for build-time identity).

- **SemVer**: `1.2.3`, `2.0.0-rc.1`. Used for human-visible versioning of contracts (`API`, `Endpoint`, `Schema`).
- **CalVer**: `2026.04`, `2026.04.29`. Used for time-ordered specs (`Vocabulary`, `Release`, `Document`, `Decision`).
- **Content hash**: `sha256:abcd...`. Used for build-time identity (`Attestation`, `Predicate`).

**Per-version strategy recording.** Each version of a node records its own `version_strategy` on the corresponding `Approval` Attestation, alongside the `version` string itself. The pairing is auditable: tools can ask "what strategy did version 2.1.0 use?" and recover the answer from Attestation history without ambiguity. The kind-level default applies only to versions whose Attestation does not specify a strategy.

**Ordering rules.** Within a single strategy, ordering follows that strategy's native rules — SemVer per [semver.org §11](https://semver.org/#spec-item-11), CalVer lexicographic, content-hash unordered. When two versions of the same node use different strategies, ordering falls back to the `claimed_at` timestamp of their respective `Approval` Attestations (earlier `claimed_at` is earlier in history). Cross-strategy "is X later than Y" is therefore always defined; tools never throw on the comparison.

**Strategy migrations are surfaced, not blocked.** Validator I-06 emits an `info`-level diagnostic whenever a node's history crosses strategies, surfacing it for tooling that wants to display the migration explicitly (release dashboards, audit reports). Strategy migrations are first-class history, not errors.

`version_id` is always present alongside `version` and is the deterministic content hash (§9.6). Attestations and Releases reference `version_id`; humans reference `version`. `version_id` is strategy-independent — its computation depends on canonicalized content, not on the version string format.

### 9.4 Realization

The `realization` field on a node is computed from `RealizationUpdate` Attestations. The predicate is registered as a `Predicate` node at `usl://core/governance/predicate/realization-update` (referenced from Attestations as `usl://core/governance/predicate/realization-update@1.0`; see §G.2).

A typical RealizationUpdate predicate body (the subject is named in the DSSE envelope's `subject_uri`, not duplicated here):

```json
{
  "signer": "usl://core/platform/deploy-bot",
  "claimant": "usl://core/platform/deploy-bot",
  "from_realization": "built",
  "to_realization": "running",
  "environment": "usl://core/infra/prod",
  "observed_at": "2026-04-29T18:22:01Z",
  "claimed_at": "2026-04-29T18:22:01Z",
  "evidence": {
    "kind": "deployment-event",
    "ref": "k8s://prod/checkout/payment-service@deploy-2026-04-29-1822"
  }
}
```

Realization is **not monotone** — `running → decommissioned → built → running` is a normal sequence over a service's life. The validator does not enforce ordering on realization transitions; it verifies only that each carries a properly-signed Attestation when the repo's Policy requires it (G-08).

When a node has no RealizationUpdate Attestations, `realization` is `unknown`, distinct from `none` ("explicitly declared not to exist") which requires an Attestation with `to_realization: none`.

### 9.5 Multi-version retention

By default, only the latest accepted version of each node lives at the canonical path:

```
nodes/<vocab>/<namespace>/<name>.<ext>
```

Older versions, if retained, live at:

```
nodes/<vocab>/<namespace>/<name>.versions/<version>.<ext>
```

The `Release` node carries edges to all included version snapshots. The substrate's history (git log) is the canonical source of intermediate edits between releases.

### 9.6 `version_id` — content hash of the node

`version_id` is the deterministic SHA-256 fingerprint of a node's semantic content. Attestations bind to it (the DSSE envelope's `subject_version_id`); Federation pins to it; Releases reference it. Cross-implementation interop requires that two implementations on the same node produce the same `version_id`.

**Algorithm.**

1. Take the node's structured data — every authored field plus inline `body` content.
2. **Exclude** these fields, which either change without semantic change or would create circularity:
   - `created_at`, `updated_at`
   - `version_id` itself
   - `lifecycle`, `realization`, `owners` (derived; recomputing from Attestations is the source of truth)
   - any field declared `transient: true` in the kind's schema
3. Canonicalize the remaining structure as JSON per RFC 8785 (JCS — JSON Canonicalization Scheme; sorted keys, no insignificant whitespace, normalized number representation).
4. Compute `sha256` of the canonical bytes. Prefix the hex digest with `sha256:` for the on-the-wire form.

**Properties.**

- **Deterministic.** Two clones of the repo at the same commit produce identical `version_id` for every node.
- **Semantic-only.** Pure metadata edits and derived-field recomputation do not bump the hash.
- **Granular.** Each node has its own `version_id`, independent of the repo's global `content_hash` (§F.1).

The same canonicalization is used by ChangeSet `subject_content_hash` binding (§G.4).

The `version` field is for humans; `version_id` is for tooling. They coexist on a node and answer different questions ("what should we call this release?" vs. "what bytes did we sign?").

## 10. Provenance

```yaml
provenance:
  source: usl://core/build/main
  generated_at: 2026-04-29T18:22:01Z
  generated_by: usl://core/acme/claude-product-agent
  imports: [usl://core/external/openapi-3.1-source]
  attribution:
    schema: usl://attribution/vocab/ai-authoring@1.0
    body:
      prompt_name: propose_decision
      model_ref: usl://acme/anthropic/claude-opus-4-6
      reasoning_summary: "Proposed strategy after reviewing four payment Decisions."
      conversation_hash: "sha256:..."
```

The `attribution` field is open-ended: implementations declare a schema URI for the body. Multiple attribution schemas can coexist; they are not mutually exclusive (a node may have been authored by an agent AND emitted by a build pipeline).

Recognized attribution vocabularies live in the Standard Extensions Bundle, not Core. Core ships the empty-schema fallback only. The two common attribution vocabularies — `attribution/vocab/ai-authoring@1.0` and `attribution/vocab/build-pipeline@1.0` — are loaded explicitly when a repo wants normalized attribution shapes.

**Federation provenance is separate.** Fields injected by the Federation resolver (`federated_from`, `federated_version_id`) do not live under `provenance` — they live under a top-level `_runtime` field that is reserved for resolver-injected, never-authored data. The `provenance` field is for authored origin claims only.

## 11. File grammar

USL files live in a versioned content store with this default layout:

```
usl.yaml                     # repo manifest (modules, vocabularies, primary_lang)
usl.lock                     # federation lockfile (only when Federation module active)
nodes/
  core/                      # vocabulary
    checkout/                # namespace
      0042-payment-strategy.md
    analytics/
      order-events.yaml
    vocab/
      core.yaml
edges/                       # optional, hoisted edges
  sha256-abc123.yaml
attestations/                # only when Governance active; content-addressed
  sha256-defg.../envelope.dsse
```

### 11.1 Markdown-with-YAML-frontmatter

Prose-heavy kinds (Decision, Document, Goal, etc.) use Markdown:

```markdown
---
uri: usl://core/checkout/0042-payment-strategy
kind: core:Decision
version: "2026.04.15"
scope: usl://core/checkout/main
description: |
  Use idempotent payment-intent capture as the canonical flow for all
  checkout paths.
tags: [payments, idempotency, stripe]
spec: { type: architecture }
relations:
  - kind: supersedes
    target: usl://core/checkout/0017-naive-charge
  - kind: governs
    target: usl://core/checkout/payment-service
    attributes:
      relationship: enforces
  - kind: references
    target: usl://core/checkout/team-payments
    attributes:
      relationship: owned-by
  - kind: traces-to
    target: usl://core/checkout/idempotent-payment
created_at: 2026-04-15T10:00:00Z
updated_at: 2026-04-29T11:30:00Z
---

# Payment strategy: idempotent capture

## Context
[…body in Markdown…]

## Decision

## Consequences
```

The frontmatter is YAML; the body is Markdown. Tools parse the frontmatter as the structured node and the Markdown as `body`. Sibling files (`name.es.md`) hold locale variants.

### 11.2 YAML or JSON

Schemas, policies, vocabularies, and similar machine-only kinds are authored as YAML or JSON natively:

```yaml
# nodes/core/analytics/order-events.yaml
uri: usl://core/analytics/order-events
kind: core:Schema
version: "2.1.0"
scope: usl://core/analytics/main
description: "Order event schema, Avro format, with PII redaction."
spec:
  format: avro
  body: |
    { "type": "record", "name": "OrderEvent", ... }
relations:
  - kind: implements
    target: usl://core/analytics/order-events-api
created_at: 2026-04-01T00:00:00Z
updated_at: 2026-04-29T11:30:00Z
```

Both authoring modes produce the same canonical node. Tools normalize at load.

### 11.3 One file = one node

Each node lives in exactly one file. Vocabularies that bundle many `extends` reference-data instances (e.g., 180 ISO 4217 currency codes) use hoisted edges (§5.4) for the bulk; the Vocabulary node itself remains a single file.

### 11.4 Bodies

Bodies are stored inline in the `body` field (default for Markdown frontmatter files; under `body:` for YAML). There is one mechanism. Large bodies stay inline; locale variants live in sibling files (§4.4).

### 11.5 The `usl.yaml` manifest

```yaml
usl_version: "0.9"
modules: [core]                                      # plus governance, federation, embeddings as needed
primary_lang: en
vocabularies:
  - usl://core/vocab/core@0.9
node_path_template: "nodes/{vocab}/{namespace}/{name}.{ext}"
edges_layout: inline                                 # or hoisted, or hybrid
```

Federation, governance, and embedding sections of `usl.yaml` are described in those modules.

## 12. Conflict resolution

The substrate (git, etc.) handles diff and merge. USL adds three rules on top.

**Three-tier resolution:**

1. **Frontmatter scalars.** Latest `updated_at` wins per field. For `version`, conflicts always require human resolution. (`lifecycle` and `realization` are derived; they don't appear in merge conflicts.)
2. **Frontmatter arrays** (`tags`, `relations`, `aliases`). Set union. Two edges with the same `(kind, target, attributes)` triple are deduplicated. Conflicting attributes on the same `(kind, target)` raise a merge conflict.
3. **Bodies.** The substrate's text-merge mechanism. YAML/JSON bodies should be re-emitted via `usl normalize` before commit so canonical key ordering minimizes spurious conflicts.

**`usl merge` helper.** Tooling provides `usl merge <a> <b>` that runs the algorithm and writes the result, or exits non-zero with a conflict report. The conflict report is itself a USL `Document` node so it can be reviewed and attested.

**Cross-branch identity.** Renames are not permitted (§3); a branch that "renames" is in fact superseding via a new node. Cross-branch supersession resolves cleanly: the supersession is a new edge, set-union merges keep both edges, and the validator surfaces the resulting graph.

Federation conflict resolution (cross-repo conflicts) lives in §F.6.

## 13. Validation and structural invariants

### 13.1 Validator stages

`usl validate` runs three stages:

1. **Schema validation.** Every node parses against its kind's JSON Schema (looked up via the vocab's `schema_url_template`, §6.1). Required fields present; types match.
2. **Reference resolution.** Every edge target resolves to a known URI.
3. **Invariant evaluation.** The Core pack runs against the resolved graph; module-specific invariants run when those modules are active; custom invariants run last.

Federation adds two preliminary stages (lockfile freshness and federation resolution) before stage 1; see §F.

### 13.2 The Core invariant pack

Eight invariants ship in Core (I-01 through I-08). Each is specified as an algorithm with inputs, predicates, and outputs. Implementations may render them in any graph language; conformance is by behavior, not by syntax.

```
I-01  required-description-on-accepted (error)
  for each node n where lifecycle(n) = 'accepted':
    if n.description is empty: emit (n.uri, 'missing-description')

I-02  orphan (warn)
  for each node n where kind(n) ∉ {Release, Vocabulary, Document, Predicate}:
    if no edge targets n: emit (n.uri, 'orphan')

I-03  supersession-cycle (error)
  for each node n where path n -[:supersedes*]-> n exists:
    emit (n.uri, 'supersession-cycle')

I-04  derived-cache-divergence (error)
  for each node n where any of {lifecycle, realization, owners} is materialized in storage:
    if materialized value ≠ value computed from Attestations and edges:
      emit (n.uri, field, 'derived-cache-divergence')

I-05  cross-context-without-map (error)
  for each edge (a)-[r]->(b) where scope(a) ≠ scope(b)
                              and both scopes are BoundedContexts:
    if r.kind = 'references' and r.attributes.relationship ∈ context-map values:
      ok
    else:
      emit (a.uri, b.uri, 'cross-context-without-map')

I-06  version-strategy-migration (info)
  for each node n with version-history entries v1, v2, ...:
    if any pair (vi, vj) uses different version_strategy:
      emit (n.uri, vi.version, vj.version, vi.version_strategy, vj.version_strategy,
            'version-strategy-migration')

I-07  retired-reference (error)
  for each edge (n)-[r]->(target) where lifecycle(target) = 'retired'
                                    and lifecycle(n) ≠ 'retired':
    emit (n.uri, target.uri, 'retired-reference')

I-08  tombstoned-reference (error)
  for each edge (n)-[r]->(target) where lifecycle(target) = 'tombstoned':
    emit (n.uri, target.uri, 'tombstoned-reference')
```

The invariants are pseudocode. Each named procedure (`lifecycle(n)`, `realization(n)`, `scope(n)`) is defined in §15.1 with deterministic semantics. Implementations are free to compile them to openCypher, Datalog, SQL, or in-memory traversals; the spec defines what the invariant means, not how it runs.

### 13.3 Performance budget

The Core pack runs in O(N + E). Reference targets:

- 10K nodes / 100K edges: <100 ms.
- 100K nodes / 1M edges: <1 s in-memory; <500 ms with a graph-DB index.
- 1M nodes / 10M edges: requires a graph-DB index; <2 s.

### 13.4 Custom invariants

Authors add invariants under `invariants/<name>.<ext>` with a YAML header declaring id, severity, and description. Invariants are themselves USL nodes (`kind: core:Invariant`) so they can be versioned, owned, and attested. The body may be in any graph query language the implementation supports; `usl validate` invokes the engine declared in the Invariant's `spec.engine`.

## 14. Performance architecture

USL distinguishes three layers, with one source of truth and two derived materializations.

### 14.1 Substrate requirements

Layer 1's "files in a versioned content store" assumes a substrate with five properties. Any substrate meeting them can host a USL repository; git is the canonical reference.

1. **Content-addressable file storage** — files identified by path; bytes retrievable by path.
2. **Version history with diff semantics** — given two points in history, what changed.
3. **Concurrent-edit reconciliation** — when two authors modify the same node, a mechanism to reconcile.
4. **History preservation** — past versions remain retrievable.
5. **Multi-author attribution** — each change is attributable to an authoring identity.

### 14.2 The three layers

**Layer 1 — Source of truth: files in a versioned content store.** Always authoritative. Tooling re-derives layers 2 and 3 from these files.

**Layer 2 — Read model (optional, performance accelerator).** A graph index plus the materialized cache for derived fields (`lifecycle`, `realization`, `owners`). SQLite for repos under 100K edges; graph DB (Neo4j, Memgraph, Apache AGE) for larger graphs. Pre-computed by `usl index`; rebuilt on commit (or incrementally watched). `usl query` reads from layer 2 if available; falls back to in-memory layer-1 traversal otherwise.

**Layer 3 — Vector index (optional, retrieval accelerator).** Embeddings produced by `usl embed` (§E) live in a separate vector store. The store carries a manifest so cross-store comparison is meaningful. `usl search` reads from layer 3.

The pipeline is incremental: a single-file change updates only the affected nodes/edges/embeddings, not the whole repo.

## 15. Query patterns

USL uses **openCypher** as its surface query language (an ISO-GQL-aligned syntax). Implementations are free to support either openCypher or full GQL.

**Kind-label convention.** USL queries use bare kind names as openCypher node labels (`(d:Decision)`, `(c:Component)`); the implementation maps each label to the fully-qualified `kind` string at compile time.

**Default view.** Bare `MATCH` returns nodes with `lifecycle ≠ tombstoned`. Filter further with the canonical predicates below or with the CLI's `--canonical` / `--accepted-only` flags.

```cypher
// Decisions in a bounded context (any non-tombstoned lifecycle)
MATCH (d:Decision)
WHERE d.scope = 'usl://core/checkout/main'
RETURN d.uri, d.description, lifecycle(d)
ORDER BY d.updated_at DESC

// Goals with no implementations
MATCH (g:Goal)
WHERE NOT EXISTS { MATCH (any)-[:implements]->(g) }
RETURN g.uri

// Components depending on a deprecated API
MATCH (c:Component)-[:depends-on]->(api:API)
WHERE lifecycle(api) = 'deprecated'
RETURN c.uri, api.uri
```

### 15.1 Canonical derived predicates

Every USL implementation MUST expose these procedures, with these exact semantics. Truth values MUST be identical across implementations given the same graph.

| Procedure | Subject | Definition |
|---|---|---|
| `lifecycle(n)` | any | Returns the latest lifecycle state derived from `Approval`, `Withdrawal`, and `Tombstone` Attestations on n. Default `proposed` when no Attestations exist. |
| `realization(n)` | any | Returns the latest realization state derived from `RealizationUpdate` Attestations on n. Default `unknown`. |
| `owners(n)` | any | Returns the set of Principals reached by outbound `references` edges from n with `attributes.relationship: owned-by`. |
| `scope(n)` | any | Returns the URI of n's containing scope (n.scope). |
| `withdrawn(n)` | any | True iff `lifecycle(n) = 'retired' AND latest_attestation(n).predicate_uri ENDS WITH 'withdrawal@1.0'`. |
| `implemented(c)` | any | A node exists with `implements` edge to c. |
| `built(n)` | any | `realization(n) IN ['built', 'running']`. |
| `running(n)` | any | If n has any `RealizationUpdate` Attestation: `realization(n) = 'running'`. If n is a contract (API/Endpoint/Schema): walks `(impl)-[:implements]->(n)` and returns true iff any implementer has `realization = 'running'`. |
| `decommissioned(n)` | any | `realization(n) = 'decommissioned'`. |
| `consumed(api)` | API | A node has `depends-on` edge to api. |
| `governed(n)` | any | A `Policy` has `governs` edge to n. |
| `tested(n)` | any | A `Test` has `evidence-for` edge to n with a positive `test-result` Attestation. |
| `traced(n)` | any | n has `traces-to` edge to a `Goal` or `Document`. |
| `superseded(n)` | any | A successor exists with `supersedes` edge to n. |
| `released(n)` | any | A `Release` has `contains` edge to n. |
| `deployed_in(c, env)` | Component, Environment | The latest `RealizationUpdate` Attestation on c naming env has `to_realization = 'running'` and is not superseded by a later Attestation moving away. |
| `spec_only(n)` | any | `lifecycle(n) = 'accepted' AND realization(n) IN ['none', 'planned', 'unknown']`. Spec'd but not built. |
| `lingering(n)` | any | `lifecycle(n) IN ['retired', 'deprecated'] AND realization(n) = 'running'`. The audit-critical case. |

Predicates compose with standard Cypher boolean operators:

- "Components implemented but not tested" — `WHERE implemented(c) AND NOT tested(c)`
- "Components spec'd but not built" — `WHERE spec_only(c)`
- "Code running for retired specs" — `WHERE lingering(n)`
- "Goals without implementations" — `WHERE NOT implemented(g)`
- "Components in an accepted Release that aren't running" — `WHERE released(c) AND lifecycle(c) = 'accepted' AND NOT running(c)`

### 15.2 Time-travel queries (normative)

Every derived predicate accepts an optional `at:` parameter that evaluates against the graph as of a specific timestamp:

```cypher
MATCH (c:Component {uri: 'usl://core/checkout/payment-service'})
RETURN realization(c, at: '2026-04-15T00:00:00Z') AS state_then,
       realization(c) AS state_now
```

`at:` evaluates against the Attestation history: only Attestations with `claimed_at ≤ <at>` are considered. Time-travel makes the lifecycle/realization split fully auditable: "was this retired-in-spec running on 2026-04-15?" is a single query.

### 15.3 Vector search

When the Embeddings module is active, queries combine vector search with graph filters:

```cypher
CALL usl.search('idempotent payment', limit: 20) YIELD node, score
WHERE node.scope = 'usl://core/checkout/main'
  AND lifecycle(node) = 'accepted'
RETURN node.uri, node.kind, node.description, score
```

The full procedure contract is in §E.

### 15.4 Scope-aware queries

By default, queries originate from a scope:

```bash
$ usl query --scope usl://core/checkout/main "MATCH (d:Decision) RETURN d.uri"
```

The query engine adds scope filters automatically per §8.

## 16. Tooling baseline (Core)

A conformant USL Core implementation provides the CLI verbs and MCP tools below. Modules add their own verbs and tools; they are exposed as discoverable MCP tools, not hidden behind dispatch.

### 16.1 The `usl` CLI (Core verbs)

| Command | Description |
|---|---|
| `usl validate [path...]` | Run schema, reference, and Core invariant validation. Module-specific invariants run when modules active. |
| `usl query "<cypher>"` [--scope X] [--view canonical/accepted-only/include-tombstones] | Execute a graph query. |
| `usl resolve <uri>` [--at <timestamp>] | Debug URI resolution. With `--at`, returns the node as of that timestamp. |
| `usl normalize` | Rewrite repo to canonical layout (inline edges hoisted or vice versa; YAML key ordering). |
| `usl diff <a> <b>` | Semantic diff between two graphs or two versions of one node. |
| `usl merge <a> <b>` | Three-tier semantic merge (frontmatter / body / edges). |
| `usl index` | Pre-compute the read model (layer 2) including derived-field cache. |
| `usl release <name> --version <v>` | Build a Release node from current accepted state. |
| `usl serve --mcp [--port N]` | Run the MCP server. |

### 16.2 The MCP server

The MCP (Model Context Protocol) server is the primary integration surface for AI clients. Each operation is exposed as a discoverable tool — there is no dispatch verb that hides module-specific operations behind a string parameter.

Core tools:

| Tool | Purpose | Required scope |
|---|---|---|
| `usl.query` | Execute an openCypher query. | read |
| `usl.search` | Vector search (when Embeddings module is active). | read |
| `usl.read` | Resolve a URI and return the full node. Accepts optional `at:` for time-travel. | read |
| `usl.write` | Create or update a node; runs validation before commit. | write |
| `usl.export` | Project a node to a native format (when an adapter is registered from the Projection Registry). | read |

Module-specific tools follow the namespacing convention `usl.<module>.<verb>`:

- Governance: `usl.governance.open_changeset`, `usl.governance.commit_changeset`, `usl.governance.approve`, `usl.governance.bundle_approve`, `usl.governance.withdraw`, `usl.governance.delegate`, `usl.governance.revoke`, `usl.governance.tombstone`, `usl.governance.comment`, `usl.governance.request_changes`. (Each tool wraps signing one Attestation of the named predicate type, surfacing the action-vocabulary verb directly to clients.)
- Federation: `usl.federation.list`, `usl.federation.refresh`, `usl.federation.diff`, `usl.federation.upgrade`, `usl.federation.impact`, `usl.federation.update`.

Every tool appears in the MCP `list_tools` response. Clients see the full surface without a discovery call.

**Resources exposed.** Every node addressable by URI is also addressable as an MCP resource. Resources are read-only; mutation goes through `usl.write`. The MCP server publishes resource list-changed notifications when nodes are added, updated, retired, or tombstoned.

**Authorization.** When the Governance module is active, the MCP server consults the active Policy to decide which tools and resources the connecting client can use. Without Governance, a permissive default applies (read for any client, write only for clients presenting a configured token).

## 17. Reference JSON Schemas

The canonical schemas live at `https://w3id.org/usl/v0.9/`. They load as out-of-band JSON Schema files at toolchain startup (§6.2).

### 17.1 The base node schema

```json
{
  "$id": "https://w3id.org/usl/v0.9/node.schema.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["uri", "kind", "version", "scope", "created_at", "updated_at"],
  "properties": {
    "uri": {"type": "string", "pattern": "^usl://[a-z][a-z0-9.-]*/[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*(@[A-Za-z0-9.:_-]+)?$"},
    "kind": {"type": "string", "pattern": "^[a-z][a-z0-9-]*:[A-Z][A-Za-z0-9]*$"},
    "version": {"type": "string"},
    "version_id": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"},
    "scope": {"type": "string", "format": "uri"},
    "tags": {"type": "array", "items": {"type": "string"}},
    "visibility": {"enum": ["public", "internal", "private"], "default": "public"},
    "description": {"type": "string"},
    "body": {"type": "string"},
    "spec": {"type": "object"},
    "relations": {"type": "array", "items": {"$ref": "#/$defs/Edge"}},
    "provenance": {"$ref": "#/$defs/Provenance"},
    "aliases": {"type": "array", "items": {"type": "string"}},
    "extensions": {"type": "object"},
    "created_at": {"type": "string", "format": "date-time"},
    "updated_at": {"type": "string", "format": "date-time"}
  },
  "$defs": {
    "Edge": {
      "type": "object",
      "required": ["kind", "target"],
      "properties": {
        "kind": {"type": "string"},
        "target": {"type": "string", "format": "uri"},
        "confidence": {"type": "number", "minimum": 0, "maximum": 1, "default": 1},
        "valid_from": {"type": "string", "format": "date-time"},
        "valid_to": {"type": ["string", "null"], "format": "date-time"},
        "source": {"type": "string"},
        "attributes": {"type": "object"}
      }
    },
    "Provenance": {
      "type": "object",
      "properties": {
        "source": {"type": "string"},
        "generated_at": {"type": "string", "format": "date-time"},
        "generated_by": {"type": "string"},
        "imports": {"type": "array", "items": {"type": "string"}},
        "attribution": {
          "type": "object",
          "required": ["schema", "body"],
          "properties": {
            "schema": {"type": "string", "format": "uri"},
            "body": {"type": "object"}
          }
        }
      }
    }
  }
}
```

Required-content constraints on `accepted` nodes (description, ownership) are enforced as invariant I-01 against the derived lifecycle, not as a JSON Schema conditional.

The `_runtime` field (resolver-injected federation provenance, §10) is permitted by JSON Schema's `additionalProperties: true`. Tools strip it on serialization.

### 17.2 Per-kind schemas

Each kind has a separate schema discovered via the vocab's `schema_url_template`. For Core, the template is `https://w3id.org/usl/v0.9/kinds/{kind_suffix}.schema.json`. Below is the shape pattern for a kind with a discriminator (`API`):

```json
{
  "$id": "https://w3id.org/usl/v0.9/kinds/API.schema.json",
  "allOf": [
    {"$ref": "https://w3id.org/usl/v0.9/node.schema.json"},
    {
      "properties": {
        "kind": {"const": "core:API"},
        "version_strategy": {"const": "semver"},
        "spec": {
          "type": "object",
          "required": ["protocol"],
          "properties": {
            "protocol": {"enum": ["rest", "graphql", "grpc", "asyncapi", "mcp", "webhook", "sse", "websocket"]},
            "servers": {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": {"type": "string", "format": "uri"},
                  "environment": {"type": "string"}
                }
              }
            },
            "base_path": {"type": "string"},
            "version_strategy": {"enum": ["header", "path", "query", "media-type", "none"]},
            "default_security_ref": {"type": "string"},
            "rate_limit_ref": {"type": "string"}
          }
        }
      }
    }
  ]
}
```

**Principal.spec** — actor identity, key registry. Used by every Governance invariant.

```json
{
  "$id": "https://w3id.org/usl/v0.9/kinds/Principal.schema.json",
  "allOf": [
    {"$ref": "https://w3id.org/usl/v0.9/node.schema.json"},
    {
      "properties": {
        "kind": {"const": "core:Principal"},
        "version_strategy": {"const": "calver"},
        "spec": {
          "type": "object",
          "required": ["type"],
          "properties": {
            "type": {"enum": ["human", "agent", "group", "service-account"]},
            "identifiers": {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["method"],
                "properties": {
                  "method": {"type": "string", "description": "did:key | did:web | oidc | x509 | github | email | webauthn | other"},
                  "value": {"type": "string"},
                  "issuer": {"type": "string"},
                  "verified_at": {"type": "string", "format": "date-time"}
                }
              }
            },
            "keys": {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["kid"],
                "properties": {
                  "kid": {"type": "string"},
                  "jwk": {"type": "object"},
                  "valid_from": {"type": "string", "format": "date-time"},
                  "valid_to": {"type": ["string", "null"], "format": "date-time"}
                }
              }
            },
            "operator": {"type": "string", "format": "uri", "description": "Required when type=agent."},
            "model_ref": {"type": "string", "format": "uri", "description": "Optional. When type=agent, URI of the Model node behind the agent."},
            "members": {"type": "array", "items": {"type": "string", "format": "uri"}, "description": "Required when type=group."}
          },
          "allOf": [
            {"if": {"properties": {"type": {"const": "agent"}}}, "then": {"required": ["operator"]}},
            {"if": {"properties": {"type": {"const": "group"}}}, "then": {"required": ["members"]}}
          ]
        }
      }
    }
  ]
}
```

**Component.spec** — deployable software unit.

```json
{
  "$id": "https://w3id.org/usl/v0.9/kinds/Component.schema.json",
  "allOf": [
    {"$ref": "https://w3id.org/usl/v0.9/node.schema.json"},
    {
      "properties": {
        "kind": {"const": "core:Component"},
        "version_strategy": {"const": "semver"},
        "spec": {
          "type": "object",
          "required": ["type"],
          "properties": {
            "type": {"enum": ["service", "library", "function", "job", "mobile-app"]},
            "language": {"type": "string"},
            "repo": {"type": "string", "format": "uri"},
            "build_ref": {"type": "string"}
          }
        }
      }
    }
  ]
}
```

**Predicate.spec** — definition of an Attestation predicate (§G.2).

```json
{
  "$id": "https://w3id.org/usl/v0.9/kinds/Predicate.schema.json",
  "allOf": [
    {"$ref": "https://w3id.org/usl/v0.9/node.schema.json"},
    {
      "properties": {
        "kind": {"const": "core:Predicate"},
        "version_strategy": {"const": "semver"},
        "spec": {
          "type": "object",
          "required": ["body_schema"],
          "properties": {
            "body_schema": {
              "type": "object",
              "description": "JSON Schema (draft 2020-12) for the Attestation predicate body conforming to this Predicate."
            },
            "subject_kinds": {
              "type": "array",
              "items": {"type": "string"},
              "description": "Kinds this predicate may target. Empty = any kind."
            }
          }
        }
      }
    }
  ]
}
```

**Vocabulary.spec** — type registry. Used by every loader.

```json
{
  "$id": "https://w3id.org/usl/v0.9/kinds/Vocabulary.schema.json",
  "allOf": [
    {"$ref": "https://w3id.org/usl/v0.9/node.schema.json"},
    {
      "properties": {
        "kind": {"const": "core:Vocabulary"},
        "version_strategy": {"const": "calver"},
        "spec": {
          "type": "object",
          "required": ["prefix", "schema_url_template"],
          "properties": {
            "prefix": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9.-]*$",
              "description": "The vocabulary's namespace prefix (e.g., 'core', 'fhir', 'acme.platform')."
            },
            "schema_url_template": {
              "type": "string",
              "format": "uri",
              "description": "URL template with {kind_suffix} placeholder. Validators substitute the kind suffix to fetch per-kind schemas. Example: https://w3id.org/usl/v0.9/kinds/{kind_suffix}.schema.json"
            },
            "predicate_schema_url_template": {
              "type": "string",
              "format": "uri",
              "description": "Optional. URL template with {predicate_suffix} placeholder for Predicate body schemas this vocabulary registers."
            },
            "kinds": {
              "type": "array",
              "description": "Node kinds this vocabulary introduces. Each entry names a kind suffix and notes its discriminator field (if any).",
              "items": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {"type": "string", "description": "Kind suffix (e.g., 'Patient' for fhir:Patient)."},
                  "discriminator": {"type": "string", "description": "Optional spec field that selects schema variants."},
                  "description": {"type": "string"}
                }
              }
            },
            "edges": {
              "type": "array",
              "description": "Edge kinds this vocabulary introduces. Source/target restrictions tie edges to specific kinds.",
              "items": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {"type": "string", "description": "Edge kind suffix (e.g., 'cross-team-dependency' for acme:cross-team-dependency)."},
                  "source_kinds": {"type": "array", "items": {"type": "string"}},
                  "target_kinds": {"type": "array", "items": {"type": "string"}},
                  "attributes_schema": {"type": "object", "description": "JSON Schema for the edge's attributes block."}
                }
              }
            },
            "predicates": {
              "type": "array",
              "description": "Predicate URIs this vocabulary registers (each is a separate core:Predicate node).",
              "items": {"type": "string", "format": "uri"}
            },
            "jsonld_context": {
              "type": "string",
              "format": "uri",
              "description": "Optional JSON-LD @context URL for type-resolution interop."
            }
          }
        }
      }
    }
  ]
}
```

A Vocabulary node is a regular USL node and inherits everything that comes with that: it has owners, lifecycle, attestability, federation pinning. Loading a vocabulary in `usl.yaml` is just a URI reference to the node; the resolver fetches it through the same machinery as any other node.

The remaining 13 Core kinds — `Endpoint`, `Schema`, `Test`, `Policy`, `Environment`, `Release`, `Goal`, `Decision`, `Document`, `BoundedContext`, `Role`, `Invariant`, `Attestation` — are pinned at `https://w3id.org/usl/v0.9/kinds/<Kind>.schema.json`.

### 17.3 Reusable sub-schemas

Two sub-schemas are reused across multiple kinds; they live at `https://w3id.org/usl/v0.9/sub/`.

**CodePointer** — used by any kind whose body lives in code outside USL.

```json
{
  "$id": "https://w3id.org/usl/v0.9/sub/CodePointer.schema.json",
  "type": "object",
  "required": ["path", "language"],
  "properties": {
    "path": {"type": "string"},
    "function": {"type": "string"},
    "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
    "language": {"type": "string", "enum": ["python", "go", "typescript", "javascript", "rust", "java", "kotlin", "swift", "cpp", "c", "csharp", "ruby", "shell", "sql", "other"]},
    "repo": {"type": "string", "format": "uri"}
  }
}
```

**ForeignCode** — used for embedded foreign-language snippets (inline policy conditions, gateway guards).

```json
{
  "$id": "https://w3id.org/usl/v0.9/sub/ForeignCode.schema.json",
  "type": "object",
  "required": ["language", "code"],
  "properties": {
    "language": {"type": "string"},
    "code": {"type": "string"}
  }
}
```

### 17.4 Null vs absent

A field that is omitted from the YAML/JSON encoding means "unknown" — the absence carries no claim. A field set to `null` means "explicitly declared not to exist." Validators distinguish:

- `description` field absent: no description has been authored.
- `description: null`: the author explicitly declared no description applies (rare; usually used for machine-only kinds where prose is irrelevant).
- `extensions.acme:priority` absent vs. `extensions.acme:priority: null`: the same distinction applies to extension fields.

For derived fields (`lifecycle`, `realization`, `owners`), the same convention holds at the *Attestation* level — an absent Attestation history means "unknown" (`realization` defaults to `unknown`); an explicit Attestation with `to_realization: none` means "explicitly declared not to exist." This convention applies to every optional field in v0.9.

---

## Module 2 — Governance

Governance adds approval workflows, roles, signed Attestations, and predicate definitions on top of Core. Repositories that need authorization, multi-party sign-off, or auditable change control enable this module:

```yaml
# usl.yaml
modules: [core, governance]
```

Without Governance, every Principal can perform any action on any node. With Governance, Policy nodes constrain who can authorize lifecycle transitions, Attestation nodes carry the cryptographic evidence, and Predicate nodes define what each Attestation means.

## G.1 Roles

USL ships three canonical roles that every Governance-enabled repository inherits.

| Role | URI | Capabilities |
|---|---|---|
| **Reader** | `usl://core/governance/reader` | Read nodes per scope; query the graph. Cannot write. |
| **Author** | `usl://core/governance/author` | Read + create/edit `proposed` nodes + sign Attestations of any predicate other than `approval@1.0`. |
| **Approver** | `usl://core/governance/approver` | All Author capabilities + sign `approval@1.0` Attestations. |

A Principal holds a Role via a `governs` edge from a Policy that grants it. Multiple Roles can be held simultaneously. A Principal acting on a node MUST hold at least one Role authorizing the action; the MCP server's role-gated writes (§16.2) and the runtime authorization check enforce this at write time, while validator G-01 verifies that the resulting Attestations meet the required quorum.

**Inheritance.** Repositories needing more specific roles define their own Role nodes that extend a baseline:

```yaml
uri: usl://core/security/security-approver
kind: core:Role
spec:
  derived_from: usl://core/governance/approver
  additional_constraints:
    required_for_kinds: [core:Policy]
relations:
  - kind: extends
    target: usl://core/governance/approver
    attributes: { merge: deep }
```

**Owner vs. Approver.** Owner is the *accountable party* named on a node via `references` edges with `relationship: owned-by`. Approver is the *authority to advance lifecycle*. The two often overlap but are distinct: a Component can be owned by `team-payments` and require approval from `team-security`. This is a Policy decision.

## G.2 Attestations and Predicates

**The picture in one paragraph.** An Attestation is a USL node whose body is a cryptographically signed claim about another node. The claim's *shape* is defined by a Predicate (also a USL node, separately versioned and ownable). Three identities meet on every Attestation: the **signer** (whose key produced the signature), the **claimant** (whose authority backs the claim — often the same Principal, sometimes a delegator), and the **subject** (the node being attested about, reached via the Attestation's `evidence-for` edge).

### G.2.1 The DSSE envelope

DSSE (Dead Simple Signing Envelope) is the signature format. The envelope wraps the claim:

```json
{
  "payloadType": "application/vnd.usl.attestation+json",
  "payload": "<base64 of canonical claim JSON>",
  "signatures": [
    {"sig": "<base64>", "keyid": "did:key:z6Mk..."}
  ]
}
```

The claim JSON references:

- `subject_uri` — the USL URI of the subject node.
- `subject_version_id` — content hash of the subject at signing time.
- `predicate_uri` — URI of a `Predicate` node defining the claim shape.
- `predicate` — the structured assertion conforming to that Predicate's `spec.body_schema`.

**Authoritative sources on conflict.** Two fields can appear in more than one place; the spec pins which is authoritative:

- **Subject.** The Attestation node's `evidence-for` edge target is authoritative. The envelope's `subject_uri` MUST equal this; if a tool finds them disagreeing, it rejects the Attestation as malformed.
- **Signer.** The DSSE envelope's `signatures[].keyid` (resolved against the signing Principal's `spec.keys[]`) is authoritative. Predicate bodies that carry a `signer` field carry it as an informational mirror — it MUST equal the envelope-derived value; tools reject on mismatch.

This rule keeps cryptographic provenance in one place (the envelope) and structural identity in another (the edge), with the predicate body as a queryable convenience.

### G.2.2 Predicates as nodes

A Predicate is a regular USL node:

```yaml
uri: usl://core/governance/predicate/approval
kind: core:Predicate
version: "1.0.0"
scope: usl://core/scope/global
description: "Authorize a lifecycle transition on a single node."
spec:
  body_schema:
    type: object
    required: [signer, claimant, to_lifecycle, claimed_at]
    properties:
      signer: {type: string, format: uri}
      claimant: {type: string, format: uri}
      to_lifecycle: {enum: [accepted, deprecated, retired]}
      claimed_at: {type: string, format: date-time}
      delegation_ref: {type: string}
      comment: {type: string}
  subject_kinds: []     # any kind
```

Core ships ten Predicates as regular nodes under the `usl://core/governance/predicate/` namespace:

| Predicate URI | Purpose |
|---|---|
| `usl://core/governance/predicate/approval@1.0` | Authorize a lifecycle transition on one node. |
| `usl://core/governance/predicate/bundle-approval@1.0` | Authorize a lifecycle transition on every member of a ChangeSet. |
| `usl://core/governance/predicate/withdrawal@1.0` | Reject a `proposed` node, transitioning it to `retired`. Carries `rationale`. |
| `usl://core/governance/predicate/tombstone@1.0` | Remove a node from the resolver. Terminal. |
| `usl://core/governance/predicate/delegation@1.0` | One Principal authorizes another to act on their behalf. |
| `usl://core/governance/predicate/revocation@1.0` | Revoke a prior Attestation. |
| `usl://core/governance/predicate/test-result@1.0` | Record a Test execution outcome. |
| `usl://core/governance/predicate/realization-update@1.0` | Update a node's derived `realization` value. |
| `usl://core/governance/predicate/comment@1.0` | Threaded discussion attached to any node or ChangeSet. |
| `usl://core/governance/predicate/request-changes@1.0` | Approver's "won't approve until X is addressed." Blocks the issuer's own approvals on the subject. |

Predicates inherit everything from being nodes: versioning, ownership, attestability, federation pinning. To audit a Predicate definition itself, sign an Attestation against it. To upgrade a Predicate to v2, supersede the v1 node. To use a custom Predicate, author a `core:Predicate` node in your own vocab and reference it from Attestations.

Every predicate body carries a `claimant` field — the Principal whose authority backs the claim. For `approval@1.0` this is the Approver; for `realization-update@1.0` it is the deployment-bot or human reconciler; for `delegation@1.0` it is the delegating Principal. The signer (in the DSSE envelope) is whoever's key produced the signature; the `claimant` is whose authority counts.

**Predicate URI versioning convention.** A Predicate node's own URI is bare (`usl://core/governance/predicate/approval`); the Predicate's `version` field carries the version. References to a Predicate from inside an Attestation's `predicate_uri` field MUST be version-pinned (`usl://core/governance/predicate/approval@1.0`) — Attestations bind to a specific Predicate version so the body schema is unambiguous. Bare references in `predicate_uri` are a validation error. This matches the general URI versioning rule from §3 (`<uri>@<version>` to pin, omit for "latest"); the additional constraint here is that Attestations always pin.

### G.2.3 Comments and discussion

Comments thread human and agent discussion onto any node — including ChangeSets, Decisions, and contracts under review. They are Attestations referencing the `comment@1.0` Predicate; the Attestation's `evidence-for` edge points at the subject.

```yaml
uri: usl://core/order-tracking/comment-on-0042-context-question
kind: core:Attestation
version: "sha256:abc123..."
spec:
  predicate_uri: usl://core/governance/predicate/comment@1.0
  predicate:
    signer: usl://core/order-tracking/bob
    claimant: usl://core/order-tracking/bob
    body: |
      Idempotency keys are scoped per-tenant in the body, but the
      Endpoint spec doesn't say so. Should we add that?
    body_format: markdown
    in_reply_to: null
    claimed_at: 2026-04-29T11:35:00Z
relations:
  - kind: evidence-for
    target: usl://core/order-tracking/0042-idempotent-capture
    attributes: { evidence_kind: comment }
```

The Predicate is a regular node:

```yaml
uri: usl://core/governance/predicate/comment
kind: core:Predicate
version: "1.0.0"
scope: usl://core/scope/global
description: "Threaded discussion attached to any node or ChangeSet."
spec:
  body_schema:
    type: object
    required: [signer, claimant, body, claimed_at]
    properties:
      signer: {type: string, format: uri}
      claimant: {type: string, format: uri}
      body: {type: string}
      body_format: {enum: [markdown, plaintext], default: markdown}
      in_reply_to: {type: string, format: uri}
      claimed_at: {type: string, format: date-time}
  subject_kinds: []     # any kind, including ChangeSet
```

**Threading.** The optional `in_reply_to` field carries the URI of the parent comment Attestation. Tools may render threads at any depth; flat single-level reply rendering is the typical default.

**Subjects.** Any node — including a `ChangeSet`, a `Decision` under review, an `Endpoint` whose semantics are debated, an `Attestation` whose claim is contested — is a valid comment subject. Federation-imported nodes are commentable locally; the comment lives in the consumer repo and is not exported back to the parent.

**Lifecycle of a comment.** A comment Attestation participates in normal Attestation governance. To correct or redact, attach a `revocation@1.0` Attestation referencing the comment, or a `tombstone@1.0` to remove from the resolver. There is no separate "edit" operation; corrections happen by issuing a new comment with `in_reply_to` pointing at the original.

**Authorization.** Authoring requires the **Author** role at minimum. Read access follows the subject's scope visibility (§8.2) — comments on a Tenant-scoped node are not visible outside the tenant. Policies MAY further restrict comment authoring per kind (e.g., comments on `Policy` nodes restricted to a `legal-team` Role).

**Edge attributes.** The `evidence-for` edge from a comment Attestation carries `attributes.evidence_kind: comment`.

### G.2.4 Request-changes

A `request-changes@1.0` Attestation is an Approver's signal that they will not approve until specific items are addressed. While a non-revoked, non-superseded request-changes is in force from a Principal P, any `approval@1.0` Attestation from P targeting the same subject is rejected at write time and flagged by validator G-10 if persisted.

```yaml
uri: usl://core/order-tracking/request-changes-on-0042
kind: core:Attestation
version: "sha256:def456..."
spec:
  predicate_uri: usl://core/governance/predicate/request-changes@1.0
  predicate:
    signer: usl://core/order-tracking/alice
    claimant: usl://core/order-tracking/alice
    rationale: |
      Add explicit handling for the duplicate-key case before I sign off.
      Reference test smoke-get-order's expectations at line 42.
    blocks_lifecycle: accepted
    claimed_at: 2026-04-29T11:40:00Z
relations:
  - kind: evidence-for
    target: usl://core/order-tracking/0042-idempotent-capture
    attributes: { evidence_kind: request-changes }
```

Predicate definition:

```yaml
uri: usl://core/governance/predicate/request-changes
kind: core:Predicate
version: "1.0.0"
scope: usl://core/scope/global
description: "An Approver's blocking request for changes before approval."
spec:
  body_schema:
    type: object
    required: [signer, claimant, rationale, claimed_at]
    properties:
      signer: {type: string, format: uri}
      claimant: {type: string, format: uri}
      rationale: {type: string}
      blocks_lifecycle: {enum: [accepted, deprecated, retired], default: accepted}
      claimed_at: {type: string, format: date-time}
  subject_kinds: []
```

**Effect on quorum.** A Policy rule's required quorum still applies. The presence of a request-changes from an otherwise-eligible Approver removes that Approver from the eligible signer set for the duration of the request. Other Approvers can still sign; if quorum is still satisfiable without the blocking Principal, the bundle moves forward.

**Resolution.** The author of the request-changes either:

- **Revokes** the request-changes (via a `revocation@1.0` Attestation referencing it) once their concerns are addressed, then optionally signs an `approval@1.0`; OR
- **Approves directly** — signing an `approval@1.0` from the same claimant on the same subject implicitly supersedes their outstanding request-changes (the new claim is later in time and contradicts the older block; validator G-10 treats the bundle as resolved).

A separate Principal cannot resolve another's request-changes. Only the issuer (or, in delegation chains, a delegate authorized to act on the issuer's behalf) may revoke or supersede it.

**Subjects.** Request-changes can target a single node or a ChangeSet. When the subject is a ChangeSet, the block applies to all member-level approvals from the same claimant; when the subject is a single node, only that node's approval is blocked.

**Authorization.** Only Principals holding a Role that authorizes the underlying lifecycle action can issue a request-changes for that action. An Author cannot issue a request-changes for `write:accept` (only Approvers can; an Author's block would be vacuous and is rejected at write time).

**Edge attributes.** The `evidence-for` edge from a request-changes Attestation carries `attributes.evidence_kind: request-changes`.

### G.2.5 Revocation

Revocation is itself an Attestation referencing the `revocation@1.0` Predicate; its `evidence-for` edge points at the original Attestation:

```yaml
kind: core:Attestation
uri: usl://core/governance/revoke-build-2026-04-29-key-compromise
version: "sha256:abc123..."
spec:
  predicate_uri: usl://core/governance/predicate/revocation@1.0
  predicate:
    signer: usl://core/governance/keymaster
    claimant: usl://core/governance/keymaster
    reason: "Key compromise"
    claimed_at: 2026-04-29T18:30:00Z
relations:
  - kind: evidence-for
    target: usl://core/build/myapp-build-2026-04-15-1100
    attributes: { evidence_kind: revocation }
```

A revoked Attestation is not removed; tools display it with a revocation annotation. Tooling consults a transparency log (Sigstore Rekor by default; configurable via `usl.yaml`).

### G.2.6 Predicate bodies — the remaining six

Four predicates are sketched in detail above (`approval`, `comment`, `request-changes`, `revocation`). The remaining six Core Predicates carry the body schemas below. Each is shipped as a `core:Predicate` node under `usl://core/governance/predicate/<name>`; references in Attestations pin the version with `@1.0`.

**`bundle-approval@1.0`** — authorize a lifecycle transition on every member of a ChangeSet.

```yaml
spec:
  body_schema:
    type: object
    required: [signer, claimant, to_lifecycle, claimed_at]
    properties:
      signer: {type: string, format: uri}
      claimant: {type: string, format: uri}
      to_lifecycle: {enum: [accepted, deprecated, retired]}
      claimed_at: {type: string, format: date-time}
      delegation_ref: {type: string, format: uri}
      comment: {type: string}
  subject_kinds: [core:ChangeSet]
```

The validator (G-01) sees `predicate_uri = bundle-approval@1.0` and credits every node listed in the targeted ChangeSet's `spec.members[]` for the named lifecycle transition.

**`withdrawal@1.0`** — reject a `proposed` node, transitioning it to `retired`.

```yaml
spec:
  body_schema:
    type: object
    required: [signer, claimant, rationale, claimed_at]
    properties:
      signer: {type: string, format: uri}
      claimant: {type: string, format: uri}
      rationale: {type: string, description: "Required free-text explanation."}
      claimed_at: {type: string, format: date-time}
  subject_kinds: []
```

The `withdrawn(n)` derived predicate (§15.1) detects this case from Attestation history.

**`tombstone@1.0`** — remove a node from the resolver. Terminal.

```yaml
spec:
  body_schema:
    type: object
    required: [signer, claimant, reason, claimed_at]
    properties:
      signer: {type: string, format: uri}
      claimant: {type: string, format: uri}
      reason: {type: string, description: "Required documented reason. G-09 enforces non-empty."}
      claimed_at: {type: string, format: date-time}
  subject_kinds: []
```

After tombstoning, the resolver returns a tombstone record carrying `{kind, version, version_id, reason, claimed_at}` for the URI rather than the node body. References are I-08 errors.

**`delegation@1.0`** — one Principal authorizes another to act on their behalf.

```yaml
spec:
  body_schema:
    type: object
    required: [delegated_by, delegate, actions, valid_from, valid_until]
    properties:
      delegated_by: {type: string, format: uri, description: "Principal granting authority. The DSSE envelope MUST be signed by this Principal's key."}
      delegate: {type: string, format: uri, description: "Principal receiving authority."}
      actions: {type: array, items: {enum: [write:propose, write:accept, write:deprecate, write:retire, write:withdraw, write:tombstone]}}
      resource: {type: object, description: "Scope restriction. May enumerate URIs, kinds, or scope blocks; conjunctive."}
      quorum_credit: {type: integer, minimum: 1, default: 1}
      max_uses: {type: [integer, null], minimum: 1, default: 1}
      valid_from: {type: string, format: date-time}
      valid_until: {type: string, format: date-time}
      redelegation_allowed: {type: boolean, default: false}
  subject_kinds: [core:Principal]
```

The `subject_uri` (in the DSSE envelope) is the *delegate* Principal — the Attestation is "about" the delegate's borrowed authority. G-06 walks the chain.

**`test-result@1.0`** — record a Test execution outcome.

```yaml
spec:
  body_schema:
    type: object
    required: [signer, claimant, outcome, ran_at]
    properties:
      signer: {type: string, format: uri}
      claimant: {type: string, format: uri, description: "Test runner Principal (CI service-account, fuzz runner, contract tester)."}
      outcome: {enum: [pass, fail, skip, error]}
      environment: {type: string, format: uri, description: "Environment URI. Matched against Test.spec.must_pass_in for G-04 to credit the run."}
      release_ref: {type: string, format: uri, description: "URI of the Release this run was performed against."}
      ran_at: {type: string, format: date-time}
      duration_ms: {type: integer, minimum: 0}
      details_ref: {type: string, description: "Optional pointer to detailed results (CI run id, log URL)."}
  subject_kinds: [core:Test]
```

**`realization-update@1.0`** — update a node's derived `realization` value.

```yaml
spec:
  body_schema:
    type: object
    required: [signer, claimant, to_realization, observed_at, claimed_at]
    properties:
      signer: {type: string, format: uri, description: "Principal whose key produced the DSSE signature; informational mirror of the envelope's keyid (envelope is authoritative on conflict)."}
      claimant: {type: string, format: uri, description: "Principal whose authority backs the observation (deploy bot, reconciler, human)."}
      from_realization: {enum: [none, planned, built, running, decommissioned, unknown]}
      to_realization: {enum: [none, planned, built, running, decommissioned]}
      environment: {type: string, format: uri, description: "Optional. The Environment URI in which the new state was observed."}
      observed_at: {type: string, format: date-time, description: "When the underlying observation was made (e.g., the deployment-event timestamp)."}
      claimed_at: {type: string, format: date-time, description: "When the Attestation was signed. Time-travel queries (§15.2) use this field, consistent with all other predicates."}
      evidence:
        type: object
        properties:
          kind: {type: string, description: "deployment-event, build-event, observability-signal, manual, etc."}
          ref: {type: string, description: "Pointer to the underlying record (build SHA, deploy id, signal URL)."}
  subject_kinds: []
```

The `subject_uri` lives in the DSSE envelope (§G.2.1); it is not duplicated in the predicate body. G-08 verifies that `claimant` is authorized to make realization claims about the subject. `observed_at` and `claimed_at` may differ when an observation is recorded after-the-fact (a reconciler signing today an Attestation about a deployment that happened last week).

## G.3 Authorization chains

When a node transitions lifecycle, the validator checks for an Attestation referencing the `approval@1.0` Predicate. As introduced in §G.2, the Attestation has two relevant Principals — **signer** (whose key produced the signature) and **claimant** (whose authority backs the claim). When they differ, the validator must follow a chain that connects them.

**The agent-and-operator setup.** A `Principal` of type `agent` carries an `operator` field pointing at another Principal — typically the human accountable for the agent. Policies can require, for sensitive kinds, that the agent's operator be human. This is the mechanism that lets repos accept agent-signed approvals while still constraining who is ultimately accountable.

Three patterns satisfy the chain — they are all the same mechanism, distinguished only by what the chain looks like:

1. **Direct.** `signer == claimant`. No chain needed.
2. **Messenger.** The agent composed the envelope on behalf of a human, but the human's own credential signed in the moment (a passkey, hardware token, or interactive OIDC flow). `signer == claimant == the human`. The agent's USL URI is recorded in `provenance.generated_by` for audit.
3. **Standing delegation.** The signer's key is theirs (typically an agent); a prior `delegation@1.0` Attestation from the claimant authorizes them to sign on the claimant's behalf. The Approval predicate carries a `delegation_ref` field pointing at the Delegation Attestation. The validator walks the chain.

For multi-hop redelegation, the chain has more than one Delegation; each hop's Delegation must permit further delegation (`redelegation_allowed: true`) and the active Policy must enable it (`allow_redelegation: true`).

The Predicate is the same across all three chain shapes; only the chain differs. The validator's job is: find the chain from `claimant` to `signer`; reject if no valid chain exists.

### G.3.1 Policy authorization rules

A Policy rule defines who may authorize what. The `claimant` block is a single predicate over Principal facts.

```yaml
kind: core:Policy
spec:
  engine: usl-native
  rules:
    # Decisions and Documents: any Approver
    - effect: permit
      action: write:accept
      resource: { kind: [core:Decision, core:Document] }
      claimant: { role: usl://core/governance/approver }
      quorum: 1

    # Sensitive kinds: two distinct Approvers, both human
    - effect: permit
      action: write:accept
      resource: { kind: [core:Release] }
      claimant:
        role: usl://core/governance/approver
        type_required: human
      quorum: 2
      require_distinct_principals: true

    # Policy approvals: agents may approve only if they have a human operator
    - effect: permit
      action: write:accept
      resource: { kind: [core:Policy] }
      claimant:
        role: usl://core/governance/approver
        type_allowed: [human, agent]
        agent_constraint: { operator_type: human }
      quorum: 2                                    # matches the default in §G.3.3 for sensitive kinds
      require_distinct_principals: true
      allow_redelegation: false
```

The `claimant` block carries:

- `role` — required Role URI.
- `type_required` — exact Principal type.
- `type_allowed` — list of permitted Principal types.
- `agent_constraint` — when an `agent` Principal is permitted, additional constraints on the agent (e.g., `operator_type` constrains the operator field on the agent's Principal node).

A Policy MAY also declare per-kind **coverage requirements** alongside its rules. Coverage tracks how many `accepted` instances of a given kind must exist within the Policy's scope:

```yaml
spec:
  engine: usl-native
  applies_to_scope: [usl://core/checkout/main]
  coverage:
    - kind: core:Endpoint
      minimum: 1
      rationale: "Every checkout API requires at least one published Endpoint."
    - kind: core:Test
      minimum: 1
      rationale: "Production-bound services must carry at least one Test."
  rules:
    # ... as above
```

Coverage is enforced by validator G-11 (§G.5): a ChangeSet whose acceptance would drop a covered kind below its `minimum` is flagged. G-11 is a warning by default; Policies wanting strict enforcement set `coverage_severity: error` at the entry level.

### G.3.2 Lifecycle action vocabulary

| Action | Lifecycle move | Predicate |
|---|---|---|
| `write:propose` | (none) → proposed | (none required) |
| `write:commit-changeset` | open → committed (ChangeSet only) | (none required) |
| `write:accept` | proposed → accepted (single node) | `approval@1.0` |
| `write:bundle-accept` | committed → accepted (ChangeSet); credits every member's `write:accept` atomically | `bundle-approval@1.0` |
| `write:deprecate` | accepted → deprecated | `approval@1.0` |
| `write:retire` | accepted/deprecated → retired | `approval@1.0` |
| `write:withdraw` | proposed → retired (latest Attestation is a Withdrawal — recoverable from history; powers `withdrawn(n)`) | `withdrawal@1.0` |
| `write:reject-changeset` | committed → rejected (ChangeSet); members are auto-withdrawn with the bundle's rationale | `withdrawal@1.0` |
| `write:tombstone` | any → tombstoned | `tombstone@1.0` |
| `write:request-changes` | (no lifecycle move; blocks the issuer's own approvals on the subject) | `request-changes@1.0` |
| `write:comment` | (no lifecycle move; threaded discussion) | `comment@1.0` |

Realization updates are not lifecycle actions — they go through `realization-update@1.0` Attestations, governed by G-08 if active. `write:request-changes` and `write:comment` are non-lifecycle actions and do not advance state; their authorization is checked at write time as described in §G.2.3 and §G.2.4.

### G.3.3 Default rules

If no Policy is authored, the validator applies sensible defaults:

- `write:propose` requires Author role; no Approval Attestation.
- `write:accept` requires 1 Approval from any Approver.
- `write:accept` for `core:Release`, `core:Policy` requires 2 distinct Approvals.
- `write:deprecate` and `write:retire` require the same approval level as the original `write:accept`.
- `write:withdraw` requires a Withdrawal Attestation signed by either the original Author or any Approver.
- `write:tombstone` requires 2 distinct Approvers and a documented `reason` in the Tombstone predicate.
- `write:request-changes` requires the Role that authorizes the lifecycle action being blocked. To block a `write:accept`, the issuer MUST hold the Approver role (an Author's would-be block is vacuous and is rejected at write time).
- `write:comment` requires the Author role at minimum; comments on private/tenant-scoped subjects follow the subject's scope visibility (§8.2).

Repositories tighten these by declaring an explicit Policy.

### G.3.4 Revocation of Approvals

If an Approval is revoked, the node's lifecycle does NOT automatically revert — the original transition timestamp stands as a historical fact — but tools display the node with a warning, and the next non-trivial edit MUST re-acquire approvals.

## G.4 ChangeSets and substrate adapters

A `core:ChangeSet` groups proposed changes for atomic review. ChangeSets are opt-in; most repos rely on the substrate adapter (§G.4.2) so they never author one directly.

```yaml
kind: core:ChangeSet
spec:
  status: open                               # open | committed | accepted | rejected
  members:
    - usl://core/checkout/0042-payment-strategy
    - usl://core/payments/charge-policy
  transitions:
    - target: usl://core/payments/legacy-charge-policy
      from_lifecycle: accepted
      to_lifecycle: deprecated
      reasoning: "Replaced by 0042"
      subject_content_hash: "sha256:..."     # JCS-canonical hash per §9.6
  rationale: |
    Replaces ad-hoc charge logic with idempotent capture per Decision 0042.
```

**Status state machine.** ChangeSet status moves through four states:

- `open` — members and transitions can still be added or removed; reviewers may comment but cannot approve.
- `committed` — the author has sealed the bundle for review (action `write:commit-changeset`). Membership and transitions become immutable. Approvers can now sign `bundle-approval@1.0` or `withdrawal@1.0`.
- `accepted` — an Approver signed `bundle-approval@1.0` (action `write:bundle-accept`). The validator atomically applies all member additions and all non-stale staged transitions.
- `rejected` — an Approver signed `withdrawal@1.0` against the bundle (action `write:reject-changeset`). Every member is auto-withdrawn (lifecycle becomes `retired` with the bundle's `rationale` propagated to each member's withdrawal-history); staged transitions are discarded; existing accepted nodes are unchanged.

**Membership uniqueness.** A `proposed` node belongs to at most one `open` ChangeSet (validator G-03).

**Optimistic-lock semantics.** A staged transition captures `from_lifecycle` at staging time. At apply time, the validator verifies the target's *current* lifecycle equals the captured `from_lifecycle`; on mismatch, the transition is marked stale and skipped. The bundle's other operations apply normally; stale transitions are reported.

**Reasoning binding.** Each staged transition's `subject_content_hash` is computed via the §9.6 JCS canonicalization over `(target, from_lifecycle, to_lifecycle, reasoning, this ChangeSet's URI)`. The Bundle-Approval signature carries the same hash; if reasoning is edited between staging and approval, the hash mismatches and the transition is rejected.

### G.4.1 Two Approval Predicates, not one with a discriminator

Single-node approval and bundle approval are distinct Predicates:

- `approval@1.0`: subject is a single node. Crediting is direct.
- `bundle-approval@1.0`: subject is a ChangeSet. Crediting flows to every node in the ChangeSet's `spec.members[]`.

The validator (G-01) reads the predicate URI to know which crediting model to use.

### G.4.2 Substrate adapter (recommended default)

Most repositories use git PRs as their unit of review. The Governance module ships a substrate adapter that maps git PRs to implicit ChangeSets at validation time, so authors never need to write a `ChangeSet` node directly:

- The PR's added `proposed` nodes become the ChangeSet's `members[]`.
- The PR's lifecycle transitions on existing nodes (i.e., new Attestations) become the ChangeSet's `transitions[]`.
- The PR description becomes the ChangeSet's `rationale`.
- The PR moving from draft to "ready for review" moves the ChangeSet's status from `open` to `committed` (action `write:commit-changeset`).
- A reviewer's signed approval (a signed commit on a `pr-approval` ref, or a Sigstore-signed PR review) becomes a `bundle-approval@1.0` Attestation against the implicit ChangeSet.
- The PR's merge commit moves the status from `committed` to `accepted` (action `write:bundle-accept`); G-11 evaluates against this transition.
- A PR closed without merging produces a `withdrawal@1.0` Attestation against the bundle, moving status to `rejected` (action `write:reject-changeset`).

The adapter's mapping is declared in `usl.yaml`:

```yaml
governance:
  substrate_adapter: git-pr           # or "none" to disable
  unattached_writes_mode: lenient     # or strict
```

Repositories that need cross-substrate or cross-repo bundling (the genuine use cases for explicit ChangeSets) author them directly. The substrate adapter and explicit ChangeSets coexist; a single workflow may use either.

### G.4.3 Unattached writes — strict vs lenient mode

When an Author writes a `proposed` node without attaching it to any ChangeSet (and the substrate adapter is disabled or doesn't fire), the repo's configured `unattached_writes_mode` determines what happens.

**Strict mode.** The write is refused. Every `write:propose` MUST be attached to an explicit open ChangeSet or covered by the substrate adapter.

**Lenient mode** (default). The write succeeds and is auto-attached to a session-scoped synthetic ChangeSet keyed by the agent's MCP session token. The synthetic ChangeSet is opened automatically on the first unattached write of a session and committed implicitly when the session ends, becoming a proposed bundle awaiting approval like any other.

## G.5 Governance invariants

Governance adds eleven invariants. They run only when the Governance module is active. Each is specified algorithmically.

```
G-01  insufficient-approvals (error)
  for each node n where lifecycle(n) ∈ {accepted, deprecated, retired}:
    direct = Attestations on n with predicate_uri=approval@1.0
                 and predicate.to_lifecycle = lifecycle(n)
    bundle = Attestations on any ChangeSet cs where
                 cs.spec.status = 'accepted'
                 and n.uri ∈ cs.spec.members
                 and predicate_uri = bundle-approval@1.0
                 and predicate.to_lifecycle = lifecycle(n)
    if size(direct ∪ bundle) < required_approvals_for(n):
      emit (n.uri, lifecycle(n), required, actual, 'insufficient-approvals')

G-02  authorization-chain-invalid (error)
  for each Attestation a where a.predicate_uri = approval@1.0
                            and a.predicate.signer ≠ a.predicate.claimant:
    if not authorization_chain_valid(a):
      emit (a.uri, 'authorization-chain-invalid')

G-03  changeset-membership-collision (error)
  for each proposed node n:
    if more than one open ChangeSet has n in spec.members:
      emit (n.uri, 'changeset-membership-collision')

G-04  release-blocked-by-test (error)
  for each Release r where lifecycle(r) = 'accepted':
    for each Component c with r-[:contains]->c:
      for each Test t with t-[:evidence-for]->c
                       and t-[:references {relationship: must-pass-in}]->env:
        if no test-result Attestation exists where:
            target = t, environment = env, outcome = 'pass', release_ref = r:
          emit (r.uri, c.uri, t.uri, env.uri, 'release-blocked-by-test')

G-05  revoked-attestation-active (warn)
  for each Attestation a where exists Attestation rev with
        rev.predicate_uri = revocation@1.0 and rev-[:evidence-for]->a:
    emit (a.uri, rev.uri, 'revoked-attestation-active')

G-06  delegation-not-in-force (error)
  for each Attestation a with predicate.delegation_ref set:
    walk chain from a back to claimant; for each Delegation d in chain:
      if d.valid_until < a.predicate.claimed_at: fail
      if a action ∉ d.actions: fail
      if a subject ∉ d.resource: fail
      if d.max_uses exceeded: fail
    if any failure: emit (a.uri, 'delegation-not-in-force')

G-07  agent-operator-mismatch (error)
  for each Attestation a where a.predicate_uri = approval@1.0:
    signer = principal_by_uri(a.predicate.signer)
    if signer.spec.type = 'agent' and policy_for(a).agent_constraint.operator_type set:
      operator = principal_by_uri(signer.spec.operator)
      if operator.spec.type ≠ required_type:
        emit (a.uri, signer.uri, required_type, actual_type, 'agent-operator-mismatch')

G-08  realization-claim-unauthorized (error)
  for each Attestation a where a.predicate_uri = realization-update@1.0:
    claimant = principal_by_uri(a.predicate.claimant)
    if not realization_claim_authorized(claimant, a.subject, a):
      emit (a.uri, claimant.uri, a.subject.uri, 'realization-claim-unauthorized')

G-09  tombstone-quorum (error)
  for each subject node s where any Tombstone Attestation targets s:
    let tombstones = all Tombstone Attestations whose evidence-for edge points at s
    if count(distinct tombstones[*].predicate.claimant) < 2:
      emit (s.uri, 'tombstone-quorum-not-met')
    if any tombstones[*].predicate.reason is empty:
      emit (tombstones[i].uri, 'tombstone-missing-reason')

  Crediting model: a node moves to lifecycle('tombstoned') only when at least two
  Tombstone Attestations from distinct claimants have been signed against it. A
  single Tombstone Attestation against a subject is staged but not yet effective
  (the resolver still returns the node body). The two-claimant requirement is a
  structural quorum on the predicate, not a chain of approving Attestations.

G-10  approval-blocked-by-request-changes (error)
  for each Attestation a where a.predicate_uri = approval@1.0:
    blocking = Attestations rc where:
      rc.predicate_uri = request-changes@1.0
      and rc.predicate.claimant = a.predicate.claimant
      and (rc.subject = a.subject
           OR (a.subject is a ChangeSet member and rc.subject = the ChangeSet)
           OR (a.subject is a ChangeSet and rc.subject ∈ a.subject.spec.members))
      and rc.predicate.blocks_lifecycle = a.predicate.to_lifecycle
      and rc is not revoked
      and no later Attestation x from rc.predicate.claimant on the same subject
          where x.claimed_at > rc.claimed_at and x.predicate_uri = approval@1.0
    if blocking is non-empty:
      emit (a.uri, blocking[0].uri, 'approval-blocked-by-request-changes')

G-11  coverage-preservation (warn)
  for each ChangeSet cs where cs.spec.status moves from 'committed' to 'accepted':
    let policy = policy_for(cs)                                 (§G.3.1)
    if policy.spec.coverage is empty: skip
    for each entry e in policy.spec.coverage where e.minimum >= 1:
      before = count of nodes n where:
        kind(n) = e.kind
        and lifecycle(n) = 'accepted'
        and scope(n) ∈ policy.applies_to_scope
      after = before
        + count of cs.spec.members nodes m where kind(m) = e.kind
                                              and m's transition lands at 'accepted'
        - count of cs.spec.transitions t where:
            kind(target(t)) = e.kind
            and target(t).lifecycle = 'accepted' (current)
            and t.to_lifecycle ∈ {deprecated, retired, tombstoned}
            and target(t) ∉ cs.spec.members              (not replaced in same bundle)
      if before >= e.minimum and after < e.minimum:
        emit (cs.uri, e.kind, before, after, e.minimum, 'coverage-preservation')
```

The named procedures (`authorization_chain_valid`, `required_approvals_for`, `realization_claim_authorized`, `policy_for`, `principal_by_uri`) are defined as part of this spec at `https://w3id.org/usl/v0.9/procedures/governance.md`. Each is a function with declared inputs, outputs, and semantics; implementations render them in their chosen language.

Implementations SHOULD provide a dry-run mode (`usl validate --dry-run [--accept <changeset-uri>]`) that runs the invariant pack against the proposed-as-if-accepted graph and reports the delta. Reviewer surfaces SHOULD additionally surface the **blast-radius summary**: count of currently-accepted nodes within one hop of the bundle's members, plus mandatory-kind coverage delta.

---

## Module 3 — Federation

**Mental model: Go modules with content-hash pinning.** A repo declares which other repos it depends on, the resolver fetches and content-hash-verifies the imported nodes, and a lockfile makes the resolution reproducible. Federation is **pull-model**: the child declares what it needs; the parent does not know who depends on it. Without Federation, a repo's graph is self-contained.

```yaml
# usl.yaml
modules: [core, federation]
```

## F.1 Repo identity

A federation-participating repo declares an identity:

```yaml
repo:
  uri: usl://acme.platform                         # used as <vocab> for nodes published by this repo
  display_name: "ACME Platform Specs"
  signing_keys:
    - kid: did:key:z6MkRepoKey...
      jwk: { kty: OKP, crv: Ed25519, x: "..." }
  current_version: "2026.04.17"
  versioning_scheme: calendar                      # "semver" | "calendar"
  content_hash: "sha256:abc123..."
  extend_content_hash: "sha256:def456..."          # optional; over the extend-tier bundle
```

The repo URI doubles as the `<vocab>` segment in URIs published by this repo. So a Component named `charge-engine` in namespace `billing` published by `usl://acme.platform` has URI `usl://acme.platform/billing/charge-engine`. Cross-repo references are normal URIs.

The `content_hash` is computed deterministically:

1. Gather every node where `lifecycle ≠ tombstoned` and `visibility ≠ private`.
2. Project to `{uri, version_id, kind, scope}`.
3. Sort by `uri`.
4. Serialize with RFC 8785 (JCS).
5. Hash with SHA-256; prefix with `sha256:`.

Two clones of the same repo at the same commit MUST produce the same `content_hash`. Repos that publish both a public bundle and an internal bundle (extend-tier) compute one hash per tier.

Repos that do not participate in federation MAY omit the `repo:` block entirely.

## F.2 Declaring parent repos

A child repo lists parents:

```yaml
extends:
  - repo: usl://acme.platform
    alias: platform                              # authoring shorthand only; does not appear in canonical URIs
    version: "2026.04"
    sha256: "sha256:abc123..."
    location: "https://github.com/acme/platform-specs"
    branch: main
    auth: env:GITHUB_TOKEN

    mode: pin                                    # pin | floating | snapshot

    include:
      kinds: [API, Endpoint, Schema, Policy]
      namespaces: ["billing/**", "subscriptions/**"]
    exclude:
      uris:
        - usl://acme.platform/legacy/deprecated-format

    trust:
      signed_by: [did:web:acme.com:platform-team]
      attested_at_or_after: "2026-01-01"
      required_attestations:
        - usl://core/governance/predicate/approval@1.0

    sovereignty:
      overridable_kinds: [Policy]
      readonly_kinds: [Schema, API]
      max_redelegation_hops: 0

    visibility_filter: read                      # "read" | "extend"
```

Multiple parents are permitted. Order is significant: same-name conflicts resolve CSS-cascade-style (earlier wins, like CSS stylesheet ordering).

## F.3 Pin modes

- **`pin`** (default; reproducible). The resolver pins to the version that satisfied `version:` at lockfile-generation time. Same lockfile → same federated graph forever.
- **`floating`**. The resolver always resolves to the parent's `current_version` at validation time. Useful for org-internal trust where freshness matters more than reproducibility.
- **`snapshot`**. A one-time copy is taken at the version satisfying `version:` and the live link is severed.

## F.4 Cross-repo URIs

Cross-repo references use the parent repo's URI directly as the `<vocab>` position:

| Form | Meaning |
|---|---|
| `usl://core/auth/v1` | A Core-vocab node in this repo (or a federated parent that owns the `core` vocab — typically only the canonical USL spec) |
| `usl://acme.platform/billing/charge-engine` | A node published by the `acme.platform` repo |

The alias declared in `extends:` is authoring shorthand only — never persisted. Canonical URIs (in node files, edges, lockfile, and Attestations) always use the parent repo's full identifier; round-tripping a cross-repo URI never requires the lockfile. Aliases do not appear as a CLI sugar in v0.9.

## F.5 Inheritance

Cross-repo inheritance uses the same `extends` edge defined in §5. Both `attributes.merge: reference` and `attributes.merge: deep` work identically across repo boundaries — the resolver fetches the parent node first, then §5's merge rules apply.

Tooling may serialize the effective view for inspection: `usl resolve <uri> --effective`. The deep-merge result is cached per `(child node version, lockfile state)` and invalidated on lockfile change.

## F.6 Conflict resolution

When a child node has the same `(vocab, namespace, name)` as a node imported from a parent:

| Parent flag | Consumer config | Behavior |
|---|---|---|
| (default) | (default) | Child wins; warn `federated-name-shadowed`. |
| `final: true` on parent | (any) | Child errors (`federated-final-shadowed`, F-02). |
| (any) | `readonly_kinds: [...]` includes the kind | Child errors (`federated-readonly-shadowed`, F-02). |
| `abstract: true` on parent | child uses `extends` with `merge: deep` | Effective view computed normally. |
| `abstract: true` on parent | (no extends) | Parent omitted from effective graph; warn `federated-abstract-not-extended`. |

**Multi-parent conflicts.** Earlier `extends:` entry wins (CSS-cascade ordering). Info-level diagnostic records what was considered. Losing nodes remain accessible via the explicit cross-repo URI.

**Effective-view caching.** The deep-merge result is cached per `(child node version, lockfile state)`. Implementations MAY persist the effective view in `.usl/cache/effective/<sha256>/`; the cache is invalidated on lockfile change.

## F.7 Visibility tiers

A node's `visibility` field (§4.2) controls federation export:

- `public` — exported in any federated bundle.
- `internal` — exported only to consumers using `visibility_filter: extend`.
- `private` — never exported; downstream references resolve to "node not found."

A parent that uses `visibility: internal` publishes two bundles (read-tier and extend-tier) and two content hashes. Consumers pin to the appropriate hash via `visibility_filter`.

## F.8 Lockfile

`usl.lock` records the exact resolved versions and hashes:

```yaml
schema_version: "0.5"
generated_at: "2026-05-01T10:00:00Z"
generated_by: "usl-cli/0.9.0"

extends:
  - repo: usl://acme.platform
    alias: platform
    requested_version: "2026.04"
    resolved_version: "2026.04.17"
    resolved_sha256: "sha256:abc123..."
    resolved_at: "2026-05-01T10:00:00Z"
    location: "https://github.com/acme/platform-specs"
    keys_verified:
      - did:key:z6MkRepoKey...
    node_count: 1283
```

Validators MUST refuse to run if `usl.yaml` and `usl.lock` are out of sync (validator F-04). The lockfile is regenerated explicitly by `usl federation update` (analogous to `npm update`); never silently mid-validation.

## F.9 Resolution algorithm

For `usl://acme.platform/billing/charge-engine`:

1. Look up `acme.platform` in `usl.lock` → `resolved_version`, `resolved_sha256`, `location`.
2. Check the local cache (`.usl/cache/<sha256>/`); fetch from `location` if absent.
3. Verify the bundle's `content_hash` equals `resolved_sha256`. Fail loudly on mismatch.
4. Verify the bundle's signature against at least one key in the parent's declared `signing_keys`.
5. Look up the requested node within the fetched bundle.
6. Apply consumer-side trust filters (`signed_by`, `attested_at_or_after`, `required_attestations`); reject and record in `federated_trust_failures` if any fail.
7. Inject `_runtime.federated_from` and `_runtime.federated_version_id` into the returned node (§10).
8. Return read-only.

Every step is deterministic. Same lockfile + same caches → same resolved nodes, every time.

## F.10 Federation invariants

Federation adds five invariants:

```
F-01  federated-reference-unresolved (error)
  for each edge (n)-[r]->(target) where target's vocab is a federated parent:
    if target ∉ resolved_federated_uris:
      emit (n.uri, r.kind, target, 'federated-reference-unresolved')

F-02  federated-shadowing-forbidden (error)
  for each child node c, parent node p where (vocab, namespace, name) match
                                          and c is local
                                          and p is federated:
    if p.final = true: emit (c.uri, p.uri, 'federated-final-shadowed')
    elif consumer.readonly_kinds includes kind(p): emit (c.uri, p.uri, 'federated-readonly-shadowed')

F-03  federation-trust-violation (error)
  for each entry in federated_trust_failures:
    emit (entry.node_uri, entry.reasons, 'federation-trust-violation')

F-04  lockfile-out-of-sync (error)
  if usl.yaml.extends and usl.lock.extends disagree on:
    repo set, alias, version satisfaction, or pinned sha256:
    emit ('lockfile-out-of-sync')

F-05  retired-federation-reference (error)
  for each local edge (n)-[r]->(target) where target is federated
                                          and lifecycle(target) = 'retired':
    emit (n.uri, target.uri, 'retired-federation-reference')
```

**Lifecycle propagation across federation.** When a parent transitions a node's lifecycle, the consumer sees it the next time `usl federation update` runs. `pin` mode requires explicit re-pinning; `floating` mode follows. The validator emits info / warn / error per the source transition: `accepted → deprecated` is `warn` (`federated-source-deprecated`), `→ retired` triggers F-05, and `→ tombstoned` triggers F-05 likewise (the tombstoned reference becomes an I-08 in the consumer's local graph).

**Tag changes** (e.g., a parent adding `experimental` to a node's tags) are not lifecycle transitions and do not produce diagnostics; consumers see the new tag on the next `usl federation update` and the default-canonical view (§5.5) reflects it.

**Federation CLI verbs.**

| Command | Description |
|---|---|
| `usl federation list` | Show every parent in `extends:` with alias, mode, resolved version, content hash, node count. |
| `usl federation refresh` | Re-fetch and re-validate `floating`-mode federations; verify `pin`-mode against caches; report drift. Does not modify the lockfile. |
| `usl federation diff <alias>` | Show what changed in the parent since the last update. |
| `usl federation upgrade <alias> --to <version>` | Bump a `pin`-mode federation explicitly. Re-resolves, re-verifies signatures, regenerates the lockfile entry. |
| `usl federation impact <alias>` | Report every local node that references nodes from the named parent. Used for upgrade blast-radius analysis. |
| `usl federation update [<alias>]` | Re-resolve all (or one) federation parents and rewrite `usl.lock`. |

These verbs are exposed as MCP tools at `usl.federation.list`, `usl.federation.refresh`, etc. — discoverable via `list_tools`.

---

## Module 4 — Embeddings

Embeddings is normative when active: implementations MUST use the canonical projection so cross-implementation retrieval is comparable. Implementations remain free to pick the embedding model and vector store, but the input bytes are pinned by the spec.

```yaml
# usl.yaml
modules: [core, embeddings]
```

## E.1 The `usl.search` procedure

Every implementation activating Embeddings exposes:

```
usl.search(query: string, limit: int, filter: optional map) → stream of (node, score)
```

- `query` — natural-language input.
- `limit` — maximum results.
- `filter` — optional structural filter applied alongside vector search (`{lifecycle: ['accepted'], scope: <uri>, kinds: ['core:API']}`).
- Returns `(node, score)` pairs sorted by relevance. Score is in `[0, 1]` for cosine similarity (or `[0, ∞)` for L2 distance, depending on the implementation's choice).

When `usl.search` cannot run (e.g., layer-1-only file walk with no vector index), it MUST fall back to substring text search over the canonical projection text and document the degradation.

## E.2 Canonical text projection (normative)

Every implementation MUST produce the projection input as:

```
embed_input(node) =
    f"{node.kind}\n"
  + f"{node.uri}\n"
  + f"{node.description}\n"
  + f"tags: {', '.join(sorted(node.tags))}\n"
  + f"body:\n{node.body[:8192]}\n"
```

Tags are sorted to ensure deterministic input. Body truncation at exactly 8192 characters is normative. Larger bodies require chunking, with chunked embeddings stored under `<uri>:chunk:<n>` keys. Re-embedding triggers when `kind`, `description`, `tags`, or `body[:8192]` change.

Implementations diverging from this projection are non-conformant. Cross-implementation similarity-score numerics are still implementation-dependent (different models, different normalization), but the input bytes are identical.

### E.2.1 Index manifest

A required `index.yaml` at the repository root declares:

```yaml
embedding:
  model: text-embedding-3-large
  dimensions: 3072
  primary_lang: en
  body_truncation: 8192
  projection: canonical                   # MUST be 'canonical' for v0.9 conformance
  chunking:
    enabled: false
  vector_store: pgvector://...
```

Tools producing embeddings record the projection used alongside the vector. Consumers MUST verify the manifest declares `projection: canonical` before treating the index as conformant.

## E.3 AI-agent retrieval patterns

Three patterns, expressed as openCypher with the `usl.search` procedure.

**Pattern A — semantic-first with scope filter.**

```cypher
CALL usl.search($user_question, limit: 30) YIELD node, score
WHERE node.scope = $context_uri
  AND lifecycle(node) = 'accepted'
RETURN node.uri, node.kind, node.description, score
ORDER BY score DESC LIMIT 10
```

**Pattern B — anchored traversal.**

```cypher
MATCH (anchor {uri: $anchor_uri})
MATCH (anchor)<-[:traces-to|implements|governs|evidence-for|references*1..3]-(related)
WHERE lifecycle(related) = 'accepted'
RETURN related.uri, related.kind, related.description LIMIT 50
```

**Pattern C — hybrid (vector + structural filter).**

```cypher
CALL usl.search('threats in checkout', limit: 50) YIELD node, score
WHERE node.kind = 'standard-extensions:Threat'
  AND node.scope = $checkout_context
  AND NOT EXISTS {
    MATCH (node)<-[g:governs]-(c)
    WHERE c.kind = 'standard-extensions:Control'
      AND g.attributes.relationship = 'mitigates'
      AND lifecycle(c) = 'accepted'
  }
RETURN node.uri, node.description, score
ORDER BY score DESC
```

(Note: `Threat` and `Control` are loaded via the Standard Extensions Bundle, whose vocabulary prefix is `standard-extensions`.)

**Pattern D — context assembly with time-travel.**

```cypher
CALL usl.search($question, limit: 20) YIELD node, score
WHERE lifecycle(node, at: $reference_time) = 'accepted'
  AND node.scope IN $allowed_contexts
WITH node, score,
     duration.between(node.updated_at, datetime()).days AS staleness_days
RETURN node.uri, node.kind, node.description, node.body,
       (score * 1.0) - (staleness_days * 0.001) AS rank
ORDER BY rank DESC LIMIT 8
```

---

## Projection registry

The Projection Registry lives at `https://w3id.org/usl/v0.9/projections/registry.md`. It catalogs bidirectional adapters between USL kinds and external native formats and is **not** part of this specification. Adapters are versioned and attested as USL `core:ProjectionAdapter` extension nodes.

The contract every adapter follows:

- **Round-trip semantics.** USL → native → USL preserves `uri`, `kind`, `version`, `description`, `tags`, edge attributes, and extension fields. Loss is reported as a `LossReport` Document node.
- **Adapter identity.** Each adapter is itself a USL node so it can be versioned, owned, and attested.

A **Tier 1** set of one-way exports SHIPS with conformant `usl` CLIs:

| Native target | From USL kind | Use |
|---|---|---|
| Mermaid | `core:Document`, extension `core:Diagram` | Rendering diagrams. |
| Markdown (Diátaxis, MADR) | `core:Document`, `core:Decision` | Building docs sites; MADR for architecture decisions. |
| JSON Schema 2020-12 | `core:Schema` (`format: json-schema`) | Runtime validators. |
| OpenAPI 3.1 | `core:API` (`protocol: rest`) + `core:Endpoint` children | Code generation, gateways. |
| BPMN 2.0 XML | extension `Process` (`type: bpmn`) | Process engines (loads Standard Extensions Bundle). |
| CycloneDX | extension `BOM` | Supply-chain ingestion. |

Bidirectional and vertical adapters (FHIR, OPC UA, OSCAL, Promptfoo, OpenLineage, etc.) live in the registry and are opt-in.

---

## Worked example: a small order-tracking repo

A complete, validating USL repo covering Core + Governance + Embeddings. The example illustrates how lifecycle and realization derive from Attestations, how Predicates are reused across signal types, and how queries compose.

### Repository layout

```
usl.yaml
nodes/
  core/
    order-tracking/
      main.yaml                                 # the BoundedContext
      0042-idempotent-capture.md                # Decision
      sub-second-updates.md                     # Goal
      order-tracker.yaml                        # Component
      order-tracker-api.yaml                    # API
      get-order.yaml                            # Endpoint
      order-status.yaml                         # Schema
      smoke-get-order.yaml                      # Test
      team-orders.yaml                          # Principal
    infra/
      prod.yaml                                 # Environment
    governance/
      default-approval.yaml                     # Policy
attestations/
  sha256-approval-0042.../envelope.dsse
  sha256-realization-tracker-running.../envelope.dsse
  sha256-test-result-smoke.../envelope.dsse
```

### `usl.yaml`

```yaml
usl_version: "0.9"
modules: [core, governance, embeddings]
primary_lang: en
vocabularies:
  - usl://core/vocab/core@0.9
governance:
  substrate_adapter: git-pr
  unattached_writes_mode: lenient
embedding:
  model: text-embedding-3-large
  projection: canonical
```

### A Decision (Markdown frontmatter)

```markdown
---
uri: usl://core/order-tracking/0042-idempotent-capture
kind: core:Decision
version: "2026.04.15"
scope: usl://core/order-tracking/main
description: Use idempotent capture for all order-status writes; guarantees retry safety.
tags: [architecture, idempotency]
spec: { type: architecture }
relations:
  - kind: references
    target: usl://core/order-tracking/team-orders
    attributes: { relationship: owned-by }
  - kind: traces-to
    target: usl://core/order-tracking/sub-second-updates
  - kind: governs
    target: usl://core/order-tracking/order-tracker
    attributes: { relationship: enforces }
created_at: 2026-04-15T10:00:00Z
updated_at: 2026-04-29T11:30:00Z
---

# 0042. Idempotent capture for order status

## Context
[…]

## Decision
[…]

## Consequences
[…]
```

(`lifecycle` is derived from the Approval Attestation shown below; it is not authored on the node.)

### A Component

```yaml
# nodes/core/order-tracking/order-tracker.yaml
uri: usl://core/order-tracking/order-tracker
kind: core:Component
version: "2.1.0"
scope: usl://core/order-tracking/main
description: Backend service that persists and serves order-status updates.
spec:
  type: service
  language: typescript
relations:
  - kind: references
    target: usl://core/order-tracking/team-orders
    attributes: { relationship: owned-by }
  - kind: implements
    target: usl://core/order-tracking/order-tracker-api
  - kind: implements
    target: usl://core/order-tracking/get-order
created_at: 2026-04-01T00:00:00Z
updated_at: 2026-04-29T18:22:01Z
```

(Realization is derived from the RealizationUpdate Attestation shown below; the deployment relationship to the prod Environment lives in that Attestation.)

### An API and its Endpoint

```yaml
# nodes/core/order-tracking/order-tracker-api.yaml
uri: usl://core/order-tracking/order-tracker-api
kind: core:API
version: "1.0.0"
scope: usl://core/order-tracking/main
description: REST API exposing order-status reads.
spec:
  protocol: rest
  base_path: /v1/orders
relations:
  - kind: contains
    target: usl://core/order-tracking/get-order
created_at: 2026-04-01T00:00:00Z
updated_at: 2026-04-15T10:00:00Z
```

```yaml
# nodes/core/order-tracking/get-order.yaml
uri: usl://core/order-tracking/get-order
kind: core:Endpoint
version: "1.0.0"
scope: usl://core/order-tracking/main
description: GET /orders/{id} — returns the current status of an order.
spec:
  style: sync
  method: GET
  path: /orders/{id}
  responses:
    "200": { content_type: application/json, schema_ref: usl://core/order-tracking/order-status }
created_at: 2026-04-01T00:00:00Z
updated_at: 2026-04-15T10:00:00Z
```

### Two Attestations (sketch — full DSSE envelopes elided)

A signed Approval moving the Decision to `accepted`:

```yaml
uri: usl://core/order-tracking/approve-0042
kind: core:Attestation
version: "sha256:abc123..."
spec:
  predicate_uri: usl://core/governance/predicate/approval@1.0
  predicate:
    signer: usl://core/order-tracking/alice
    claimant: usl://core/order-tracking/alice
    to_lifecycle: accepted
    claimed_at: 2026-04-29T11:30:00Z
relations:
  - kind: evidence-for
    target: usl://core/order-tracking/0042-idempotent-capture
    attributes: { evidence_kind: attests }
```

A signed RealizationUpdate from the deploy bot:

```yaml
uri: usl://core/order-tracking/tracker-running-2026-04-29
kind: core:Attestation
version: "sha256:def456..."
spec:
  predicate_uri: usl://core/governance/predicate/realization-update@1.0
  predicate:
    signer: usl://core/platform/deploy-bot
    claimant: usl://core/platform/deploy-bot
    from_realization: built
    to_realization: running
    environment: usl://core/infra/prod
    observed_at: 2026-04-29T18:22:01Z
    claimed_at: 2026-04-29T18:22:01Z
    evidence: { kind: deployment-event, ref: "k8s://prod/order-tracker@deploy-2026-04-29-1822" }
relations:
  - kind: evidence-for
    target: usl://core/order-tracking/order-tracker          # subject is the edge target; not duplicated in the predicate body
    attributes: { evidence_kind: deployment }
```

### Sample queries

```cypher
// What's accepted in the order-tracking context, and what's actually running?
MATCH (n)
WHERE n.scope = 'usl://core/order-tracking/main'
  AND lifecycle(n) = 'accepted'
RETURN n.kind, n.uri, realization(n)
ORDER BY n.kind, n.uri

// Components in the bundle that are spec'd but not built
MATCH (c:Component)
WHERE c.scope = 'usl://core/order-tracking/main'
  AND spec_only(c)
RETURN c.uri

// "Is the order-tracker API live?" — walks to the implementing Component
MATCH (api:API {uri: 'usl://core/order-tracking/order-tracker-api'})
RETURN running(api) AS live    // true: order-tracker has realization=running

// What did this look like a week ago?
MATCH (c:Component {uri: 'usl://core/order-tracking/order-tracker'})
RETURN realization(c, at: '2026-04-22T00:00:00Z') AS state_then,
       realization(c) AS state_now

// What does Decision 0042 govern, and what attests it?
MATCH (d:Decision {uri: 'usl://core/order-tracking/0042-idempotent-capture'})
OPTIONAL MATCH (d)-[g:governs]->(governed)
OPTIONAL MATCH (d)<-[:evidence-for]-(att:Attestation)
RETURN d.uri,
       collect(DISTINCT governed.uri) AS governed_nodes,
       collect(DISTINCT att.spec.predicate_uri) AS predicates
```

### What this example demonstrates

- **Lifecycle and realization are derived.** The Decision shows `lifecycle: accepted` only after the Approval Attestation is signed; the Component shows `realization: running` only after the RealizationUpdate Attestation. Neither is authored on the node.
- **Predicates are nodes.** Both Attestations reference `usl://core/governance/predicate/approval@1.0` and `usl://core/governance/predicate/realization-update@1.0` — regular Predicate nodes resolved through the standard mechanism.
- **Attestations carry both authorization and observation.** The same DSSE envelope shape carries the Approval (governance signal) and the RealizationUpdate (operational signal), differentiated by predicate URI.
- **Edges are sparse.** Five well-known edge kinds carry the whole graph: `governs`, `traces-to`, `implements`, `contains`, `evidence-for`.
- **Queries compose.** `running(api)` walks one hop; `spec_only(c)` reads two derived values; questions like "what's spec'd but not built?" and "what did this look like a week ago?" are single-line Cypher.

---

## Appendix A — Editorial principle (for spec maintainers)

This document is maintained as a final specification at all times. Edits replace, they do not annotate. Old text is removed when superseded; new text takes its place in the same location. The document contains no "was / becomes" tables, no migration logs, and no references to prior drafts. Anyone reading the spec at any moment sees it as if no other version ever existed. Change history lives in version control.

This rule is normative for everyone editing the spec, including AI agents.

---

## Glossary

- **Attestation** — A signed Attestation node carrying a DSSE envelope that references a Predicate. One uniform shape across all predicate types.
- **Attribution vocabulary** — A schema URI named on a node's `provenance.attribution.schema` field. Vocabularies live in extensions; Core ships none by default.
- **Authorization chain** — The connection between an Attestation's signer and claimant when they differ. Three patterns satisfy: direct, messenger, delegation.
- **Blast-radius summary** — The reviewer-facing report on what changes if a ChangeSet is signed.
- **Bounded Context** — DDD term and USL primary scoping unit; a region of consistent terminology and rules.
- **ChangeSet** — Opt-in atomic-approval bundle. Most repos use the substrate adapter (e.g., git PRs) instead of authoring ChangeSets directly.
- **Claimant** — The Principal whose authority backs an Attestation's claim. Distinct from signer (who produced the cryptographic signature).
- **Comment** — A threaded discussion entry attached to a node or ChangeSet, expressed as an Attestation referencing the `comment@1.0` Predicate. Threading via the optional `in_reply_to` field; corrections via revocation or supersession, not in-place edit.
- **Coverage requirement** — A Policy-declared minimum count of `accepted` instances for a given kind within scope. Enforced by validator G-11 — a ChangeSet that would drop a covered kind below its minimum is flagged.
- **Core** — The mandatory USL module: 18 kinds, 9 edges, 8 invariants, plus the canonical node shape, lifecycle, realization, schema validation, query, CLI/MCP baseline.
- **Decision** — A canonical core kind capturing a reasoned commitment to a course of action. Subsumes the ADR pattern via `spec.type: architecture`.
- **Default-canonical view** — A query view returning nodes where `lifecycle ∈ {accepted, deprecated}` and tags exclude `experimental`. Activated by the `--canonical` flag; the default view returns all non-tombstoned nodes.
- **Derived field** — A field computed from the Attestation history rather than authored. Core derives `lifecycle`, `realization`, and `owners`. Writing to a derived field is an error.
- **DSSE** — Dead Simple Signing Envelope; the signature format used to wrap Attestation claims.
- **Edge** — A typed, attributed relation. 9 well-known kinds in Core.
- **Effective view** — The deep-merge result of an `extends` edge with `merge: deep`. Computed at validation time and cacheable.
- **Extension vocabulary** — A namespaced bundle of additional kinds, edges, predicates, and field schemas.
- **Federation** — Cross-repo content import (optional module). Pull-model, content-hash pinned, signature-verified, lockfile-deterministic.
- **Governance** — Approvals, roles, Attestations, Predicates, ChangeSets, authorization chains (optional module).
- **JCS** — JSON Canonicalization Scheme (RFC 8785); the canonicalization used for content hashing.
- **Lifecycle** — Derived state with values `proposed`, `accepted`, `deprecated`, `retired`, `tombstoned`. Computed from Approval, Withdrawal, and Tombstone Attestations.
- **MCP** — Model Context Protocol; the integration surface the USL server exposes to AI clients.
- **Messenger pattern** — An authorization chain in which an agent composes the envelope but the human's key signs. Signer == claimant.
- **Module** — A conformance unit: Core (mandatory), Governance, Federation, Embeddings.
- **Node** — A specification with uniform shape (uri, kind, version, scope, description, body, spec, relations, provenance).
- **Normative** — Binding on conformant implementations. Non-normative material is informative only.
- **Predicate** — A `core:Predicate` node defining the body schema for a class of Attestations. First-class, versionable, attestable, federation-pinnable like any other node.
- **Principal** — Any actor (human, agent, group, service-account); a single kind with a `type` discriminator.
- **Realization** — Derived state describing the artifact's observed state in actual systems. Values: `none`, `planned`, `built`, `running`, `decommissioned`, `unknown`. Not monotone.
- **Request-changes** — An Approver's blocking signal that they will not approve a subject until specific items are addressed, expressed as an Attestation referencing the `request-changes@1.0` Predicate. While in force, the issuer's own approvals on the same subject are blocked (validator G-10). Resolved by revocation or by the issuer's later approval, which implicitly supersedes the block.
- **Standard Extensions Bundle** — A separately-versioned vocabulary that registers commonly-needed-but-not-universal kinds (`Threat`, `Control`, `Process`, `Workflow`, `SLO`, etc.).
- **Standing delegation** — An authorization chain in which a prior signed Delegation Attestation authorizes the agent's later signature.
- **Substrate** — The versioned content store hosting USL files (git, etc.).
- **Substrate adapter** — A Governance-module mechanism that maps substrate-native review (e.g., git PRs) to implicit ChangeSets, so most repos never author a ChangeSet node directly.
- **Supersession** — A new node's `supersedes` edge to an old node. Tools display the old node with a "superseded by" annotation.
- **Time-travel query** — A query that evaluates derived predicates against the graph as of a specified timestamp, via the `at:` parameter on `lifecycle()`, `realization()`, etc.
- **Tombstone** — A terminal removal of a node from the resolver. Reached when at least two Tombstone Attestations from distinct claimants target the same subject (validator G-09), each carrying a documented `reason`. A single Tombstone Attestation is staged but not yet effective.
- **URI** — A node's stable identity: `usl://<vocab>/<namespace>/<name>`. Kind is not in the URI. Versions are pinned with `@<version>`.
- **Version strategy** — One of `semver`, `calver`, `hash`. Recorded per-version on each Approval Attestation, allowing a node's history to migrate between strategies. Within a single strategy, ordering follows native rules; across strategies, ordering falls back to Attestation `claimed_at`. Strategy migrations are info-level diagnostics (I-06), not errors.
- **Visibility** — `public | internal | private`. Used by Federation for bundle export.
- **Vocabulary** — A node that registers types, edges, predicates, and field schemas. Loaded by the repo manifest.

---

End of v0.9 draft.
