diff --git a/tools/check.sh b/tools/check.sh --- a/tools/check.sh +++ b/tools/check.sh @@ -235,6 +235,7 @@ start_docs_gate "site-visual-contract" "python3 tools/test_site_visual_contract.py" start_docs_gate "observed-run-fixtures" "bash tools/test_observed_run.sh" start_docs_gate "ledger-index" "bash tools/ledger_index.sh --check" +start_docs_gate "ledger-index-fixtures" "bash tools/test_ledger_index.sh" start_docs_gate "project-operations" "python3 tools/work_orders.py check && python3 tools/scenario.py --check-definitions && python3 tools/project-status.py --check --offline" start_docs_gate "project-operations-fixtures" "python3 tools/test_project_ops.py" start_docs_gate "tangled-issue-fixtures" "python3 tools/test_tangled_issues.py" diff --git a/tools/ledger_index.sh b/tools/ledger_index.sh --- a/tools/ledger_index.sh +++ b/tools/ledger_index.sh @@ -3,14 +3,15 @@ # Binding: wiki/process/agent-scale.md slice C. # # Usage: -# tools/ledger_index.sh # write DEVLOG.md + specs.md -# tools/ledger_index.sh --check # exit 1 if indexes are stale -# tools/ledger_index.sh --devlog # only DEVLOG.md -# tools/ledger_index.sh --specs # only specs.md +# tools/ledger_index.sh # write all generated indexes +# tools/ledger_index.sh --check # exit 1 if indexes are stale +# tools/ledger_index.sh --devlog # only DEVLOG.md +# tools/ledger_index.sh --decisions # only decisions.md +# tools/ledger_index.sh --specs # only specs.md # -# Agents: add wiki/log/YYYY-MM-DD-topic.md and/or amend a Type: spec page, -# then run this tool. Do not hand-edit the generated regions of DEVLOG.md -# or specs.md. +# Agents: add a uniquely named session log, a dated decision volume, and/or +# amend a Type: spec page, then run this tool. Do not hand-edit DEVLOG.md, +# decisions.md, specs.md, or their generated regions. set -euo pipefail root=$(cd "$(dirname "$0")/.." && pwd) cd "$root" @@ -26,6 +27,7 @@ case "$arg" in --check) mode=check ;; --devlog) only=devlog ;; + --decisions) only=decisions ;; --specs) only=specs ;; -h|--help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//' @@ -108,6 +110,29 @@ def date_from_name(path: pathlib.Path) -> str: m = re.match(r"(\d{4}-\d{2}-\d{2})", path.name) return m.group(1) if m else "unknown" + +def decision_volumes(): + decision_dir = root / "wiki" / "log" / "decisions" + for path in sorted(decision_dir.glob("*.md")): + if re.fullmatch(r"\d{4}-\d{2}-\d{2}\.md", path.name): + yield path + +def gen_decisions() -> str: + lines = [ + "# Decisions (chronological)", + "", + "```", + "Type: log", + "Generated: tools/ledger_index.sh", + "```", + "This index is generated from the dated volumes under `decisions/`.", + "Append a decision to today’s volume, then run `tools/ledger_index.sh`.", + "Older volumes are historical and do not receive new entries.", + "", + ] + for path in decision_volumes(): + lines.append(f"- [{path.stem}](decisions/{path.name})") + return "\n".join(lines).rstrip() + "\n" def gen_devlog() -> str: lines = [ @@ -310,6 +335,11 @@ path = root / "wiki" / "log" / "DEVLOG.md" if write_if_needed(path, content): changed.append(str(path.relative_to(root))) +if only in ("all", "decisions"): + content = gen_decisions() + path = root / "wiki" / "log" / "decisions.md" + if write_if_needed(path, content): + changed.append(str(path.relative_to(root))) if only in ("all", "specs"): content = gen_specs() path = root / "wiki" / "process" / "specs.md" @@ -323,7 +353,7 @@ print(f" {c}") print("Run: tools/ledger_index.sh") sys.exit(1) - print("ledger index: OK (DEVLOG + specs up to date)") + print("ledger index: OK (DEVLOG + decisions + specs up to date)") sys.exit(0) if changed: diff --git a/tools/test_ledger_index.sh b/tools/test_ledger_index.sh new file mode 100644 --- /dev/null +++ b/tools/test_ledger_index.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Fixture tests for generated ledger indexes. +set -euo pipefail +cd "$(dirname "$0")/.." || exit 1 + +source_script=$PWD/tools/ledger_index.sh +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +root=$tmp/root +mkdir -p "$root/tools" "$root/wiki/log/decisions" +cp "$source_script" "$root/tools/ledger_index.sh" + +write_volume() { + local date=$1 + printf '# Decisions — %s\n\n```\nType: log\n```\n' "$date" \ + > "$root/wiki/log/decisions/$date.md" +} + +assert_check_fails() { + local label=$1 + local output + if output=$("$root/tools/ledger_index.sh" --check --decisions 2>&1); then + echo "FAIL: $label was accepted" + exit 1 + fi + if ! grep -q 'wiki/log/decisions.md' <<< "$output"; then + echo "FAIL: $label did not identify the stale decision index" + echo "$output" + exit 1 + fi +} + +write_volume 2026-08-02 +write_volume 2026-08-03 +"$root/tools/ledger_index.sh" --decisions >/dev/null +"$root/tools/ledger_index.sh" --check --decisions >/dev/null + +python3 - "$root" <<'PYTEST' +import pathlib +import re +import sys + +root = pathlib.Path(sys.argv[1]) +text = (root / "wiki/log/decisions.md").read_text(encoding="utf-8") +assert "Generated: tools/ledger_index.sh" in text +links = re.findall(r"^- \[(\d{4}-\d{2}-\d{2})\]\(decisions/([^/]+\.md)\)$", text, re.M) +assert links == [ + ("2026-08-02", "2026-08-02.md"), + ("2026-08-03", "2026-08-03.md"), +] +PYTEST + +write_volume 2026-08-04 +assert_check_fails missing-volume +"$root/tools/ledger_index.sh" --decisions >/dev/null + +cat >> "$root/wiki/log/decisions.md" <<'EOF' +- [2026-08-04](decisions/2026-08-04.md) +EOF +assert_check_fails duplicate-volume +"$root/tools/ledger_index.sh" --decisions >/dev/null + +cat >> "$root/wiki/log/decisions.md" <<'EOF' +- [2099-01-01](decisions/2099-01-01.md) +EOF +assert_check_fails nonexistent-volume + +echo "ledger index fixtures: OK" diff --git a/wiki/log/2026-08-04-decision-index-generation-defense.md b/wiki/log/2026-08-04-decision-index-generation-defense.md new file mode 100644 --- /dev/null +++ b/wiki/log/2026-08-04-decision-index-generation-defense.md @@ -0,0 +1,39 @@ +# 2026-08-04 — Decision index generation defense + +``` +Type: log +``` + +## Problem + +`wiki/process/meta.md` declared the chronological decision page a generated +index, but `tools/ledger_index.sh` generated only DEVLOG and the work-order +index. `wiki/log/decisions.md` was therefore maintained by hand, so a new dated +volume could remain unlinked and a typo or duplicate could name nonexistent or +repeated history without making the documented generated-index gate fail. + +## Change + +- `tools/ledger_index.sh` now discovers date-shaped Markdown volumes under + `wiki/log/decisions/` and emits one ascending link for each existing volume. +- `--decisions` supports focused regeneration and checking while the ordinary + all-index path includes decisions by default. +- The decision index now declares its generator in metadata and contains only + the generated projection. +- The parallel documentation wall runs a dedicated fixture proving missing, + duplicate, and nonexistent volume references all fail `--check`. + +## Verification + +- Shell syntax passed for both generator and fixture scripts. +- `tools/test_ledger_index.sh` passed all four fixture states. +- `tools/ledger_index.sh --check --decisions` passed on the generated projection. +- A direct exact-set audit matched all 30 discovered volumes, with no duplicate + or missing target. +- `./tools/check.sh --docs` passed all 17 parallel documentation gates. + +## Defense + +`wiki/process/meta.md` owns generated-index law. It now binds the decision index +to exact filesystem discovery, while the generator check compares the complete +expected bytes and the fixture exercises each way the volume/link set can drift. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,6 +11,11 @@ +## 2026-08-04 - Decision index generation defense + +- Intent: (see session log) +- Log: [wiki/log/2026-08-04-decision-index-generation-defense.md](2026-08-04-decision-index-generation-defense.md) + ## 2026-08-03 - The cover becomes the authored part - Intent: Cameron asked whether the identity-creation screen that landed on 2026-08-02 was finished. It was not. Two things were named as outstanding at landing — free text entry for names, and the "other stuff like names" the original request asked for — and reading the creation path a... diff --git a/wiki/log/decisions.md b/wiki/log/decisions.md --- a/wiki/log/decisions.md +++ b/wiki/log/decisions.md @@ -1,12 +1,12 @@ -# Decision history +# Decisions (chronological) ``` -Type: knowledge +Type: log +Generated: tools/ledger_index.sh ``` - -Decisions are split into append-only daily volumes. They explain what was -adopted, rejected, reopened, or proposed; current `Type: law` and -`Type: spec` pages define the game now. +This index is generated from the dated volumes under `decisions/`. +Append a decision to today’s volume, then run `tools/ledger_index.sh`. +Older volumes are historical and do not receive new entries. - [2026-07-05](decisions/2026-07-05.md) - [2026-07-06](decisions/2026-07-06.md) @@ -23,11 +23,18 @@ - [2026-07-17](decisions/2026-07-17.md) - [2026-07-18](decisions/2026-07-18.md) - [2026-07-19](decisions/2026-07-19.md) +- [2026-07-20](decisions/2026-07-20.md) +- [2026-07-21](decisions/2026-07-21.md) +- [2026-07-22](decisions/2026-07-22.md) +- [2026-07-23](decisions/2026-07-23.md) +- [2026-07-24](decisions/2026-07-24.md) +- [2026-07-25](decisions/2026-07-25.md) - [2026-07-26](decisions/2026-07-26.md) +- [2026-07-27](decisions/2026-07-27.md) +- [2026-07-28](decisions/2026-07-28.md) - [2026-07-29](decisions/2026-07-29.md) +- [2026-07-30](decisions/2026-07-30.md) +- [2026-07-31](decisions/2026-07-31.md) - [2026-08-01](decisions/2026-08-01.md) - [2026-08-02](decisions/2026-08-02.md) - [2026-08-03](decisions/2026-08-03.md) - -Append new decisions to the current date's volume. Never rewrite an older -volume; supersede it in current law/spec and record the newer decision. diff --git a/wiki/process/meta.md b/wiki/process/meta.md --- a/wiki/process/meta.md +++ b/wiki/process/meta.md @@ -136,10 +136,17 @@ logs it indexes remain immutable. `wiki/log/decisions.md` is an index, not a forever-growing current volume. -Append decisions to the current dated volume under `wiki/log/decisions/` and -roll to a new dated volume when the date changes. Older volumes do not receive -new decisions. In every disagreement, the current owning law/spec wins over -the decision history. +`tools/ledger_index.sh` discovers every dated volume under +`wiki/log/decisions/` and regenerates the index with exactly one link to each +existing volume and no other dated links. Append decisions to the current dated +volume and run that generator; roll to a new dated volume when the date changes. +Older volumes do not receive new decisions. In every disagreement, the current +owning law/spec wins over the decision history. + +**Defense:** `tools/ledger_index.sh --check` regenerates the decision index in +memory and rejects any committed difference. `tools/test_ledger_index.sh` proves +that a newly added volume, a duplicated link, and a nonexistent-volume link each +make that gate fail. ## Status semantics