OKF edge vocabulary is registry-driven — a vertical declares its own traversable edges
0033-registry-driven-edge-vocabulary
- Reversibility
- two-way door
DEC-0033 — Registry-driven edge vocabulary (verticals extend the graph, never fork it)
Context
The seam was found serving a real external client KB through GraphRAG (the RBA serve-proof — see First full-loop SERVE on a real external client — reconcile divergent extraction runs to one canonical KB on a quality rubric; lexical retrieval sufficient (VectorRetriever seam not yet needed)). Vertical concept types declare IdRef relationship fields — DXA's capability.delivered_by; engagement.client / capabilities / runs / sow; client.systems / engagements (packages/okf/src/verticals/dxa.ts). These validated as frontmatter but were absent from the core hardcoded EDGE_FIELDS / EdgeType in packages/okf/src/graph.ts, so edges() silently dropped them: they never became traversable graph edges, so they were invisible to GraphRAG expand (MCP agentic foundation — tenant-scoped GraphRAG over the OKF KB).
Caught concretely: a capability's delivered_by link to its delivering workflow was unreachable in expansion — 2 capability atoms whose delivering workflow could not be reached. This violated "verticals extend, never fork" (Digital Experience Agency vertical as the first reference implementation) and single-source-of-truth (Dossier — The Knowledge Model (v0) principle 2): a vertical's relationship was declared on its type yet did not exist in the graph.
Options considered
- (A — chosen) Registry-driven edge fields. A
TypeSpecdeclares its own edge fields (edges?: readonly string[]);edges()unions the core vocabulary with the registry (registeredEdgeFields()), core-first dedup so a vertical can never shadow a core label;EdgeTypewidens from a closed union toCoreEdgeType | (string & {}). - (B — rejected) Bake the vertical fields into the core
EDGE_FIELDSlist. Adddelivered_by/runs/sow/ etc. directly to the hardcoded core vocabulary.
Decision
Adopt option A. A registered edge is a plain typed edge labeled by its field name — it never inherits produces semantics, so The produces edge is canonical on the producing process only (produces canonical on a process) is untouched and validateGraph needed no change. edges() reads CORE_EDGE_FIELDS first into a seen set, then appends each registeredEdgeFields() entry not already present (core wins its label). The DXA vertical now declares its edges on each type; engagement.team is intentionally excluded — it maps real people to role ids as free strings, not an id-ref edge.
Rationale
- Rejecting B — it is the exact fork DEC-0006 forbids. Baking
delivered_by/runs/sowinto core would make them core concepts and force every future vertical to re-touch the core file to add its relationships — forking the core by accretion. It would also duplicate each vertical's relationship in two places (the type definition and the core list), an SSOT violation. - A was contained. The registry already existed and was already threaded into validation, so having
edges()consult it was one import + one dedup loop — no new machinery, just closing a gap the registry was already designed to cover. - Semantics stay clean. Registered fields are plain typed edges, never read as
produces; DEC-0007's canonical-producer rule andvalidateGraphare both untouched. The model doc (Dossier — The Knowledge Model (v0) §"Vertical edges (registry-driven)", lines 96–97) was synced to this schema-as-code. verified. Implemented in code with 3 green regression tests; the build is green (tsc -bclean,vitest run351 passed / 1 skipped). The seam was found by, and the fix proven against, a real served client KB.
Consequences
- A vertical adds traversable graph edges the same way it adds a type —
registerType(defineType(...))with anedgeslist — so verticals extend the graph, never fork it. The core file is never touched to add a vertical relationship. - Default (un-narrowed) GraphRAG
expandnow correctly traversesdelivered_byand the other DXA edges; the previously-unreachable capability→workflow links resolve. - A core label always wins its name on collision, so a vertical can never shadow or redefine a core relationship.
Known follow-up (deferred to keep this change atomic)
The MCP get_related tool still hardcodes edgeType as a closed z.enum of the 10 core labels (packages/mcp/src/server/server.ts:37-49). Default expansion already traverses registered edges correctly, but a caller cannot yet pass edgeType: "delivered_by" to NARROW expansion — the tool boundary would reject it. Closing it means deriving the tool enum from core + registeredEdgeFields() at server-wire time. Deferred as a follow-up against packages/mcp so this change stays atomic to the @dossier/okf graph layer. (MCP agentic foundation — tenant-scoped GraphRAG over the OKF KB.)
Review
Promote the follow-up to done when the MCP get_related enum is derived from the live registry (core + registered) at wire time, so narrowed expansion accepts vertical edge labels. Revisit the core-first dedup rule only if a vertical ever has a legitimate need to override a core label's meaning (it should not — that would be a fork).
Provenance
Decision and all facts verified in code at decision time: packages/okf/src/registry.ts (TypeSpec.edges + defineType options overload + registeredEdgeFields()), packages/okf/src/graph.ts (CoreEdgeType / CORE_EDGE_FIELDS + the registry-union in edges()), packages/okf/src/verticals/dxa.ts (each type declares its edges; engagement.team excluded), packages/okf/src/index.ts (exports), knowledge/model/index.md §96–97 (relationship-vocabulary doc synced to schema-as-code), packages/okf/test/graph.test.ts (3 regression tests, green). Build green: tsc -b clean, vitest run 351 passed / 1 skipped.