diff --git a/.audit/2026-09-18-coverage-gaps.md b/.audit/2026-09-18-coverage-gaps.md new file mode 100644 index 0000000..24d0e2a --- /dev/null +++ b/.audit/2026-09-18-coverage-gaps.md @@ -0,0 +1,68 @@ +# Coverage gaps — serial worklist + +- Date: 2026-09-18 +- Branch: `docs/clean-main` +- Status: open worklist. Companion to `.audit/coverage/` and `tools/check_coverage.py`. +- Rule: work one entry at a time, top to bottom. Each decision is a ruling; record it, then + update the matching `gap` rows in the ledger to their resolution and re-run the gate. + +The pilot slice (settings / auth / health) produced 12 `gap` atoms that collapse into the +seven distinct holes below. Each is a requirement the source material carries and +`docs/REQUIREMENTS.md` does not. + +## GAP-1 — test-woo failure semantics + +- Atoms: `tag:TEST-SPEC API-10`, `tag:OPENAPI test-woo-shape`, `prd decision-17`, + `tag:TEST-SPEC GC-3` (partial). +- Hole: the PoR says `test-woo` verifies the connection and resyncs (`SETTINGS-40`), but + nothing defines its result shape (`ok`, `detail`), that it fails closed on missing + credentials or non-HTTPS, or how an auth rejection differs from an unreachable store. +- Proposed home: `SETTINGS`. +- Status: open. + +## GAP-2 — health endpoint semantics + +- Atoms: `tag:TEST-SPEC H-1`, `tag:TEST-SPEC INS-1`, `tag:OPENAPI health`, `issue:#6`. +- Hole: `APP-30` names `/api/health`; nothing defines the body (`status`, `version`, + `woo_reachable`) or that health goes non-200 on DB/volume failure. +- Proposed home: `APP`. +- Status: open. + +## GAP-3 — PUT /settings rejects a WOO_BASE_URL write + +- Atoms: `tag:TEST-SPEC GC-4`, `tag:OPENAPI settings-PUT-reject`. +- Hole: `SETTINGS-5` makes the URL env-only and read-only, but nothing requires the settings + API to refuse an attempt to write it. +- Proposed home: `SETTINGS`. +- Status: open. + +## GAP-4 — AES credential round-trip + +- Atom: `tag:TEST-SPEC AES-1` (round-trip half). +- Hole: `SETTINGS-75` covers redaction; nothing asserts that an encrypted credential + decrypts back to its value. +- Proposed home: `QUALITY` (test obligation) over `SETTINGS-20/30`. +- Status: open. + +## GAP-5 — upstream HTTP errors mapped to the envelope + +- Atom: `tag:TEST-SPEC FUND-21`. +- Hole: publish field errors are mapped; there is no general rule that a 401/400/404 from + Woo is turned into the uniform envelope rather than passed through. +- Proposed home: `APP` (alongside `APP-35`). +- Status: open. + +## GAP-6 — no app-level auth + +- Atom: `prd no-app-auth`. +- Hole: no row states that the app has no login of its own and relies on network plus HTTPS. +- Proposed home: `OPS`. +- Status: open. + +## GAP-7 — httpx timeout values + +- Atom: `cur:CONTENT-WOO httpx-timeouts`. +- Hole: connect 10s / read 60s exist only in the contract draft. +- Proposed home: contract detail, not a requirement: record in `docs/contracts/` in step 3 + and mark the ledger atom `dropped` with rationale. +- Status: open. diff --git a/.audit/2026-09-18-migration-todos.md b/.audit/2026-09-18-migration-todos.md new file mode 100644 index 0000000..0e37444 --- /dev/null +++ b/.audit/2026-09-18-migration-todos.md @@ -0,0 +1,48 @@ +# Migration TODOs — source docs + +- Date: 2026-09-18 +- Branch: `docs/clean-main` +- Purpose: edits deferred until Phase 2 step 3, when the capability and contract docs are + migrated into `docs/capabilities/` and `docs/contracts/`, made branch-neutral, and + converted to cite requirement IDs. +- Rule: these docs are not edited now. `REQUIREMENTS.md` is the only normative text and is + already correct for everything below. + +## CAPTURE.md + +1. Line 16, "Length of the slab is aligned with the long side of the picture" — ruled dead. + Remove. +2. Lines 128–129, the `kind=inventory` role label — discarded. The "first inventory photo is + the measurement reference" half is `CAPTURE-115`; keep it as a citation, drop the label. +3. Line 140, editor/storefront alpha presentation — discarded. +4. The sensitivity mapping (line 69) and display/source-pixels rule (lines 58–62) are now + `CAPTURE-41` and `CAPTURE-141`; cite them instead of restating. +5. Line 82 busy mark ("more than a fraction of a second"; as-built is 300 ms) — ruled a + nit, not a requirement. Leave as design prose or drop. + +## CREATE.md + +1. Grade cardinality: change "1 or more" to 1 in all three places (attributes table, + ready set, data shape). Ruling: Grade is exactly one. +2. `fig-*` cardinality: change "1 or more / required" to optional in the tags table and the + ready set. Ruling: tags optional; the Figure attribute is at least one. +3. Ready-set images: change "at least one inventory PNG" to two (clean calibration plus its + dimension-overlay copy). Ruling: `REVIEW-5` is the PoR. +4. Remove "cap at 5 images" from the create sequence. +5. Remove "user typed widths/sqft/bdft wins"; the calculated fields are read-only. +6. Fix the Call 2 line so the model writes the title/description and the deterministic + template is the fallback, not the assembler. + +## CONTENT-WOO.md + +1. Em dash in the title template and example → hyphen. +2. "Templates (always on)" and "templates inject those numbers after the prose comes back" → + template is the fallback only; the model writes when Call 2 runs. +3. `fig-*` "1+ required" and the ready set including `fig-*` tags → tags optional. +4. "≤5 inventory photos" → no cap; drop the "only `kind=inventory`" clause. +5. The empty `attributes` element shape is the known deferred decision; keep the placeholder. + +## PHOTOS.md + +No changes. Overlay legibility and arrow/text drawing detail were ruled design, not +requirements. diff --git a/.audit/2026-09-18-reconciliation-decisions.md b/.audit/2026-09-18-reconciliation-decisions.md index d2216c1..2b3cf44 100644 --- a/.audit/2026-09-18-reconciliation-decisions.md +++ b/.audit/2026-09-18-reconciliation-decisions.md @@ -10,14 +10,11 @@ context is not lost between sessions. Loose ends are collected in section 16. ## 0. Resume here (for a new session) -- Repo: `D:\dev\SlabUploader`. Branch: `docs/clean-main` (pushed to origin). -- State: `main` fast-forwarded to `d754cac` (all shipped code); old specs preserved behind tag `pre-simplification`; Phase 1 scaffold committed as `b42e3cb`; reconciled requirement set drafted as `238fcc4` (128 rows, 8 namespaces). -- Immediate next step: Ty reviews `docs/REQUIREMENTS.md` (Gitea branch `docs/clean-main`) and lists rewrite problems by requirement ID; the next session folds corrections in. -- After that: migrate capability and contract docs into `docs/capabilities/` and `docs/contracts/`, make them branch-neutral, and have them cite IDs instead of restating requirements. -- Gate that must stay green: `python tools/check_requirements.py`. -- Read first: `docs/STRUCTURE.md` (contract and namespaces), `docs/REQUIREMENTS.md`, `docs/MILESTONES.md`. -- Old-spec source: `git show pre-simplification:docs/.md`. Raw extractions: `C:\Users\Ty\AppData\Local\Temp\opencode\slab-inventory\`. -- Deferred rulings: section 18. +**SUPERSEDED 2026-09-18. Read `.audit/2026-09-18-session-handoff.md` instead.** + +This section is kept only so the history below still reads correctly. The current state, +locked rulings, gate output, and next steps all live in the handoff. Everything below this +point is the decision log from the original reconciliation, not the current resume point. ## 1. Why this exists diff --git a/.audit/2026-09-18-session-handoff.md b/.audit/2026-09-18-session-handoff.md new file mode 100644 index 0000000..1fc51ed --- /dev/null +++ b/.audit/2026-09-18-session-handoff.md @@ -0,0 +1,102 @@ +# Session handoff — requirements reconciliation + +- Date: 2026-09-18 +- Branch: `docs/clean-main` +- HEAD: `1603467` (pushed to origin) +- Working tree: uncommitted. All new work below is local only. Nothing has been committed + or pushed since `1603467`. +- This file supersedes the stale "Resume here" section 0 of + `2026-09-18-reconciliation-decisions.md`. + +## Read first + +1. `docs/STRUCTURE.md` — structure contract, ID namespaces, the step-5 ID stride rule. +2. `docs/REQUIREMENTS.md` — the PoR. 136 requirements. +3. `.audit/2026-09-18-migration-todos.md` — deferred source-doc edits for step 3. +4. `.audit/2026-09-18-coverage-gaps.md` — the serial gap worklist (GAP-1..GAP-7). +5. `.audit/coverage/README.md` — the coverage ledger schema. + +## Gates + +``` +python tools/check_requirements.py -> PASS, 12 docs, 8 namespaces, 136 requirements +python tools/check_coverage.py -> PASS, 40 atoms, 12 gaps, 20/136 IDs claimed +python tools/check_coverage.py --strict -> FAIL by design until coverage is complete +``` + +## What this session did + +1. Renumbered every requirement in steps of 5 per namespace (see STRUCTURE.md). Nothing + cites IDs yet, so this was free. First IDs: `CAPTURE-5`, `METADATA-5`, etc. +2. Applied owner corrections: `PUBLISH-8` (no image cap), `METADATA-16` (template is the + fallback only), `REVIEW-8` (calculated fields are not mutable), `PUBLISH-30` + (attempt-level failure wording), `PUBLISH-35` and `PUBLISH-36/37/38` (error triage). +3. Walked `CAPTURE.md`, `PHOTOS.md`, `CREATE.md`, `CONTENT-WOO.md` section by section. + Added five rows (uncommitted): `CAPTURE-41`, `CAPTURE-141`, `METADATA-66`, `PUBLISH-31`, + `SETTINGS-51`. +4. Built the coverage method: `tools/check_coverage.py`, `.audit/coverage/` (schema plus a + settings/auth/health pilot ledger), and the gap worklist. + +## Rulings locked this session (do not relitigate) + +1. `METADATA-1`, `-2`, `-3` stay as written. +2. Audit decisions beat older specs. Title separator is the hyphen ` - `. +3. Grade is exactly 1. `fig-*` tags are optional. The Figure attribute is at least 1. + Ready set is two images; `REVIEW-5` is the PoR. +4. Tag scope is important: sync keeps only the figure and feature tag families so non-wood + Woo metadata does not pollute the workflow (`SETTINGS-51`). +5. Content-generation logging stays (`METADATA-66`), it is the trace/debug/tuning record. +6. Partial-publish orphaned media: log it, surface a manual cleanup note, no auto-delete + (`PUBLISH-31`). +7. Order of the coverage walk: current docs, then tag docs, then the archived PRD, then the + Gitea main design issues (#1–#20). TEST-SPEC is done LAST, reconciled against a finished + PoR. + +## Open work + +1. Continue the coverage walk doc by doc, section by section. Remaining current docs: + `PRODUCT`, `README`, `UAT-PRACTICE`. Then the tag docs, the archived PRD, the main + issues, and finally TEST-SPEC. +2. The pilot ledger `coverage/settings-auth-health.md` mixes sources. When a walked source + collides with it, split the ledger into per-source files first (this write was started + and reverted once; get explicit approval). +3. Resolve the coverage gaps in one pass, then step 3 migration + (`docs/capabilities/`, `docs/contracts/`, branch-neutral, cite IDs), then retire the + working records. + +## Known gaps + +1. `.audit/2026-09-18-coverage-gaps.md` has GAP-1..GAP-7, all open. GAP-1 (`test-woo` + failure semantics) and GAP-2 (health/`woo_reachable`) are the ones with the most + downstream content. +2. The coverage ledger currently reports 12 `gap` atoms from the pilot slice. +3. Still-open decisions: the `attributes` element shape on `POST /products`, and server + persistence (full CRUD vs a thinner path). +4. Discussed but unresolved: Woo credentials are stored server-side, so two phones share one + credential row and a wrong password locks both out. A "last-known-good plus auth-gated + promotion" rule was proposed; no requirement row yet. + +## Process rules + +1. This is planning work. Walk in plan mode (read-only); flip to build only for edits the + owner has explicitly approved. +2. No file gets created, edited, or deleted without the owner saying go for that specific + change. +3. Where a doc and the shipped code disagree, the as-built code is what the PoR documents. +4. Raw evidence for any "done": gate output, diff, or command output. + +## Environment + +1. Subagents `general` and `explore` are routed to the local SWIFT model + (`deepblu5090-8084/Swift-Qwen3.8-27B-Q5_K_M.gguf`) in the global config + `~/.config/opencode/opencode.jsonc`. Config loads at startup; restart after edits. +2. SWIFT is suitable for mechanical extraction only (read and list). Dispositions and + rulings stay with the owner. It never writes. +3. `.audit/docs-structure-pass.tsv` is finished history from 2026-09-09. Leave it. + +## Uncommitted inventory + +- Modified: `docs/REQUIREMENTS.md` (+5 rows). +- Untracked: `.audit/2026-09-18-coverage-gaps.md`, + `.audit/2026-09-18-migration-todos.md`, `.audit/2026-09-18-session-handoff.md`, + `.audit/coverage/`, `tools/check_coverage.py`. diff --git a/.audit/coverage/README.md b/.audit/coverage/README.md new file mode 100644 index 0000000..9275fc5 --- /dev/null +++ b/.audit/coverage/README.md @@ -0,0 +1,57 @@ +# Coverage ledger + +Provenance for every requirement in `docs/REQUIREMENTS.md`. One row per source atom. +Kept under `.audit/` on purpose, so it is not part of the `docs/` structure contract. + +`tools/check_coverage.py` reads every `*.md` in this directory except this file. + +## Row format + +``` +| Source | Atom | Requirement | Disposition | PoR ID | Rationale | +``` + +- **Source** — where the atom came from, prefixed by kind: `tag:TEST-SPEC`, `tag:OPENAPI`, + `prd`, `cur:CREATE`, `issue:#5`, `audit:14`. +- **Atom** — the stable key inside that source, e.g. `H-1`, `settings-PUT-reject`. +- **Requirement** — a short restatement, enough to recognise it. +- **Disposition** — one of the values below. +- **PoR ID** — the `docs/REQUIREMENTS.md` ID(s), comma-separated. `—` when none. +- **Rationale** — required for everything that is not plain `ported`. + +## Dispositions + +| Value | Meaning | PoR ID | Rationale | +|---|---|---|---| +| `ported` | fully represented by the cited row(s) | required | optional | +| `duplicate` | same rule as another atom | optional | required | +| `new` | PoR row with no source atom; a reconciliation decision | required | required | +| `superseded` | replaced by a later source or decision | optional | required | +| `deferred` | intentionally out of current scope | optional | required | +| `dropped` | deliberately discarded | `—` | required | +| `gap` | not represented, not deliberately dropped: an open hole | `—` | required | + +`gap` is the finding. It is the thing this ledger exists to surface. + +## Checks + +Forward (always): + +1. Every row has six well-formed columns and a known disposition. +2. A `(Source, Atom)` pair is unique across the ledger. +3. `ported` and `new` cite at least one defined requirement ID; every cited ID resolves. +4. `dropped` and `gap` carry no ID and a non-empty rationale. +5. Every non-`ported` row carries a non-empty rationale. + +Reverse (`--strict` only, once coverage is complete): + +6. Every ID in `docs/REQUIREMENTS.md` is claimed by at least one atom, or is `new`. +7. No `gap` rows remain. + +## Run + +``` +python tools/check_coverage.py +python tools/check_coverage.py --strict +python tools/check_coverage.py --ledger-dir # for negative tests +``` diff --git a/.audit/coverage/settings-auth-health.md b/.audit/coverage/settings-auth-health.md new file mode 100644 index 0000000..c05de9d --- /dev/null +++ b/.audit/coverage/settings-auth-health.md @@ -0,0 +1,47 @@ +# Coverage ledger — settings / auth / health (pilot) + +Seed slice proving the method. Sources: the `pre-simplification` tag, the archived PRD, +the current branch docs, and the Gitea issues. + +| Source | Atom | Requirement | Disposition | PoR ID | Rationale | +|---|---|---|---|---|---| +| tag:TEST-SPEC | GC-1 | SLAB-UAT-* creates a Woo draft regardless of woo_create_status | ported | PUBLISH-10 | — | +| tag:TEST-SPEC | GC-2 | duplicate SKU returns 409 with edit_sku and open_existing | ported | PUBLISH-15 | — | +| tag:TEST-SPEC | GC-3 | test-woo returns ok with taxonomy and always resyncs on success | ported | SETTINGS-40 | — | +| tag:TEST-SPEC | GC-4 | WOO_BASE_URL compose-only, GET read-only, PUT rejects a write with 422 | gap | — | compose-only and read-only live in SETTINGS-5; the API-level 422 rejection has no row | +| tag:TEST-SPEC | GC-5 | FastAPI refuses to start on missing or http WOO_BASE_URL | ported | SETTINGS-10 | — | +| tag:TEST-SPEC | W-3 | taxonomy anchor unchanged on no-op, bumps only on stored-subset hash change | ported | SETTINGS-55 | sync and rebuild live in SETTINGS-50 | +| tag:TEST-SPEC | W-4 | test-woo always runs a taxonomy resync after a successful test | duplicate | SETTINGS-40 | same rule as GC-3 | +| tag:TEST-SPEC | AES-1 | credential encrypt/decrypt round-trips; ciphertext never appears in GET | gap | — | redaction lives in SETTINGS-75; the round-trip assertion has no row | +| tag:TEST-SPEC | AES-2 | missing SLAB_AES_KEY refuses startup, no plaintext fallback | ported | SETTINGS-25 | — | +| tag:TEST-SPEC | API-10 | test-woo fails closed on missing credentials or non-HTTPS URL | gap | — | no row defines test-woo's failure behavior | +| tag:TEST-SPEC | FUND-21 | 401/400/404 mapped to an actionable error, not raw passthrough | gap | — | publish fields are mapped by PUBLISH-20 and PUBLISH-35; the settings handshake mapping is absent | +| tag:TEST-SPEC | H-1 | health 200 with woo_reachable false when credentials are not configured | gap | — | APP-30 names /api/health only | +| tag:TEST-SPEC | INS-1 | health non-200 on DB or volume failure; woo_reachable false when the store is unreachable | gap | — | not represented | +| tag:TEST-SPEC | UIE-8 | Settings screen: WP creds and test-woo, inference and test, pricing, brand/GEO, create status, read-only URL | ported | SETTINGS-5, SETTINGS-15, SETTINGS-20, SETTINGS-30, SETTINGS-35, SETTINGS-65 | screen composition itself belongs to the contract/UX docs | +| tag:TEST-SPEC | OPS-3 | compose health, test-woo ok, create status draft at deploy | ported | QUALITY-110 | — | +| tag:OPENAPI | health | GET /api/health returns status, version, woo_reachable | gap | — | APP-30 names the path; the body is absent | +| tag:OPENAPI | settings-PUT-reject | PUT /settings cannot write woo_base_url, 422 | duplicate | — | same rule as GC-4 | +| tag:OPENAPI | settings-secrets | secrets are write-only and never echoed | ported | SETTINGS-75 | — | +| tag:OPENAPI | test-woo-shape | test-woo returns ok, detail, taxonomy_anchor, taxonomy_last_sync_at | gap | — | return shape absent | +| tag:OPENAPI | taxonomy-GET | taxonomy GET grouped, with anchor and last_sync_at | ported | SETTINGS-50, SETTINGS-55, SETTINGS-60 | — | +| tag:ARCHITECTURE | s5-server-secrets | Woo creds, inference key, AES key, WOO_BASE_URL are server-side only | ported | SETTINGS-5, SETTINGS-15, SETTINGS-20, SETTINGS-25, SETTINGS-30 | — | +| tag:ARCHITECTURE | s5-client-prefs | sheet and sensitivity preferences persist in localStorage | ported | CAPTURE-145 | — | +| tag:ARCHITECTURE | s7-taxonomy | last_sync_at, anchor, and sync triggers | ported | SETTINGS-50, SETTINGS-55, SETTINGS-60 | — | +| tag:ARCHITECTURE | s3-woo-only-fastapi | Woo is called only by FastAPI | ported | APP-25 | — | +| tag:DATA-MODEL | settings-table | settings.value_enc is AES-GCM; WOO_BASE_URL is not a settings row | ported | SETTINGS-5, SETTINGS-20, SETTINGS-30 | — | +| tag:DEPLOYMENT | env-table | env carries WOO_BASE_URL, Woo credentials, inference key | ported | OPS-10, SETTINGS-5, SETTINGS-30 | — | +| tag:DEPLOYMENT | rotate-creds | rotate Woo credentials then test-woo | duplicate | SETTINGS-40 | rotation is procedure; the verification is test-woo | +| tag:DEPLOYMENT | aes-rekey | SLAB_AES_KEY change requires re-encrypting settings | deferred | — | deferred ruling, audit section 18 item 7 | +| prd | decision-15 | Woo credentials are server-side only, never in the browser | ported | SETTINGS-15, SETTINGS-20 | — | +| prd | decision-16 | inference endpoint is configurable: base URL, key, model | ported | SETTINGS-30 | — | +| prd | decision-17 | test connection verifies text and vision support | gap | — | SETTINGS-45 covers the inference probe; the Woo side of test connection is only SETTINGS-40 | +| prd | no-app-auth | no app-level auth; HTTPS plus trusted network | gap | — | no row states the absence of app-level auth | +| prd | FR41 | Settings UI for inference, Woo creds, pricing, brand/GEO, prompts | ported | SETTINGS-15, SETTINGS-20, SETTINGS-30, SETTINGS-35 | prompts are post-M3, audit section 11 | +| cur:CREATE | settings-url | WOO_BASE_URL from env, read-only; change means edit .env and recreate | ported | SETTINGS-5 | — | +| cur:CREATE | test-inference-shape | test-inference returns ok, mode, vision_capable, detail | ported | SETTINGS-45 | — | +| cur:CONTENT-WOO | httpx-timeouts | httpx connect 10s, read 60s | gap | — | not represented | +| issue:#5 | app-password | app password, dedicated user, AES-GCM, browser never holds it, sole caller | ported | SETTINGS-15, SETTINGS-20, APP-25 | — | +| issue:#11 | url-compose | WOO_BASE_URL declared in compose, not user-editable | ported | SETTINGS-5 | — | +| issue:#14 | taxonomy-refresh | taxonomy refresh triggers and anchor | ported | SETTINGS-50, SETTINGS-55, SETTINGS-60 | — | +| issue:#6 | instrument-health | health checks and instrumentation | gap | — | same hole as H-1 and INS-1 | diff --git a/AGENTS.md b/AGENTS.md index 94c2b3d..c46e932 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,8 @@ 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. +**Current state, locked rulings, and next steps: read `.audit/2026-09-18-session-handoff.md` first.** +The Milestone 3 text below is the pre-reconciliation state and is not authoritative until Phase 2 completes. The "Agent context" and "Milestone 3" sections below describe paused intent, not the current task. ## Structure contract (read before creating any doc) diff --git a/docs/README.md b/docs/README.md index ccc7916..e405cfe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,9 @@ # Docs map (branch `m3-ds`) +> **Phase 2 in progress (2026-09-18).** Requirements are being reconciled on `docs/clean-main`. +> Current state, rulings, and next steps: `.audit/2026-09-18-session-handoff.md`. The Milestone 3 +> text in this file is the pre-reconciliation state, not the current task. + Read this file first. It tells you what SlabUploader is for, what already ships, and what Milestone 3 must build. **Who this is for.** Mill owner (Ty), humans reviewing scope, and agents implementing on this branch. diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 812c8d4..48a4a05 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -22,6 +22,7 @@ Sources reconciled 2026-09-18 from the current branch docs plus the `pre-simplif | CAPTURE-30 | The calibration photo drives mask, length axis, measure, and crop; other batch photos reuse the approved sheet, sensitivity, and crop rules. | shipped | — | | CAPTURE-35 | 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-40 | Mask controls are exactly Sheet (auto, green, black) and Sensitivity (0 to 100, step 1); Edge offset and Feather are removed. | shipped | — | +| CAPTURE-41 | Sensitivity 0 to 100 maps to green-sheet chroma tightness and black-sheet brightness cutoff in the worker; both widen as sensitivity rises. | shipped | frontend/src/mask/sheetKeep.test.ts | | CAPTURE-45 | 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-50 | 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-55 | The mask must be a single contiguous slab contour; otherwise the resolution is a retake. | shipped | — | @@ -42,6 +43,7 @@ Sources reconciled 2026-09-18 from the current branch docs plus the `pre-simplif | CAPTURE-130 | 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-135 | The client computes all deterministic math; the server stores the client numbers and never recomputes them as source of truth. | shipped | — | | CAPTURE-140 | 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-141 | The preview is display-only: it fits the device width with a height cap so controls stay visible, and mask, rectangle, crop, scale, sqft, and PNG encode use source photo pixels, never the preview's CSS size. | shipped | frontend/src/capture/fitDisplay.test.ts | | CAPTURE-145 | Capture knob preferences persist in localStorage until reset to default. | shipped | — | | CAPTURE-150 | Mask tuning has no apply step and no server round trip. | shipped | — | | CAPTURE-155 | Failures such as a broken mask or undersized source never silently guess; the resolution is retake. | shipped | — | @@ -65,6 +67,7 @@ Sources reconciled 2026-09-18 from the current branch docs plus the `pre-simplif | METADATA-55 | Call 2 is user-triggered only and produces name, short_description, and description; it never runs automatically. | M3 | — | | METADATA-60 | 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-65 | The model never invents or alters a dimension; every number it prints comes from the fact block. | M3 | — | +| METADATA-66 | Every content generation, from a deterministic template or from the model, is logged with its inputs, the model request and response when a model was used, and the final stored content. | M3 | — | | METADATA-70 | With inference off, deterministic templates produce name, short_description, and description so create never lacks prose. | M3 | — | | METADATA-75 | Inference never blocks create and never auto-publishes. | M3 | — | | METADATA-80 | The fallback title, used only when Call 2 is not run (inference disabled or the text endpoint missing or down), uses the primary figure term name as the character, with a hyphen separator. | M3 | — | @@ -95,6 +98,7 @@ Sources reconciled 2026-09-18 from the current branch docs plus the `pre-simplif | PUBLISH-20 | A stale value on submit is a field-level error that shows the current options and never a raw code. | M3 | — | | PUBLISH-25 | 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-30 | On failure the submission attempt is marked failed, photos are retained, sync_log is written, and a plain message is shown on the affected field or action; retry works from the same screen. | M3 | — | +| PUBLISH-31 | If image uploads succeed but the product POST fails, the orphaned WooCommerce media is left in place, the failure is logged, and a manual cleanup note is surfaced; this version does not auto-delete it. | M3 | — | | PUBLISH-35 | The owner never sees raw status codes, stack traces, or internal error codes. | M3 | — | | PUBLISH-36 | Every publish failure is classified for the owner as recoverable (he can act) or unrecoverable (outside his control), and the plain message states which it is and the next step. | M3 | — | | PUBLISH-37 | A recoverable failure points at the field to fix, or offers Retry while the draft and photos are retained. | M3 | — | @@ -118,6 +122,7 @@ Sources reconciled 2026-09-18 from the current branch docs plus the `pre-simplif | SETTINGS-40 | test-woo verifies the connection and always resyncs taxonomy on success. | M3 | — | | SETTINGS-45 | 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-50 | 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-51 | Taxonomy sync keeps only the store's figure and feature product-tag families; all other store product tags are ignored so non-wood product metadata does not enter the workflow. | M3 | — | | SETTINGS-55 | Every successful sync updates taxonomy_last_sync_at; taxonomy_anchor bumps only when the stored-subset hash changes, including deletions. | M3 | — | | SETTINGS-60 | 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-65 | woo_create_status defaults to draft. | M3 | — | diff --git a/tools/check_coverage.py b/tools/check_coverage.py new file mode 100644 index 0000000..ec78397 --- /dev/null +++ b/tools/check_coverage.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Enforce the coverage ledger described in .audit/coverage/README.md. + +Run from the repo root: + + python tools/check_coverage.py # forward checks; report gaps + python tools/check_coverage.py --strict # also require full reverse coverage + python tools/check_coverage.py --ledger-dir + +Forward checks always run. `--strict` adds the reverse rule and fails on any `gap`. +Exit 0 = clean for the selected mode. Exit 1 = problems. +""" +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +REQUIREMENTS = ROOT / "docs" / "REQUIREMENTS.md" +DEFAULT_LEDGER = ROOT / ".audit" / "coverage" + +REQUIREMENT_ROW = re.compile( + r"^\|\s*([A-Z]+)-(\d+)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*$" +) +SEPARATOR = re.compile(r"^-+$") + +DISPOSITIONS = {"ported", "duplicate", "new", "superseded", "deferred", "dropped", "gap"} +NEEDS_ID = {"ported", "new"} +FORBIDS_ID = {"dropped", "gap"} +PLACEHOLDER = {"", "\u2014", "-"} + + +def defined_ids() -> set[str]: + ids: set[str] = set() + if not REQUIREMENTS.exists(): + return ids + for line in REQUIREMENTS.read_text(encoding="utf-8").splitlines(): + match = REQUIREMENT_ROW.match(line) + if match: + ids.add(f"{match.group(1)}-{match.group(2)}") + return ids + + +def cells_of(line: str) -> list[str] | None: + """Return the six cells of a ledger data row, or None for non-rows.""" + stripped = line.strip() + if not stripped.startswith("|"): + return None + cells = [cell.strip() for cell in stripped.split("|")][1:-1] + if not cells: + return None + if cells[0].lower() == "source": + return None + if all(SEPARATOR.match(cell) for cell in cells if cell): + return None + return cells + + +def parse_ids(field: str) -> list[str]: + parts = [part.strip() for part in field.split(",")] + return [part for part in parts if part not in PLACEHOLDER] + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check the coverage ledger.") + parser.add_argument("--strict", action="store_true", help="require full reverse coverage") + parser.add_argument("--ledger-dir", type=Path, default=DEFAULT_LEDGER) + args = parser.parse_args() + + ids = defined_ids() + errors: list[str] = [] + gaps: list[str] = [] + claimed: set[str] = set() + seen_atoms: dict[tuple[str, str], str] = {} + counts: dict[str, int] = {} + + ledger_dir: Path = args.ledger_dir + if not ledger_dir.exists(): + print(f"FAIL check_coverage: ledger directory missing: {ledger_dir}") + return 1 + + files = [path for path in sorted(ledger_dir.glob("*.md")) if path.name != "README.md"] + if not files: + print(f"FAIL check_coverage: no ledger files in {ledger_dir}") + return 1 + + for path in files: + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + cells = cells_of(line) + if cells is None: + continue + loc = f"{path.name}:{number}" + if len(cells) != 6: + errors.append(f"{loc} expected 6 columns, got {len(cells)}") + continue + source, atom, requirement, disposition, poer, rationale = cells + if not source or not atom or not requirement: + errors.append(f"{loc} empty Source, Atom, or Requirement") + continue + if disposition not in DISPOSITIONS: + errors.append(f"{loc} unknown disposition '{disposition}'") + continue + + counts[disposition] = counts.get(disposition, 0) + 1 + key = (source, atom) + if key in seen_atoms: + errors.append(f"{loc} duplicate atom {source} {atom} (first at {seen_atoms[key]})") + else: + seen_atoms[key] = loc + + cited = parse_ids(poer) + if disposition in NEEDS_ID and not cited: + errors.append(f"{loc} {source} {atom}: {disposition} needs a PoR ID") + if disposition in FORBIDS_ID and cited: + errors.append(f"{loc} {source} {atom}: {disposition} must not cite a PoR ID") + if disposition != "ported" and rationale in PLACEHOLDER: + errors.append(f"{loc} {source} {atom}: {disposition} needs a rationale") + for rid in cited: + if rid not in ids: + errors.append(f"{loc} {source} {atom}: unknown PoR ID {rid}") + else: + claimed.add(rid) + + if disposition == "gap": + gaps.append(f"{source} {atom}: {rationale}") + + if args.strict: + for rid in sorted(ids): + if rid not in claimed: + errors.append(f"unclaimed requirement ID (no source atom and not marked new): {rid}") + for gap in gaps: + errors.append(f"open gap: {gap}") + + total_rows = sum(counts.values()) + summary = ", ".join(f"{key}={counts[key]}" for key in sorted(counts)) + claimed_pct = (len(claimed) / len(ids) * 100) if ids else 0.0 + + if errors: + print(f"FAIL check_coverage: {len(errors)} problem(s)") + for error in errors: + print(f" - {error}") + return 1 + + print( + f"PASS check_coverage: {total_rows} atoms ({summary}); " + f"claimed {len(claimed)}/{len(ids)} IDs ({claimed_pct:.0f}%); " + f"{len(gaps)} gap(s)" + ) + for gap in gaps: + print(f" gap: {gap}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())