diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2c4e92b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Report a defect in ADR Toolkit +title: "" +labels: bug +--- + +## Summary + +## Steps to Reproduce + +1. +2. +3. + +## Expected Behavior + +## Actual Behavior + +## Environment + +- Harness: Claude Code / Codex CLI / Gemini CLI / Antigravity CLI / generic / other +- ADR Toolkit version (`skills/adr-toolkit/VERSION`): +- OS: + +## Relevant Output + +Paste the command and its output, including `--json` output where +applicable. + +``` +``` + +## Scope + +- [ ] This affects `adr.py validate`, `create`, `index`, `check`, `graph`, + or `search`. +- [ ] This affects a specific harness adapter (Codex, Gemini CLI, + Antigravity, or generic). diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..4f7f064 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Propose new functionality for ADR Toolkit +title: "" +labels: enhancement +--- + +## Problem + +What are you trying to do that ADR Toolkit does not support today? + +## Proposed Solution + +## Alternatives Considered + +## Additional Context + +- [ ] I checked `project-roadmap.md` and `improvements.md` for existing + plans that already cover this. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..304542a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +## Summary + +- + +## ADR Impact + +- [ ] I checked whether this changes an accepted architectural decision. +- [ ] I linked related ADRs, or this PR has no ADR impact. +- [ ] I ran `adr.py check` when the change touches governed paths. + +Related ADRs: + +## Verification + +- [ ] `python3 -m pytest -q` +- [ ] `python3 scripts/sync_version.py --check` +- [ ] `python3 skills/adr-toolkit/scripts/adr.py validate --dir docs/decisions --json` +- [ ] `python3 skills/adr-toolkit/scripts/adr.py index --dir docs/decisions --json` + +## Notes + +- diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f20f9ec..5fb11b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,3 +42,56 @@ jobs: python-version: "3.12" - name: Check manifest versions and descriptions are in sync run: python scripts/sync_version.py --check + + harness-parity: + # Installs the real Codex CLI and Gemini CLI and drives their own plugin + # commands against this repo, the same way a contributor would. Manifest + # JSON shape is already covered by tests/unit/test_*_adapter.py; this job + # instead proves discovery -> install -> list -> the installed skill + # package's script layer still runs, so an upstream CLI change or a + # manifest edit that breaks real installation fails CI instead of + # surfacing later as a user-reported install failure. Antigravity CLI + # (agy) has no public package registry distribution, so it stays a + # manually verified adapter only -- see adapters/antigravity/README.md. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install Codex CLI + run: npm install -g @openai/codex@0.151.0 + - name: Install Gemini CLI + run: npm install -g @google/gemini-cli@0.46.0 + - name: Verify Codex CLI adapter end to end + run: | + set -euo pipefail + export CODEX_HOME="$(mktemp -d)" + REPO_ROOT="$(pwd)" + codex plugin marketplace add "$REPO_ROOT" + INSTALLED_PATH="$(codex plugin add adr-toolkit@adr-toolkit-marketplace --json | jq -r '.installedPath')" + codex plugin list + SCRATCH="$(mktemp -d)" + cd "$SCRATCH" && git init -q + python3 "$INSTALLED_PATH/skills/adr-toolkit/scripts/adr.py" preflight --json | jq -e '.ok == true' + python3 "$INSTALLED_PATH/skills/adr-toolkit/scripts/adr.py" init --dir docs/decisions --json | jq -e '.ok == true' + python3 "$INSTALLED_PATH/skills/adr-toolkit/scripts/adr.py" validate --dir docs/decisions --json | jq -e '.ok == true' + - name: Verify Gemini CLI adapter end to end + run: | + set -euo pipefail + REPO_ROOT="$(pwd)" + mkdir -p adapters/gemini-cli/skills + ln -s "$REPO_ROOT/skills/adr-toolkit" adapters/gemini-cli/skills/adr-toolkit + export HOME="$(mktemp -d)" + gemini extensions validate "$REPO_ROOT/adapters/gemini-cli" + printf 'y\ny\n' | gemini extensions install "$REPO_ROOT/adapters/gemini-cli" --consent + gemini extensions list + INSTALLED_PATH="$HOME/.gemini/extensions/adr-toolkit" + SCRATCH="$(mktemp -d)" + cd "$SCRATCH" && git init -q + python3 "$INSTALLED_PATH/skills/adr-toolkit/scripts/adr.py" preflight --json | jq -e '.ok == true' + python3 "$INSTALLED_PATH/skills/adr-toolkit/scripts/adr.py" init --dir docs/decisions --json | jq -e '.ok == true' + python3 "$INSTALLED_PATH/skills/adr-toolkit/scripts/adr.py" validate --dir docs/decisions --json | jq -e '.ok == true' diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f8fb524 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,44 @@ +# Code of Conduct + +## Our Standard + +Participation in this project — issues, pull requests, discussions, and +reviews — is expected to stay respectful and focused on the technical +problem. Disagreement about a decision or an approach is normal; personal +attacks, harassment, discriminatory language, or deliberately derailing a +discussion are not. + +## Expected Behavior + +- Assume good faith and give reviewers/contributors the benefit of the + doubt. +- Keep feedback specific to the code, design, or decision under + discussion. +- Accept that maintainers may decline a change and explain why, without + that being an invitation to escalate. + +## Unacceptable Behavior + +- Harassment, insults, or discriminatory remarks directed at a person + rather than their contribution. +- Publishing others' private information without consent. +- Sustained disruption of issues, pull requests, or reviews. + +## Reporting + +Report a violation privately to the repository owner rather than escalating +in a public issue or pull request thread. Include what happened, where +(link to the issue/PR/comment), and when. + +## Enforcement + +The repository owner reviews reports and may edit or remove content, warn a +participant, or restrict further participation in the project's spaces, +proportional to the violation. Enforcement decisions are not owed a public +explanation when doing so would expose reporter identity or private +details. + +## Scope + +This applies to all project spaces (issues, pull requests, discussions, +commits, and comments) and to representing the project in public spaces. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4445b47 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Contributing + +Thanks for helping improve ADR Toolkit. This project is intentionally small: +the agent can reason and draft, but deterministic scripts own file writes, +IDs, lifecycle transitions, validation, indexes, and machine-readable output. + +## Branches + +Follow the repository policy in `AGENTS.md`. + +- Branch from `develop` for normal work. +- Use `feature/*`, `fix/*`, or `docs/*` for short-lived work branches. +- Merge normal work back to `develop` through a pull request. +- Release branches merge to `master` and back to `develop`. +- Tags named `v*` are created only from `master`. + +## Local Checks + +Run the relevant checks before opening a pull request: + +```bash +python3 -m pytest -q +python3 scripts/sync_version.py --check +python3 skills/adr-toolkit/scripts/adr.py validate --dir docs/decisions --json +python3 skills/adr-toolkit/scripts/adr.py index --dir docs/decisions --json +``` + +For changes that may affect accepted architectural decisions, also run: + +```bash +python3 skills/adr-toolkit/scripts/adr.py check --uncommitted --dir docs/decisions --json +``` + +## ADR Changes + +- Keep ADR source files flat under `docs/decisions/`. +- Do not hand-edit generated indexes or lifecycle links to repair validation + errors. Change the source ADR or use the lifecycle command. +- Use `adr.py create`, `status`, `deprecate`, `supersede`, and `exception` for + deterministic mutations. +- Accepted ADRs are append-only in spirit: use supersession for changed + decisions instead of rewriting history. + +## Public Repository Hygiene + +Public repository branch/tag protection is expected to enforce PRs, required +checks, conversation resolution, and force-push/deletion restrictions on +`develop`, `master`, and `v*` tags once the repository is public. diff --git a/README.md b/README.md index 3840378..5db17d3 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,26 @@ untruncated count and `truncated` is `total > count`. from `related` (used during RECORD's DISCOVER stage to find precedent for a *new* draft, with a broader OR-across-fields match). +## Relationship graph + +`index` embeds a Mermaid relationship graph in `docs/decisions/README.md` +whenever ADRs have `related` or `supersedes` links, so GitHub can render the +navigation view directly. + +For a standalone graph artifact, export Mermaid and SVG files: + +```bash +python skills/adr-toolkit/scripts/adr.py graph --dir docs/decisions --format both --json +``` + +This writes `docs/decisions/relationships.mmd` and +`docs/decisions/relationships.svg`. SVG is the default image artifact because it +stays sharp when zoomed and does not require Mermaid CLI, Node, or a browser. +With `--format both`, `--output build/adr-relationships` is treated as a file +prefix and writes `build/adr-relationships.mmd` plus +`build/adr-relationships.svg`; relative output paths are resolved from +`--root`, matching the other repository-scoped commands. + ### Why storage stays flat ADRs live as `NNNN-slug.md` in one flat `docs/decisions/` directory — no diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..84f604e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ +# Security Policy + +## Supported Versions + +Security fixes target the latest released version of ADR Toolkit. + +## Reporting a Vulnerability + +Please report security issues privately to the repository owner before opening a +public issue. Include: + +- affected version or commit +- reproduction steps +- expected impact +- whether the issue affects generated ADR content, command execution, path + handling, release automation, or plugin installation + +Do not include exploit details in a public issue until a fix or mitigation is +available. + +## Scope + +In scope: + +- command injection or unsafe path handling in deterministic scripts +- release workflow or version provenance problems +- plugin manifest behavior that can install or expose unintended files +- validation bugs that silently report clean results for invalid ADR state + +Out of scope: + +- disagreement with an ADR's architectural recommendation +- AI-generated prose quality +- unsupported third-party harness behavior outside this repository's adapters diff --git a/adapters/antigravity/README.md b/adapters/antigravity/README.md index fc9331b..205d586 100644 --- a/adapters/antigravity/README.md +++ b/adapters/antigravity/README.md @@ -3,6 +3,9 @@ Antigravity plugins are a `plugin.json` marker file plus optional sibling directories (`skills/`, `agents/`, `rules/`), per `antigravity.google/docs/cli/plugins/`. This manifest needs only `name`. +**Manually verified against Antigravity's `agy` CLI 1.1.13** (`agy +--version`): validate, install, and discovery all work — see "Verification +status" below. ## Install @@ -14,18 +17,62 @@ directories (`skills/`, `agents/`, `rules/`), per ln -s "$(pwd)/skills/adr-toolkit" adapters/antigravity/skills/adr-toolkit ``` -2. Install per Antigravity's own plugin-install documentation. -3. Confirm Antigravity lists `adr-toolkit`, then run +2. Install with Antigravity's own plugin CLI: + + ```bash + agy plugin install "$(pwd)/adapters/antigravity" + ``` + +3. Confirm Antigravity lists `adr-toolkit`: + + ```bash + agy plugin list + ``` + +4. Either way, run `python skills/adr-toolkit/scripts/adr.py preflight --json` from your target repository to confirm the script layer works standalone. -**Verification status:** this adapter's manifest and directory layout are -verified against Antigravity's published schema, but — unlike the Codex -and Gemini CLI adapters — no end-to-end install-and-run was performed, -because no `antigravity` CLI binary was available in the development -environment that built this adapter. Treat it as unverified until someone -with the actual CLI confirms it, and update this note when they do. - The symlink is created at install time, not committed to this repo — committing a real symlink breaks on Windows checkouts that don't have `core.symlinks` enabled, which this project's CI can't assume. + +## Verification status + +Manually verified against Antigravity's `agy` CLI 1.1.13 (`agy --version`) +in an isolated `HOME=$(mktemp -d)` so no state was written to the real +`~/.gemini` (Antigravity's plugin CLI stores state under `.gemini/config/` +in the active home directory). + +``` +$ agy plugin validate "$(pwd)/adapters/antigravity" + [ok] /adapters/antigravity + [check] skills : 1 processed + - agents : skipped (not found) + - commands : skipped (not found) + - mcpServers : skipped (not found) + - hooks : skipped (not found) + +$ agy plugin install "$(pwd)/adapters/antigravity" + [ok] adr-toolkit + [check] skills : 1 processed + +$ agy plugin list +{ + "imports": [ + { + "name": "adr-toolkit", + "source": "antigravity", + "importedAt": "2026-08-31T06:11:34Z", + "components": ["skills"] + } + ] +} +``` + +The installed snapshot lands under `/.gemini/config/plugins/adr-toolkit/` +and carries the whole `skills/adr-toolkit/` package. The script layer was +run directly out of it against a scratch git repository, producing correct +`"ok": true` output for `preflight`, `init --dir docs/decisions`, and +`validate --dir docs/decisions` — matching the Codex and Gemini CLI +adapters' verified depth. diff --git a/adapters/codex/README.md b/adapters/codex/README.md index 2abb0ca..80d8ac4 100644 --- a/adapters/codex/README.md +++ b/adapters/codex/README.md @@ -95,8 +95,8 @@ $ codex plugin add adr-toolkit@adr-toolkit-marketplace --json "pluginId": "adr-toolkit@adr-toolkit-marketplace", "name": "adr-toolkit", "marketplaceName": "adr-toolkit-marketplace", - "version": "0.1.0", - "installedPath": "/plugins/cache/adr-toolkit-marketplace/adr-toolkit/0.1.0", + "version": "0.2.0", + "installedPath": "/plugins/cache/adr-toolkit-marketplace/adr-toolkit/0.2.0", "authPolicy": "ON_INSTALL" } @@ -105,7 +105,7 @@ Marketplace `adr-toolkit-marketplace` /.claude-plugin/marketplace.json PLUGIN STATUS VERSION PATH -adr-toolkit@adr-toolkit-marketplace installed, enabled 0.1.0 +adr-toolkit@adr-toolkit-marketplace installed, enabled 0.2.0 ``` The installed snapshot under `/plugins/cache/` carries the @@ -129,9 +129,8 @@ Two details worth knowing: documented marketplace model, not a defect; the repo root is the correct thing to register, and it works. - **The version Codex reports comes from the plugin manifest it reads.** - Installing from this repo's root reports `0.1.0`, taken from - `.claude-plugin/plugin.json`. Installing a marketplace root whose plugin - manifest is only this adapter's `.codex-plugin/plugin.json` reports - `1.0.0` instead, because that manifest carries no `version` field and - Codex substitutes a default. Adding an explicit `version` to - `.codex-plugin/plugin.json` is tracked as a follow-up. + Installing from this repo's root reports the version in + `.claude-plugin/plugin.json`, which is kept in sync with + `skills/adr-toolkit/VERSION` by `scripts/sync_version.py --check`. The + adapter-local `.codex-plugin/plugin.json` intentionally has no `version` + field because the verified install path does not read it. diff --git a/changelog.md b/changelog.md index dab90e5..05ae2be 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,13 @@ Lightweight human-readable summary of meaningful repository changes. ## Unreleased +- Added ADR relationship graph navigation: `adr.py index` now embeds a Mermaid + graph in the generated decision log when relationships exist, and + `adr.py graph` exports standalone `relationships.mmd` plus a crisp SVG + navigation artifact without requiring Node, Mermaid CLI, or browser + rendering. +- Added public repository readiness docs: `CONTRIBUTING.md`, `SECURITY.md`, + and a pull request template covering ADR impact and local verification. - Added `adr.py search`: finds an existing ADR by keyword (title **and** body — `related`'s keyword match was title-only before), tags, status, `--id`, or the real file path it governs, with deterministic best-match diff --git a/docs/decisions/0011-adr-relationship-graph-public-readiness.md b/docs/decisions/0011-adr-relationship-graph-public-readiness.md new file mode 100644 index 0000000..f849353 --- /dev/null +++ b/docs/decisions/0011-adr-relationship-graph-public-readiness.md @@ -0,0 +1,125 @@ +--- +id: ADR-0011 +title: Expose ADR relationships as Mermaid and SVG navigation artifacts +status: accepted +date: 2026-08-31 +locale: en +decision_makers: + - YangSeungHyun +related: + - ADR-0001 + - ADR-0006 + - ADR-0008 + - ADR-0009 +affected_paths: + - README.md + - CONTRIBUTING.md + - SECURITY.md + - .github/PULL_REQUEST_TEMPLATE.md + - project-roadmap.md + - docs/decisions/README.md + - docs/decisions/relationships.mmd + - docs/decisions/relationships.svg + - skills/adr-toolkit/SKILL.md + - skills/adr-toolkit/scripts/adr.py + - skills/adr-toolkit/scripts/commands/graph.py + - skills/adr-toolkit/scripts/commands/index.py + - skills/adr-toolkit/scripts/core/relationships.py + - tests/unit/test_graph_command.py + - tests/unit/test_relationships.py + - tests/unit/test_index.py + - tests/unit/test_adr_cli.py +tags: + - navigation + - graph + - documentation + - governance + - v0.2.0 +retrospective: false +--- + +# Expose ADR relationships as Mermaid and SVG navigation artifacts + +## Context and Problem Statement + +ADR Toolkit intentionally keeps ADR source files flat under `docs/decisions/`, but a growing decision log still needs navigable relationship context. The generated index already listed supersession and related links as text, but users also wanted a graph view and a crisp image artifact. Raster screenshots were not acceptable because relationship graphs rely on small text and lines that become blurry when exported as PNG. + +The same session also prepared the repository for public open-source use with contributor, security, and pull request guidance. That public surface should point contributors at the same deterministic ADR navigation and verification workflow rather than adding a separate documentation system. + +## Decision Drivers + +* Preserve the flat `docs/decisions/` storage model. +* Make relationship navigation visible in GitHub without requiring local tooling. +* Provide a sharp standalone image artifact for documents and reviews. +* Avoid making Node, Mermaid CLI, browser automation, or screenshot rendering required dependencies. +* Keep command output deterministic JSON and path handling consistent with the existing repository-scoped commands. +* Make public contribution guidance ask reviewers to consider ADR impact and verification. + +## Considered Options + +* Keep only the text relationship lists in the generated decision index. +* Embed Mermaid in the generated index and require Mermaid CLI or browser rendering for image export. +* Embed Mermaid in the generated index and generate standalone Mermaid plus Python-rendered SVG artifacts from the same relationship model. + +## Decision Outcome + +Chosen option: **embed Mermaid in the generated index and generate standalone Mermaid plus Python-rendered SVG artifacts**, because GitHub can render Mermaid directly while SVG gives a crisp vector artifact without adding external rendering dependencies. + +`adr.py index` now appends a Mermaid `flowchart LR` block when `related` or `supersedes` edges exist. `adr.py graph --format mermaid|svg|both` exports `relationships.mmd` and/or `relationships.svg`; with `--format both`, a custom `--output` is treated as a prefix so one path can produce both extensions. Relative output paths resolve from `--root`, matching the other repository-scoped commands. + +The public readiness docs added in the same decision are `CONTRIBUTING.md`, `SECURITY.md`, and `.github/PULL_REQUEST_TEMPLATE.md`. They make ADR impact, local verification, and security reporting explicit for future open-source contributors. + +### Consequences + +* Good: GitHub readers get an inline relationship graph without installing anything. +* Good: users who need an image get SVG, which remains sharp when zoomed or embedded in documents. +* Good: graph artifacts reuse the same deterministic relationship model as validation and index generation. +* Good: public contributors now see the expected branch, ADR, security, and verification workflow before opening a PR. +* Bad: the Python SVG renderer is a deliberately small navigation artifact, not a full Mermaid renderer or layout engine. +* Bad: generated `relationships.mmd` and `relationships.svg` are additional files that must be regenerated when ADR relationships change. + +### Confirmation + +* `tests/unit/test_relationships.py` covers Mermaid rendering, label escaping, no-edge behavior, and SVG output. +* `tests/unit/test_graph_command.py` covers default export, custom output prefix behavior for `--format both`, and `--root`-relative output paths. +* `tests/unit/test_index.py` covers Mermaid graph insertion in the generated decision index. +* `tests/unit/test_adr_cli.py` covers parser registration for `adr.py graph`. +* `python3 -m pytest -q` passes with 395 tests. +* `python3 scripts/sync_version.py --check`, `adr.py validate`, `adr.py index`, and `adr.py graph --format both` all pass. + +## Implementation Constraints + +```yaml +constraints: + - id: graph-export-no-external-renderer + kind: forbidden_import + paths: ["skills/adr-toolkit/scripts/commands/graph.py", "skills/adr-toolkit/scripts/core/relationships.py"] + pattern: ["^\\s*import\\s+subprocess\\b", "^\\s*from\\s+subprocess\\b", "^\\s*import\\s+playwright\\b", "^\\s*from\\s+playwright\\b", "^\\s*import\\s+selenium\\b", "^\\s*from\\s+selenium\\b"] + severity: major + message: "ADR graph export must stay deterministic and must not require external renderers or browser automation." +``` + +## Pros and Cons of the Options + +### Keep only text relationship lists + +* Good, because it requires no new command or generated artifact. +* Bad, because relationship navigation remains harder to scan as ADR count grows, and users still lack a sharp visual artifact. + +### Mermaid plus external renderer + +* Good, because Mermaid CLI can produce familiar graph output. +* Bad, because requiring Node, browser automation, or screenshot rendering expands the dependency surface and reintroduces blurry PNG output unless users tune scale settings. + +### Mermaid plus Python-rendered SVG + +* Good, because the README stays GitHub-native and the standalone image remains vector-sharp. +* Good, because it keeps graph export inside the existing deterministic Python package. +* Bad, because the SVG layout is intentionally simple and may need a richer layout strategy if real ADR graphs become dense. + +## Revisit Triggers + +* Real repositories produce dense relationship graphs that the simple SVG layout cannot make readable. +* Users need a PNG export despite SVG availability, and can define acceptable scale/DPI behavior. +* Mermaid syntax compatibility changes on GitHub and breaks the embedded graph. +* Multiple repositories need a combined cross-repository graph rather than one graph per ADR directory. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 955800a..864d41a 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -12,6 +12,7 @@ - [ADR-0008 — Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress](0008-check-exceptions-annotate-only.md) - [ADR-0009 — --json is a documented no-op; CLI output is always JSON](0009-json-flag-always-json-contract.md) - [ADR-0010 — Codex skill-creator's quick_validate.py incompatibility is not this project's problem](0010-codex-quick-validate-not-applicable.md) +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) ### Superseded - [ADR-0003 — Localize only index.py's generated strings, not agent-composed text](0003-localize-only-index-py-s-generated-strings-not-agent-composed-text.md) @@ -48,6 +49,7 @@ ### documentation - [ADR-0010 — Codex skill-creator's quick_validate.py incompatibility is not this project's problem](0010-codex-quick-validate-not-applicable.md) +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) ### exceptions - [ADR-0008 — Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress](0008-check-exceptions-annotate-only.md) @@ -57,6 +59,10 @@ ### governance - [ADR-0008 — Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress](0008-check-exceptions-annotate-only.md) +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + +### graph +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) ### harness-support - [ADR-0004 — Adapter packaging: manifest-only directories, install-time symlinks, verified formats](0004-adapter-packaging-manifest-only-directories-install-time-symlinks-verified-formats.md) @@ -68,6 +74,9 @@ ### mvp-scope - [ADR-0002 — Limit CHECK's conflict detection to structural evidence only](0002-limit-check-s-conflict-detection-to-structural-evidence-only.md) +### navigation +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + ### output-contract - [ADR-0009 — --json is a documented no-op; CLI output is always JSON](0009-json-flag-always-json-contract.md) @@ -83,6 +92,7 @@ - [ADR-0007 — Promote CHECK's kind-to-confidence mapping to a stable output field](0007-check-confidence-field.md) - [ADR-0008 — Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress](0008-check-exceptions-annotate-only.md) - [ADR-0009 — --json is a documented no-op; CLI output is always JSON](0009-json-flag-always-json-contract.md) +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) ## By affected path @@ -96,6 +106,9 @@ ### `.claude-plugin/plugin.json` - [ADR-0004 — Adapter packaging: manifest-only directories, install-time symlinks, verified formats](0004-adapter-packaging-manifest-only-directories-install-time-symlinks-verified-formats.md) +### `.github/PULL_REQUEST_TEMPLATE.md` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + ### `.github/workflows/` - [ADR-0005 — Adopt Git Flow with direct-tag release automation](0005-adopt-git-flow-with-direct-tag-release-automation.md) @@ -105,10 +118,17 @@ ### `AGENTS.md` - [ADR-0005 — Adopt Git Flow with direct-tag release automation](0005-adopt-git-flow-with-direct-tag-release-automation.md) +### `CONTRIBUTING.md` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + ### `README.md` - [ADR-0007 — Promote CHECK's kind-to-confidence mapping to a stable output field](0007-check-confidence-field.md) - [ADR-0008 — Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress](0008-check-exceptions-annotate-only.md) - [ADR-0009 — --json is a documented no-op; CLI output is always JSON](0009-json-flag-always-json-contract.md) +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + +### `SECURITY.md` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) ### `adapters/` - [ADR-0004 — Adapter packaging: manifest-only directories, install-time symlinks, verified formats](0004-adapter-packaging-manifest-only-directories-install-time-symlinks-verified-formats.md) @@ -116,9 +136,21 @@ ### `docs/decisions/` - [ADR-0001 — Record architecture decisions](0001-record-architecture-decisions.md) +### `docs/decisions/README.md` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + +### `docs/decisions/relationships.mmd` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + +### `docs/decisions/relationships.svg` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + ### `examples/quickstart.md` - [ADR-0007 — Promote CHECK's kind-to-confidence mapping to a stable output field](0007-check-confidence-field.md) +### `project-roadmap.md` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + ### `scripts/sync_version.py` - [ADR-0005 — Adopt Git Flow with direct-tag release automation](0005-adopt-git-flow-with-direct-tag-release-automation.md) @@ -129,6 +161,7 @@ - [ADR-0008 — Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress](0008-check-exceptions-annotate-only.md) - [ADR-0009 — --json is a documented no-op; CLI output is always JSON](0009-json-flag-always-json-contract.md) - [ADR-0010 — Codex skill-creator's quick_validate.py incompatibility is not this project's problem](0010-codex-quick-validate-not-applicable.md) +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) ### `skills/adr-toolkit/VERSION` - [ADR-0005 — Adopt Git Flow with direct-tag release automation](0005-adopt-git-flow-with-direct-tag-release-automation.md) @@ -148,6 +181,7 @@ - [ADR-0006 — Localize deterministic ADR generation through repository configuration](0006-localized-adr-generation.md) - [ADR-0008 — Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress](0008-check-exceptions-annotate-only.md) - [ADR-0009 — --json is a documented no-op; CLI output is always JSON](0009-json-flag-always-json-contract.md) +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) ### `skills/adr-toolkit/scripts/commands/check.py` - [ADR-0002 — Limit CHECK's conflict detection to structural evidence only](0002-limit-check-s-conflict-detection-to-structural-evidence-only.md) @@ -164,9 +198,13 @@ ### `skills/adr-toolkit/scripts/commands/exception.py` - [ADR-0008 — Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress](0008-check-exceptions-annotate-only.md) +### `skills/adr-toolkit/scripts/commands/graph.py` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + ### `skills/adr-toolkit/scripts/commands/index.py` - [ADR-0003 — Localize only index.py's generated strings, not agent-composed text](0003-localize-only-index-py-s-generated-strings-not-agent-composed-text.md) - [ADR-0006 — Localize deterministic ADR generation through repository configuration](0006-localized-adr-generation.md) +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) ### `skills/adr-toolkit/scripts/commands/init.py` - [ADR-0003 — Localize only index.py's generated strings, not agent-composed text](0003-localize-only-index-py-s-generated-strings-not-agent-composed-text.md) @@ -192,6 +230,9 @@ - [ADR-0003 — Localize only index.py's generated strings, not agent-composed text](0003-localize-only-index-py-s-generated-strings-not-agent-composed-text.md) - [ADR-0006 — Localize deterministic ADR generation through repository configuration](0006-localized-adr-generation.md) +### `skills/adr-toolkit/scripts/core/relationships.py` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + ### `skills/adr-toolkit/scripts/core/rendering.py` - [ADR-0003 — Localize only index.py's generated strings, not agent-composed text](0003-localize-only-index-py-s-generated-strings-not-agent-composed-text.md) - [ADR-0006 — Localize deterministic ADR generation through repository configuration](0006-localized-adr-generation.md) @@ -209,11 +250,24 @@ ### `tests/integration/test_cli.py` - [ADR-0009 — --json is a documented no-op; CLI output is always JSON](0009-json-flag-always-json-contract.md) +### `tests/unit/test_adr_cli.py` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + ### `tests/unit/test_check.py` - [ADR-0007 — Promote CHECK's kind-to-confidence mapping to a stable output field](0007-check-confidence-field.md) +### `tests/unit/test_graph_command.py` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + +### `tests/unit/test_index.py` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + +### `tests/unit/test_relationships.py` +- [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) + ## Chronological (newest first) +- 2026-08-31 — [ADR-0011 — Expose ADR relationships as Mermaid and SVG navigation artifacts](0011-adr-relationship-graph-public-readiness.md) - 2026-08-30 — [ADR-0001 — Record architecture decisions](0001-record-architecture-decisions.md) - 2026-08-30 — [ADR-0002 — Limit CHECK's conflict detection to structural evidence only](0002-limit-check-s-conflict-detection-to-structural-evidence-only.md) - 2026-08-30 — [ADR-0003 — Localize only index.py's generated strings, not agent-composed text](0003-localize-only-index-py-s-generated-strings-not-agent-composed-text.md) @@ -234,3 +288,28 @@ ### Related - ADR-0006 "Localize deterministic ADR generation through repository configuration" related to: ADR-0003 "Localize only index.py's generated strings, not agent-composed text" +- ADR-0011 "Expose ADR relationships as Mermaid and SVG navigation artifacts" related to: ADR-0001 "Record architecture decisions" +- ADR-0011 "Expose ADR relationships as Mermaid and SVG navigation artifacts" related to: ADR-0006 "Localize deterministic ADR generation through repository configuration" +- ADR-0011 "Expose ADR relationships as Mermaid and SVG navigation artifacts" related to: ADR-0008 "Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress" +- ADR-0011 "Expose ADR relationships as Mermaid and SVG navigation artifacts" related to: ADR-0009 "--json is a documented no-op; CLI output is always JSON" + +```mermaid +flowchart LR + ADR_0001["ADR-0001
Record architecture decisions"] + ADR_0002["ADR-0002
Limit CHECK's conflict detection to structural evidence only"] + ADR_0003["ADR-0003
Localize only index.py's generated strings, not agent-composed text"] + ADR_0004["ADR-0004
Adapter packaging: manifest-only directories, install-time symlinks, verified formats"] + ADR_0005["ADR-0005
Adopt Git Flow with direct-tag release automation"] + ADR_0006["ADR-0006
Localize deterministic ADR generation through repository configuration"] + ADR_0007["ADR-0007
Promote CHECK's kind-to-confidence mapping to a stable output field"] + ADR_0008["ADR-0008
Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress"] + ADR_0009["ADR-0009
--json is a documented no-op; CLI output is always JSON"] + ADR_0010["ADR-0010
Codex skill-creator's quick_validate.py incompatibility is not this project's problem"] + ADR_0011["ADR-0011
Expose ADR relationships as Mermaid and SVG navigation artifacts"] + ADR_0006 -.->|related| ADR_0003 + ADR_0006 -->|supersedes| ADR_0003 + ADR_0011 -.->|related| ADR_0001 + ADR_0011 -.->|related| ADR_0006 + ADR_0011 -.->|related| ADR_0008 + ADR_0011 -.->|related| ADR_0009 +``` diff --git a/docs/decisions/relationships.mmd b/docs/decisions/relationships.mmd new file mode 100644 index 0000000..f36a1e0 --- /dev/null +++ b/docs/decisions/relationships.mmd @@ -0,0 +1,18 @@ +flowchart LR + ADR_0001["ADR-0001
Record architecture decisions"] + ADR_0002["ADR-0002
Limit CHECK's conflict detection to structural evidence only"] + ADR_0003["ADR-0003
Localize only index.py's generated strings, not agent-composed text"] + ADR_0004["ADR-0004
Adapter packaging: manifest-only directories, install-time symlinks, verified formats"] + ADR_0005["ADR-0005
Adopt Git Flow with direct-tag release automation"] + ADR_0006["ADR-0006
Localize deterministic ADR generation through repository configuration"] + ADR_0007["ADR-0007
Promote CHECK's kind-to-confidence mapping to a stable output field"] + ADR_0008["ADR-0008
Deterministic CHECK policy exceptions: schema-validated, annotate-only, never suppress"] + ADR_0009["ADR-0009
--json is a documented no-op; CLI output is always JSON"] + ADR_0010["ADR-0010
Codex skill-creator's quick_validate.py incompatibility is not this project's problem"] + ADR_0011["ADR-0011
Expose ADR relationships as Mermaid and SVG navigation artifacts"] + ADR_0006 -.->|related| ADR_0003 + ADR_0006 -->|supersedes| ADR_0003 + ADR_0011 -.->|related| ADR_0001 + ADR_0011 -.->|related| ADR_0006 + ADR_0011 -.->|related| ADR_0008 + ADR_0011 -.->|related| ADR_0009 diff --git a/docs/decisions/relationships.svg b/docs/decisions/relationships.svg new file mode 100644 index 0000000..7afc55f --- /dev/null +++ b/docs/decisions/relationships.svg @@ -0,0 +1,52 @@ + +ADR relationship graph +Vector export of ADR supersession and related-decision links. + + +ADR relationship graph + +ADR-0001 +Record architecture decisions + +ADR-0002 +Limit CHECK's conflict detection ... + +ADR-0003 +Localize only index.py's generate... + +ADR-0004 +Adapter packaging: manifest-only ... + +ADR-0005 +Adopt Git Flow with direct-tag re... + +ADR-0006 +Localize deterministic ADR genera... + +ADR-0007 +Promote CHECK's kind-to-confidenc... + +ADR-0008 +Deterministic CHECK policy except... + +ADR-0009 +--json is a documented no-op; CLI... + +ADR-0010 +Codex skill-creator's quick_valid... + +ADR-0011 +Expose ADR relationships as Merma... + +ADR-0006 related ADR-0003 + +ADR-0006 supersedes ADR-0003 + +ADR-0011 related ADR-0001 + +ADR-0011 related ADR-0006 + +ADR-0011 related ADR-0008 + +ADR-0011 related ADR-0009 + diff --git a/docs/superpowers/specs/2026-08-31-public-readiness-and-adr-graph-design.md b/docs/superpowers/specs/2026-08-31-public-readiness-and-adr-graph-design.md new file mode 100644 index 0000000..14b0d33 --- /dev/null +++ b/docs/superpowers/specs/2026-08-31-public-readiness-and-adr-graph-design.md @@ -0,0 +1,27 @@ +# Public Readiness and ADR Graph Design + +## Scope + +Prepare ADR Toolkit for public open-source use and add a sharper ADR navigation +view without changing the canonical flat `docs/decisions/` storage model. + +## Decisions + +- Keep ADR Markdown files flat in `docs/decisions/`. +- Add a Mermaid relationship graph to the generated decision index so GitHub can + render it directly. +- Add a deterministic `adr.py graph` export command that writes + `relationships.mmd` and, when requested, a vector `relationships.svg`. +- Generate SVG directly from the same relationship model in Python. The SVG is a + navigation artifact, not a pixel screenshot, so it remains crisp when zoomed. +- Keep PNG out of the default workflow. Raster export can be revisited later if + users explicitly need it. +- Add public repository hygiene docs: contributing guidance, security reporting, + and a pull request template. + +## Validation + +- Unit tests must cover Mermaid block rendering, Mermaid file export, SVG file + export, and graph behavior when no relationships exist. +- Existing ADR validation, index generation, version drift, and full pytest must + pass before completion. diff --git a/handoff.md b/handoff.md index a942d74..4bdfde1 100644 --- a/handoff.md +++ b/handoff.md @@ -2,55 +2,109 @@ ## Current task (2026-08-31) -`v0.2.0` release gate is in progress — the only remaining work -(`improvements.md`'s single open item). Everything else (multilingual ADR -generation, CHECK hardening, exception mechanism, ADR search and -relationship navigation) is implemented and merged into this branch; see -`changelog.md` for what shipped and `git log` for exactly how. +Public-readiness, ADR navigation graph, and harness-parity work is on branch +`feature/project-roadmap-implements` in: -Work runs in the linked worktree at -`/Users/yangseunghyeon/orca/workspaces/ADR-toolkit/develop-2`, branch -`feature/v0.2.0-multilingual-and-check-confidence`, pushed to `origin`. +`/Users/yangseunghyeon/orca/workspaces/ADR-toolkit/develop` + +Latest commits (newest first): + +- `2f2f604 ci: add harness-parity job for Codex and Gemini CLI end-to-end install` +- `e28eb5d docs: record Antigravity CLI adapter verification against agy 1.1.13` +- `b1bcb93 docs: add code of conduct and GitHub issue templates` +- `b8db211 docs: correct stale ADR count in project-roadmap.md` +- `35c81aa docs: record ADR-0011 for relationship graph and public readiness work` +- `8f0343e feat: add ADR relationship graph exports` + +Working tree is clean; every item below is already committed. + +Implemented this session: + +- `adr.py graph --format mermaid|svg|both` exports deterministic Mermaid and + Python-rendered SVG artifacts; `adr.py index` embeds the Mermaid graph in + the generated decision log. ADR-0011 records this decision. +- Public repository hygiene: `CONTRIBUTING.md`, `SECURITY.md`, + `.github/PULL_REQUEST_TEMPLATE.md`, `CODE_OF_CONDUCT.md`, and + `.github/ISSUE_TEMPLATE/{bug_report,feature_request}.md`. +- Harness parity, manually re-verified end-to-end against the real CLIs + installed on this machine (not just manifest-schema checks): + - Codex CLI 0.151.0 -- `codex plugin marketplace add/add/list`, then + `preflight`/`init`/`validate` from the installed snapshot, all + `"ok": true`. + - Gemini CLI 0.46.0 -- `gemini extensions validate/install/list`, then the + same three commands from the installed snapshot, all `"ok": true`. + - Antigravity CLI (`agy` 1.1.13) -- previously undocumented as available; + `agy plugin validate/install/list` plus the same three commands all + `"ok": true`. `adapters/antigravity/README.md` updated from "unverified" + to a recorded transcript, matching the Codex/Gemini READMEs' format. + - New `harness-parity` CI job (`.github/workflows/test.yml`) automates the + Codex and Gemini checks above on every push/PR, pinned to the exact + verified versions. Every step was dry-run locally against those same + CLI versions before landing. Antigravity stays manual-only: `agy` has no + npm/package-registry distribution, so a CI runner can't install it. + - `project-roadmap.md`'s "Harness parity" section narrowed to what's + actually still open: extending `harness-parity` past + preflight/init/validate to check/search/graph/create, and automating + Antigravity once it has an installable distribution. +- `project-roadmap.md` audited end-to-end against the codebase; confirmed no + other section describes already-shipped work. ## Next step -**PR #2 open against `develop`, CI green (6/6), `mergeable`**: -https://github.com/SHcommit/ADR-toolkit/pull/2 +Open or update a PR for `feature/project-roadmap-implements` into `develop` +so the new `harness-parity` CI job actually runs on GitHub's runners at +least once before being trusted (everything above was dry-run locally with +the same pinned CLI versions, but never through the real Actions runner). -Release gate steps, each needing separate explicit owner approval — do not -perform any of the remaining ones without asking first, "CI is green" does -not imply approval for the next step: +The broader `v0.2.0` release gate still needs separate explicit owner approval +for each step: -1. ✅ **Done** — v0.2.0 version bump (`skills/adr-toolkit/VERSION` → - `0.2.0`, propagated via `scripts/sync_version.py`); CI re-ran green. -2. Merge PR #2 into `develop` (normal merge or squash — ask which). -3. Cut a `release/*` branch into `master` and back into `develop`, per +1. Merge the PR into `develop`. +2. Cut a `release/*` branch into `master` and back into `develop`, per `AGENTS.md`'s Git Flow policy. -4. Push a `v0.2.0` tag on `master` — - `.github/workflows/release.yml` then runs the full suite, verifies - manifest versions against the tag, and publishes the GitHub Release. +3. Push a `v0.2.0` tag on `master`; `.github/workflows/release.yml` then runs + the full suite, verifies manifest versions against the tag, and publishes + the GitHub Release. + +Do not perform merge, release branch, push, or tag operations without explicit +owner approval. The repository owner has said the switch to public is coming +"soon" but deliberately not yet -- don't start the "Public and enterprise +governance" roadmap items (they're gated on the repo actually being public). + +## Latest local verification + +After `2f2f604` (current HEAD): -Latest local verification (2026-08-31): `python3 -m pytest -q` → `386 -passed`; `sync_version.py --check` → exit 0; `adr.py validate` → 10 ADRs, -no errors; `git status --short` → clean. +- `python3 -m pytest -q` -> `395 passed` +- `python3 scripts/sync_version.py --check` -> exit 0 +- `adr.py validate --dir docs/decisions --json` -> 11 ADRs, no errors +- `adr.py index --dir docs/decisions --json` -> 11 ADRs, no warnings +- `adr.py graph --dir docs/decisions --format both --json` -> 6 rendered graph + edges, no warnings +- `harness-parity` job's Codex and Gemini steps dry-run locally against + Codex CLI 0.151.0 and Gemini CLI 0.46.0 -- both passed, matching what the + workflow file runs. -An open, undecided idea from this session: measuring how much LLM-token -input/output the deterministic core saves an agent versus doing the same -work by hand (e.g. CHECK returning a structured finding instead of the -agent scanning a full diff). Deliberately deferred — no scope or -destination decided. +Run the full verification suite again before any further commit, and after +the PR's first real CI run, confirm `harness-parity` actually went green on +GitHub's runners (not just locally) before relying on it as a gate. ## Open risks -- CHECK deliberately cannot prove prose, business rationale, or - organizational claims; those remain human-review evidence. The exception - mechanism extends this — an exception is recorded and annotated, never a - silent pass. +- `harness-parity`'s Codex/Gemini steps have only ever run locally on this + macOS dev machine, never on GitHub's `ubuntu-latest` runners. Watch the + first real CI run for environment differences (npm registry access, + network egress rules, `jq` availability -- present by default on + `ubuntu-latest` but unconfirmed here). +- `adr.py check --uncommitted` reports a `VIOLATED` finding for superseded + ADR-0003 when changes touch `index.py`; this branch intentionally follows + ADR-0006, which supersedes ADR-0003. Note that in PR review rather than + treating it as a code defect. +- CHECK deliberately cannot prove prose, business rationale, or organizational + claims; those remain human-review evidence. - Search/relationship matching is deterministic substring/exact/prefix - matching, untested at real scale (this repo has 10 ADRs); revisit - `project-roadmap.md`'s navigation-and-scale items if that changes. + matching, untested at real scale. Revisit roadmap scale items if an adopting + repository reaches hundreds of ADRs. - GitHub branch/tag protection is intentionally unavailable on the current private plan; configure and API-verify after the repository goes public - (owner's own follow-up, not scheduled here). -- Version bump is done; release branch, push, and tag remain unauthorized - until the owner approves each step above. + (owner has deferred this deliberately, not blocked on it). diff --git a/project-roadmap.md b/project-roadmap.md index 25e1a1b..d64bf98 100644 --- a/project-roadmap.md +++ b/project-roadmap.md @@ -15,22 +15,38 @@ before implementation. Concrete selected work belongs in `improvements.md`. ## Harness parity -- Full cross-harness fixture/golden matrix for Codex, Gemini CLI, and - Antigravity CLI at the depth currently available for Claude Code. -- Harness-specific hook support beyond Claude Code SessionStart when equivalent - stable extension points exist. +- ~~Automate the Codex CLI and Gemini CLI adapters' install-and-run + verification.~~ **Done (2026-08-31).** `.github/workflows/test.yml`'s + `harness-parity` job installs the real Codex CLI and Gemini CLI and runs + `preflight`/`init`/`validate` from each one's installed snapshot on every + push and pull request. +- Extend `harness-parity` coverage beyond `preflight`/`init`/`validate` to + `check`, `search`, `graph`, and `create` once a real regression in one of + those commands under a specific harness demonstrates the gap matters. +- Automate the Antigravity CLI (`agy`) adapter the same way once it has a + package-registry distribution a CI runner can install non-interactively; + today it has none, so `adapters/antigravity/README.md`'s manual + verification is the only signal. +- ~~Harness-specific hook support beyond Claude Code SessionStart when + equivalent stable extension points exist.~~ **Evaluated, not pursued + (2026-08-31).** The precondition is now true: Codex CLI has a config-driven + `SessionStart`/`UserPromptSubmit` hook system (`~/.codex/hooks.json`), and + Gemini CLI ships `gemini hooks migrate` specifically to port Claude Code + hooks over. But ADR Toolkit doesn't use a hook even on Claude Code today + (it relies entirely on skill auto-discovery), and a hook that fires on + every session regardless of relevance cuts against this project's own + restraint principle (max 3 questions, judge what's significant, minimize + interruption). The plausible use cases (nudge about an unfinished draft + ADR, warn about a governed path) are already covered by deliberately + invoking `discover` and `check` rather than an always-on hook. Revisit + only if real usage shows people miss something that `discover`/`check` + can't catch without a session-start nudge -- not just because the + extension points now exist. ## ADR navigation and scale -- ~~Add a general search command and relationship visibility.~~ **Done - (2026-08-31).** `adr.py search` (keyword now matches body, not just - title; tag/status/governed-path filters; deterministic ranking) and a - Relationships section in the generated index. See `improvements.md`'s - Done entry and `docs/superpowers/specs/2026-08-31-adr-search-and-relationships-design.md`. -- Render `related`/`supersedes` relationships as a rendered graph image (DOT/ - Mermaid), not just the text-based index lists search/index now provide. - Test whether 500+ decisions require sharding, alternate indexes, or a - real search index (this repo has 10 ADRs; substring/tag/path matching is + real search index (this repo has 11 ADRs; substring/tag/path matching is untested at that scale). - Improve related-decision discovery beyond path/tag/keyword/body-substring only after real misses demonstrate the need for semantic retrieval. diff --git a/skills/adr-toolkit/SKILL.md b/skills/adr-toolkit/SKILL.md index e9ff071..c2c3ac0 100644 --- a/skills/adr-toolkit/SKILL.md +++ b/skills/adr-toolkit/SKILL.md @@ -298,3 +298,17 @@ CHECK's conflict detection is deliberately limited to structural evidence from `constraints:` blocks (see `references/conflict-rules.md`). It does not certify the entire architecture and never attempts semantic or AST-level analysis; see `project-roadmap.md` for what that fuller scope would look like. + +`graph` is a read-only navigation export: + +```bash +python skills/adr-toolkit/scripts/adr.py graph --dir docs/decisions --format both --json +``` + +It writes Mermaid (`relationships.mmd`) and SVG (`relationships.svg`) graph +artifacts from the same relationship model used by `index`. SVG is generated +directly by Python so the image stays sharp without requiring Mermaid CLI, +Node, or browser rendering. +When `--format both` and `--output` are used together, `--output` is a file +prefix; for example `--output build/adr-relationships` writes +`build/adr-relationships.mmd` and `build/adr-relationships.svg`. diff --git a/skills/adr-toolkit/scripts/adr.py b/skills/adr-toolkit/scripts/adr.py index cb93296..9d139dc 100755 --- a/skills/adr-toolkit/scripts/adr.py +++ b/skills/adr-toolkit/scripts/adr.py @@ -13,6 +13,7 @@ diff, discover, exception, + graph, index, init, preflight, @@ -138,6 +139,13 @@ def build_parser() -> argparse.ArgumentParser: p_exception.add_argument("--dry-run", dest="dry_run", action="store_true") _add_json_flag(p_exception) + p_graph = sub.add_parser("graph") + p_graph.add_argument("--dir", default="docs/decisions") + p_graph.add_argument("--root", default=".") + p_graph.add_argument("--format", choices=("mermaid", "svg", "both"), default="both") + p_graph.add_argument("--output") + _add_json_flag(p_graph) + p_search = sub.add_parser("search") p_search.add_argument("--id") p_search.add_argument("--keyword") @@ -166,6 +174,7 @@ def build_parser() -> argparse.ArgumentParser: "diff": diff.run, "check": check.run, "exception": exception.run, + "graph": graph.run, "search": search.run, } diff --git a/skills/adr-toolkit/scripts/commands/graph.py b/skills/adr-toolkit/scripts/commands/graph.py new file mode 100644 index 0000000..19c9ffb --- /dev/null +++ b/skills/adr-toolkit/scripts/commands/graph.py @@ -0,0 +1,65 @@ +"""Export ADR relationships as Mermaid and SVG graph artifacts.""" +from pathlib import Path + +from scripts.core import frontmatter as fm +from scripts.core.adr_directory import iter_adr_files +from scripts.core.relationships import render_mermaid, render_svg, resolve +from scripts.core.repository_paths import resolve_from_root + + +def run(args) -> dict: + root = Path(getattr(args, "root", ".")) + adr_dir = resolve_from_root(root, args.dir) + entries = [] + warnings = [] + + for entry, parsed in iter_adr_files(adr_dir): + if parsed is None: + continue + try: + data, _ = fm.parse(entry.read_text(encoding="utf-8")) + except fm.FrontmatterError as exc: + warnings.append({"code": "BAD_FRONTMATTER", "file": entry.name, "detail": str(exc)}) + continue + entries.append({ + "id": data.get("id", f"ADR-{parsed[0]:04d}"), + "filename": entry.name, + "title": data.get("title", parsed[1]), + "related": data.get("related", []), + "supersedes": data.get("supersedes", []), + "superseded_by": data.get("superseded_by"), + }) + + output = getattr(args, "output", None) + format_ = getattr(args, "format", "both") + outputs = [] + if format_ in {"mermaid", "both"}: + path = _output_path(root, adr_dir, output, format_, "mermaid") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(render_mermaid(entries), encoding="utf-8") + outputs.append(str(path)) + if format_ in {"svg", "both"}: + path = _output_path(root, adr_dir, output, format_, "svg") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(render_svg(entries), encoding="utf-8") + outputs.append(str(path)) + + rendered_edges = [edge for edge in resolve(entries) if edge.type in {"related", "supersedes"}] + return { + "ok": True, + "operation": "graph", + "count": len(rendered_edges), + "outputs": outputs, + "warnings": warnings, + } + + +def _output_path(root: Path, adr_dir: Path, output: str, requested_format: str, actual_format: str) -> Path: + suffix = f".{'mmd' if actual_format == 'mermaid' else 'svg'}" + if not output: + return adr_dir / f"relationships{suffix}" + + base = resolve_from_root(root, output) + if requested_format == "both": + return base.with_suffix(suffix) + return base diff --git a/skills/adr-toolkit/scripts/commands/index.py b/skills/adr-toolkit/scripts/commands/index.py index 526f020..b9148df 100644 --- a/skills/adr-toolkit/scripts/commands/index.py +++ b/skills/adr-toolkit/scripts/commands/index.py @@ -5,7 +5,7 @@ from scripts.core.adr_directory import iter_adr_files from scripts.core.config import ConfigError, resolve_locale from scripts.core.locale import load_locale -from scripts.core.relationships import resolve +from scripts.core.relationships import render_mermaid, resolve from scripts.core.repository_paths import resolve_from_root # Last-resort English headers, used when even the English locale file is @@ -156,4 +156,11 @@ def _render(entries: list, strings: dict) -> str: f"{target['id']} \"{target['title']}\"" ) + visual_edges = [edge for edge in edges if edge.type in {"related", "supersedes"}] + if visual_edges: + lines.append("") + lines.append("```mermaid") + lines.extend(render_mermaid(entries).rstrip().splitlines()) + lines.append("```") + return "\n".join(lines) + "\n" diff --git a/skills/adr-toolkit/scripts/core/relationships.py b/skills/adr-toolkit/scripts/core/relationships.py index 865ccbd..d9f8bd0 100644 --- a/skills/adr-toolkit/scripts/core/relationships.py +++ b/skills/adr-toolkit/scripts/core/relationships.py @@ -1,4 +1,5 @@ """Canonical ADR relationship model, shared by index.py and validate.py.""" +from html import escape from typing import NamedTuple @@ -69,3 +70,116 @@ def dfs(node, path, path_set): dfs(node, [], set()) return cycles + + +def render_mermaid(entries: list) -> str: + """Render ADR relationships as GitHub-compatible Mermaid flowchart text.""" + by_id = _entries_by_id(entries) + relationships = resolve(entries) + visual_edges = _visual_edges(relationships) + lines = ["flowchart LR"] + + for adr_id in sorted(by_id): + entry = by_id[adr_id] + label = _mermaid_label(adr_id, entry.get("title", adr_id)) + lines.append(f" {_node_id(adr_id)}[\"{label}\"]") + + if not visual_edges: + lines.append(' EMPTY["No relationships recorded"]') + + for relationship in sorted(visual_edges): + source = _node_id(relationship.source) + target = _node_id(relationship.target) + if relationship.type == "related": + lines.append(f" {source} -.->|related| {target}") + else: + lines.append(f" {source} -->|supersedes| {target}") + + return "\n".join(lines) + "\n" + + +def render_svg(entries: list) -> str: + """Render a small deterministic SVG navigation artifact. + + This is intentionally not a full Mermaid renderer. It uses the same graph + model and writes vector text/lines directly so the exported artifact stays + sharp without requiring Node or browser automation. + """ + by_id = _entries_by_id(entries) + relationships = _visual_edges(resolve(entries)) + ordered_ids = sorted(by_id) + width = 960 + row_height = 82 + edge_height = 28 + node_height = max(1, len(ordered_ids)) * row_height + edge_start = 72 + node_height + height = edge_start + max(1, len(relationships)) * edge_height + 48 + y_by_id = {adr_id: 72 + index * row_height for index, adr_id in enumerate(ordered_ids)} + + lines = [ + f'', + "ADR relationship graph", + "Vector export of ADR supersession and related-decision links.", + '', + '', + 'ADR relationship graph', + ] + + for adr_id in ordered_ids: + y = y_by_id[adr_id] + title = str(by_id[adr_id].get("title", adr_id)) + lines.extend([ + f'', + f'{escape(adr_id)}', + f'{escape(_truncate(title, 36))}', + ]) + + if relationships: + pair_totals = {} + for relationship in relationships: + pair = (relationship.source, relationship.target) + pair_totals[pair] = pair_totals.get(pair, 0) + 1 + pair_seen = {} + for index, relationship in enumerate(sorted(relationships)): + pair = (relationship.source, relationship.target) + pair_seen[pair] = pair_seen.get(pair, 0) + 1 + offset = (pair_seen[pair] - (pair_totals[pair] + 1) / 2) * 14 + source_y = y_by_id.get(relationship.source, 72) + 26 + offset + target_y = y_by_id.get(relationship.target, 72) + 26 + offset + edge_y = edge_start + index * edge_height + label = f"{relationship.source} {relationship.type} {relationship.target}" + dash = ' stroke-dasharray="5 5"' if relationship.type == "related" else "" + lines.extend([ + f'', + f'{escape(label)}', + ]) + else: + lines.append('No relationships recorded') + + lines.append("") + return "\n".join(lines) + "\n" + + +def _entries_by_id(entries: list) -> dict: + return {entry["id"]: entry for entry in entries if entry.get("id")} + + +def _visual_edges(relationships: list) -> list: + return [r for r in relationships if r.type in {"related", "supersedes"}] + + +def _node_id(adr_id: str) -> str: + return adr_id.replace("-", "_") + + +def _mermaid_label(adr_id: str, title: str) -> str: + safe_title = " ".join(str(title).split()) + safe_title = escape(safe_title, quote=False) + safe_title = safe_title.replace('"', "'").replace("[", "(").replace("]", ")") + return f"{adr_id}
{safe_title}" + + +def _truncate(value: str, limit: int) -> str: + if len(value) <= limit: + return value + return value[: limit - 3] + "..." diff --git a/tests/unit/test_adr_cli.py b/tests/unit/test_adr_cli.py index a122cc0..b8378cc 100644 --- a/tests/unit/test_adr_cli.py +++ b/tests/unit/test_adr_cli.py @@ -53,6 +53,20 @@ def test_index_locale_is_unset_when_omitted_so_repository_default_can_win(): assert args.root == "." +def test_graph_command_accepts_format_and_output_options(): + args = adr.build_parser().parse_args([ + "graph", + "--dir", "docs/decisions", + "--format", "svg", + "--output", "docs/decisions/relationships.svg", + ]) + + assert args.operation == "graph" + assert args.format == "svg" + assert args.output == "docs/decisions/relationships.svg" + assert adr.HANDLERS["graph"].__module__ == "scripts.commands.graph" + + def test_init_locale_is_optional_and_constrained(): parser = adr.build_parser() diff --git a/tests/unit/test_graph_command.py b/tests/unit/test_graph_command.py new file mode 100644 index 0000000..c03c378 --- /dev/null +++ b/tests/unit/test_graph_command.py @@ -0,0 +1,120 @@ +from types import SimpleNamespace + +from scripts.commands import graph + + +def _write_adr(adr_dir, filename, *, id_, title, related=None, supersedes=None, superseded_by=None): + lines = [ + "---", + f"id: {id_}", + f"title: {title}", + "status: accepted", + "date: 2026-08-31", + "decision_makers: []", + ] + if related: + lines.append("related:") + lines.extend(f" - {value}" for value in related) + else: + lines.append("related: []") + lines.append("affected_paths: []") + lines.append("tags: []") + lines.append("retrospective: false") + if supersedes: + lines.append("supersedes:") + lines.extend(f" - {value}" for value in supersedes) + if superseded_by: + lines.append(f"superseded_by: {superseded_by}") + lines.extend(["---", "", f"# {title}", ""]) + (adr_dir / filename).write_text("\n".join(lines), encoding="utf-8") + + +def test_graph_command_writes_mermaid_and_svg_exports(tmp_path): + adr_dir = tmp_path / "docs" / "decisions" + adr_dir.mkdir(parents=True) + _write_adr(adr_dir, "0001-old.md", id_="ADR-0001", title="Old", superseded_by="ADR-0002") + _write_adr(adr_dir, "0002-new.md", id_="ADR-0002", title="New", supersedes=["ADR-0001"]) + + result = graph.run(SimpleNamespace( + dir="docs/decisions", + root=str(tmp_path), + output=None, + format="both", + )) + + assert result["ok"] is True + assert result["count"] == 1 + assert result["outputs"] == [ + str(adr_dir / "relationships.mmd"), + str(adr_dir / "relationships.svg"), + ] + assert "ADR_0002 -->|supersedes| ADR_0001" in ( + adr_dir / "relationships.mmd" + ).read_text(encoding="utf-8") + assert "ADR-0002 supersedes ADR-0001" in ( + adr_dir / "relationships.svg" + ).read_text(encoding="utf-8") + + +def test_graph_command_can_write_only_mermaid_to_custom_output(tmp_path): + adr_dir = tmp_path / "docs" / "decisions" + adr_dir.mkdir(parents=True) + _write_adr(adr_dir, "0001-a.md", id_="ADR-0001", title="A", related=["ADR-0002"]) + _write_adr(adr_dir, "0002-b.md", id_="ADR-0002", title="B") + output = tmp_path / "graph.mmd" + + result = graph.run(SimpleNamespace( + dir=str(adr_dir), + root=str(tmp_path), + output=str(output), + format="mermaid", + )) + + assert result["ok"] is True + assert result["outputs"] == [str(output)] + assert "ADR_0001 -.->|related| ADR_0002" in output.read_text(encoding="utf-8") + + +def test_graph_command_uses_custom_output_as_prefix_for_both_formats(tmp_path): + adr_dir = tmp_path / "docs" / "decisions" + adr_dir.mkdir(parents=True) + _write_adr(adr_dir, "0001-a.md", id_="ADR-0001", title="A", related=["ADR-0002"]) + _write_adr(adr_dir, "0002-b.md", id_="ADR-0002", title="B") + + result = graph.run(SimpleNamespace( + dir="docs/decisions", + root=str(tmp_path), + output="build/adr-relationships", + format="both", + )) + + assert result["ok"] is True + assert result["outputs"] == [ + str(tmp_path / "build" / "adr-relationships.mmd"), + str(tmp_path / "build" / "adr-relationships.svg"), + ] + assert (tmp_path / "build" / "adr-relationships.mmd").is_file() + assert (tmp_path / "build" / "adr-relationships.svg").is_file() + + +def test_graph_command_resolves_relative_single_output_against_root(tmp_path, monkeypatch): + caller = tmp_path / "caller" + repo = tmp_path / "repo" + adr_dir = repo / "docs" / "decisions" + caller.mkdir() + adr_dir.mkdir(parents=True) + _write_adr(adr_dir, "0001-a.md", id_="ADR-0001", title="A", related=["ADR-0002"]) + _write_adr(adr_dir, "0002-b.md", id_="ADR-0002", title="B") + monkeypatch.chdir(caller) + + result = graph.run(SimpleNamespace( + dir="docs/decisions", + root=str(repo), + output="docs/decisions/custom.svg", + format="svg", + )) + + assert result["ok"] is True + assert result["outputs"] == [str(repo / "docs" / "decisions" / "custom.svg")] + assert (repo / "docs" / "decisions" / "custom.svg").is_file() + assert not (caller / "docs" / "decisions" / "custom.svg").exists() diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index f917870..a62b60e 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -229,6 +229,8 @@ def test_index_relationships_section_shows_supersession_with_titles(tmp_path): assert "## Relationships" in readme assert "ADR-0003" in readme and "Old decision" in readme assert "ADR-0006" in readme and "New decision" in readme + assert "```mermaid" in readme + assert "ADR_0006 -->|supersedes| ADR_0003" in readme def test_index_relationships_section_omits_adr_with_no_relationships(tmp_path): @@ -245,3 +247,4 @@ def test_index_relationships_section_omits_adr_with_no_relationships(tmp_path): readme = (adr_dir / "README.md").read_text(encoding="utf-8") assert "Lonely decision" not in readme.split("## Relationships")[1] + assert "```mermaid" not in readme diff --git a/tests/unit/test_relationships.py b/tests/unit/test_relationships.py index 97b9dde..0717478 100644 --- a/tests/unit/test_relationships.py +++ b/tests/unit/test_relationships.py @@ -2,15 +2,17 @@ Relationship, find_cycles, missing_targets, + render_mermaid, + render_svg, resolve, supersession_mismatches, ) ENTRIES = [ - {"id": "ADR-0003", "related": [], "supersedes": [], "superseded_by": "ADR-0006"}, - {"id": "ADR-0006", "related": [], "supersedes": ["ADR-0003"], "superseded_by": None}, - {"id": "ADR-0007", "related": ["ADR-0002"], "supersedes": [], "superseded_by": None}, - {"id": "ADR-0002", "related": [], "supersedes": [], "superseded_by": None}, + {"id": "ADR-0003", "title": "Old decision", "related": [], "supersedes": [], "superseded_by": "ADR-0006"}, + {"id": "ADR-0006", "title": "New decision", "related": [], "supersedes": ["ADR-0003"], "superseded_by": None}, + {"id": "ADR-0007", "title": "Confidence field", "related": ["ADR-0002"], "supersedes": [], "superseded_by": None}, + {"id": "ADR-0002", "title": "Structural checks", "related": [], "supersedes": [], "superseded_by": None}, ] @@ -103,3 +105,39 @@ def test_find_cycles_ignores_related_edges(): {"id": "ADR-0002", "related": ["ADR-0001"]}, ] assert find_cycles(resolve(mutually_related)) == [] + + +def test_render_mermaid_includes_supersession_and_related_edges_with_labels(): + mermaid = render_mermaid(ENTRIES) + + assert mermaid.startswith("flowchart LR\n") + assert "ADR_0006[\"ADR-0006
New decision\"]" in mermaid + assert "ADR_0006 -->|supersedes| ADR_0003" in mermaid + assert "ADR_0007 -.->|related| ADR_0002" in mermaid + assert "ADR_0003 -->|superseded by| ADR_0006" not in mermaid + + +def test_render_mermaid_reports_no_relationships_when_graph_has_no_edges(): + mermaid = render_mermaid([{"id": "ADR-0001", "title": "Lonely decision"}]) + + assert "ADR_0001[\"ADR-0001
Lonely decision\"]" in mermaid + assert "No relationships recorded" in mermaid + + +def test_render_mermaid_escapes_title_text_for_label_safety(): + mermaid = render_mermaid([ + {"id": "ADR-0001", "title": "Use \"ports\" [core]\nnow"}, + ]) + + assert "Use <A&B> 'ports' (core) now" in mermaid + assert "\nnow" not in mermaid + + +def test_render_svg_returns_crisp_vector_navigation_artifact(): + svg = render_svg(ENTRIES) + + assert svg.startswith("ADR relationship graph" in svg + assert "ADR-0006" in svg and "New decision" in svg + assert "ADR-0006 supersedes ADR-0003" in svg + assert "ADR-0007 related ADR-0002" in svg