Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/traverse-cli/src/federation_operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ mod tests {
"peer_id": "peer-a",
"trust_model": "allowlist",
"allowed_scopes": ["Public"],
"approved_spec_refs": ["026-federation-registry-routing"],
"approved_spec_refs": ["078-federation-registry-routing"],
"approved_at": "2026-04-10T00:00:00Z",
"revoked_at": null
}},
Expand Down Expand Up @@ -492,7 +492,7 @@ mod tests {
RegistryScopeManifest::Public,
RegistryScopeManifest::Private,
],
approved_spec_refs: vec!["026-federation-registry-routing".to_string()],
approved_spec_refs: vec!["078-federation-registry-routing".to_string()],
approved_at: "2026-04-10T00:00:00Z".to_string(),
revoked_at: None,
}
Expand Down
8 changes: 4 additions & 4 deletions crates/traverse-registry/src/federation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2398,7 +2398,7 @@ mod tests {
assert_eq!(private_snapshot.provenance.origin_peer_id, "peer-governed");
assert_eq!(
private_snapshot.provenance.approval_chain[0].spec_ref,
"026-federation-registry-routing"
"078-federation-registry-routing"
);
assert!(federation.governance_decisions().iter().any(|decision| {
decision.decision_kind == GovernanceDecisionKind::SnapshotAcceptance
Expand Down Expand Up @@ -2648,7 +2648,7 @@ mod tests {
peer_id: "peer-local".to_string(),
trust_model: "local-only".to_string(),
allowed_scopes: Vec::new(),
approved_spec_refs: vec!["026-federation-registry-routing".to_string()],
approved_spec_refs: vec!["078-federation-registry-routing".to_string()],
approved_at: "2026-04-09T19:30:00Z".to_string(),
revoked_at: None,
}),
Expand Down Expand Up @@ -2683,7 +2683,7 @@ mod tests {
peer_id: peer_id.to_string(),
trust_model: "shared-api-token".to_string(),
allowed_scopes: scopes,
approved_spec_refs: vec!["026-federation-registry-routing".to_string()],
approved_spec_refs: vec!["078-federation-registry-routing".to_string()],
approved_at: "2026-04-09T19:30:00Z".to_string(),
revoked_at: None,
}
Expand Down Expand Up @@ -2771,7 +2771,7 @@ mod tests {
source: traverse_contracts::ProvenanceSource::Greenfield,
author: "enricopiovesan".to_string(),
created_at: "2026-04-09T19:00:00Z".to_string(),
spec_ref: Some("026-federation-registry-routing".to_string()),
spec_ref: Some("078-federation-registry-routing".to_string()),
adr_refs: vec![],
exception_refs: vec![],
},
Expand Down
2 changes: 1 addition & 1 deletion crates/traverse-registry/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use traverse_contracts::Lifecycle;

const METADATA_GRAPH_KIND: &str = "metadata_graph_snapshot";
const METADATA_GRAPH_SCHEMA_VERSION: &str = "1.0.0";
const METADATA_GRAPH_GOVERNING_SPEC: &str = "015-metadata-graph";
const METADATA_GRAPH_GOVERNING_SPEC: &str = "077-metadata-graph";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataGraphSnapshot {
Expand Down
80 changes: 80 additions & 0 deletions docs/decision-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -925,3 +925,83 @@ ADR-0016, and its own artifacts. The `embedded-trace-api` implementation ticket
may move from Blocked to Ready. Its first delivery must prove that a
Trace Explorer-equivalent Web consumer can browse local traces without HTTP
and that baseline embedder conformance remains compatible.

## Decision 33: Land Two Orphaned Governing Specs to Fix a Pre-Existing Stale-Spec-ID Bug

- **Date**: 2026-07-21
- **Status**: Accepted
- **Governing specs**: `077-metadata-graph`, `078-federation-registry-routing`
- **Related pull request**: (this change)
- **Related repo**: flagged from `traverse-framework/registry`'s
`specs/014-extraction-compatibility` decision-log entry 33, which found
this bug while auditing `traverse-registry` ahead of extraction but left
it unfixed as out of that repo's scope (registry-scope-only).

### Context

Two literal spec-ID strings embedded in `crates/traverse-registry/src/`
did not correspond to any spec in `specs/governance/approved-specs.json`:
`"015-metadata-graph"` (a `const` in `graph.rs`, used as every projected
metadata-graph snapshot's `governing_spec`) and
`"026-federation-registry-routing"` (used only in `federation.rs` and
`federation_operator.rs` test fixtures, as sample `TrustRecord.
approved_spec_refs` / `ApprovalChainEntry.spec_ref` values). Both slots
(`015`, `026`) were reassigned to unrelated specs (`015-capability-
discovery-mcp`, `026-event-broker`) during the v0.2.0 governance batch
(issue #209, issue #207), so `approved_spec_registry_contains()` has
returned `false` for both original strings since that batch landed —
independent of the registry-extraction work that surfaced it.

Investigation (issue #37 comment history, git history of `graph.rs` via
issue #62, and the abandoned `origin/022-mcp-wasm-server` branch at commit
`b81a17b`) found that in both cases a real, complete spec document had been
written and, in the federation case, even self-marked "Status: Approved" —
but neither was ever merged into `approved-specs.json` before its numeric
slot was taken by different work. The implementations (`graph.rs` via PR
#97, `federation.rs` via PR #240 and follow-ons) shipped anyway, each
assuming its governing spec had been formally approved when it had not.

### Decision

Rather than point the two stale constants at an existing-but-unrelated
approved spec (which would repeat the same category of error in a new
form), land the two already-written spec documents under fresh IDs:
`077-metadata-graph` (written retroactively against the shipped
`graph.rs` behavior, since no committed draft of the original
`015-metadata-graph` could be found in any branch) and
`078-federation-registry-routing` (the original `026-federation-registry-
routing` document, recovered from `b81a17b`, reviewed against the shipped
`federation.rs`/`federation_operator.rs` behavior and found still
accurate, with its own stale cross-references to two other still-
unapproved draft specs removed rather than carried forward). Both new
`approved-specs.json` entries govern the specific source files their spec
actually describes (`graph.rs`; `federation.rs` and
`federation_operator.rs`) rather than the whole crate. `graph.rs`'s
`METADATA_GRAPH_GOVERNING_SPEC` constant and all `federation.rs`/
`federation_operator.rs` test-fixture literals are updated to the new IDs.

### Alternatives Considered

- Point the constants at `015-capability-discovery-mcp` /
`026-event-broker` (today's real owners of those numeric slots) —
rejected, since neither actually governs metadata-graph projection or
federation routing; this would fabricate a false governance claim
identical in kind to the bug being fixed.
- Point the constants at the closest broad existing spec that already
governs `traverse-registry/` (e.g. `007-workflow-registry-traversal`) —
rejected; `007` explicitly excludes "full metadata graph query model"
from its own scope, and no approved spec describes federation routing.
- Leave the constants unfixed, matching `traverse-framework/registry`'s
deliberate choice not to paper over this with passthrough entries —
appropriate for that repo (out of its scope, and a passthrough there
would mask rather than fix), but this repo owns the actual bug and can
close it properly instead of leaving it permanently broken.

### Outcome

`077-metadata-graph` and `078-federation-registry-routing` are approved and
immutable, each governing exactly the source file(s) it describes.
`approved_spec_registry_contains()` now returns `true` for the spec IDs
actually embedded in `traverse-registry`'s shipped code. All existing
`traverse-registry` and `traverse-cli` tests pass unchanged in behavior —
only the literal spec-ID strings changed.
192 changes: 192 additions & 0 deletions specs/077-metadata-graph/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Feature Specification: Metadata Graph Projection

**Feature Branch**: `077-metadata-graph`
**Created**: 2026-07-21
**Status**: Approved
**Input**: User description: "Land the governing spec for the metadata graph projection that `crates/traverse-registry/src/graph.rs` already implements, closing a governance gap where the shipped code referenced a spec ID (`015-metadata-graph`) that was never merged into the approved spec registry before that numeric slot was reassigned to an unrelated spec (`015-capability-discovery-mcp`, issue #209)."

## Background

Issue #37 ("Specify metadata graph model") tracked a draft spec at
`specs/015-metadata-graph/`, but a 2026-03-29 comment on that issue states it
must remain unapproved until "reviewed, approved, and merged into the
approved spec registry." That draft was never committed to any branch and no
such merge ever happened. Issue #62 ("Implement metadata graph projection")
nonetheless shipped `project_metadata_graph` and its supporting types
referencing `015-metadata-graph` as `governing_spec`, on the assumption the
slice had been approved. It had not. The `015` slot was later assigned to an
unrelated spec (`015-capability-discovery-mcp`, issue #209) during the
v0.2.0 governance batch, permanently orphaning the original constant.

This spec is written retroactively against the already-shipped, already-
tested implementation (`crates/traverse-registry/src/graph.rs`, landed via
PR #97) rather than inventing new behavior — its purpose is to close the
governance gap, not to change what the code does.

## User Scenarios & Testing *(mandatory)*

### User Story 1 - Project a Unified Graph Across Registries (Priority: P1)

As a platform developer, I want the capability, event, and workflow
registries projected into a single graph so that I can answer discovery and
explainability questions ("what does this capability publish or depend on?")
without querying each registry independently.

**Why this priority**: Without a unified projection, callers must
cross-reference three independent registries by hand to answer any
relationship question, which does not scale and is error-prone.

**Independent Test**: Register a mix of capabilities, events, and workflows
with cross-references between them, call `project_metadata_graph`, and
verify every registered artifact appears as exactly one node and every
declared relationship appears as an edge of the correct kind.

**Acceptance Scenarios**:

1. **Given** registered capabilities, events, and workflows, **When** the
metadata graph is projected, **Then** the snapshot contains one node per
distinct `(kind, scope, id, version)` artifact.
2. **Given** a capability that emits or consumes a registered event, **When**
the graph is projected, **Then** a `Publishes` or `SubscribesTo` edge
connects the capability node to the event node.
3. **Given** a capability with a workflow reference, or a workflow whose
definition composes capabilities and references events, **When** the
graph is projected, **Then** `References` and `Composes` edges connect the
corresponding nodes.
4. **Given** multiple versions of the same artifact are registered, **When**
the graph is projected, **Then** `Supersedes` edges connect each version to
its immediate predecessor in version order.

### User Story 2 - Look Up a Node Under an Explicit Scope Policy (Priority: P2)

As a caller resolving a capability, event, or workflow reference, I want to
look up its graph node under an explicit public/private preference so that
scope-sensitive callers get deterministic results instead of an arbitrary
match.

**Why this priority**: Ambiguous scope resolution (returning whichever
matching node happens to sort first) would make caller behavior
unpredictable across otherwise-identical requests.

**Independent Test**: Register the same `(kind, id, version)` in both public
and private scope, call `find_node` under each `MetadataGraphLookupScope`
variant, and verify the returned node matches that variant's documented
policy.

**Acceptance Scenarios**:

1. **Given** a node exists only in public scope, **When** looked up under
`PublicOnly` or `PreferPrivate`, **Then** the public node is returned.
2. **Given** the same artifact exists in both scopes, **When** looked up
under `PreferPrivate`, **Then** the private node is returned.
3. **Given** the same artifact exists in both scopes, **When** looked up
under `PublicOnly`, **Then** the public node is returned, never the
private one.
4. **Given** no matching node exists, **When** looked up under any scope,
**Then** `find_node` returns `None` rather than panicking.

### User Story 3 - Traverse Outgoing Relationships From a Node (Priority: P3)

As a caller exploring the graph, I want to list a node's outgoing edges so
that I can traverse relationships (e.g., "what does this workflow compose?")
without re-deriving them from the source registries.

**Why this priority**: Traversal is the payoff of building the graph at all;
without it, callers would still need to re-inspect raw registry records.

**Independent Test**: Project a graph with a node that has multiple outgoing
edges of different kinds, call `outgoing_edges`, and verify exactly the
expected edges are returned in a deterministic order.

**Acceptance Scenarios**:

1. **Given** a node with outgoing edges, **When** `outgoing_edges` is called
with that node's ID, **Then** every edge whose `from_node_id` matches is
returned and no others.
2. **Given** a node with no outgoing edges, **When** `outgoing_edges` is
called, **Then** an empty list is returned.

### Edge Cases

- Two registered artifacts collide on the same `(kind, scope, id, version)`
node ID: the projection MUST deduplicate to a single node rather than
emitting duplicates.
- An event, capability, or workflow reference points at an artifact that is
not registered in the target scope: no edge is emitted for that reference
rather than the projection failing.
- The same relationship is derivable from both scopes (e.g., a public and a
private copy of the referenced event both exist): edges MUST be
deduplicated by `edge_id` so the same logical relationship is not emitted
twice.

## Requirements *(mandatory)*

### Functional Requirements

- **FR-001**: The system MUST project every registered capability, event,
and workflow into a `MetadataGraphNode`, keyed by `(kind, scope, id,
version)`, with no duplicate node IDs in the resulting snapshot.
- **FR-002**: The system MUST derive `Publishes` and `SubscribesTo` edges
from each capability's declared `emits` and `consumes` event references,
for every scope in which the referenced event is actually registered.
- **FR-003**: The system MUST derive a `References` edge from a capability to
its declared workflow reference, and from a workflow to each event
referenced by its definition's edges, when the referenced artifact is
registered in that scope.
- **FR-004**: The system MUST derive a `Composes` edge from a workflow to
each capability referenced by its definition's nodes, when that capability
is registered in that scope.
- **FR-005**: The system MUST derive a `Supersedes` edge chain linking each
version of an artifact to its immediate predecessor, ordered by version
comparison, independently per `(kind, scope, id)`.
- **FR-006**: The system MUST deterministically sort nodes and edges and
deduplicate edges by `edge_id`, so identical inputs always produce an
identical snapshot.
- **FR-007**: The system MUST support looking up a node by kind, artifact ID,
and version under an explicit lookup scope policy (`All`, `PublicOnly`,
`PreferPrivate`) with documented, deterministic precedence.
- **FR-008**: The system MUST support listing a node's outgoing edges by
node ID.
- **FR-009**: Every produced snapshot MUST record its `kind`,
`schema_version`, `governing_spec`, `generated_at` timestamp, and the set
of source specs (`005-capability-registry`, `007-workflow-registry-
traversal`, `011-event-registry`, and this spec) as generation evidence.

### Key Entities *(include if feature involves data)*

- **MetadataGraphSnapshot**: The full projected graph at a point in time —
kind, schema version, governing spec, generation evidence, nodes, edges.
- **MetadataGraphNode**: A single capability, event, or workflow artifact
represented in the graph, carrying its scope, ID, version, lifecycle,
summary, and owning team.
- **MetadataGraphEdge**: A directed, typed relationship (`References`,
`Publishes`, `SubscribesTo`, `Composes`, `Supersedes`) between two nodes.
- **MetadataGraphGenerationEvidence**: The set of source specs a given
snapshot was projected under.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: Projecting the graph for a fixed set of registry inputs always
yields byte-identical node and edge ordering across repeated calls.
- **SC-002**: Every relationship expressed in a capability, event, or
workflow contract (emits, consumes, workflow reference, composition,
version lineage) is discoverable as exactly one edge in the projected
graph, with no relationship silently dropped or duplicated.
- **SC-003**: `find_node` under any lookup scope returns a result consistent
with that scope's documented precedence in 100% of registered-artifact
lookups, and `None` for unregistered lookups.

## Assumptions

- The graph is projected on demand from the three existing registries; it is
not independently persisted or incrementally maintained.
- Cross-registry references that point at unregistered artifacts are treated
as absent relationships, not validation errors — reference validation is
the responsibility of the owning registry (`005-capability-registry`,
`007-workflow-registry-traversal`, `011-event-registry`), not this
projection.
- This spec governs the projection and lookup surface implemented in
`crates/traverse-registry/src/graph.rs`; exposing the graph through a
runtime, CLI, or MCP surface is a separate, future governed slice.
Loading
Loading