Compare commits
2 Commits
d754cac9ef
...
238fcc4f15
| Author | SHA1 | Date | |
|---|---|---|---|
| 238fcc4f15 | |||
| b42e3cb8ce |
@@ -0,0 +1,203 @@
|
||||
# Requirements reconciliation — decision record
|
||||
|
||||
- Date: 2026-09-18
|
||||
- Branch: `docs/clean-main`
|
||||
- Participants: Ty + opencode
|
||||
- Status: working record. Gets distilled into `docs/REQUIREMENTS.md`, then retired.
|
||||
|
||||
This captures the decisions made while consolidating the scattered requirements, so
|
||||
context is not lost between sessions. Loose ends are collected in section 16.
|
||||
|
||||
## 1. Why this exists
|
||||
|
||||
Requirements are scattered across layers of docs (old `main` specs, current branch docs,
|
||||
`archive/`), with duplicates and contradictions. Goal: one clean `main` holding all shipped
|
||||
code plus one reconciled requirement set, with M3 scoped as a subset of it. Coding is paused
|
||||
until that exists.
|
||||
|
||||
## 2. Git state established
|
||||
|
||||
1. `pre-simplification` tag created at `13bfa792bb3ed968b333675c85bcec0c451e2635` (old main tip; retains the deleted specs: ARCHITECTURE, IMPL-PLAN, OPENAPI, DATA-MODEL, TEST-SPEC, UX, UX-DESIGN, TECH-SPEC-PIPELINE, DEPLOYMENT, PROMPTS, Build-release-standard).
|
||||
2. `main` fast-forwarded to `d754cac` (the M1>M2>M3 code line). `main` now holds all shipped code plus current docs.
|
||||
3. Local `m3-ds` created, tracking `origin/m3-ds`.
|
||||
4. Phase 1 scaffold committed as `b42e3cb` on branch `docs/clean-main`.
|
||||
5. Nothing pushed. `origin/main` still at the old tip until we decide to push.
|
||||
|
||||
## 3. Process rules adopted
|
||||
|
||||
1. Coding is paused until the requirement set is reconciled.
|
||||
2. Requirements live only in `docs/REQUIREMENTS.md`, one row per requirement, with stable IDs.
|
||||
3. Milestones are expressed as explicit subsets of requirement IDs (`docs/MILESTONES.md`).
|
||||
4. Conflict resolution rule: current/later docs plus the code that actually runs beat the tag and `archive/`. "Typically later docs prevail."
|
||||
5. The structure is enforced by `tools/check_requirements.py` (registered docs, unique IDs, traceability).
|
||||
6. Verification discipline: no "done" without raw evidence; CI is the only mechanical gate.
|
||||
|
||||
## 4. Namespaces (owner jobs, not tech layers)
|
||||
|
||||
1. `CAPTURE` — ecommerce-ready pics: intake, pick calibration photo, BG removal, crop, axis, length/width, calculations, inventory PNG, dimension overlay, lineup.
|
||||
2. `METADATA` — building the listing: attributes and tags, title, short/deep description, the two inference calls.
|
||||
3. `REVIEW` — confirm and correct: confidence display, field gating, tweaks, per-slab price suggestion and override, ready gate.
|
||||
4. `PUBLISH` — WooCommerce create, draft/live, duplicate SKU, cleanup.
|
||||
5. `SETTINGS` — configured before the session: Woo connection, inference config, species `$/bdft` rates, brand/geo.
|
||||
6. `APP` — foundation: device draft, slab status, resume, server seams, secret handling.
|
||||
7. `OPS` — foundation: deploy, hosts, release, UAT.
|
||||
8. `QUALITY` — foundation: test obligations, vectors, anti-metrics.
|
||||
|
||||
## 5. Capture decisions
|
||||
|
||||
1. Mask controls are exactly two: `Sheet` (auto/green/black) and `Sensitivity` (0–100). Edge offset and Feather are dropped. The Sheet toggle itself is a candidate for removal (auto-detect stays).
|
||||
2. Sensitivity changes are debounced 2000 ms; a Sheet change runs an immediate pass.
|
||||
3. The owner uploads all photos of the slab, then selects one as the calibration photo. That photo drives every downstream step, is duplicated with dimensions added, and becomes the first showcase image.
|
||||
4. Crop is 90% fill, shorter side at least 800 px when the source allows (shipped values beat the old 80%/1600).
|
||||
5. Length and thickness are the only user-set numbers. Widths, sqft, and bdft are computed from the approved mask and are immutable in production (no typed override).
|
||||
6. Widths are sampled every 6 inches along the approved axis, dropping the first and last 6 inches.
|
||||
7. Thickness band uses the round-up model: measured `1"` lands in the `1–1.5"` store band.
|
||||
8. Woo image order is locked: clean calibration PNG, then dimension-overlay copy, then remaining batch photos in the order added. Minimum two images per listing; cap five.
|
||||
|
||||
## 6. Ready set (submission-ready)
|
||||
|
||||
Required:
|
||||
|
||||
1. At least two images — clean first pic plus its dimension-overlay copy.
|
||||
2. `sku` matching `[A-Z0-9-]{3,24}`.
|
||||
3. `length_in` and `thickness_in`.
|
||||
4. Widths / sqft / bdft (computed, immutable).
|
||||
5. `categories` — exactly one species leaf.
|
||||
6. `categories` — at least one wood-category leaf.
|
||||
7. `attributes[Edge Type]` — one term.
|
||||
8. `attributes[Figure]` — at least one term.
|
||||
9. `attributes[Grade]` — exactly one term.
|
||||
10. `attributes[Thickness]` — one term (round-up band).
|
||||
11. `attributes[Moisture]` — one term, default kiln-dried.
|
||||
12. `regular_price`.
|
||||
13. `name`, `short_description`, `description`.
|
||||
|
||||
Optional: all Woo product tags (figure family and features family).
|
||||
|
||||
Woo schema field mapping lives in section 6 of this record and will become the `METADATA` requirements.
|
||||
|
||||
Note: title / short description / description are required on the product. Because deterministic templates are the inference-off fallback, they are guaranteed non-empty at create.
|
||||
|
||||
## 7. Taxonomy and categories
|
||||
|
||||
1. Categories sync from Woo; the product is assigned to leaf categories. Parents are never assigned but matter in context: the site shows them (breadcrumbs/filters) and Call 2 receives the parent path as context.
|
||||
2. Species is exactly one synced leaf. Wood category is one or more synced leaves.
|
||||
3. In review, the owner designates a primary wood category and any secondary ones. Default primary is the first category selected. How the owner corrects it, and how they stay aware of it, is a UX problem deferred to design.
|
||||
4. Primary drives the title. Primary plus secondary drives the description prose.
|
||||
5. The primary leaf selects a specialized Call 2 prompt. This is the "common pattern."
|
||||
6. Wood-category leaves: Knife blanks, Specialty cuts, Dimensional wood, Live edge wood, Bowl blanks, logs and caps, Pen blanks, Pepper mill blanks. All are settled prose/prompt work items except `Specialty cuts`, which is parked.
|
||||
7. All Woo product tags are optional. Tags come from the synced Woo set; no hardcoded names or prefixes. Wrinkle: one figure tag is bare `fig` (Figured), so a `fig-*` glob would drop it — the filter must be data-driven.
|
||||
8. Current tags. Features: `feat-mincr` (Crack/Check minor), `feat-resin` (Resin Ready). Figure: `fig` (Figured), `fig-brdseye`, `fig-chatoy`, `fig-crotch`, `fig-curl`, `fig-fback`, `fig-flame`, `fig-hfig`, `fig-marbled`, `fig-quilted`, `fig-splt`.
|
||||
|
||||
## 8. Locked attributes
|
||||
|
||||
1. `attributes[Edge Type]` — 1 term.
|
||||
2. `attributes[Figure]` — at least 1 term; includes `plain` for basic slabs; never left empty.
|
||||
3. `attributes[Grade]` — exactly 1 term.
|
||||
4. `attributes[Thickness]` — 1 term, round-up band.
|
||||
5. `attributes[Moisture]` — 1 term, default kiln-dried, never inferred.
|
||||
6. Missing attributes or terms are skipped, never created. v1 does not invent taxonomy.
|
||||
|
||||
## 9. Text generation (Call 2)
|
||||
|
||||
1. Call 2 produces `name`, `short_description`, and `description`.
|
||||
2. The template is a consistency/style guide in the prompt; the model has creative license. It is not assembled deterministically in code.
|
||||
3. With inference off, the deterministic template is the fallback that fills those three fields.
|
||||
4. Call 2 receives all metadata confirmed or changed in review — not the raw Call 1 suggestions.
|
||||
5. Dimensions are supplied as a literal fact block (species, figure term, length, thickness, average width, width min–max, board feet). The model uses those exact strings and invents no numbers.
|
||||
6. There is no `L × W × T` rectangle assumption; slabs are irregular and width varies.
|
||||
7. The dimension-overlay image is not sent to the model.
|
||||
8. A post-check for number drift is parked as `later`; if it is ever added it is plain deterministic comparison, not an AI call.
|
||||
9. The title separator is a hyphen: ` - `.
|
||||
|
||||
## 10. Inference transport
|
||||
|
||||
1. Calls are asynchronous: the request returns `202`, the client polls and shows phase feedback.
|
||||
2. Phases: uploading, analyzing, still working. Polling is the heartbeat that distinguishes dead connectivity from a working model.
|
||||
3. Tunable defaults: ack 15 s, upload-stall 20 s, overall inference deadline 120 s after ack, poll every 2 s backing off to 10 s.
|
||||
4. On timeout or failure, fail gracefully with a plain message and a Retry action. Retry is offered only if `test-inference` previously passed. A retry replaces any in-flight job (idempotent).
|
||||
5. Inference input is the clean processed image (masked, cropped), composited on soft white, no alpha, downscaled to at most 1024 px long edge, JPEG quality 80.
|
||||
6. Only the pixel cap is exposed in Settings. JPEG quality 80 lives in a server config file.
|
||||
7. Call 1 and Call 2 both receive the images. Call 2 additionally receives the literal fact block.
|
||||
8. Provider threads are dropped; always full resend to the provider.
|
||||
9. Inference never blocks create and never auto-publishes.
|
||||
|
||||
## 11. Prompt management
|
||||
|
||||
1. Prompts are stored in SQLite as immutable revisions, seeded from bundled repo defaults on first run.
|
||||
2. Saving appends a revision; an active pointer selects which revision is live.
|
||||
3. Reset to default re-activates the bundled default; reset to a revision re-activates that revision.
|
||||
4. Each inference call records the revision id/hash it used.
|
||||
5. A runtime debug flag gates both the Advanced settings UI and the prompt-write endpoints. Single build, not two.
|
||||
6. This is post-M3 (`later`).
|
||||
|
||||
## 12. Pricing
|
||||
|
||||
1. A flat `$/bdft` rate per species is configured in Settings, matched to a synced species leaf. No tiers in v1.
|
||||
2. The per-slab suggestion is `bdft × rate`; the owner may override and the override sticks. No rule means an empty price the owner types.
|
||||
3. The price field and its suggestion live in the review screen.
|
||||
|
||||
## 13. Publish
|
||||
|
||||
1. Create sequence is the documented one: validate the ready set, look up the SKU (stop if it exists), refresh taxonomy if older than one hour, resolve categories/tags/attributes, upload inventory PNGs, `POST /products`, then finalize.
|
||||
2. On success the local slab row is deleted. WooCommerce is the only source of truth; `sync_log` is retained; photos are purged; the device draft is cleared.
|
||||
3. `SLAB-UAT-*` always creates a Woo draft.
|
||||
4. A duplicate SKU blocks create; there is no update path.
|
||||
5. A stale value on submit is a field-level error telling the owner to pick from the current list.
|
||||
|
||||
## 14. Settings
|
||||
|
||||
1. Compose env: `WOO_BASE_URL` (https only), `SLAB_AES_KEY`.
|
||||
2. Encrypted in SQLite: `woo_wp_username`, `woo_app_password`, `inference_base_url`, `inference_api_key`.
|
||||
3. Plain settings: `inference_model`, `inference_enabled`, `content_llm_enabled`, `woo_create_status` (default draft), `brand_voice`, `geo_context`.
|
||||
4. Derived: `taxonomy_last_sync_at`, `taxonomy_anchor`.
|
||||
5. Auth is a WordPress application password for a dedicated low-privilege user; not consumer keys, not the login password.
|
||||
|
||||
## 15. Scope mapping
|
||||
|
||||
Shipped: M1 Capture, M2 Photos, capture knob prefs.
|
||||
|
||||
M3: store-connection and AI-connection settings; taxonomy sync; the five attributes; Call 1; Call 2; title/description generation; review screen with field gating; pricing; publish; local draft cleanup.
|
||||
|
||||
Later: prompt-management UI, U2Net, offline capture, OCR/ruler, batch create, number-drift check, Specialty cuts prose, primary-category correction UX.
|
||||
|
||||
## 16. Open / parked items
|
||||
|
||||
1. `attributes` element shape on `POST /products` is open — needs a live Whidbey product sample or the official Woo REST fields. Do not invent.
|
||||
2. Server persistence approach is open — full OpenAPI slab CRUD vs a thinner FastAPI path.
|
||||
3. `Specialty cuts` leaf prose is parked.
|
||||
4. Primary-category correction and awareness is a UX problem, deferred.
|
||||
5. Number-drift check is `later`.
|
||||
6. Prompt management is post-M3.
|
||||
7. The QUALITY/OPS extraction re-run is still pending (the first pass was truncated).
|
||||
8. "Later docs prevail" is the rule, but the tag/archive still hold material not yet reconciled (e.g. U2Net, browser floor, TES vectors, deploy details).
|
||||
|
||||
## 17. Work items
|
||||
|
||||
1. ~~Re-run the QUALITY/OPS extraction.~~ Done 2026-09-18 (TEST-SPEC ~110 rows; plan/deploy/release ~120 rows).
|
||||
2. Write `docs/REQUIREMENTS.md` namespace by namespace, starting with `CAPTURE`.
|
||||
3. Draft the per-leaf title/prose work once the `METADATA` section is being written.
|
||||
4. Migrate capability/contract docs into their homes, make docs branch-neutral, rebuild the structure check, wire CI.
|
||||
|
||||
## 18. Rulings deferred to a final cleanup pass
|
||||
|
||||
These surfaced during the QUALITY/OPS extraction. They do not block writing `REQUIREMENTS.md`; park them and reconcile in the cleanup pass.
|
||||
|
||||
1. Test directory layout: old TEST-SPEC mandates Vitest and `frontend/src/__tests__/`; shipped code uses the Node test runner (`node --import tsx --test`) and `frontend/tests/`. Contradiction.
|
||||
2. Test coverage target (~95% for deterministic modules) — adopt or drop.
|
||||
3. "The Test Bar" role and who authors reports versus Ty's sign-off.
|
||||
4. `docs/checks/check-structure.sh` gate timing and criteria — now replaced by `tools/check_requirements.py`.
|
||||
5. Deploy ports: proposal `18080/18443` versus UAT `:10881`, and `APP_HOSTNAME` binding.
|
||||
6. Backup retention "7 daily + 4 weekly" has no verification test.
|
||||
7. `SLAB_AES_KEY_OLD` rekey env placement (host vs compose).
|
||||
8. Smoke step 5: local `published` versus Woo `draft` mismatch — intended or not.
|
||||
9. Release process: `Build-release-standard` points at GitHub while the repo is Gitea.
|
||||
10. Taxonomy sync endpoint path divergence across docs.
|
||||
11. Ownership of `deploy/INVENTORY.md` across Gate B vs Phase 6.
|
||||
12. Scope labels: do shipped/M3/later map to phases or to release milestones.
|
||||
13. `C1`/`C2` (Call 1 / Call 2) namespace ownership ambiguity.
|
||||
14. Whether `UAT-PRACTICE.md` change control is a gate.
|
||||
15. Definition of "real fixtures" acceptance for TV-3, TV-6, TV-7, TV-10.
|
||||
16. `INF-1` ownership: SETTINGS toggle versus QUALITY suite obligation.
|
||||
17. Server-side API surface (`revision_conflict`, validation module, slab CRUD) belongs to the open server-persistence decision.
|
||||
18. `output_px_min` "never below 1600" from old DEPLOYMENT/IMPL-PLAN versus shipped 800 floor.
|
||||
@@ -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`.
|
||||
|
||||
@@ -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/` |
|
||||
|
||||
|
||||
@@ -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: —
|
||||
@@ -0,0 +1,179 @@
|
||||
# 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 a test path, a requirement ID, or `—` (traceability gap). A path given for a `shipped` requirement must exist.
|
||||
|
||||
Sources reconciled 2026-09-18 from the current branch docs plus the `pre-simplification` tag; decisions recorded in `.audit/2026-09-18-reconciliation-decisions.md`.
|
||||
|
||||
## CAPTURE — ecommerce-ready capture
|
||||
|
||||
| ID | Requirement | Scope | Test |
|
||||
|---|---|---|---|
|
||||
| CAPTURE-1 | Mask, measure, crop, and PNG output are a deterministic function of photo pixels and user inputs, with no inference, model, or network in the math. | shipped | — |
|
||||
| CAPTURE-2 | Supported clients are iOS Safari and Android Chrome, plus desktop Chrome, Edge, and Firefox; unsupported clients get a hard stop, and there are no polyfills. | shipped | — |
|
||||
| CAPTURE-3 | Capture is online only; offline capture is deferred. | later | — |
|
||||
| CAPTURE-4 | Photos are taken on a green or black sheet with the slab fully on the sheet; the first photo is top-down with the lens parallel to the broad face. | shipped | — |
|
||||
| CAPTURE-5 | A batch of one or more photos of one slab is uploaded, and the owner selects which photo is the calibration photo. | shipped | — |
|
||||
| CAPTURE-6 | The calibration photo drives mask, length axis, measure, and crop; other batch photos reuse the approved sheet, sensitivity, and crop rules. | shipped | — |
|
||||
| CAPTURE-7 | Sheet detection samples a 2 to 5 percent strip on all four borders and classifies green or black by HSV; otherwise the owner picks the sheet. | shipped | frontend/src/mask/sheetKeep.test.ts |
|
||||
| CAPTURE-8 | Mask controls are exactly Sheet (auto, green, black) and Sensitivity (0 to 100, step 1); Edge offset and Feather are removed. | shipped | — |
|
||||
| CAPTURE-9 | A Sensitivity change debounces 2000 ms; a Sheet change runs immediately; the overlay recomputes in a worker and the last good overlay stays visible. | shipped | frontend/src/capture/session.calibrate.test.ts |
|
||||
| CAPTURE-10 | The mask overlay renders on the source photo and the owner approves it; approval is blocked when the mask has no slab pixels. | shipped | — |
|
||||
| CAPTURE-11 | The mask must be a single contiguous slab contour; otherwise the resolution is a retake. | shipped | — |
|
||||
| CAPTURE-12 | The length axis is the long side of the minimum-area bounding rectangle of the mask; the owner rotates it to the prevailing length and confirms. | shipped | frontend/src/geometry/rect.test.ts |
|
||||
| CAPTURE-13 | On a nearly square slab the owner chooses which axis is length. | shipped | — |
|
||||
| CAPTURE-14 | The owner enters length and thickness in 1/8 inch steps. | shipped | frontend/src/capture/measureInputs.test.ts |
|
||||
| CAPTURE-15 | scale equals longest_axis_px divided by measured length, where longest_axis_px is the mask span end to end along the approved axis, not the rectangle edge. | shipped | — |
|
||||
| CAPTURE-16 | Widths sample every 6 inches perpendicular to the approved axis, dropping the first and last 6 inches; min, max, and average are reported; if length is 12 inches or less all three are zero. | shipped | frontend/src/measure/measure.test.ts |
|
||||
| CAPTURE-17 | sqft equals mask_pixels divided by scale squared divided by 144, rounded half-up to 2 decimals. | shipped | frontend/src/measure/measure.test.ts |
|
||||
| CAPTURE-18 | bdft equals sqft times thickness_in, rounded half-up to 2 decimals, with no divide-by-12; 1 sqft at 1 inch equals 1 bdft. | shipped | frontend/src/measure/measure.test.ts |
|
||||
| CAPTURE-19 | The inventory PNG is 8-bit RGBA with alpha preserved through encode and upload. | shipped | frontend/src/png/inventory.test.ts |
|
||||
| CAPTURE-20 | Crop is 3:4 portrait, slab centered, 90 percent fill at the widest and tallest extremes, on a transparent background. | shipped | frontend/src/png/inventory.test.ts |
|
||||
| CAPTURE-21 | After crop the shorter side is at least 800 px when the source allows; otherwise warn and require a new photo, and never upscale. | shipped | frontend/src/png/inventory.test.ts |
|
||||
| CAPTURE-22 | A non-calibration photo that yields an empty mask or fails the size floor is excluded with a per-photo error while the rest proceed. | shipped | frontend/src/capture/session.batchApply.test.ts |
|
||||
| CAPTURE-23 | Scale, widths, sqft, and bdft stay tied to the calibration photo; non-calibration photos are visual inventory only. | shipped | frontend/src/capture/session.batchApply.test.ts |
|
||||
| CAPTURE-24 | The dimension overlay is black and oriented to the approved length axis, with the length graphic beside the wood on the right in axis space. | shipped | frontend/src/png/overlay.test.ts |
|
||||
| CAPTURE-25 | Overlay width stations run every 12 inches starting 6 inches from the bottom while station is at most length minus 6; the bottom end follows the calibration photo EXIF orientation. | shipped | frontend/src/png/overlay.test.ts |
|
||||
| CAPTURE-26 | The photos-ready lineup order is clean calibration PNG, dimension-overlay copy, then remaining batch photos in capture order. | shipped | frontend/src/capture/photosReadyLineup.test.ts |
|
||||
| CAPTURE-27 | The client computes all deterministic math; the server stores the client numbers and never recomputes them as source of truth. | shipped | — |
|
||||
| CAPTURE-28 | Load-bearing client APIs are image decode, canvas or OffscreenCanvas, a worker, PNG encode, IndexedDB, and fetch; no OpenCV, ONNX, or U2Net on the shipped path. | shipped | — |
|
||||
| CAPTURE-29 | Capture knob preferences persist in localStorage until reset to default. | shipped | — |
|
||||
| CAPTURE-30 | Mask tuning has no apply step and no server round trip. | shipped | — |
|
||||
| CAPTURE-31 | Failures such as a broken mask or undersized source never silently guess; the resolution is retake. | shipped | — |
|
||||
| CAPTURE-32 | Length is stored and displayed to the nearest 1/8 inch, rounded values are what is stored, and sqin appears nowhere in the UI, tests, or copy. | shipped | — |
|
||||
| CAPTURE-33 | U2Net on-demand server matting is deferred. | later | — |
|
||||
|
||||
## METADATA — listing attributes and copy
|
||||
|
||||
| ID | Requirement | Scope | Test |
|
||||
|---|---|---|---|
|
||||
| METADATA-1 | The listing taxonomy is exactly the synced WooCommerce set; the app never hardcodes, invents, or falls back to its own lists. | M3 | — |
|
||||
| METADATA-2 | Product categories are exactly one species leaf and one or more wood-category leaves; parents group only and are never assigned. | M3 | — |
|
||||
| METADATA-3 | The five locked attributes are Edge Type (1), Figure (at least 1, includes plain), Grade (exactly 1), Thickness (1, round-up band), and Moisture (1, default kiln-dried, never inferred). | M3 | — |
|
||||
| METADATA-4 | Missing attributes or terms are skipped, never created. | M3 | — |
|
||||
| METADATA-5 | Species, character, length, and free-text thickness are not product attributes. | M3 | — |
|
||||
| METADATA-6 | Product tags come from the synced Woo set and are all optional; no hardcoded prefixes, and the bare fig tag is kept. | M3 | — |
|
||||
| METADATA-7 | Call 1 runs only when inference is enabled, automatically after a calibrated draft uploads or on demand, and sends the clean processed photos at 1024 px long edge or less plus user metadata and the synced taxonomy. | M3 | — |
|
||||
| METADATA-8 | Call 1 returns species, wood categories, edge, figure, grade, and feature-tag suggestions constrained to the synced lists, each with confidence. | M3 | — |
|
||||
| METADATA-9 | Call 1 confidence at or above the threshold (default 0.7) pre-fills review fields and below it leaves them empty; suggestions are always editable. | M3 | — |
|
||||
| METADATA-10 | Typed values always win over inferred values. | M3 | — |
|
||||
| METADATA-11 | Call 2 is user-triggered only and produces name, short_description, and description; it never runs automatically. | M3 | — |
|
||||
| METADATA-12 | Call 2 receives all metadata confirmed or changed in review plus the literal dimension fact block; the template guides style and the model has creative license. | M3 | — |
|
||||
| METADATA-13 | The model never invents or alters a dimension; every number it prints comes from the fact block. | M3 | — |
|
||||
| METADATA-14 | With inference off, deterministic templates produce name, short_description, and description so create never lacks prose. | M3 | — |
|
||||
| METADATA-15 | Inference never blocks create and never auto-publishes. | M3 | — |
|
||||
| METADATA-16 | The title uses the primary figure term name as the character, with a hyphen separator. | M3 | — |
|
||||
| METADATA-17 | The primary wood category selects a specialized Call 2 prompt. | M3 | — |
|
||||
| METADATA-18 | The dimension fact block carries exact strings for species, figure term, length, thickness, average width, width min to max, and board feet; there is no L by W by T rectangle assumption. | M3 | — |
|
||||
| METADATA-19 | The dimension-overlay image is never sent to the model. | M3 | — |
|
||||
|
||||
## REVIEW — confirm, tweak, ready
|
||||
|
||||
| ID | Requirement | Scope | Test |
|
||||
|---|---|---|---|
|
||||
| REVIEW-1 | Submission-ready requires at least two images (clean first pic plus its dimension-overlay copy), SKU, length, thickness, computed widths and sqft and bdft, exactly one species leaf, at least one wood-category leaf, one Edge Type, at least one Figure, exactly one Grade, one Thickness band, one Moisture, and a price. | M3 | — |
|
||||
| REVIEW-2 | Title, short_description, and description are required on the product and are satisfied by typed, generated, or templated text. | M3 | — |
|
||||
| REVIEW-3 | In review the owner declares one primary wood category and any secondary ones; primary drives the title and primary plus secondary drive the description; the default primary is the first category selected. | M3 | — |
|
||||
| REVIEW-4 | Every empty required field shows an inline nudge as the owner works; the slab cannot reach ready until the full set is filled, and there is no single submit-time gap list. | M3 | — |
|
||||
| REVIEW-5 | The price suggestion is bdft times the species rate from Settings; the owner may override and the override sticks; no rule leaves the price empty for manual entry. | M3 | — |
|
||||
| REVIEW-6 | Widths, sqft, and bdft are read-only in production, derived from the mask, and are not overridable. | M3 | — |
|
||||
| REVIEW-7 | A picked taxonomy id that disappeared after a refresh shows as no longer valid and is never silently swapped for another value. | M3 | — |
|
||||
| REVIEW-8 | The owner can edit any field, including generated title and description, until create. | M3 | — |
|
||||
|
||||
## PUBLISH — store submission
|
||||
|
||||
| ID | Requirement | Scope | Test |
|
||||
|---|---|---|---|
|
||||
| PUBLISH-1 | The create sequence validates the ready set, looks up the SKU and stops if it exists, refreshes taxonomy when the last sync is over one hour old, resolves categories and tags and attributes, uploads images, calls POST products, then finalizes. | M3 | — |
|
||||
| PUBLISH-2 | SKUs matching SLAB-UAT-* always create a WooCommerce draft regardless of the create-status setting. | M3 | — |
|
||||
| PUBLISH-3 | A duplicate SKU blocks create and offers edit-SKU or open-existing; this version has no update path. | M3 | — |
|
||||
| PUBLISH-4 | A stale value on submit is a field-level error that shows the current options and never a raw code. | M3 | — |
|
||||
| PUBLISH-5 | On success the server sets woo_product_id, purges photos, deletes the local slab row, and clears the device draft; WooCommerce is the source of truth. | M3 | — |
|
||||
| PUBLISH-6 | On failure the slab is marked failed, photos are retained, sync_log is written, and a plain field-level message is shown; retry works from the same screen. | M3 | — |
|
||||
| PUBLISH-7 | The owner never sees raw status codes, stack traces, or internal error codes. | M3 | — |
|
||||
| PUBLISH-8 | Images upload as processed PNGs with alpha, capped at five, and only inventory images are uploaded. | M3 | — |
|
||||
| PUBLISH-9 | The Woo image order is the photos-ready lineup: clean calibration, dimension-overlay copy, then the rest in capture order. | M3 | — |
|
||||
| PUBLISH-10 | Published means the store create succeeded, not that the product is live when status is draft. | M3 | — |
|
||||
| PUBLISH-11 | Only non-null fields are sent; the payload carries name, type simple, status, sku, regular_price, description, short_description, categories, tags, images, attributes, virtual false, downloadable false, catalog_visibility visible. | M3 | — |
|
||||
|
||||
## SETTINGS — configured once
|
||||
|
||||
| ID | Requirement | Scope | Test |
|
||||
|---|---|---|---|
|
||||
| SETTINGS-1 | WOO_BASE_URL lives in compose or .env, is https only, is injected into FastAPI, and is shown read-only in Settings; it is not a SQLite settings row. | M3 | — |
|
||||
| SETTINGS-2 | FastAPI refuses to start when WOO_BASE_URL is missing or not https. | M3 | — |
|
||||
| SETTINGS-3 | Woo auth is a WordPress application password for a dedicated low-privilege user, not consumer keys and not the admin login. | M3 | — |
|
||||
| SETTINGS-4 | Woo username and application password are entered in Settings and stored AES-GCM in SQLite; the browser never keeps them. | M3 | — |
|
||||
| SETTINGS-5 | SLAB_AES_KEY lives in compose or .env as a 32-byte base64 value, never in the DB or repo, and a missing key refuses startup. | M3 | — |
|
||||
| SETTINGS-6 | Inference base URL and API key are stored encrypted in SQLite, while inference_model, inference_enabled, content_llm_enabled, brand_voice, and geo_context are settings. | M3 | — |
|
||||
| SETTINGS-7 | Species rates are configured in Settings as one flat dollars-per-bdft value per synced species leaf, with no tiers. | M3 | — |
|
||||
| SETTINGS-8 | test-woo verifies the connection and always resyncs taxonomy on success. | M3 | — |
|
||||
| SETTINGS-9 | test-inference is a two-turn probe returning ok, mode (stateful, stateless, or fail), vision_capable, and detail; fail blocks Call 2 only. | M3 | — |
|
||||
| SETTINGS-10 | Taxonomy sync pulls categories, tags, and attributes with terms, rebuilds the cache in one transaction, and on failure returns 502 while keeping the previous cache. | M3 | — |
|
||||
| SETTINGS-11 | Every successful sync updates taxonomy_last_sync_at; taxonomy_anchor bumps only when the stored-subset hash changes, including deletions. | M3 | — |
|
||||
| SETTINGS-12 | The client re-pulls the full taxonomy only when the anchor advances, and publish refreshes when the last sync is over one hour old. | M3 | — |
|
||||
| SETTINGS-13 | woo_create_status defaults to draft. | M3 | — |
|
||||
| SETTINGS-14 | The vision pixel cap is the only user-facing vision setting; the JPEG quality lives in server config. | M3 | — |
|
||||
| SETTINGS-15 | Secrets never appear in any GET response; only a configured boolean is returned. | M3 | — |
|
||||
|
||||
## APP — device draft and state
|
||||
|
||||
| ID | Requirement | Scope | Test |
|
||||
|---|---|---|---|
|
||||
| APP-1 | Slab status runs draft, calibrated, ready, publishing, published, or failed. | shipped | — |
|
||||
| APP-2 | The device draft is separate from a WooCommerce draft and is restored on open through Resume; Continue restores the last approved state and Start fresh discards. | shipped | — |
|
||||
| APP-3 | The device draft is cleared after a successful create. | M3 | — |
|
||||
| APP-4 | Slab ids are client-generated, timestamps are UTC ISO-8601, and client_rev and server_rev support optimistic concurrency. | M3 | — |
|
||||
| APP-5 | FastAPI is the only caller of WooCommerce and of the inference endpoint; the browser holds no store or inference credentials. | shipped | — |
|
||||
| APP-6 | The API base is /api/v1 and health is at /api/health. | M3 | — |
|
||||
| APP-7 | Errors use a uniform envelope and are JSON; lists are returned in full without pagination. | M3 | — |
|
||||
| APP-8 | The server stores the client-sent derived numbers and does not recompute them as source of truth. | shipped | — |
|
||||
|
||||
## OPS — deploy and release
|
||||
|
||||
| ID | Requirement | Scope | Test |
|
||||
|---|---|---|---|
|
||||
| OPS-1 | Deploy is Docker Compose plus Caddy on an inventoried host, reachable only over LAN or Tailscale with HTTPS. | shipped | — |
|
||||
| OPS-2 | WOO_BASE_URL and SLAB_AES_KEY are supplied as compose environment and never stored in the repo. | shipped | — |
|
||||
| OPS-3 | deploy/INVENTORY.md is filled before any port bind, covering host, hostname, ports, and DNS reachability from the phone. | shipped | — |
|
||||
| OPS-4 | Services bind to localhost and Caddy is the only public surface. | shipped | — |
|
||||
| OPS-5 | Base images are pinned, not latest. | shipped | — |
|
||||
| OPS-6 | Backup is a WAL-safe SQLite backup plus a tar of the images volume, with retention of 7 daily and 4 weekly. | later | — |
|
||||
| OPS-7 | Restore is stop, restore DB and images, start, and it is tested at least once before backup is considered done. | later | — |
|
||||
| OPS-8 | The UAT environment is ubudual5060 at http://100.64.0.9:10881; port 10880 is off limits. | M3 | — |
|
||||
| OPS-9 | Inference is a remote, configurable endpoint, not part of the app host. | shipped | — |
|
||||
| OPS-10 | The repository on gitea-atd is the source of truth. | shipped | — |
|
||||
|
||||
## QUALITY — verification
|
||||
|
||||
| ID | Requirement | Scope | Test |
|
||||
|---|---|---|---|
|
||||
| QUALITY-1 | Tests are written with the code; no feature ships without a passing test and its shown output. | shipped | — |
|
||||
| QUALITY-2 | Deterministic modules are test-driven and covered before any UI consumes them. | shipped | — |
|
||||
| QUALITY-3 | Unit tests for the deterministic core cover vectors TV-1 through TV-10. | shipped | — |
|
||||
| QUALITY-4 | The full suite passes with inference disabled, asserting zero inference calls. | M3 | — |
|
||||
| QUALITY-5 | Pass or fail is only claimed with raw runner evidence; missing evidence means not run. | shipped | — |
|
||||
| QUALITY-6 | Ty reads raw results and owns gate exit; agents never declare a gate green. | shipped | — |
|
||||
| QUALITY-7 | Integration tests use a Woo mock and cover success, error, duplicate SKU, stale field, and taxonomy anchor. | M3 | — |
|
||||
| QUALITY-8 | Property checks assert bdft is monotonic in thickness and square feet. | shipped | — |
|
||||
| QUALITY-9 | The deterministic functions assert the bdft identity and the no-upscale path. | shipped | — |
|
||||
| QUALITY-10 | UI and end-to-end tests run against a Compose stack with mocked Woo. | M3 | — |
|
||||
| QUALITY-11 | Pipeline vector TV-1: an interior green patch not touching the border survives; U2Net is not invoked. | shipped | — |
|
||||
| QUALITY-12 | Pipeline vector TV-2: dark interior wood on a black sheet survives, and flood fill isolates only border-connected regions. | shipped | — |
|
||||
| QUALITY-13 | Pipeline vector TV-3: a 96 inch slab, 1.5 inch thick, 0.64 sqft mask yields 0.96 bdft. | shipped | — |
|
||||
| QUALITY-14 | Pipeline vector TV-4: width samples at 6 inch stations on a 96 inch axis report min, max, and average perpendicular to the confirmed axis. | shipped | — |
|
||||
| QUALITY-15 | Pipeline vector TV-5: one square end and one 45 degree cut, the length axis follows the prevailing length. | shipped | — |
|
||||
| QUALITY-16 | Pipeline vector TV-6: 3:4 crop, 90 percent fill at extremes, transparent PNG, shorter side at least 800 when the source allows. | shipped | — |
|
||||
| QUALITY-17 | Pipeline vector TV-7: an undersized source warns, is not upscaled, and is not publishable until retake. | shipped | — |
|
||||
| QUALITY-18 | Pipeline vector TV-8: bdft 0.96 at 12.50 per bdft gives a 12.00 recommendation, an override of 150.00 is stored, and no rule leaves the price empty for manual entry. | M3 | — |
|
||||
| QUALITY-19 | Pipeline vector TV-9: a 1 inch thickness rounds up into the synced Woo thickness band, not a hardcoded one. | M3 | — |
|
||||
| QUALITY-20 | Pipeline vector TV-10: half-up 2 decimal rounding, including 96.04 to 96, 0.641 to 0.64, 11.525 to 11.53, 12.345 to 12.35. | shipped | — |
|
||||
| QUALITY-21 | Dishonest proxies that do not count as pass: hardcoded or fixture taxonomy treated as synced, a Woo mock that never hits the real store for the exit walk, unit tests alone without a real SLAB-UAT-* draft, consumer-key auth, inference wired as dead config or required for create, claiming ready with empty fields, or parking work on the wrong branch. | M3 | — |
|
||||
| QUALITY-22 | The M3 exit evidence is a real SLAB-UAT-* Woo draft matching the reviewed ready set with inference off, plus test-woo and test-inference results. | M3 | — |
|
||||
| QUALITY-23 | Browser end-to-end capture through publish is verified on iOS Safari, Android Chrome, and desktop Chrome, Edge, and Firefox. | later | — |
|
||||
| QUALITY-24 | No raw status code, stack trace, or internal code reaches the owner in any tested path. | M3 | — |
|
||||
@@ -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 test path, a requirement ID, or `—`. A path given for a `shipped` requirement must exist; a given ID must resolve to a defined requirement.
|
||||
|
||||
## 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` references resolve: a path given for a `shipped` requirement exists; an ID given resolves to a defined requirement; `—` is allowed.
|
||||
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` |
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
Template: decision record. One file per decision.
|
||||
Record why a choice was made when the reason is not obvious from the code or requirements.
|
||||
-->
|
||||
# ADR-<n>: <title>
|
||||
|
||||
- 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>
|
||||
Vendored
+17
@@ -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>
|
||||
Vendored
+11
@@ -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>
|
||||
Vendored
+6
@@ -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 `—` |
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/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] = {}
|
||||
test_refs: list[tuple[str, str, 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}'")
|
||||
test_ref = test.strip()
|
||||
if test_ref not in PLACEHOLDER:
|
||||
test_refs.append((loc, rid, test_ref, scope))
|
||||
|
||||
# 5. resolve test references: an existing path, a requirement ID, or —
|
||||
for loc, rid, ref, scope in test_refs:
|
||||
if ID_TOKEN.match(ref):
|
||||
if ref not in ids:
|
||||
errors.append(f"{loc} {rid} test ref {ref} is not a defined requirement")
|
||||
elif "/" in ref or "\\" in ref or "." in ref:
|
||||
if scope == "shipped" and not (ROOT / ref).exists():
|
||||
errors.append(f"{loc} {rid} shipped test path not found: {ref}")
|
||||
else:
|
||||
errors.append(f"{loc} {rid} invalid Test ref '{ref}' (use a path, an ID, or —)")
|
||||
|
||||
# 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())
|
||||
Reference in New Issue
Block a user