docs: audit links, glossary, readability pass #2

Open
eSlider wants to merge 9 commits from docs/improvement into main
10 changed files with 168 additions and 14 deletions
Showing only changes of commit 10a18f1321 - Show all commits
+6
View File
@@ -30,9 +30,15 @@ jobs:
git fetch --depth=50 origin "${{ gitea.ref }}" git fetch --depth=50 origin "${{ gitea.ref }}"
git checkout FETCH_HEAD git checkout FETCH_HEAD
- name: Fetch base branch for ADR immutability
run: git fetch --depth=1 origin main
- name: Install checker dependencies - name: Install checker dependencies
run: pip install --quiet pyyaml run: pip install --quiet pyyaml
- name: ADR immutability (no edits to accepted records)
run: python scripts/check_adrs.py --base origin/main
- name: Documentation rules (links, glossary, readability) - name: Documentation rules (links, glossary, readability)
run: python scripts/check_docs.py run: python scripts/check_docs.py
+6 -1
View File
@@ -61,7 +61,12 @@ Status / Context / Options / Decision / Consequences format. Start from
When a change alters a boundary, contract, or trade-off, add an ADR (copy When a change alters a boundary, contract, or trade-off, add an ADR (copy
[`docs/adr/TEMPLATE.md`](docs/adr/TEMPLATE.md), take the next number, update the [`docs/adr/TEMPLATE.md`](docs/adr/TEMPLATE.md), take the next number, update the
index) and cross-link it from the affected design doc. A later reversal gets a index) and cross-link it from the affected design doc. A later reversal gets a
new ADR that supersedes the old one — never edit an Accepted record. new ADR that supersedes the old one — **never edit an Accepted record**.
**Immutable:** every `docs/adr/ADR-*.md` file is frozen once on `main`. CI rejects
any change that modifies an existing ADR. Updates go in a new numbered ADR only.
The index ([`docs/adr/README.md`](docs/adr/README.md)) and [`TEMPLATE.md`](docs/adr/TEMPLATE.md)
may still change; decision bodies do not.
## Runtime & operations (sim environment) ## Runtime & operations (sim environment)
+1 -1
View File
@@ -117,7 +117,7 @@ Default review voice = caveman-full:
- [ ] Tests added or updated; `pytest tests/ -q` passes locally - [ ] Tests added or updated; `pytest tests/ -q` passes locally
- [ ] `docker build` in `simulator/` succeeds (test stage included) - [ ] `docker build` in `simulator/` succeeds (test stage included)
- [ ] Documentation updated if behaviour or boundaries changed - [ ] Documentation updated if behaviour or boundaries changed
- [ ] **ADR added** if the change alters an architectural boundary, contract, or trade-off (see [`docs/adr/`](docs/adr/)) - [ ] **ADR added** if the change alters an architectural boundary, contract, or trade-off (see [`docs/adr/`](docs/adr/)) — never edit an existing `ADR-*.md`; supersede with a new number
- [ ] English only; no identifying references - [ ] English only; no identifying references
- [ ] Conventional commit message - [ ] Conventional commit message
+1 -1
View File
@@ -17,7 +17,7 @@ Partitions are the normal case, not the exception.
| Option | Pros | Cons | | Option | Pros | Cons |
| --- | --- | --- | | --- | --- | --- |
| A — Cluster orchestrator spanning the swarm | Familiar tooling | Assumes stable membership; a partition stalls coordination; single point of failure in the air | | A — Cluster orchestrator spanning the swarm | Familiar tooling | Assumes stable membership; a partition stalls coordination; single point of failure in the air |
| B — Each drone autonomous, coordinating through exchanged data | Survives partitions; no air-side control plane to fail | No global view; behavior must be derivable from local state | | B — Each drone autonomous, coordinating through exchanged data | Survives partitions; no air-side control plane to fail | No global view; behaviour must be derivable from local state |
## Decision ## Decision
+3 -7
View File
@@ -6,13 +6,9 @@ Accepted (2026-07-08)
## Context ## Context
Data lives across [storage floors](../00-glossary.md#t0--hot) T0T4. Primary persistent Data lives in three places: on the drone's NVMe in flight (T1), in the ground
stores are [T1](../00-glossary.md#t1--warm) (drone NVMe in flight) and warehouse after landing (T3), and in the dev/simulation environment. If each
[T3](../00-glossary.md#t3--warehouse) (ground warehouse after landing); tier used a different format or layout, offload would need transformation code,
[T0](../00-glossary.md#t0--hot) is the in-memory hot window,
[T2](../00-glossary.md#t2--shared) optional on-board derived replication,
[T4](../00-glossary.md#t4--dev) dev/sim slices. If each floor used a different
format or layout, offload would need transformation code,
schemas would drift, and a query proven on the bench would not run unchanged on schemas would drift, and a query proven on the bench would not run unchanged on
flight data. flight data.
+1 -1
View File
@@ -25,7 +25,7 @@ new derived data triggers downstream action through hooks; nothing polls asking
- Air bandwidth scales with the number of peers and the broadcast rate, not - Air bandwidth scales with the number of peers and the broadcast rate, not
with sensor resolution. with sensor resolution.
- The [source of truth](../00-glossary.md#source-of-truth) is the only place with the full picture; in-flight decisions use - The warehouse is the only place with the full picture; in-flight decisions use
the local window plus peer broadcasts. the local window plus peer broadcasts.
- Broadcast staleness per peer becomes a first-class health signal - Broadcast staleness per peer becomes a first-class health signal
([`../07-observability.md`](../07-observability.md)). ([`../07-observability.md`](../07-observability.md)).
+3 -3
View File
@@ -27,14 +27,14 @@ decisions specific to the platform are ADRs and belong here.
| [ADR-0005](ADR-0005-wireguard-beneath-ssh.md) | WireGuard beneath SSH for the mesh transport | Accepted | | [ADR-0005](ADR-0005-wireguard-beneath-ssh.md) | WireGuard beneath SSH for the mesh transport | Accepted |
| [ADR-0006](ADR-0006-iac-boundaries.md) | IaC split: Ansible hosts, Terraform ground, Flux ground-only, fleet manifest for drones | Accepted | | [ADR-0006](ADR-0006-iac-boundaries.md) | IaC split: Ansible hosts, Terraform ground, Flux ground-only, fleet manifest for drones | Accepted |
| [ADR-0007](ADR-0007-non-root-image.md) | Run the simulator image as a non-root user | Accepted | | [ADR-0007](ADR-0007-non-root-image.md) | Run the simulator image as a non-root user | Accepted |
| [ADR-0008](ADR-0008-runtime-data-paths.md) | Shared [`var/t1`](../00-glossary.md#t1--warm) / [`var/t3`](../00-glossary.md#t3--warehouse) runtime paths; state out of Git | Accepted | | [ADR-0008](ADR-0008-runtime-data-paths.md) | Shared `var/t1` / `var/t3` runtime paths; state out of Git | Accepted |
| [ADR-0009](ADR-0009-bounded-lake-scans.md) | Bound observability lake scans to a flight window and cache | Accepted | | [ADR-0009](ADR-0009-bounded-lake-scans.md) | Bound observability lake scans to a flight window and cache | Accepted |
## Writing a new ADR ## Writing a new ADR
1. Copy [`TEMPLATE.md`](TEMPLATE.md) to `ADR-NNNN-short-slug.md` (next free number). 1. Copy [`TEMPLATE.md`](TEMPLATE.md) to `ADR-NNNN-short-slug.md` (next free number).
2. Context, Options, Decision, Consequences. 2. Fill in Context, Options (if more than one was real), Decision, Consequences.
3. Add a row to the index above. 3. Add a row to the index above.
4. Cross-link the ADR from the design doc it affects (and vice versa). 4. Cross-link the ADR from the design doc it affects (and vice versa).
Accepted = immutable. Reversal → new ADR supersedes. Never edit Accepted. Keep the tone laconic: state the decision and why, skip the narrative.
+2
View File
@@ -18,6 +18,7 @@ Tracked from [issue #1](https://git.produktor.io/eSlider/swarm-house/issues/1) a
| Glossary fragments | error | All `00-glossary.md#…` links use anchors that exist in the glossary | | Glossary fragments | error | All `00-glossary.md#…` links use anchors that exist in the glossary |
| Section vs term links | warning | Links to glossary *section* headers (`#swarm--robotics`, …) are listed; prefer `###` term anchors where they exist | | Section vs term links | warning | Links to glossary *section* headers (`#swarm--robotics`, …) are listed; prefer `###` term anchors where they exist |
| Bare cross-refs | error | Prose cross-references to numbered docs without a proper markdown link | | Bare cross-refs | error | Prose cross-references to numbered docs without a proper markdown link |
| ADR immutability | error | No modifications to existing `docs/adr/ADR-*.md` (compare `origin/main`); add new ADR to supersede |
Warnings are printed in the job log but do not fail CI. Errors exit non-zero and block merge. Warnings are printed in the job log but do not fail CI. Errors exit non-zero and block merge.
@@ -46,6 +47,7 @@ Docs-only changes therefore still get link and glossary validation on `main`; th
```bash ```bash
python scripts/check_docs.py python scripts/check_docs.py
python scripts/check_adrs.py --base origin/main
python scripts/check_docs.py --warnings-as-errors # treat section-link warnings as failures python scripts/check_docs.py --warnings-as-errors # treat section-link warnings as failures
``` ```
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Reject modifications to accepted Architecture Decision Records."""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ADR_PREFIX = "docs/adr/ADR-"
def _git(*args: str) -> str:
return subprocess.check_output(
["git", *args],
cwd=ROOT,
text=True,
stderr=subprocess.DEVNULL,
).strip()
def _exists_on(base: str, path: str) -> bool:
try:
_git("cat-file", "-e", f"{base}:{path}")
return True
except subprocess.CalledProcessError:
return False
def find_modified_adrs(base: str) -> list[str]:
try:
diff_out = _git("diff", "--name-only", f"{base}...HEAD", "--", "docs/adr/")
except subprocess.CalledProcessError:
return []
modified: list[str] = []
for rel in diff_out.splitlines():
if not rel.startswith(ADR_PREFIX) or not rel.endswith(".md"):
continue
if _exists_on(base, rel):
modified.append(rel)
return sorted(modified)
def run_check(base: str = "origin/main") -> int:
modified = find_modified_adrs(base)
if modified:
print("Immutable ADR violation — accepted records cannot be edited:")
for path in modified:
print(f" error: {path}")
print(
"\nReversal or refinement → new ADR-NNNN file. "
"Never edit an existing ADR-*.md."
)
return 1
print(f"ADR immutability OK (base {base})")
return 0
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--base",
default="origin/main",
help="Git ref to compare against (default: origin/main)",
)
args = parser.parse_args()
sys.exit(run_check(base=args.base))
if __name__ == "__main__":
main()
+70
View File
@@ -0,0 +1,70 @@
"""Tests for scripts/check_adrs.py — ADR immutability gate."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
import pytest
SCRIPTS = Path(__file__).resolve().parents[2] / "scripts"
sys.path.insert(0, str(SCRIPTS))
from check_adrs import find_modified_adrs, run_check # noqa: E402
def _git(cwd: Path, *args: str) -> None:
subprocess.check_call(["git", *args], cwd=cwd, stdout=subprocess.DEVNULL)
def test_no_modified_adrs_on_current_branch() -> None:
assert run_check(base="origin/main") == 0
def test_detects_adr_edit_in_sandbox(tmp_path: Path) -> None:
_git(tmp_path, "init")
_git(tmp_path, "config", "user.email", "ci@test")
_git(tmp_path, "config", "user.name", "ci")
adr = tmp_path / "docs/adr"
adr.mkdir(parents=True)
(adr / "ADR-0001-test.md").write_text("v1\n")
_git(tmp_path, "add", ".")
_git(tmp_path, "commit", "-m", "add adr")
_git(tmp_path, "branch", "-M", "main")
(adr / "ADR-0001-test.md").write_text("v2\n")
_git(tmp_path, "checkout", "-b", "feature")
_git(tmp_path, "add", ".")
_git(tmp_path, "commit", "-m", "edit adr")
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr("check_adrs.ROOT", tmp_path)
try:
assert find_modified_adrs("main") == ["docs/adr/ADR-0001-test.md"]
assert run_check(base="main") == 1
finally:
monkeypatch.undo()
def test_new_adr_allowed(tmp_path: Path) -> None:
_git(tmp_path, "init")
_git(tmp_path, "config", "user.email", "ci@test")
_git(tmp_path, "config", "user.name", "ci")
adr = tmp_path / "docs/adr"
adr.mkdir(parents=True)
(adr / "ADR-0001-test.md").write_text("v1\n")
_git(tmp_path, "add", ".")
_git(tmp_path, "commit", "-m", "add adr")
_git(tmp_path, "branch", "-M", "main")
(adr / "ADR-0002-new.md").write_text("new\n")
_git(tmp_path, "checkout", "-b", "feature")
_git(tmp_path, "add", ".")
_git(tmp_path, "commit", "-m", "add adr 2")
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr("check_adrs.ROOT", tmp_path)
try:
assert find_modified_adrs("main") == []
assert run_check(base="main") == 0
finally:
monkeypatch.undo()