Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/ways/kg/documentation/way.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ resolvable edges, no supersede cycles, and the vacated-range guard.
overwritten every build — their frontmatter is **emitted by the generator**
(`cli/scripts/*`), never hand-injected, or the next regen wipes it.
- **Retired-range guard is ON** (`legacy: {retired: true}`): no doc/ADR/source may
reference the vacated legacy range (ADR numbers 1–99). The scan honors `.gitignore`, so
reference the vacated legacy range (ADR numbers 1–99) — except the defining ADR
(`legacy.defining_adr: ADR-900`), which names those numbers legitimately. The scan honors `.gitignore`, so
gitignored corpora (e.g. `examples/.../claude-ai-history`) and scratch are
skipped. Raw audit/scan byproducts go to `*-raw.json` (gitignored), not committed.
- mkdocs strips unknown frontmatter, so catalog ids never reach readers — they are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,18 @@ tooling — was generalized out of this repo into the agent-ways framework as it
canonical *documentation model* (the framework's own `ADR-302`, unrelated to this
project's `ADR-302`). This repo is that model's **reference implementation**, and
`docs/scripts/{doc,doclint.py}` are **vendored copies** of the canonical tools
(copy-not-symlink, refreshed from canonical). That is why the vendored sources
cite `ADR-302` (the upstream model) and credit `ADR-908`/`ADR-900` (this repo's
local decisions) — the two numbers name two different things and both are correct.
The framework-side reference is prose only: do **not** add `ADR-302` to this ADR's
`related:` edges, since here that id resolves to the multimodal-ingestion ADR and
would be a false graph edge.
(copy-not-symlink, refreshed from canonical). The canonical tools are
**ADR-number-agnostic** — they refer to "your project's documentation-catalog ADR"
rather than hardcoding a number — so the vendored copies need no local citation
patch; *this* ADR (ADR-908) is that documentation-catalog ADR for this repo. One
caution survives the generalization: the framework's `ADR-302` is a different
document from this project's `ADR-302` (multimodal ingestion), so do **not** add
`ADR-302` to this ADR's `related:` edges — here it would resolve to the wrong node
and become a false graph edge.

The vacated-range guard (`legacy: {retired: true}`) exempts the **defining ADR**
named by `legacy.defining_adr` (here `ADR-900`), which legitimately names retired
1–99 numbers; every other doc, ADR, and source file is still policed.

### Numbering freeze

Expand Down
1 change: 1 addition & 0 deletions docs/architecture/adr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ legacy:
range: [1, 99]
label: "Retired (renumbered into domains 2026-06-15)"
retired: true # vacated range — doclint fails the build on any reference into 1–99
defining_adr: ADR-900 # the ADR that vacated the range; exempt — it names retired numbers legitimately

# Viewer command for `adr view`
# Use {file} as placeholder for the file path
Expand Down
7 changes: 4 additions & 3 deletions docs/scripts/doc
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
"""
doc — a librarian for the documentation catalog.

Presents the catalog (frontmatter `id`/`domain`/`mode`, ADR-908) over the shared
domain bands: a domain×mode coverage matrix, page listings, and a gap report that
weighs doc coverage against ADR count per domain.
Presents the catalog (frontmatter `id`/`domain`/`mode`) over the shared domain
bands: a domain×mode coverage matrix, page listings, and a gap report that weighs
doc coverage against ADR count per domain. See your project's
documentation-catalog ADR for the model.

This is the *diagnostic* front-end. The *test* is `doclint` (the CI linter);
`doc lint` invokes it. Sibling of the `adr` tool — same `adr.yaml` config.
Expand Down
30 changes: 19 additions & 11 deletions docs/scripts/doclint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

Extends the ADR linter (`docs/scripts/adr lint`) from ADRs to the whole `docs/`
tree, treating docs and ADRs as a single *decision graph*: nodes are records,
edges are `related`/`supersedes` references. See ADR-302 (unified documentation
model); generalized from the knowledge-graph-system reference implementation
(its ADR-908/ADR-900).
edges are `related`/`supersedes` references. See your project's
documentation-catalog decision record for the model; this tool was generalized
from the knowledge-graph-system reference implementation.

It checks:

Expand All @@ -25,8 +25,10 @@
ignored, so a repo can adopt the catalog gradually instead of all-at-once.
- **mkdocs nav is optional.** No `mkdocs.yml` → the orphan check is skipped.
- **The retired-range guard is opt-in.** Set `legacy: {retired: true}` in
`adr.yaml` to fail on references into a vacated pre-domain range (the
ADR-900 move). Repos that still use their legacy range leave it off (default).
`adr.yaml` to fail on references into a vacated pre-domain range. Set
`legacy: {defining_adr: ADR-NNN}` to exempt the ADR that vacated the range (it
names retired numbers legitimately). Repos still using their legacy range leave
both off (default).
- **Project root is discovered** (git, then walking up for
`docs/architecture/adr.yaml`), so the script works whether it is a symlink
into the ways corpus (agent-ways dogfooding) or a vendored copy in a project.
Expand Down Expand Up @@ -99,7 +101,6 @@ def get_project_root() -> Path:
".sh", ".yml", ".yaml", ".json"}
RETIRED_SKIP_PARTS = {"node_modules", "dist", "site", ".git"}
RETIRED_EXEMPT_NAMES = {"adr.yaml"}
RETIRED_EXEMPT_PREFIXES = ("ADR-900-",)
RETIRED_ALLOW_MARKER = "doclint-allow-retired"
ADR_ANYREF_RE = re.compile(r"\bADR-0*(\d+)(\.\d+)?\b")

Expand Down Expand Up @@ -165,7 +166,7 @@ def parse_frontmatter(path: Path) -> dict:
def _as_ref_list(value) -> list:
"""Coerce a related/supersedes value into target strings, stripping wikilinks.

ADR-302 edges are Obsidian `[[wikilinks]]`; the inside is the catalog id or
Catalog edges are Obsidian `[[wikilinks]]`; the inside is the catalog id or
ADR reference. The aliased form `[[target|alias]]` keeps only `target`. Bare
strings are accepted too (pre-wikilink ADRs).
"""
Expand Down Expand Up @@ -377,15 +378,20 @@ def _retired_scan_files():
yield from REPO.rglob("*") # non-git fallback


def check_retired_refs(lo: int, hi: int):
"""Scan repo for references into a vacated pre-domain range (opt-in)."""
def check_retired_refs(lo: int, hi: int, exempt_prefixes: tuple = ()):
"""Scan repo for references into a vacated pre-domain range (opt-in).

exempt_prefixes: filename prefixes to skip — typically the ADR that vacated
the range (it names retired numbers legitimately), from adr.yaml
legacy.defining_adr.
"""
hits = []
for f in _retired_scan_files():
if not f.is_file() or f.suffix not in RETIRED_SCAN_EXTS:
continue
if RETIRED_SKIP_PARTS & set(f.parts):
continue
if f.name in RETIRED_EXEMPT_NAMES or f.name.startswith(RETIRED_EXEMPT_PREFIXES):
if f.name in RETIRED_EXEMPT_NAMES or (exempt_prefixes and f.name.startswith(exempt_prefixes)):
continue
try:
text = f.read_text()
Expand Down Expand Up @@ -473,7 +479,9 @@ def effective(node, severity):
legacy = cfg.get("legacy", {}) or {}
if legacy.get("retired"):
lo, hi = (int(x) for x in legacy.get("range", [1, 99]))
retired_hits = check_retired_refs(lo, hi)
defining = legacy.get("defining_adr")
exempt = (f"{defining}-",) if defining else ()
retired_hits = check_retired_refs(lo, hi, exempt)
if retired_hits:
print(f"\nRetired-range references (ADR-{lo}..{hi} are vacated):")
for rel, ln, ref in sorted(retired_hits):
Expand Down
Loading