@kompiro/adr-tools
v0.0.13
Published
Frontmatter-driven ADR (Architecture Decision Record) validator, extractor, regenerator, and visualizer.
Maintainers
Readme
@kompiro/adr-tools
Frontmatter-driven ADR (Architecture Decision Record) tooling. Validates ADR
metadata, enforces relationship consistency (supersedes, depends_on,
refines, ...), extracts effective sets, regenerates index files, renders
Mermaid views, and verifies code-level assumptions.
Originally extracted from the kompiro/karasu monorepo.
Status: pre-1.0, API may change.
Install
Published to the public npm registry:
pnpm add -D @kompiro/adr-tools
# or
npm install --save-dev @kompiro/adr-toolsNo registry configuration or authentication is required.
Standalone binary (no Node required)
For environments without a Node toolchain (other projects, Go/other-language devcontainers, etc.), install the self-contained executable published to GitHub Releases:
curl -fsSL https://raw.githubusercontent.com/kompiro/adr-tools/main/install.sh | shThe script detects your OS/arch, downloads the matching binary, verifies its
SHA256, and installs it to ~/.local/bin/adr. Override with ADR_VERSION
(release tag) or INSTALL_DIR.
While this repository is private, downloading requires authentication: install
the GitHub CLI and run gh auth login (preferred),
or set GITHUB_TOKEN (the curl fallback also needs jq). In a devcontainer,
add the one-liner above as a RUN step in your Dockerfile.
The binary embeds everything it needs, so
adr initworks with no companion files. The generated config's$schemapoints at the npm package path, so JSON Schema autocompletion in editors only resolves when the package is also installed via npm.
Quick start
After installing, the adr binary is on your project's PATH:
# Generate a starter config in CWD
npx adr init
# Edit adr.config.json to define your topics and concerns
# Validate ADRs under docs/adr/
npx adr validate
# Regenerate effective.md, graph.md, graph/<topic>.md
npx adr regenerateCLI
adr <subcommand> [options]
Subcommands:
init generate a starter adr.config.json in CWD
validate schema and cross-reference validation of ADRs
regenerate rewrite effective.md, graph.md, and graph/<topic>.md
extract query the ADR set (effective | slice | closure)
visualize render Markdown / Mermaid views of the ADR set
check-assumptions verify file: / symbol: / grep: assumptions in ADRs
check-permalinks verify permalink: sources exist and deep anchors resolveConfiguration (adr.config.json)
{
"$schema": "./node_modules/@kompiro/adr-tools/dist/config.schema.json",
"idFormat": "date-sequence",
"topics": ["architecture", "infrastructure", "process"],
"concerns": ["security", "performance", "ci"],
"paths": {
"adrDir": "docs/adr",
"outputs": {
"effective": "effective.md",
"graph": "graph.md",
"graphByTopic": "graph/"
}
}
}idFormatselects the ADR id / filename convention (see below). Defaults to"date-sequence"when omitted.topicsandconcernsdefine the controlled vocabulary checked against ADR frontmatter. Use[]to disable vocabulary enforcement (fields stay required, but any string is accepted).paths.outputspaths are relative topaths.adrDir.permalink(optional) opts intopermalink:frontmatter support andadr check-permalinks. See below.assumptions.rangePin(optional) sets howvalidatereports an assumption that pins a caret range to a full version. Defaults to"warn". See below.
Keeping assumptions: from rotting (assumptions.rangePin)
assumptions: earns its place by failing CI when the world an ADR relies on
moves. That only works when what is asserted is the decision. An assumption
that writes the literal dependency version instead fails on the next routine
bump — of a decision nobody revisited:
assumptions:
- 'grep: package.json :: "oxfmt": "\^0.62.0"' # red the day 0.63.0 lands
- 'grep: package.json :: "oxfmt": "\^0\.' # holds for every 0.xThe repair costs more than it looks. A bot that raised the bump cannot edit the ADR to fix the CI it just turned red, so someone has to recognise the failure as the repo's own rather than upstream's and raise a second PR carrying the same bump.
validate reports an assumption where a caret or tilde range is asserted
down to a full major.minor.patch. The caret already says the tail may move, so
asserting the tail contradicts the range on the same line; stopping at the major
says what was decided and survives every bump the caret permits.
"assumptions": { "rangePin": "error" }| Value | Behaviour |
|---|---|
| "warn" (default) | prints, does not affect the exit code |
| "error" | fails validate |
| "off" | disables the check |
An exact pin is deliberately exempt: "pkg": "1.2.3" with no caret is a
decision about 1.2.3, so the version is the content of the assumption. Keying
on the range operator rather than on the version shape is also what keeps
identifiers that merely look like versions — an SPDX id such as
BlueOak-1.0.0 — out of scope without an exclusion list to maintain.
Linking an ADR to a rendered structure (permalink:)
An ADR can point at a rendered architecture view with a permalink: block:
permalink:
- short: https://taka.example/AbCdEf # optional click-through pointer
source: docs/architecture/system.krs # required: in-repo file of record
view: system # optional default viewThe source is the record (the link is restorable from it even if short
dies); a source may carry a #fragment deep anchor addressing a specific
element. adr check-permalinks then verifies, for each entry: source exists,
short is a well-formed non-fragment URL (offline shape check — the link is
not fetched), and the deep anchor still resolves.
Resolving a #fragment is language-specific, so enable a resolver kind:
"permalink": { "kind": "krs" }The built-in krs kind resolves karasu
#krs-<view>-<id> anchors by rendering the .krs and checking the anchor still
exists — catching a rename/removal that dangled the link. It lazily loads the
optional peer dependency @karasu-tools/core, so install it only when you use
this kind:
pnpm add -D @karasu-tools/coreRecommending @<sha> pinning for repo-backed permalinks
If your short links are repo-backed permalinks — a hosted resolver that
renders a repo's file at a git ref, e.g.
…/<owner>/<repo>[/<path>][@<ref>]#anchor — you can have check-permalinks
recommend pinning them to an immutable commit SHA. List the resolver host(s):
"permalink": { "kind": "krs", "repoBackedHosts": ["nest.example"] }A short on one of these hosts that is not pinned to a full 40-hex SHA
(ref-less, or @HEAD / @branch / @tag / abbreviated SHA) is reported as a
recommendation — a non-fatal warn that prints but does not fail CI —
so the ADR is nudged toward pointing at the structure as of the decision. The
check is offline (host + URL shape only; the ref is never resolved) and keys on
host, not route form, so it is independent of whether the resolver uses a bare
or prefixed path. Absent/empty ⇒ the check is inert.
check-permalinks validates an ADR↔source consistency, so wire it to run on
changes to both ADRs and the source files (e.g. an unfiltered CI step),
not just ADR paths.
idFormat
| Value | Filename | Frontmatter id | Use when |
|---|---|---|---|
| date-sequence (default) | YYYYMMDD-NN-<slug>.md | ADR-YYYYMMDD-NN | You want monotonic date-ordered ids and don't care about Issue/PR linkage |
| issue-number | <n>-<slug>.md (no zero padding) | ADR-<n> | You want the filename to encode the GitHub Issue (or PR) number so Issue ↔ ADR linkage is visible at a glance |
Numbering policy under issue-number is up to the host project — a common
order is Issue number → PR number → local sequence (max existing + 1).
The validator and body cross-reference scan adapt automatically. Mixing formats in one corpus is not supported; pick one per project.
Generated outputs (effective.md, graph.md, graph/*.md) list ADRs in
numeric id order under both formats, so unpadded issue-number ids sort
ADR-99 → ADR-999 → ADR-1000 rather than lexically. Note that under
issue-number, id order tracks issue creation order, which only approximates
decision chronology — the frontmatter date field remains the source of truth
for when a decision was made.
ADR file format
ADRs are Markdown files with YAML frontmatter:
---
id: ADR-20260101-01
title: Adopt frontmatter-driven ADRs
status: accepted
date: 2026-01-01
topic: process
depends_on: []
related_to: []
supersedes: []
---
# ADR-20260101-01: Adopt frontmatter-driven ADRs
## Background
...Reference templates
This repo ships starter templates you can copy into your project:
docs/adr/TEMPLATE.md— frontmatter + body skeleton for a new ADRdocs/adr/README.md— index, numbering rules, status transitions, and operating notes
Library API
import {
loadConfig,
validateDirectory,
buildGeneratedFiles,
evaluateAllPermalinks,
} from "@kompiro/adr-tools";
const config = loadConfig();
const { errors, warnings, parsed } = validateDirectory(config.paths.adrDir, config);
const files = buildGeneratedFiles(parsed, config);
const permalinks = await evaluateAllPermalinks(parsed, ".", config);Development
pnpm install
pnpm test
pnpm run build # tsup -> dist/ (npm package)
pnpm run build:bin # bun --compile -> dist/bin/ (standalone binaries; needs bun)See CONTRIBUTING.md for the contribution workflow and SECURITY.md for reporting vulnerabilities.
License
MIT — see LICENSE.
