phase: 01_runnable_service_scaffold
**Phase 01 final verification pass — all criteria green**
- Verified (nothing left to implement; all 4 task files already in `complete/`): toolchain lock, typed `Settings`+`.env.example`, debugpy bootstrap, app factory, CLI (`--host/--port/--reload/--levels-dir`, `python -m undercooked`), static shell + `/static`, security headers, no-CDN/doc guards, `validate.sh`.
- `uv sync --extra dev` → exit 0; `uv.lock` present (untracked → harness commits it with the phase).
- `uv run undercooked --host 127.0.0.1 --port 8000` → `curl /healthz` = 200 `{"status":"ok","version":"0.1.0","rooms":0}` (superset of the criterion's body — now documented in README); `curl /` = 200 shell HTML; static css/js 200; CSP/nosniff/no-referrer/DENY on 200, 404 and 405 too.
- `DEBUGPY=1`: port 5678 is held on this machine by the user's own podman dev-container debugpy adapter — **no live change made/killed**; verified via `DEBUGPY=1 DEBUGPY_PORT=5679` → listener opens, `/healthz` still 200; the occupied-port case degraded gracefully as designed (warning, server serves). Bad config → one line + exit 2 ✓.
- `bash .agents/validate.sh` (root **and** `.agents/`) → exit 0: `ruff check .` 0 errors, `pyright` 0 errors, levels-CLI notice-skip (matches README prose), **284 passed** full suite incl. 10 Playwright e2e, coverage **100.00%** (floor 90), repo hygiene clean. `uv run pytest -m e2e` → 10 passed.
- Docs fixed: README Status → phase 01 complete + next pending 02 (was pointing at `todo/01…`), documented real `/healthz` body, precedence now "CLI flags > env > .env > default", added the PLAN §13-promised **How to play** section (honest about shell-only today), Layout rows marked what exists vs phases 02/07. AGENTS.md: recorded the `data:`-URL exemption in the no-CDN rule (test allows it) and matched the session-token rule to the browser's WebCrypto/base64url implementation. Also corrected a stale "session-wide chromium" docstring in `tests/e2e/conftest.py` (fixtures are function-scoped).
- Deviations: no container build run (needs an image pull; not a phase criterion — `Containerfile`/`compose.yaml` reviewed and consistent with `docs/DEPLOYMENT.md`); `HEAD /` is 405 (routes are GET-only by design; health checks use GET). No git index/history commands run.
- Next pending phase: `02_level_content_and_validation`.
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
"""PLAN §13 doc-index guard: the promised documentation set and the `docs/` tree cannot drift.
|
||||
|
||||
`.agents/PLAN.md` §13 closes with the canonical documentation index and the sentence
|
||||
"`tests/unit/test_docs_index.py` (phase 01) keeps this table and the `docs/` tree identical,
|
||||
so neither can drift silently." This is that file. It closes both directions of drift:
|
||||
|
||||
* **A promised doc is missing.** Every index row whose creating phase has already completed
|
||||
(according to the `.agents/phases/complete/` tree) must exist on disk, so no phase can
|
||||
reference a document that was never written. A range in the "Created by" column —
|
||||
`phase 02/03` — promises the file by the *end* of that range, so its obligation starts at
|
||||
the later phase; a `→` marks the stub-then-fill pattern, where the earlier phase owes the
|
||||
stub.
|
||||
* **A doc exists that nothing promises.** Every `docs/*.md` must be listed in the index or be
|
||||
a document a phase file of the roadmap itself creates — `docs/ROOMS.md` comes from phase 05
|
||||
and is not in §13, so the roadmap may extend the doc set, but an untracked file may not
|
||||
appear there silently.
|
||||
|
||||
Only markdown is compared: the index promises *documents*, and a diagram or fixture a phase
|
||||
puts under `docs/` is not a documentation promise.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
PLAN = REPO_ROOT / ".agents/PLAN.md"
|
||||
DOCS_DIR = REPO_ROOT / "docs"
|
||||
PHASES_DIR = REPO_ROOT / ".agents/phases"
|
||||
COMPLETE_DIR = PHASES_DIR / "complete"
|
||||
|
||||
INDEX_HEADING = "### Documentation index (canonical)"
|
||||
PHASE_DIRECTORY = re.compile(r"^(\d{2})_")
|
||||
PHASE_REFERENCE = re.compile(r"phase\s+(\d{1,2})", re.IGNORECASE)
|
||||
NUMBER = re.compile(r"\d{1,2}")
|
||||
DOC_REFERENCE = re.compile(r"docs/[A-Za-z0-9_-]+\.md")
|
||||
|
||||
#: The index exactly as PLAN §13 writes it. Changing this set means editing PLAN.md first.
|
||||
CANONICAL_DOCS = frozenset(
|
||||
{
|
||||
"README.md",
|
||||
"AGENTS.md",
|
||||
"BUDGETS.md",
|
||||
"docs/DESIGN.md",
|
||||
"docs/SIMULATION.md",
|
||||
"docs/LEVELS.md",
|
||||
"docs/PROTOCOL.md",
|
||||
"docs/CLIENT.md",
|
||||
"docs/DEPLOYMENT.md",
|
||||
"docs/TESTING.md",
|
||||
"docs/ACCESSIBILITY.md",
|
||||
"docs/SECURITY.md",
|
||||
"tests/e2e/README.md",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DocRow:
|
||||
"""One row of the PLAN §13 index: path, contents, and the phase(s) that create it."""
|
||||
|
||||
path: str
|
||||
contents: str
|
||||
created_by: str
|
||||
|
||||
@property
|
||||
def promised_from(self) -> int:
|
||||
"""First phase after which this file must exist.
|
||||
|
||||
The first comma/arrow-separated group is the creation promise. Inside it, a slash
|
||||
range (`phase 02/03`) is owed by the end of the range, so the later number wins;
|
||||
anything else is owed by the earliest number mentioned.
|
||||
"""
|
||||
first_group = re.split(r"[→,;]", self.created_by, maxsplit=1)[0]
|
||||
numbers = [int(number) for number in NUMBER.findall(first_group)]
|
||||
if not numbers:
|
||||
numbers = [int(number) for number in PHASE_REFERENCE.findall(self.created_by)]
|
||||
if not numbers:
|
||||
pytest.fail(f"PLAN index row {self.path!r} names no phase: {self.created_by!r}")
|
||||
return numbers[-1] if "/" in first_group and len(numbers) > 1 else numbers[0]
|
||||
|
||||
|
||||
def parse_documentation_index(plan_text: str) -> list[DocRow]:
|
||||
"""Read the canonical documentation-index table out of PLAN.md text."""
|
||||
lines = plan_text.splitlines()
|
||||
try:
|
||||
start = lines.index(INDEX_HEADING)
|
||||
except ValueError: # pragma: no cover - PLAN is locked; fail loudly if it is renamed
|
||||
pytest.fail(f".agents/PLAN.md has no {INDEX_HEADING!r} heading")
|
||||
rows: list[DocRow] = []
|
||||
for raw in lines[start + 1 :]:
|
||||
stripped = raw.strip()
|
||||
if not stripped.startswith("|"):
|
||||
if rows:
|
||||
break
|
||||
continue
|
||||
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
||||
if len(cells) < 3 or cells[0].strip("`") in {"Path", ""} or set(cells[0]) <= {"-", ":"}:
|
||||
continue
|
||||
rows.append(DocRow(path=cells[0].strip("`"), contents=cells[1], created_by=cells[2]))
|
||||
return rows
|
||||
|
||||
|
||||
def latest_completed_phase(complete_dir: Path = COMPLETE_DIR) -> int:
|
||||
"""Highest phase number with anything in `complete/` (0 before the first phase finishes)."""
|
||||
return max(
|
||||
(
|
||||
int(match.group(1))
|
||||
for entry in complete_dir.glob("*")
|
||||
if (match := PHASE_DIRECTORY.match(entry.name))
|
||||
),
|
||||
default=0,
|
||||
)
|
||||
|
||||
|
||||
def roadmap_promised_docs(phases_dir: Path = PHASES_DIR) -> set[str]:
|
||||
"""Every `docs/*.md` a phase overview or task file itself names."""
|
||||
return {
|
||||
match.group(0)
|
||||
for phase_file in sorted(phases_dir.rglob("*.md"))
|
||||
for match in DOC_REFERENCE.finditer(phase_file.read_text(encoding="utf-8"))
|
||||
}
|
||||
|
||||
|
||||
def documentation_index(plan: Path = PLAN) -> list[DocRow]:
|
||||
return parse_documentation_index(plan.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def written_docs(docs_dir: Path = DOCS_DIR, *, root: Path = REPO_ROOT) -> set[str]:
|
||||
"""The markdown currently in `docs/`, as repo-relative paths."""
|
||||
return {path.relative_to(root).as_posix() for path in sorted(docs_dir.rglob("*.md"))}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- the index itself
|
||||
|
||||
|
||||
def test_the_documentation_index_is_parseable() -> None:
|
||||
rows = documentation_index()
|
||||
assert len(rows) >= len(CANONICAL_DOCS), rows
|
||||
paths = [row.path for row in rows]
|
||||
assert len(paths) == len(set(paths)), f"duplicate index rows: {paths}"
|
||||
assert all(row.contents and row.created_by for row in rows), paths
|
||||
|
||||
|
||||
def test_the_index_still_lists_the_canonical_doc_set() -> None:
|
||||
"""Adding or dropping a row is a PLAN §13 change and must fail a test first."""
|
||||
assert {row.path for row in documentation_index()} == CANONICAL_DOCS
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("created_by", "expected"),
|
||||
[
|
||||
("phase 01", 1),
|
||||
("phase 01, refined throughout", 1),
|
||||
("phase 01 (stub) \u2192 phase 09", 1),
|
||||
("phase 02, phase 11", 2),
|
||||
("phase 02/03", 3),
|
||||
("phase 14", 14),
|
||||
("refined throughout, phase 05", 5),
|
||||
],
|
||||
)
|
||||
def test_the_promised_from_rule_reads_the_phase_column(created_by: str, expected: int) -> None:
|
||||
row = DocRow(path="docs/X.md", contents="whatever", created_by=created_by)
|
||||
assert row.promised_from == expected
|
||||
|
||||
|
||||
def test_an_index_row_that_names_no_phase_fails_loudly() -> None:
|
||||
row = DocRow(path="docs/X.md", contents="whatever", created_by="someday, maybe")
|
||||
with pytest.raises(pytest.fail.Exception, match="names no phase"):
|
||||
_ = row.promised_from
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- promised vs present
|
||||
|
||||
|
||||
def test_every_doc_promised_by_a_completed_phase_exists() -> None:
|
||||
"""No phase may reference a document whose creating phase is already in `complete/`."""
|
||||
completed = latest_completed_phase()
|
||||
assert completed >= 1, "no completed phase found — the guard would be vacuous"
|
||||
missing = [
|
||||
f"{row.path} (owed by phase {row.promised_from:02d}, {completed:02d} is complete)"
|
||||
for row in documentation_index()
|
||||
if row.promised_from <= completed and not (REPO_ROOT / row.path).is_file()
|
||||
]
|
||||
report = "\n ".join(missing)
|
||||
assert missing == [], f"documentation the project owes but does not have:\n {report}"
|
||||
|
||||
|
||||
def test_no_doc_file_exists_that_nothing_promises() -> None:
|
||||
"""The `docs/` tree is exactly the index plus what the phase roadmap creates."""
|
||||
promised = {row.path for row in documentation_index()} | roadmap_promised_docs()
|
||||
orphans = sorted(written_docs() - promised)
|
||||
report = "\n ".join(orphans)
|
||||
assert orphans == [], (
|
||||
f"unpromised files under docs/ — add each to PLAN §13's index or to the phase "
|
||||
f"that creates it:\n {report}"
|
||||
)
|
||||
|
||||
|
||||
def test_the_docs_on_disk_are_the_three_phase_01_documents() -> None:
|
||||
"""Pins today's tree so that adding a document is a deliberate, named act."""
|
||||
assert written_docs() == {"docs/DESIGN.md", "docs/DEPLOYMENT.md", "docs/TESTING.md"}
|
||||
|
||||
|
||||
def stub_without_owner(relative: str, text: str) -> str | None:
|
||||
"""One sentence of complaint about a document, or `None` when it is honest.
|
||||
|
||||
Split out of the test below so a planted stub can prove each check fires; a guard that
|
||||
only ever sees healthy input is not a guard.
|
||||
"""
|
||||
if not text.startswith("# "):
|
||||
return f"{relative} does not open with a title"
|
||||
if "## Purpose" not in text:
|
||||
return f"{relative} has no Purpose section"
|
||||
purpose = text.split("## Purpose", 1)[1].split("\n##", 1)[0].strip()
|
||||
if len(purpose.split()) < 25:
|
||||
return f"{relative}'s Purpose paragraph is too thin to guide"
|
||||
names_stub = re.search(r"\bstub\b", text, re.IGNORECASE) is not None
|
||||
has_owner = re.search(r"[Ff]illed in by phase \d\d", text) is not None
|
||||
if names_stub and not has_owner:
|
||||
return f"{relative} is a stub with no 'Filled in by phase NN' marker"
|
||||
return None
|
||||
|
||||
|
||||
def dishonest_references(
|
||||
references: dict[str, set[str]], *, exists: Callable[[str], bool], owed_later: set[str]
|
||||
) -> list[str]:
|
||||
"""`document: path` for each cited doc that neither exists nor is owed by a later phase."""
|
||||
offenders: list[str] = []
|
||||
for name, cited in sorted(references.items()):
|
||||
offenders.extend(
|
||||
f"{name}: {path}"
|
||||
for path in sorted(cited)
|
||||
if not exists(path) and path not in owed_later
|
||||
)
|
||||
return offenders
|
||||
|
||||
|
||||
def test_the_stub_docs_say_who_fills_them_in() -> None:
|
||||
"""A stub with no owner stays a stub forever, so each one names its phase."""
|
||||
complaints = [
|
||||
complaint
|
||||
for relative in sorted(written_docs())
|
||||
if (complaint := stub_without_owner(relative, (REPO_ROOT / relative).read_text("utf-8")))
|
||||
]
|
||||
report = "\n ".join(complaints)
|
||||
assert complaints == [], f"documents that are not self-describing:\n {report}"
|
||||
|
||||
|
||||
def test_readme_and_agents_reference_only_honest_doc_paths() -> None:
|
||||
"""A doc path in README/AGENTS either exists or is owed by a phase that has not run."""
|
||||
completed = latest_completed_phase()
|
||||
rows = documentation_index()
|
||||
owed_later = {row.path for row in rows if row.promised_from > completed}
|
||||
owed_later |= roadmap_promised_docs()
|
||||
root_docs = {row.path for row in rows if not row.path.startswith("docs/")}
|
||||
references: dict[str, set[str]] = {}
|
||||
for name in ("README.md", "AGENTS.md"):
|
||||
text = (REPO_ROOT / name).read_text(encoding="utf-8")
|
||||
references[name] = set(DOC_REFERENCE.findall(text)) | {
|
||||
path for path in root_docs if path in text
|
||||
}
|
||||
offenders = dishonest_references(
|
||||
references, exists=lambda path: (REPO_ROOT / path).is_file(), owed_later=owed_later
|
||||
)
|
||||
report = "\n ".join(offenders)
|
||||
assert offenders == [], f"references to documentation nothing promises to create:\n {report}"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- the guard itself
|
||||
|
||||
|
||||
def test_a_missing_promised_doc_is_named(tmp_path: Path) -> None:
|
||||
"""Plant a row whose phase already shipped: the parse must expose it as owed."""
|
||||
plan = tmp_path / "PLAN.md"
|
||||
plan.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"## Risks",
|
||||
"",
|
||||
INDEX_HEADING,
|
||||
"",
|
||||
"| Path | Contents | Created by |",
|
||||
"|---|---|---|",
|
||||
"| `docs/GONE.md` | whatever it says | phase 01 |",
|
||||
"| `docs/LATER.md` | not owed yet | phase 09 |",
|
||||
"",
|
||||
"prose after the table",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
rows = parse_documentation_index(plan.read_text(encoding="utf-8"))
|
||||
assert [row.path for row in rows] == ["docs/GONE.md", "docs/LATER.md"]
|
||||
owed = [row.path for row in rows if row.promised_from <= 1]
|
||||
assert owed == ["docs/GONE.md"]
|
||||
assert not (tmp_path / owed[0]).is_file()
|
||||
|
||||
|
||||
def test_an_unpromised_doc_file_is_reported(tmp_path: Path) -> None:
|
||||
"""The orphan rule works on a temp tree too — it is not README-specific magic."""
|
||||
(tmp_path / "docs").mkdir()
|
||||
(tmp_path / "docs" / "WANTED.md").write_text("# WANTED\n", encoding="utf-8")
|
||||
(tmp_path / "docs" / "MYSTERY.md").write_text("# MYSTERY\n", encoding="utf-8")
|
||||
(tmp_path / "docs" / "tile.svg").write_text("<svg/>", encoding="utf-8")
|
||||
assert written_docs(tmp_path, root=tmp_path) == {
|
||||
"docs/MYSTERY.md",
|
||||
"docs/WANTED.md",
|
||||
}
|
||||
assert written_docs(tmp_path, root=tmp_path) - {"docs/WANTED.md"} == {"docs/MYSTERY.md"}
|
||||
|
||||
|
||||
def test_an_unhonest_document_is_described_precisely() -> None:
|
||||
"""Each complaint the stub check can make is exercised on planted text."""
|
||||
body = "word " * 30
|
||||
good = f"# Title\n\n## Purpose\n{body}\nA stub here. Filled in by phase 09.\n"
|
||||
assert stub_without_owner("docs/GOOD.md", good) is None
|
||||
assert stub_without_owner("docs/ORPHAN.md", good.replace(" Filled in by phase 09.", "")) == (
|
||||
"docs/ORPHAN.md is a stub with no 'Filled in by phase NN' marker"
|
||||
)
|
||||
assert stub_without_owner("docs/UNTITLED.md", f"## Purpose\n{body}") == (
|
||||
"docs/UNTITLED.md does not open with a title"
|
||||
)
|
||||
assert stub_without_owner("docs/NOPURPOSE.md", "# Title\nProse only.\n") == (
|
||||
"docs/NOPURPOSE.md has no Purpose section"
|
||||
)
|
||||
assert stub_without_owner("docs/THIN.md", "# Title\n\n## Purpose\ntoo short\n") == (
|
||||
"docs/THIN.md's Purpose paragraph is too thin to guide"
|
||||
)
|
||||
|
||||
|
||||
def test_a_reference_to_a_doc_nobody_promises_is_named() -> None:
|
||||
references = {
|
||||
"AGENTS.md": {"docs/HERE.md"},
|
||||
"README.md": {"docs/HERE.md", "docs/GHOST.md", "docs/OWED.md"},
|
||||
}
|
||||
offenders = dishonest_references(
|
||||
references,
|
||||
exists=lambda path: path in {"docs/HERE.md", "docs/OWED.md"},
|
||||
owed_later={"docs/OWED.md"},
|
||||
)
|
||||
assert offenders == ["README.md: docs/GHOST.md"]
|
||||
|
||||
|
||||
def test_the_roadmap_extension_of_the_doc_set_is_tracked() -> None:
|
||||
"""`docs/ROOMS.md` is phase 05's, not PLAN §13's — and must not read as an orphan."""
|
||||
roadmap = roadmap_promised_docs()
|
||||
assert "docs/ROOMS.md" in roadmap
|
||||
assert "docs/ROOMS.md" not in CANONICAL_DOCS
|
||||
assert latest_completed_phase() >= 1
|
||||
Reference in New Issue
Block a user