A locally-run web app that stores, manages, and visualizes the Granite Switch project knowledge graph — Business Goals → Requirements → ADRs → Components → Plans, plus Issues, Experiments, and Insights that connect across them.
It has two consumers:
- A human browsing the graph in a browser (polished Cytoscape.js visualization with multiple layouts, filtering, hover previews, and a click-to-pin detail panel).
- A Claude skill reading and writing the graph over a local HTTP API.
The stack is intentionally simple: a FastAPI backend, a buildless React +
Cytoscape.js frontend loaded from CDN (no Node/npm required), and a single
graph.json file for storage so the data is easy to inspect, diff, and
version.
The app reads its data from a subject repo — you must tell it which repo:
bash knowledge-graph/run.sh --repo /path/to/subject-repo(Or set the repo via env: REPO=/path/to/subject-repo bash knowledge-graph/run.sh.)
This creates a local venv, installs fastapi/uvicorn/pydantic, and starts
the server. Then open:
http://localhost:8081
Use a different port with PORT=9000 bash knowledge-graph/run.sh --repo ….
The backend serves both the API and the web UI on the same port, so there is a single command for the whole app.
If --repo is missing, or the repo has no .claude/spec directory, the app
fails immediately with a clear error rather than starting empty:
[knowledge-graph] ERROR: no subject repo given.
usage: bash run.sh --repo /path/to/subject-repo (or set REPO=…)
The graph's node files live inside the subject repo, under
.claude/spec/<type-dir>/<id>.md (YAML frontmatter for structured fields +
outbound links, markdown body for the description):
<subject-repo>/.claude/spec/
├── business-goals/ BG-01.md …
├── requirements/ REQ-001.md …
├── adrs/ ADR-001.md …
├── components/ CMP-001.md … ← Component files
├── plans/ PLAN-1.md …
├── issues/ ISSUE-8.md …
├── experiments/ …
└── insights/ INSIGHT-1.md …
The app resolves this as <repo-path>/.claude/spec/<type>/, where <repo-path>
comes from --repo (surfaced to the backend as the KG_REPO env var). For
tests you can point the backend straight at a spec dir with KG_DATA_DIR.
The app only reads these files; skills and users edit them. The editing flow is: add / edit / delete a file, then trigger a reload so the running app re-reads it into the in-memory graph:
# reload everything
curl -s -X POST http://localhost:8081/api/reload
# reload just one type (detects deletions within it)
curl -s -X POST http://localhost:8081/api/reload -d '{"types":["CMP"]}'
# reload specific files (paths relative to the repo's .claude/spec/)
curl -s -X POST http://localhost:8081/api/reload -d '{"paths":["components/CMP-005.md"]}'The response reports exactly what changed and any per-file problems:
{ "added": [...], "updated": [...], "removed": [...], "unchanged": [...], "errors": [{path, error}], "total": N }. A malformed or schema-invalid file
appears in errors and is skipped — it never crashes the graph. The .md
files are the single source of truth; the in-memory graph is rebuilt from them
on startup and on every reload.
Nodes are rendered as dark "blueprint plates" with a hairline border and a left
colour tab in their type colour. Labels are ID-first: at a glance each plate
shows its mono ID (CMP-005), and the full title fades in once you zoom past
~0.78 (the canvas hint bottom-left tracks this). At 74 nodes a wrapped title is
unreadable when the graph is fitted to the window, so the ID — short and unique —
is what stays legible. BG lanes always shows titles, since its columns are wide.
Zoom-to-fit is clamped to a legibility floor (0.52) and re-centred on the densest region: fitting the raw bounding box let a few loosely connected outliers shrink the core until nothing could be read.
Edges are
directed and labeled with the relationship (supports, addresses,
implements, tracks, informs, relates-to, plus the two
component-to-component edges contains and depends-on). Component edges are
drawn visually distinct: contains (hierarchy) is a solid cyan line with a
filled arrow; depends-on is a dashed cyan line. These are the only edges that
carry a meaningful subtype.
Layouts (switch in the top bar):
- Force (gravity — the default, best for an overview of the full graph).
- Architecture — components are the anchors. Root components spread across
the top; components a parent
containsare nested below it; each component's linked nodes (ADRs, REQs, issues, insights) cluster in a grid directly beneath it. Nodes linked to no component sit in an "unassigned" cluster at the far right. Answers "what is the system made of, and what touched each part." - BG lanes — a table-style view: one horizontal swimlane per Business Goal; within each lane that goal's chain reads left→right in fixed sub-columns (BG | REQ | COMPONENT | ADR | PLAN | Issues). Rectangles are wide and spread edge-to-edge across the window (and widen further when you collapse the detail panel); node size stays fixed and readable, and the view scrolls vertically to reach lower lanes rather than shrinking to fit. Groups all requirements of the same goal together — best for reading a large graph.
- Hierarchy (breadth-first), Circle, Grid.
Filtering (left panel, all combinable, one-click reset): free-text search on title/ID, node-type toggles, and — after selecting a node — "Direct neighbors" or "Connected subgraph". The type toggles double as the legend — each row is a swatch in the type's colour plus its node count, so there is no separate floating legend over the canvas. Types with zero nodes stay listed (the schema still defines them) but render hollow and dimmed. The node/link readout top-right turns amber whenever a filter is actually narrowing the view. The subgraph is directed lineage: it shows only nodes on a directed path through the selected node — its ancestors up the pipeline (the goals it serves) and its descendants down (what builds on it), not the whole connected blob.
Details: hover a node for a quick callout (it follows the cursor and flips
side near a window edge); hovering also lights that node's edges amber and shows
their relationship labels, so you can trace connections without pinning. Click to
pin the full record (all fields + links in and out) in the right panel for
copying — a pinned node gets a wide halo so it stays findable in a dense field,
and its own edges keep their labels. The detail panel collapses (the ›
button) to give the graph the full window width; a ‹ Details button brings it
back.
Look & feel: the UI is a dark "blueprint instrument" — a two-tier grid
substrate with corner registration marks, hairline rules instead of stacked
cards, Archivo for UI text and IBM Plex Mono for identifiers/readouts. The eight
node-type colours (served by the API) are the only saturated colour; amber is the
single UI accent, used for state rather than decoration. Motion is one
staggered load-in and short transitions, and it all collapses under
prefers-reduced-motion.
Every node has id, type, title, description, and links (a list of
{ "target": "<id>", "rel": "<label>" }). Type-specific fields:
| Type | Required (beyond title) | Optional | May link to |
|---|---|---|---|
BG (Business Goal) |
priority (HARD|Directional) |
stakeholders[] |
— |
REQ (Requirement) |
level (MUST|SHOULD|MAY), priority (MVP|Post-MVP|Optional), state (Satisfied|Partial|Not satisfied) |
satisfied_when |
BG |
ADR |
status (Proposed|Accepted|Rejected|Superseded|Deprecated) |
confidence (High|Medium|Low), date, rationale, consequences |
CMP |
CMP (Component) |
name, responsibility, code_location |
status (active|planned|deprecated) |
ADR, REQ, CMP (contains / depends-on) |
PLAN |
— | status (Draft|Approved|In progress|Done|Abandoned) |
ADR, REQ |
ISSUE |
— | number, url, state (open|closed) |
REQ, ADR, CMP |
EXPERIMENT |
— | results_summary, outcome (success|failure|inconclusive) |
ADR |
INSIGHT |
— | — | any type |
The API is the authoritative source — GET /schema (or GET /schema/<type>,
e.g. GET /schema/component) returns this exactly, including allowed link
targets and the default relationship label per type. IDs follow the per-type
prefix (BG-01, REQ-001, ADR-001, CMP-001, PLAN-1,
ISSUE-1, EXP-1, INSIGHT-1); each <id>.md file is named by its id.
A Component is a high-level building block of the system (the composer, the switch layer, the per-backend decoders, the Triton kernel, …). Components describe current structure; ADRs record decisions — keep the two distinct. There should be only ~a dozen for the whole system, not one per class.
Component files live in data/components/, one file per component named by id:
---
id: CMP-005
type: CMP
name: vLLM Backend Decoder # required; used as the node title
responsibility: The fast-inference GraniteSwitch model for vLLM … # required
code_location: granite-switch/src/granite_switch/vllm/… # required
status: active # optional: active | planned | deprecated
links:
- target: CMP-006
rel: contains # hierarchy: this component contains CMP-006
- target: CMP-001
rel: depends-on # dependency
- target: ADR-021 # ADRs that shape this component
rel: depends-on
- target: REQ-020 # requirements it satisfies
rel: relates-to
---
Prose describing the component (the body is the node's description).Component edges — the only edges in the graph with a meaningful subtype:
contains— hierarchy (e.g. a backend contains its switch/LoRA layer). Rendered as a solid cyan line.depends-on— dependency (the default rel for a bare component→component link). Rendered as a dashed cyan line.
Allowed component links: → ADR, → REQ, → CMP (contains/depends-on).
Issues and Insights may link to a component like any other node.
Files are validated on load: a component missing a required field, using a
bad status, or linking to a disallowed target type is reported per-file in the
reload response's errors (and server log) and skipped — it never takes the
graph down.
The subject repo's .claude/spec/ holds the real Granite Switch data: 6
Business Goals, 28 Requirements, 21 ADRs, 8 Components, 10
Issues, and Insights. The link spine is REQ supports BG, ADR addresses CMP
(every ADR points to the component(s) it shapes), CMP depends-on ADR /
CMP → REQ (a component's driving decisions and the requirements it satisfies),
and ISSUE tracks …. Edit the files under .claude/spec/ and call
POST /api/reload to see changes; there is no separate seed or JSON cache.
JSON in, JSON out. CORS is enabled for any localhost / 127.0.0.1 origin.
Base URL below assumes port 8081.
curl http://localhost:8081/schemacurl http://localhost:8081/schema/REQ
curl http://localhost:8081/schema/component # friendly names/dirs also resolvecurl http://localhost:8081/graphOptional body scopes it ({"types":[…]} or {"paths":[…]}); no body reloads
everything. Returns {added, updated, removed, unchanged, errors, total}.
curl -X POST http://localhost:8081/api/reload -d '{"types":["CMP"]}'curl "http://localhost:8081/nodes?type=ADR"curl http://localhost:8081/nodes/REQ-001Body is an init object matching the type's schema, including its links.
Returns the created node (with its assigned id) and HTTP 201.
curl -X POST http://localhost:8081/nodes \
-H 'Content-Type: application/json' \
-d '{
"type": "REQ",
"title": "Streaming token output",
"description": "The server must stream tokens as they are generated.",
"level": "SHOULD",
"priority": "Post-MVP",
"state": "Not satisfied",
"links": [{"target": "BG-02", "rel": "supports"}]
}'Missing/invalid fields return a clear error:
curl -X POST http://localhost:8081/nodes \
-H 'Content-Type: application/json' \
-d '{"type":"REQ","title":"Broken"}'
# -> 400 {"detail":"Missing required field 'level' for type REQ."}curl -X PATCH http://localhost:8081/nodes/REQ-010 \
-H 'Content-Type: application/json' \
-d '{"state": "Satisfied"}'curl -X DELETE http://localhost:8081/nodes/INSIGHT-1
# -> {"deleted":"INSIGHT-1"} (also removes every edge touching it)curl -X POST http://localhost:8081/links \
-H 'Content-Type: application/json' \
-d '{"source":"EXP-1","target":"ADR-004","rel":"informs"}'curl -X DELETE http://localhost:8081/links \
-H 'Content-Type: application/json' \
-d '{"source":"EXP-1","target":"ADR-004"}'A companion skill at .claude/skills/knowledge-graph/SKILL.md documents how a
Claude skill creates, links, and queries nodes via this API. Note: .claude/
is gitignored in this repo, so the skill lives locally and is not committed —
copy it elsewhere if you want to version it.
- Single-user local tool; no auth. Writes to
graph.jsonare atomic (temp-file + rename) but not hardened for heavy concurrent writers — fine for one human plus one skill. - No build step: the frontend loads React, Babel-standalone, and Cytoscape.js from CDN, so an internet connection is needed on first page load.