Skip to content

Latest commit

 

History

21 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

adr-graph

Graph-integrity tooling for Architecture Decision Records — an MCP server and a CLI (same logic, one entry point) over a directory of ADR markdown files.

It treats the ADR corpus as a typed knowledge graph built from three reference channels:

  • frontmatter typed edges — supersedes, superseded_by, related
  • body wikilinks — [[ADR-123]], [[123-slug|ADR-123]]
  • body markdown links — [ADR-123](./123-slug.md)

OKF: the lingua franca

This server standardizes on Google's Open Knowledge Format (OKF) v0.1 — an open, vendor-neutral format for representing knowledge as a directory of markdown files with YAML frontmatter. OKF is designed to be authored by people, generated by agents, exchanged across organizations, and consumed by both.

Required frontmatter: type (freeform — adr, playbook, metric, …)

Recommended frontmatter: title, description, resource, tags, timestamp

Every tool in this server reads and writes OKF-conformant documents. The migrate_okf tool upgrades a legacy corpus to full conformance, and okf_conformance reports field coverage and spec violations.

Topology is signal, not just validation

An incomplete graph is not automatically a broken one. Every "incomplete" finding is dispositioned as either an intentional frontier (a decision you'll connect later) or genuine rot:

Finding Intentional (signal) Accidental (defect)
Singleton disallow_singletons: false in policy and (status: proposed/draft/seed, standalone: true, or seed tag) Default case: disconnected node (defect)
Dead link target listed in the node's planned: / forward_refs: undeclared reference to a missing ADR
Subject scope subject_scope: outside the tree and discharged_by: says how it is observed subject_scope: outside the tree with no declared discharge
Cross-root ref target resolves in a root listed in sibling_roots: target resolves nowhere

validate returns ok: false on genuine rot — orphan singletons (by default), undeclared dead links, broken reciprocity, OKF violations (missing type field), or undischarged subject scopes. When a corpus policy sets disallow_singletons: false (or configures seed_statuses), intentional singletons are excused as signals rather than failures.

Where a claim's subject lives

A dead link is a claim whose target cannot be resolved. A decision can also make a claim whose subject cannot be observed — a control that lives in per-checkout state (a git hook, a local daemon) or in a deployed environment. Nothing in a source tree can establish that such a control is actually running, so a check that reports green over one is reporting on something it cannot see.

Two optional frontmatter keys make that declarable, and therefore dispositionable:

subject_scope: per-machine              # commit (default) | deployment | per-machine
discharged_by: heartbeat:my-guard-ran   # heartbeat | named-unverifiable
  • commit — the default, and the sound case: the subject is the tree at the SHA. Omit the key entirely and this is what you get. Never a defect.
  • per-machine / deployment — the subject is not in the tree. The node must say how it becomes observable: heartbeat (the control emits a liveness signal and its silence alarms) or named-unverifiable (the blind spot is stated rather than papered over).
  • Declared without a discharge → defect. That is a claim with no observable subject, and it is the shape that lets an uninstalled control report green for months.

This is a disposition over a declared field. adr-graph reads what a node says about its own subject; it never parses or evaluates a requirement predicate — that belongs to whatever verifier your corpus uses.

A lookup that cannot say "I don't know" will say something else

hover_context resolves a file path to its governing ADRs by matching code_paths globs. A lookup like that has three possible outcomes, not two, and collapsing them is how a tool ends up making a confident false claim:

provenance meaning
matched at least one code_paths glob matched this path
no_explicit_match an index exists; this path is not in it — not indexed, not unconstrained
no_code_paths_declared no ADR declares code_paths at all — the tool cannot answer, and a non-match carries no information whatsoever

Graph.governing_adrs_with_provenance(file_path) returns {query, provenance, result, matched_via, index_size, corpus_size}. result is None rather than [] when there is no answer, because an empty collection collides with "the answer is none". Graph.get_governing_adrs() still returns a plain list for callers that only need matches — but anything reporting to a human or an agent should use the provenance form.

This is not hypothetical. Run against a 67-ADR corpus in which zero ADRs declared code_paths, the previous implementation returned "No architectural decisions explicitly govern this path" for every file in the repository — an assertion of absence from an index that did not exist.

matched_via reports which pattern matched. code_paths uses fnmatch, where * crosses /: src/* governs the entire subtree, and src/** behaves identically to src/*. That behaviour is disclosed rather than silently changed, so an over-broad glob is visible at the call site instead of quietly widening a decision's reach. Check the pattern before treating a match as intentional.

Policy lives in the corpus, not in a sidecar

Which statuses count as frontiers, which tags, and which scopes demand a discharge are all corpus opinions. They are read from a policy node: any top-level markdown file in the root whose frontmatter declares type: policy.

---
type: policy
title: Corpus disposition policy
disallow_singletons: false # allow intentional frontiers (defaults to true)
seed_statuses: [proposed, draft, seed]
seed_tags: [standalone, frontier]
scopes_requiring_discharge: [per-machine, deployment]
sibling_roots: [../../infrastructure/docs/adr, ../../web/docs/adr]
---

sibling_roots matters more than it looks. A corpus is per-repo. A product with several repos has several ADR roots, and a decision in one routinely cites a decision in another. Load one root without declaring the others and every such reference is reported broken — because a validator that cannot say "outside my root" says "missing" instead.

Measured on a real four-root corpus: 94 reported broken links, 73 distinct targets, all of them resolving in a sibling root, none absent anywhere. Declaring sibling_roots moved that count to zero and reclassified them as signals.cross_root_refs. If your corpus reports alarming dead-link numbers, check this before believing them.

A node, not a .toml beside the repo — deliberately. A sidecar that goes missing falls back to defaults silently, giving you a gate weaker than the one you declared with nothing to notice it. A policy node's presence is a function of the tree SHA, so its absence is visible to the same tooling that reports dead links, and changing it is a reviewable diff in the corpus rather than an untracked config edit. validate reports which was used as meta.policy_source ("defaults" when no node is present).

Keys you omit keep their documented defaults; keys you set replace them wholesale.

Use as an MCP server

Point it at a corpus with ADR_GRAPH_ROOT, then register the stdio server:

{
  "mcpServers": {
    "adr-graph": {
      "command": "adr-graph",
      "env": { "ADR_GRAPH_ROOT": "/path/to/docs/adr" }
    }
  }
}

Tools:

  • validate(root?): Full topology report.
  • okf_conformance(root?): OKF v0.1 conformance report — violations, warnings, field coverage.
  • find_singletons(root?): Intended frontiers vs orphan suspects.
  • find_dead_links(root?): Planned vs broken dead links.
  • check_reciprocity(root?): Mirroring checks for supersedes edges.
  • neighbors(adr, depth?, root?): Authored-link neighborhood context.
  • export(fmt?, root?): Render as json, mermaid, or okf (bundle summary).
  • supersede(superseding, superseded, root?): Atomically write supersession edges.
  • reconcile_related(adr?, apply?, root?): Derive frontmatter related links from body refs.
  • read(adr, root?): Read ADR body and metadata (includes description, resource).
  • list(status?, tag?, limit?, offset?, root?): Filtered catalog of ADRs with pagination.
  • search(query, status?, limit?, offset?, root?): Title substring search with pagination.
  • path(from_adr, to_adr, root?): BFS shortest path.
  • set_status(adr, status, root?): Single-field frontmatter update.
  • rename(old, new, dry_run?, root?): Renumber/rename ADR and update all references.
  • drift(root?): Disagreements between body links and frontmatter relations.
  • blast_radius(adr, root?): Downstream transitive dependencies.
  • propose_adr(title, status?, context?, tags?, root?): Scaffold a new ADR file.
  • hover_context(file_path, root?): Return architectural context for a file path (matches against code_paths globs in ADR frontmatter).
  • migrate_okf(dry_run?, root?): Migrate corpus to OKF v0.1 — adds type, converts date→timestamp, synthesizes descriptions, generates index.md. Dry-run by default.
  • audit_diff(files?, git_diff?, root?): Actively audit code changes or files against governing ADR invariants (expect: present, expect: absent).
  • task_briefing(task, files?, root?): Synthesize an architectural briefing, invariant checklist, downstream blast radius, and neighborhood context for an implementation task.
  • coverage(source_dirs?, churn_days?, root?): Comprehensive codebase governance coverage report, identifying subsystems, high-churn shadow architecture (ungoverned files with frequent git commits), and stale code paths.
  • scaffold_invariants(adr, apply?, root?): Inspect governed code files and automatically synthesize candidate <!-- adr:requirements --> blocks for class definitions, configs, and exported interfaces.
  • install_hook(hook_type?, force?): Install a zero-config executable git pre-commit hook that validates graph integrity and audits staged changes against invariants before every commit.

Also exposes the adr://{adr_id} resource to get raw markdown content natively.

Use as a CLI / CI gate

Same binary, with a subcommand. Exits non-zero on rot or invariant violations, so it drops straight into CI or a pre-commit hook:

adr-graph validate /path/to/docs/adr                      # exit 1 if broken links, singletons, or reciprocity breaks
adr-graph okf-conformance                                  # OKF v0.1 field coverage and violations
adr-graph audit src/auth/config.py src/db/pool.py          # audit files against governing ADR invariants (exit 1 on violation)
adr-graph briefing "Implement JWT refresh tokens" src/auth # synthesize architectural briefing & invariant checklist
adr-graph coverage                                         # codebase coverage & high-churn shadow architecture report
adr-graph scaffold-invariants ADR-2 --apply                # synthesize & append invariant block to ADR-2
adr-graph install-hook                                     # install executable .git/hooks/pre-commit gate
adr-graph singletons                                       # intentional frontier vs orphan suspects
adr-graph neighbors ADR-401 2                              # authored-link neighbourhood (grounding context)
adr-graph reconcile ADR-3 --apply                          # derive frontmatter `related` from body links
adr-graph export mermaid                                   # render for visualization
adr-graph export okf                                       # OKF bundle metadata summary
adr-graph read ADR-3                                       # retrieve body text and frontmatter of ADR-3
adr-graph list --status proposed --limit 20 --offset 10    # list ADRs matching filters with pagination
adr-graph search "auth" --status accepted                  # search ADRs by title
adr-graph path ADR-1 ADR-5                                 # get BFS shortest path from ADR-1 to ADR-5
adr-graph set-status ADR-3 deprecated                      # update ADR status field
adr-graph rename ADR-3 ADR-30 --apply                      # cascade renumber ADR-3 to ADR-30
adr-graph drift                                            # find nodes where yaml edges and body links disagree
adr-graph blast-radius ADR-3                               # find all transitive dependents of ADR-3
adr-graph propose "Use PostgreSQL" "PostgreSQL spec"       # scaffold a new ADR file
adr-graph migrate-okf --apply                              # migrate corpus to OKF (dry-run without --apply)

Root resolution: explicit arg → ADR_GRAPH_ROOT./docs/adr.

Install

pip install -e .

About

Graph-integrity tooling for Architecture Decision Records — an **MCP server** and a **CLI** (same logic, one entry point) over a directory of ADR markdown files.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages