docs(phase2): coverage gate, migration TODOs, session handoff

- add tools/check_coverage.py and .audit/coverage ledger (schema + settings pilot)
- add coverage gap worklist and source-doc migration TODOs
- add session handoff; redirect stale resume points in AGENTS.md, docs/README.md, reconciliation record
- add rows: CAPTURE-41, CAPTURE-141, METADATA-66, PUBLISH-31, SETTINGS-51
This commit is contained in:
2026-09-18 12:54:08 -07:00
parent 1603467747
commit 84d7681c6f
10 changed files with 495 additions and 9 deletions
+68
View File
@@ -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.
+48
View File
@@ -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.
@@ -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/<FILE>.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
+102
View File
@@ -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`.
+57
View File
@@ -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 <path> # for negative tests
```
+47
View File
@@ -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 |
+2 -1
View File
@@ -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)
+4
View File
@@ -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.
+5
View File
@@ -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 | — |
+157
View File
@@ -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 <path>
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())