Switchplinth reads the ADRs, RFCs and design notes already in a repo and draws the architecture they describe. Switch a decision to one of its rejected options and the map redraws to show what would change. You don't keep a diagram up to date or adopt a template; the documents you already have are the input.
A repo's architecture usually gets explained across a pile of documents. An ADR records one choice. An RFC argues for a change that may or may not have shipped. A design note describes a service as it looked the week someone wrote it. The README describes the current setup and skips the reasons.
Each of those is fine on its own. It gets hard when a question spans them. Answering "why is Kafka here?" means reading several files and piecing the argument back together. Answering "what would have changed with SQS?" means doing the same and then guessing, because the rejected option is usually one sentence, something like "SQS would reduce infrastructure work". Nothing says which services and connections would actually be different.
So I wanted to be able to ask why a component exists and get the passage that says so, and to ask what a rejected option would have changed and get back a list of components and connections.
What already exists
adr-tools and Log4brains create, link and publish decision records. C4 and Structurizr model the system, and Structurizr can attach imported ADRs to parts of the model. Backstage TechDocs puts Markdown docs on a service's catalog page.
I couldn't find a way in any of them to tie a statement about the system back to the passage it came from, along with whether a person agreed with it. I also couldn't find a way to switch to a rejected option and see what it does to the system model. That's the part I built.
How Switchplinth works
Switchplinth reads the Markdown already in a repo and treats each decision as the thing being modeled, with the documents as evidence.
Extracted claims point at their source passage by file path, heading path, a SHA-256 hash of the normalized text, and the surrounding text. Line numbers are only used for display, so the link survives edits.
Everything extracted starts as inferred. You can confirm, reject or correct it. A review is a new record that supersedes the previous one, so the original inference is never rewritten, and undo just appends another record that restores the earlier judgment.
Options are stored as operations on one base graph of entities and relationships: add, remove or replace an entity or relationship, optionally with a precondition like expect: {exists: false}. A scenario picks one option per decision. Compiling it applies those operations to a copy of the base graph and reports conflicts (two decisions writing the same ID, or a failed precondition) and invalid assumptions (a relationship whose endpoint doesn't exist in that scenario).
The UI draws the graph, shows the cited passages for a component, and lets you switch a decision's option and watch the map update. An MCP server exposes the same data to coding agents through five read-only tools (list_decisions, search_decisions, inspect_architecture, inspect_entity, diff_scenarios), and each takes the same decision-to-option map, so an agent can ask "why" and "what if" before it touches code.
The data lives as YAML in a .decision/ folder next to the Markdown, one record per file, so it goes through normal pull-request review. The search indexes are caches rebuilt from that YAML.
How the Markdown becomes a graph
Your Markdown and a .decision/ folder of YAML records are the only canonical data. Everything else is rebuilt or recomputed from them.
- Discovery finds every
.mdfile, skipping version-control, cache and build folders plus any you configure. - Each file is split at headings and blank lines. Every passage becomes an evidence anchor: source path, heading path, a hash of the normalized text, and the surrounding context. On a rescan, an anchor follows its passage if the passage moved, and is flagged for review if the match is ambiguous.
- Extraction runs either deterministic mock rules (tuned to the bundled sample) or a model through LiteLLM. A model gets one retry when its option deltas fail the schema, and every entity, decision, and option it keeps must quote exactly one passage.
- Each record is validated against the
decision/v1schema and written as its own YAML file by atomic replace. Records start asinferred. - Human review appends judgments and undos; it never edits the inference.
- On each request, the compiler applies the selected options' typed deltas to one base graph and reports conflicts and invalid assumptions.
- SQLite full-text and Qdrant vector indexes are rebuilt from the evidence after each scan. Delete them and nothing is lost.
- The UI map, What-if previews, "why" search, and five MCP tools all read from that compiled graph and those indexes.
An example
The repo ships a made-up food-delivery system: nine Markdown files (design notes, RFCs and decision notes, no ADR template) with seven decisions in them. One decision note says:
We chose Apache Kafka because replay and per-order ordering matter more than reducing broker operations.
Amazon SQS would reduce infrastructure work. A dead-letter queue would isolate repeatedly failing messages, but replaying a selected range of historical order events would require additional application support.
A different note, about payment retries, picked SQS.
This is the stored SQS option for event delivery, trimmed (I added the # comment):
- id: option-2d494dede037e255
title: Amazon SQS
delta:
base: architecture
operations:
- op: add_entity
entity: {id: resource-a755d86bd792bfe6, kind: resource,
name: Order event stream (Amazon SQS)}
expect: {exists: false}
- op: add_relationship
relationship: {id: relationship-27d4255d512327af, kind: publishes_to,
from: entity-225ac6a9466f917c, # Order Service
to: resource-a755d86bd792bfe6}
expect: {exists: false}
Each option adds its own resource and points it at a shared technology node. No option writes to the shared node, which is why two decisions can both use SQS without a conflict.
Switching event delivery to SQS gives this diff:
- Order Service -[publishes_to]-> Order event stream (Apache Kafka)
- Order event stream (Apache Kafka) -[implemented_by]-> Apache Kafka
+ Order Service -[publishes_to]-> Order event stream (Amazon SQS)
+ Order event stream (Amazon SQS) -[implemented_by]-> Amazon SQS
The payment retry queue is untouched and still points at the same Amazon SQS node. Kafka stays in the graph with no edges, and the compiler reports no conflicts or invalid assumptions.
PostgreSQL shows up twice in the sample too. The reporting design note says the analytics database "is physically separate from the PostgreSQL instance owned by the Order Service", so they get separate IDs. Switchplinth never merges entities because their names match; only a human-confirmed identity link groups them.
Up next
The bundled sample uses mock extraction written for its own nine files, so it demonstrates the model, not extraction quality. On the same sample, a local Qwen3.8-Flash-Next run through Lemonade, with reasoning turned off, found all 7 decisions with the same selected options as the mock after one failed attempt. Gemma-4-26B-A4B-it found 3 of 7. That's one fixture and one good run per model, so I'm not claiming much from it yet.
Next I want to:
- measure extraction recall on real repositories
- test hosted providers (OpenAI, Anthropic)
- add authentication so the API and MCP server aren't limited to one local user
- read formats other than Markdown
- catch conflicts that don't share an ID, like two options adding different queues for the same job
Code: https://github.com/bcmyguest/switchplinth. Site: https://bcmyguest.github.io/switchplinth/.
A question
When your team rejects an option, is it written down concretely enough to say "this replaces that"? Or is the alternative a single sentence?
I built Switchplinth. This article was drafted with AI assistance and reviewed by me. Coding agents wrote much of the code under my direction.

Top comments (0)