docs: audit links, glossary, readability pass #2
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,9 @@ Accepted (2026-07-08)
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Data lives across [storage floors](../00-glossary.md#t0--hot) T0–T4. 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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