From b42e3cb8ceeea1c5a27fb010cba5c380679c3454 Mon Sep 17 00:00:00 2001 From: Ty Balascio Date: Fri, 18 Sep 2026 08:32:49 -0700 Subject: [PATCH] docs(structure): add requirements contract, job namespaces, and enforcement gate Phase 1 scaffold for the docs consolidation: - docs/STRUCTURE.md: artifact registry, 8 owner-job ID namespaces, routing and change rules - docs/REQUIREMENTS.md, docs/MILESTONES.md: normative schema, empty pending Phase 2 port - docs/_templates/: requirement-row, capability, contract, adr - docs/capabilities/, docs/contracts/: registered homes - tools/check_requirements.py: enforces registry, IDs, and traceability - AGENTS.md: coding paused banner + structure-contract routing rule - README.md: pointers to the structure/requirements/milestones set Additive only: no doc moves, no requirement text, no CI wiring yet. --- AGENTS.md | 13 +++ README.md | 5 +- docs/MILESTONES.md | 21 ++++ docs/REQUIREMENTS.md | 58 +++++++++++ docs/STRUCTURE.md | 106 ++++++++++++++++++++ docs/_templates/adr.md | 20 ++++ docs/_templates/capability.md | 17 ++++ docs/_templates/contract.md | 11 +++ docs/_templates/requirement-row.md | 6 ++ docs/capabilities/README.md | 13 +++ docs/contracts/README.md | 16 ++++ tools/README.md | 9 ++ tools/check_requirements.py | 149 +++++++++++++++++++++++++++++ 13 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 docs/MILESTONES.md create mode 100644 docs/REQUIREMENTS.md create mode 100644 docs/STRUCTURE.md create mode 100644 docs/_templates/adr.md create mode 100644 docs/_templates/capability.md create mode 100644 docs/_templates/contract.md create mode 100644 docs/_templates/requirement-row.md create mode 100644 docs/capabilities/README.md create mode 100644 docs/contracts/README.md create mode 100644 tools/README.md create mode 100644 tools/check_requirements.py diff --git a/AGENTS.md b/AGENTS.md index 0748ea3..94c2b3d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,18 @@ # AGENTS.md +## STOP — coding paused (Phase 2: requirements reconciliation) + +Do not implement Milestone 3 or any feature right now. Trunk docs are being consolidated. +Scope authority is `docs/REQUIREMENTS.md` + `docs/MILESTONES.md`. M3 will be re-derived as a subset of requirement IDs. +The Milestone 3 text below is the pre-reconciliation state and is not authoritative until Phase 2 completes. + +## Structure contract (read before creating any doc) + +Requirements live only in `docs/REQUIREMENTS.md`; milestone scope in `docs/MILESTONES.md`. +Before creating or moving any doc, look up its home in `docs/STRUCTURE.md` (artifact registry + namespace routing). +If no home exists, stop and ask Ty — do not create a new artifact. +Run `python tools/check_requirements.py` after any docs change; it must pass. + ## Agent context Read `docs/README.md` first, then this file, `docs/CAPTURE.md`, `docs/PHOTOS.md`, and `docs/CREATE.md`. diff --git a/README.md b/README.md index 4021e12..bf106e4 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,14 @@ Start at [`docs/README.md`](docs/README.md) for goals, what already shipped, and | Artifact | File | |---|---| | Docs map | `docs/README.md` | +| Structure contract | `docs/STRUCTURE.md` | +| Requirements (normative) | `docs/REQUIREMENTS.md` | +| Milestones (ID subsets) | `docs/MILESTONES.md` | | Product overview | `docs/PRODUCT.md` | | Invariants and agent context | `AGENTS.md` | | Milestone 1 Capture (shipped) | `docs/CAPTURE.md` | | Milestone 2 Photos (shipped) | `docs/PHOTOS.md` | -| Milestone 3 Create (current) | `docs/CREATE.md` | +| Milestone 3 Create (paused — requirements reconciliation) | `docs/CREATE.md` | | UAT practice walk | `docs/UAT-PRACTICE.md` | | Hosting | `deploy/` | diff --git a/docs/MILESTONES.md b/docs/MILESTONES.md new file mode 100644 index 0000000..f731810 --- /dev/null +++ b/docs/MILESTONES.md @@ -0,0 +1,21 @@ +# Milestones + +A milestone is an explicit subset of requirement IDs from `docs/REQUIREMENTS.md`. No prose scope here. + +Populated in Phase 2. Until then every milestone is empty and coding is paused. + +## M1 — Capture (shipped) + +- IDs: — + +## M2 — Photos (shipped) + +- IDs: — + +## M3 — Create (paused) + +- IDs: — + +## Later + +- IDs: — diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md new file mode 100644 index 0000000..b762603 --- /dev/null +++ b/docs/REQUIREMENTS.md @@ -0,0 +1,58 @@ +# Requirements + +Normative source of truth. One row per requirement. Enforced by `tools/check_requirements.py`. + +A requirement is defined here and nowhere else. Capability and contract docs cite IDs; they do not restate them. + +Format and rules: `docs/STRUCTURE.md`. +Scope: `shipped` | `M3` | `later`. `Test` is required for `shipped` and `M3`; use `—` for `later`. + +Populated in Phase 2 from the `pre-simplification` tag and the current docs. + +## CAPTURE — ecommerce-ready capture + +| ID | Requirement | Scope | Test | +|---|---|---|---| +| — | *(none yet)* | — | — | + +## METADATA — listing attributes and copy + +| ID | Requirement | Scope | Test | +|---|---|---|---| +| — | *(none yet)* | — | — | + +## REVIEW — confirm, tweak, ready + +| ID | Requirement | Scope | Test | +|---|---|---|---| +| — | *(none yet)* | — | — | + +## PUBLISH — store submission + +| ID | Requirement | Scope | Test | +|---|---|---|---| +| — | *(none yet)* | — | — | + +## SETTINGS — configured once + +| ID | Requirement | Scope | Test | +|---|---|---|---| +| — | *(none yet)* | — | — | + +## APP — device draft and state + +| ID | Requirement | Scope | Test | +|---|---|---|---| +| — | *(none yet)* | — | — | + +## OPS — deploy and release + +| ID | Requirement | Scope | Test | +|---|---|---|---| +| — | *(none yet)* | — | — | + +## QUALITY — verification + +| ID | Requirement | Scope | Test | +|---|---|---|---| +| — | *(none yet)* | — | — | diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md new file mode 100644 index 0000000..175014e --- /dev/null +++ b/docs/STRUCTURE.md @@ -0,0 +1,106 @@ +# Structure contract + +This file defines the shape of the `docs/` system and how requirements enter it. +`tools/check_requirements.py` enforces it. + +If the repo and this file disagree, fix one deliberately. Do not let them drift. + +## Artifact registry + +Every Markdown file under `docs/` is registered here. `docs/archive/**` and `docs/_templates/**` are exempt. + +| Path | Role | +|---|---| +| `docs/README.md` | index | +| `docs/PRODUCT.md` | intent | +| `docs/STRUCTURE.md` | meta | +| `docs/REQUIREMENTS.md` | requirements | +| `docs/MILESTONES.md` | scope | +| `docs/CAPTURE.md` | capability | +| `docs/PHOTOS.md` | capability | +| `docs/CREATE.md` | capability | +| `docs/CONTENT-WOO.md` | contract | +| `docs/UAT-PRACTICE.md` | practice | +| `docs/capabilities/README.md` | index | +| `docs/contracts/README.md` | index | + +Roles: + +- **index** — navigation only. +- **intent** — why and what; not testable requirements. +- **meta** — this contract. +- **requirements** — the only normative text. +- **scope** — milestone-to-ID mapping. +- **capability** — behavior and acceptance for one capability; cites IDs, does not define them. +- **contract** — cross-cutting interface detail; cites IDs, does not define them. +- **practice** — human walkthrough. + +## ID namespaces + +Namespaces are **owner jobs, not technology layers**. A requirement belongs to the job it serves, and each namespace owns that job's screen, rules, server behavior, and data. There is no separate API, data, or UX layer to fight over. + +All requirement text lives only in `docs/REQUIREMENTS.md`. This table assigns the home and prefix. + +| Prefix | The owner is... | Covers | +|---|---|---| +| `CAPTURE` | capturing ecommerce-ready pics | photo intake, pick calibration/dimension photo, BG removal, crop, axis, length/width, calculations, inventory PNG, dimension overlay, lineup | +| `METADATA` | building the listing | wood attributes and tags, title, short/deep description, via up to 2 inference calls | +| `REVIEW` | reviewing and correcting | confidence display, field gating, tweaks, per-slab price suggestion and override, ready gate | +| `PUBLISH` | submitting to the store | WooCommerce create, draft/live, duplicate SKU, cleanup | +| `SETTINGS` | configuring before the session | Woo connection, inference config, species `$/bdft` rates, brand/geo | +| `APP` | (foundation, not a job) | device draft, slab status, resume, server seams, secret handling | +| `OPS` | (foundation, not a job) | deploy, hosts, release, UAT | +| `QUALITY` | (foundation, not a job) | test obligations, vectors, anti-metrics | + +Price has two homes by design: the species `$/bdft` rates are configured in `SETTINGS`; the per-slab suggestion and override live in `REVIEW`. + +## Requirement format + +Defined only in `docs/REQUIREMENTS.md`, as table rows: + +``` +| ID | Requirement | Scope | Test | +| CAPTURE-1 | Statement of one testable requirement | shipped | frontend/src/measure/measure.test.ts | +``` + +- `Scope` is `shipped`, `M3`, or `later`. +- `Test` is a repo-relative path to the test that proves the requirement. Required for `shipped` and `M3`; use `—` for `later`. + +## How to change a requirement + +- **Modify:** edit the single row in `REQUIREMENTS.md`; update `Test`; if scope changed, update `MILESTONES.md`. Do not create a doc. +- **Add:** pick the prefix and home from the namespace table; append a row; add the test. Do not create a doc. +- **New domain:** that is a structure change. Add a namespace row and a home here first, deliberately. Do not create a drive-by artifact. + +Nothing outside `REQUIREMENTS.md` may define a requirement. Capability and contract docs cite IDs only. + +## Enforced by `tools/check_requirements.py` + +1. Every `docs/` Markdown file (except `archive/**` and `_templates/**`) is registered above. +2. Every registered file exists on disk. +3. Requirement IDs match `^[A-Z]+-\d+$`, are unique, and use a registered prefix. +4. Scope is one of `shipped | M3 | later`. +5. `Test` paths for `shipped | M3` requirements exist on disk. +6. Requirement rows appear only in `REQUIREMENTS.md`. +7. Every ID listed in `MILESTONES.md` exists in `REQUIREMENTS.md`. + +## Planned (Phase 2) + +- Migrate capability docs to `docs/capabilities/` and contracts to `docs/contracts/`. +- Populate `REQUIREMENTS.md` from the `pre-simplification` tag and the current docs. +- Enforce branch-neutral language in trunk docs. +- Wire this check into CI. + +## Migration plan (Phase 2) + +| Current | Canonical home | +|---|---| +| `docs/CAPTURE.md` | `docs/capabilities/CAPTURE.md` | +| `docs/PHOTOS.md` | `docs/capabilities/PHOTOS.md` | +| `docs/CREATE.md` | `docs/capabilities/CREATE.md` | +| `docs/CONTENT-WOO.md` | `docs/contracts/CONTENT-WOO.md` | +| tag `OPENAPI.md` | `docs/contracts/API.md` | +| tag `DATA-MODEL.md` | `docs/contracts/DATA-MODEL.md` | +| tag `UX.md` | `docs/contracts/UX.md` | +| tag `PROMPTS.md` | `docs/contracts/PROMPTS.md` | +| tag `TEST-SPEC.md` | `docs/contracts/TEST-SPEC.md` | diff --git a/docs/_templates/adr.md b/docs/_templates/adr.md new file mode 100644 index 0000000..65d76b3 --- /dev/null +++ b/docs/_templates/adr.md @@ -0,0 +1,20 @@ + +# ADR-: + +- Date: <yyyy-mm-dd> +- Status: `<proposed | accepted | superseded>` + +## Context + +<the situation and forces> + +## Decision + +<what was decided> + +## Consequences + +<what this makes easier, harder, or impossible> diff --git a/docs/_templates/capability.md b/docs/_templates/capability.md new file mode 100644 index 0000000..ada0577 --- /dev/null +++ b/docs/_templates/capability.md @@ -0,0 +1,17 @@ +<!-- +Template: capability doc. Home: docs/capabilities/<NAME>.md +Behavior and acceptance only. Cite requirement IDs; never define them. +--> +# <Capability> + +Status: `<shipped | in progress | planned>` + +Cites: `<IDs from docs/REQUIREMENTS.md>` + +## Behavior + +<what the capability does, in plain terms> + +## Acceptance + +<what must be observed for this capability to be considered done, and where the evidence lives> diff --git a/docs/_templates/contract.md b/docs/_templates/contract.md new file mode 100644 index 0000000..8825a2b --- /dev/null +++ b/docs/_templates/contract.md @@ -0,0 +1,11 @@ +<!-- +Template: contract doc. Home: docs/contracts/<NAME>.md +Cross-cutting interface detail. Cite requirement IDs; never define them. +--> +# <Contract> + +Cites: `<IDs from docs/REQUIREMENTS.md>` + +## <Interface detail> + +<schemas, endpoints, prompts, screen rules — non-normative detail that supports the cited requirements> diff --git a/docs/_templates/requirement-row.md b/docs/_templates/requirement-row.md new file mode 100644 index 0000000..badea74 --- /dev/null +++ b/docs/_templates/requirement-row.md @@ -0,0 +1,6 @@ +<!-- +Template: one requirement row. +Append to the correct namespace section of docs/REQUIREMENTS.md. +Do not define this requirement anywhere else. +--> +| `<PREFIX>-<n>` | <one testable statement> | `<shipped|M3|later>` | `<repo-relative test path>` or `—` | diff --git a/docs/capabilities/README.md b/docs/capabilities/README.md new file mode 100644 index 0000000..3a5dfa9 --- /dev/null +++ b/docs/capabilities/README.md @@ -0,0 +1,13 @@ +# Capability docs + +One file per capability. Behavior and acceptance only. + +Rules (`docs/STRUCTURE.md`): a capability doc **cites** requirement IDs from `docs/REQUIREMENTS.md`; it never defines or restates them. + +Expected files (Phase 2 migration): + +- `CAPTURE.md` — Milestone 1 Capture +- `PHOTOS.md` — Milestone 2 Photos +- `CREATE.md` — Milestone 3 Create + +Template: `docs/_templates/capability.md`. diff --git a/docs/contracts/README.md b/docs/contracts/README.md new file mode 100644 index 0000000..459cd38 --- /dev/null +++ b/docs/contracts/README.md @@ -0,0 +1,16 @@ +# Contract docs + +Cross-cutting interface detail shared by capabilities. + +Rules (`docs/STRUCTURE.md`): a contract doc **cites** requirement IDs from `docs/REQUIREMENTS.md`; it never defines or restates them. + +Expected files (Phase 2 migration): + +- `API.md` — FastAPI HTTP contract (from tag `OPENAPI.md`) +- `DATA-MODEL.md` — SQLite schema and status machine (from tag `DATA-MODEL.md`) +- `UX.md` — screens, field recovery, outcomes (from tag `UX.md`) +- `PROMPTS.md` — Call 1 / Call 2 prompt text (from tag `PROMPTS.md`) +- `TEST-SPEC.md` — test pyramid, vectors, dishonest proxies (from tag `TEST-SPEC.md`) +- `CONTENT-WOO.md` — listing copy and Woo payload detail (migrated from `docs/CONTENT-WOO.md`) + +Template: `docs/_templates/contract.md`. diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..3a04bef --- /dev/null +++ b/tools/README.md @@ -0,0 +1,9 @@ +# tools + +Structure and verification gates. These are the only sanctioned way to claim the docs are clean or a slice is "done". + +| Script | Purpose | Run | +|---|---|---| +| `check_requirements.py` | Enforce `docs/STRUCTURE.md`: registered docs, requirement IDs, traceability. | `python tools/check_requirements.py` | + +Run from the repo root. Exit 0 = pass, exit 1 = fail. diff --git a/tools/check_requirements.py b/tools/check_requirements.py new file mode 100644 index 0000000..8442dd5 --- /dev/null +++ b/tools/check_requirements.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Enforce docs/STRUCTURE.md. + +Run from the repo root: + + python tools/check_requirements.py + +Exit 0 = the docs structure is consistent. Exit 1 = it is not. +Checks are listed in docs/STRUCTURE.md. +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DOCS = ROOT / "docs" +STRUCTURE = DOCS / "STRUCTURE.md" +REQUIREMENTS = DOCS / "REQUIREMENTS.md" +MILESTONES = DOCS / "MILESTONES.md" + +EXEMPT_PREFIXES = (("docs", "archive"), ("docs", "_templates")) + +ROLES = "index|intent|meta|requirements|scope|capability|contract|practice" +REGISTRY_ROW = re.compile(rf"^\|\s*`(docs/[^`]+\.md)`\s*\|\s*({ROLES})\s*\|") +NAMESPACE_ROW = re.compile(r"^\|\s*`([A-Z]+)`\s*\|") +REQUIREMENT_ROW = re.compile( + r"^\|\s*([A-Z]+)-(\d+)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*$" +) +MILESTONE_IDS = re.compile(r"^-\s*IDs:\s*(.*)$") +ID_TOKEN = re.compile(r"^[A-Z]+-\d+$") + +SCOPES = {"shipped", "M3", "later"} +PLACEHOLDER = {"", "—", "-", "*(none yet)*"} + + +def main() -> int: + errors: list[str] = [] + + if not STRUCTURE.exists(): + print("FAIL check_requirements: docs/STRUCTURE.md is missing") + return 1 + + structure = STRUCTURE.read_text(encoding="utf-8") + + registered: dict[str, str] = {} + for line in structure.splitlines(): + match = REGISTRY_ROW.match(line) + if match: + registered[match.group(1)] = match.group(2) + + namespaces = { + match.group(1) + for line in structure.splitlines() + if (match := NAMESPACE_ROW.match(line)) + } + if not namespaces: + errors.append("STRUCTURE.md declares no ID namespaces") + + # 1. every non-exempt doc is registered + for path in sorted(DOCS.rglob("*.md")): + rel = path.relative_to(ROOT).as_posix() + if path.relative_to(ROOT).parts[:2] in EXEMPT_PREFIXES: + continue + if rel not in registered: + errors.append(f"unregistered doc: {rel}") + + # 2. every registered doc exists + for rel in registered: + if not (ROOT / rel).exists(): + errors.append(f"registered doc missing on disk: {rel}") + + # 3-5. requirement rows + ids: dict[str, str] = {} + if not REQUIREMENTS.exists(): + errors.append("docs/REQUIREMENTS.md is missing") + else: + for number, line in enumerate( + REQUIREMENTS.read_text(encoding="utf-8").splitlines(), 1 + ): + match = REQUIREMENT_ROW.match(line) + if not match: + continue + prefix, digit, _statement, scope, test = match.groups() + rid = f"{prefix}-{digit}" + loc = f"docs/REQUIREMENTS.md:{number}" + if prefix not in namespaces: + errors.append(f"{loc} unknown namespace '{prefix}'") + if rid in ids: + errors.append(f"{loc} duplicate id {rid} (first seen at {ids[rid]})") + else: + ids[rid] = loc + if scope not in SCOPES: + errors.append(f"{loc} {rid} invalid scope '{scope}'") + if scope in {"shipped", "M3"}: + if test.strip() in PLACEHOLDER: + errors.append(f"{loc} {rid} scope {scope} needs a Test path") + elif not (ROOT / test.strip()).exists(): + errors.append(f"{loc} {rid} test path not found: {test.strip()}") + + # 6. requirement rows appear only in REQUIREMENTS.md + for path in sorted(DOCS.rglob("*.md")): + if path.relative_to(ROOT).parts[:2] in EXEMPT_PREFIXES or path in { + REQUIREMENTS, + STRUCTURE, + }: + continue + rel = path.relative_to(ROOT).as_posix() + for number, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), 1 + ): + if REQUIREMENT_ROW.match(line): + errors.append(f"{rel}:{number} requirement row outside REQUIREMENTS.md") + + # 7. milestone IDs resolve + if not MILESTONES.exists(): + errors.append("docs/MILESTONES.md is missing") + else: + for number, line in enumerate( + MILESTONES.read_text(encoding="utf-8").splitlines(), 1 + ): + match = MILESTONE_IDS.match(line) + if not match: + continue + for token in match.group(1).split(","): + token = token.strip().strip("`").strip() + if token in PLACEHOLDER: + continue + if not ID_TOKEN.match(token): + errors.append(f"MILESTONES.md:{number} not an ID: '{token}'") + elif token not in ids: + errors.append(f"MILESTONES.md:{number} unknown ID {token}") + + if errors: + print(f"FAIL check_requirements: {len(errors)} problem(s)") + for error in errors: + print(f" - {error}") + return 1 + + print( + f"PASS check_requirements: {len(registered)} docs, " + f"{len(namespaces)} namespaces, {len(ids)} requirements" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())