docs: audit links, glossary, readability pass #2
@@ -30,9 +30,15 @@ jobs:
|
||||
git fetch --depth=50 origin "${{ gitea.ref }}"
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- name: Fetch base branch for ADR immutability
|
||||
run: git fetch --depth=1 origin main
|
||||
|
||||
- name: Install checker dependencies
|
||||
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)
|
||||
run: python scripts/check_docs.py
|
||||
|
||||
|
||||
@@ -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
|
||||
[`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
|
||||
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)
|
||||
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ Default review voice = caveman-full:
|
||||
- [ ] Tests added or updated; `pytest tests/ -q` passes locally
|
||||
- [ ] `docker build` in `simulator/` succeeds (test stage included)
|
||||
- [ ] 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
|
||||
- [ ] Conventional commit message
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Partitions are the normal case, not the exception.
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -6,13 +6,9 @@ Accepted (2026-07-08)
|
||||
|
||||
## Context
|
||||
|
||||
Data lives across [storage floors](../00-glossary.md#t0--hot) T0–T4. Primary persistent
|
||||
stores are [T1](../00-glossary.md#t1--warm) (drone NVMe in flight) and
|
||||
[T3](../00-glossary.md#t3--warehouse) (ground warehouse after landing);
|
||||
[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,
|
||||
Data lives in three places: on the drone's NVMe in flight (T1), in the ground
|
||||
warehouse after landing (T3), and in the dev/simulation environment. If each
|
||||
tier 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
|
||||
flight data.
|
||||
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
- Broadcast staleness per peer becomes a first-class health signal
|
||||
([`../07-observability.md`](../07-observability.md)).
|
||||
|
||||
+3
-3
@@ -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-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-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 |
|
||||
|
||||
## Writing a new ADR
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
| 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.
|
||||
|
||||
@@ -46,6 +47,7 @@ Docs-only changes therefore still get link and glossary validation on `main`; th
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user