Commit Graph

56 Commits

Author SHA1 Message Date
Ty 693719e85f fix(review): #89 explicit metadata lock — deterministic copy on lock, not on review
The Review screen now has an explicit 'Lock & Build Title' step. Until the
owner locks, title/short_description stay empty (no 'ready' copy from an
unlocked AI draft). On lock the server validates the taxonomy, sets
metadata_locked=1, builds + persists the deterministic title/short_description
(owner prose wins), and returns the ready state.

- schema.sql: add metadata_locked INTEGER DEFAULT 0 to slabs
- db/__init__.py: bump _SCHEMA_SENTINELS for metadata_locked
- slabs.py: expose metadata_locked in GET; reject it in PUT (server-managed)
- review.py: POST /{id}/review action=lock path; regular review reports
  stored (empty) copy until locked
- test_review.py: 8 tests covering lock gate, owner-prose-wins, stale taxonomy
- frontend/api/slabs.ts: Slab.metadata_locked, ReviewResult.locked,
  ReviewInput.action, lockSlab() helper
- Review.svelte: lockMetadata() with correct commit-token ordering (bump AFTER
  inner commit so the inner PUT isn't superseded), Lock button, locked banner
- e2e/m3-create.spec.ts: pare to single golden path (capture→review→lock→
  ready→publish→done); mock respects optimistic concurrency (client_rev) so
  stale PUTs 409 like the real backend
2026-09-20 23:36:31 -07:00
Ty 7287a55707 fix(inference): #86 tighten Call 1 prompt and closed lists
Ship locked expert Call 1 seed: order cats before edge, species=best
confidence, mill-count edge rules, Wild for cookies/burl, multi figure,
feature_tag_ids only (no free-text bags, no grade). LISTS keys match the
seed; feat- prefix stripped for the model. Gate and review UI apply
feature_tag_ids. Seed auto-activates on deploy when still seed-authored.
2026-09-20 22:54:36 -07:00
Ty cec03c5bd3 fix(m3): AI-assist scope + confidence gate + grouped wood categories
- inference: _taxonomy_lists hands the model the synced species leaves + wood
  leaves + case-insensitive attribute terms (was furniture roots / empty)
- inference: CALL1_SEED pins exact flat schema + *_confidence keys, drops grade
- inference: _gate maps field->confidence key correctly (was species_id_confidence),
  reads nested shape as safety net, omits grade (METADATA-40)
- review.svelte: figure gets AI assist, grade dropped from AI (owner judgment)
- MultiSelect: grouped mode (parents non-selectable headers, selectable leaves)
- METADATA-40 updated: grade not suggested by Call 1
2026-09-20 21:47:36 -07:00
Ty e36979617b fix(m3): uuid secure-context fallback + middle-tier SKU business rules
- frontend: uuid() helper (crypto.randomUUID -> getRandomValues) for plain-HTTP LAN UAT
- slabs.py: empty SKU at create -> NULL (no-SKU-yet is transient, filled in Review)
- review.py: ready-gate enforces REVIEW-45 (SKU 3-24 alnum+hyphen) in the middle tier
- DB CHECK stays as backstop only (referential/structural), per layering principle
2026-09-20 21:15:29 -07:00
Ty 1133201f30 M3 Review→Publish UI (#77): one-column flow, searchable pickers, fig/feat tags, e2e
- Review screen: slug-resolved species/wcat groupers, case-insensitive attrs,
  fig-*/feat-* tag pickers (METADATA-30/40), AI-assist labels + AI bubbles.
- App orchestrator: single writable flow store, capture→review→publish, Resume boot.
- API client (slabs.ts) mirroring live backend routes; Blob name fix.
- Playwright e2e (7 tests, hermetic mock): canvas PNG seed, client-UUID mock,
  SKU blur commit, ready-state reactive fix; expect timeout 4s, 2 workers.
- e2e green 7/7 in 6s; check_requirements + check_coverage PASS.
2026-09-20 14:34:24 -07:00
Ty 18c262b5b4 SETTINGS-35: species $/bdft in Settings + taxonomy parent fix (#76)
Add Settings species pricing UI (children of Species grouper by slug,
never hardcoded ids). Map Woo REST parent→parent_id on taxonomy sync;
upsert categories so slab FKs survive re-sync. Tests: parent map, FK-safe
sync, TV-8 0.96×12.50→12.00, Playwright mock e2e. UAT verified on 5060.
2026-09-20 12:37:52 -07:00
Ty d1e862770d Settings: split Test Connection into WooCommerce and Inference sections 2026-09-20 07:42:37 -07:00
Ty d614123503 Settings: render UAT secrets as plain text inputs (Svelte forbids dynamic type on bound input) 2026-09-20 07:39:33 -07:00
Ty 34ee7eff60 Settings: temporary UAT secret-unmask gate (SLAB_UAT_UNMASK_SECRETS)
Owner-directed, LAN/HTTP dev-test only. When the env flag is set,
GET /api/v1/settings returns decrypted secret values (string) instead of
{configured: bool}; the UI prefills and shows them in clear text. Default
off -> SETTINGS-75 masking and the existing tests stay intact. Revert
before any non-UAT deployment.
2026-09-20 07:38:38 -07:00
Ty e51753b6d0 Fix Settings black-on-black contrast (Gitea #75)
Scope .settings styles to the panel: light #eee text on the dark
#121212 surface for labels, and white inputs/selects/options with dark
text. Both pairs exceed WCAG 4.5:1. Add settings-contrast e2e as the
regression guard for the Settings form plus the sibling dev panels.
2026-09-20 01:10:07 -07:00
Ty 7575471397 test(exit): M3 exit wave — inference-off suite + Woo-mock integration + Compose E2E + UAT walk
- test_exit_inference_off.py (QUALITY-20/120): full create->photo->review->publish
  happy path with inference OFF, asserts zero inference_log rows and no raw codes/
  tracebacks reach the owner; not-ready publish 422 with plain field-level message.
- test_integration_woo_mock.py (QUALITY-35): Woo mock covering success, error,
  duplicate SKU, stale field (no raw code), and taxonomy anchor bump on change.
- m3-create.spec.ts (QUALITY-50): Playwright E2E — all M3 dev screens render,
  Review/Publish controls load, Generate control visible only when enabled+reachable.
- deploy/uat_walk.sh (QUALITY-110): ready-to-run human walk for the real SLAB-UAT-*
  draft (inference off) against the live store — NOT a mock.

Verified: pytest 61 passed; frontend unit 88 passed; e2e 4 passed; gates exit 0.
2026-09-19 17:12:14 -07:00
Ty 8a8a89ed3e feat(inference): S7 Call 1/Call 2 + prompt revision REST + test-inference + Generate text UI
- backend/inference.py: prompt seed (rev 0) + append-only revision REST
  (POST /prompts/{call}/revisions, /activate, GET; METADATA-105/106). Call 1 vision
  classification (1024px processed photos + synced taxonomy, 0.7 confidence gate,
  METADATA-35/40/45), Call 2 long description (user-triggered, METADATA-55/60/65),
  inference_log per call (METADATA-66), inference-off server_description (METADATA-70),
  test-inference probe (SETTINGS-45). Stdlib urllib, no new deps.
- frontend: create/inference/GenerateText.svelte (control visible only when content
  enabled + endpoint reachable, METADATA-110; Call 2 + Call 1 buttons) + dev toggle.
- tests: 6 inference tests (seed+revision, call1 gate, call2 stores desc, off->409,
  server desc, test-inference unconfigured).

Verified: pytest 54 passed; gates exit 0; npm build ok.
2026-09-19 17:04:01 -07:00
Ty 5e03f45660 feat(publish): S6 Woo create sequence + UAT draft + dup/stale/partial failure + Publish UI
- backend/publish.py: POST /slabs/{id}/publish. Sequence (PUBLISH-5): validate ready
  set -> SKU lookup (409 on dup, PUBLISH-15) -> refresh taxonomy if >1h (SETTINGS-60)
  -> resolve cats/tags/5 attributes -> upload processed PNGs -> POST /products ->
  finalize (set woo_product_id, purge photos, PUBLISH-25). SLAB-UAT-* always draft
  (PUBLISH-10). Failure: mark failed, retain photos, sync_log, plain recoverable/
  unrecoverable message (PUBLISH-30/36/37/38). Partial (media ok, POST failed):
  orphan media logged + manual cleanup note (PUBLISH-31). Payload: only non-null,
  slug = name+sku, sold_individually true (PUBLISH-55/60).
- frontend: create/publish/Publish.svelte (slab id, publish, result + recoverable/
  orphan tags) + dev toggle.
- tests: 5 publish tests (success+purge, UAT draft, not-ready 422, dup 409,
  partial-media 502). Woo transport mocked.

Verified: pytest 48 passed; gates exit 0; npm build ok.
2026-09-19 17:00:26 -07:00
Ty 732310fdbe feat(review): S5 pricing rules + deterministic title/short desc + server-derived ready + Review UI
- backend/pricing.py: /admin/pricing CRUD (one flat $/bdft per species leaf, SETTINGS-35).
- backend/review.py: POST /slabs/{id}/review -> deterministic title + short description
  (7 leaf patterns, length ft/in split, thickness 1/8-in precision, METADATA-80/85/90),
  price suggestion (bdft x species rate, REVIEW-25), server-derived ready + missing set
  (REVIEW-5).
- frontend: create/review/Review.svelte (slab id, primary category, price override,
  ready state + generated copy) + dev toggle.
- tests: 3 pricing + 4 review tests (title no-dims, ready set, price suggestion,
  knife-blank dims).

Verified: pytest 43 passed; gates exit 0; npm build ok.
2026-09-19 16:52:33 -07:00
Ty 714a227e57 feat(photos): S3 multipart BLOB storage + original/processed GET + 410 after purge + attach UI
- backend/photos.py: POST /slabs/{id}/photos (multipart original+processed ->
  slab_photos BLOBs, re-upload replaces), GET original/processed (image/png, 410
  once purged), GET list. Cites CAPTURE-95/105/130, PUBLISH-40.
- frontend: create/photos/PhotoAttach.svelte (slab id, original/processed files,
  calibration flag, lineup) + dev toggle.
- tests: 5 photos tests (upload+get both variants, 410 after purge, 404 slab,
  reupload replaces, list).

Verified: pytest 36 passed; gates exit 0; npm build ok; live QA upload->200,
get original/processed->200, purge->410.
2026-09-19 16:47:07 -07:00
Ty d83c9d5c21 feat(woo): S4 Woo client + taxonomy sync + test-woo + admin taxonomy
- backend/woo.py: stdlib urllib Woo v3 client. Basic-auth, HTTPS-only, X-WP-TotalPages
  pagination, typed errors (CredentialsMissing/Auth/NotFound/Server/Connection),
  injectable transport for tests, WP core media upload.
- backend/taxonomy.py: sync pulls categories/tags/attributes/terms into cache in one
  commit (rollback keeps prior cache on failure); fig-*/feat-* tag scoping; stable
  taxonomy_anchor hash; get_cached grouped admin view.
- backend/app.py: POST /settings/test-woo (fail-closed, resync on success),
  GET /admin/taxonomy, POST /admin/taxonomy/sync (502 on Woo failure).
- frontend: Settings Test Woo button + result display.
- tests: 8 woo/taxonomy tests (auth header, non-https refusal, pagination, missing
  creds, 401, scoped sync, failure-keeps-cache, anchor stability).

Verified: pytest 31 passed; check_requirements + check_coverage exit 0; npm build ok;
live QA test-woo fail-closed + sync 502.
2026-09-19 16:40:37 -07:00
Ty cc5241f7da feat(slabs): S2 CRUD + optimistic concurrency + soft delete + slab list UI
- backend/slabs.py: create (calibrated, client-derived stored as sent), list (excl
  soft-deleted, most recent), get, update (single compare-and-set on server_rev,
  409 revision_conflict / 409 duplicate_sku / 422 stale_field, status server-derived),
  delete (soft, published rejected). Explicit column allowlists, parameterized SQL,
  JSON array fields, commits.
- backend/app.py: wire slabs router.
- frontend: src/api/slabs.ts client + create/slabs/SlabList.svelte + dev toggle.
- tests: 9 slabs tests (create/dup/list/get/update/rev-conflict/dup-sku/soft-delete/
  published-delete) with full taxonomy seeding.

Verified: pytest 23 passed; check_requirements + check_coverage exit 0; npm build ok.
2026-09-19 16:29:06 -07:00
Ty b3380f3848 feat(settings): S1 AES-GCM crypto + Settings API/UI (masked write-only secrets)
- backend/crypto.py: AES-GCM (cryptography.AESGCM); key=base64(SLAB_AES_KEY)
  must be exactly 32 bytes; value_enc=base64(nonce(12)||ct); DecryptError on tamper
- backend/settings.py: get_settings (secrets->{configured:bool}, woo_base_url env
  read-only, flags default false), update_settings (rejects woo_base_url SETTINGS-85,
  encrypts secrets, partial plain writes)
- backend/app.py: wire GET/PUT /api/v1/settings
- frontend/src/create/settings/Settings.svelte: masked form + partial PUT
- tests: crypto roundtrip/tamper, masked GET, encrypted PUT, reject woo_base_url,
  flags default, decrypt-fail->configured:false (TDD)

Verified live: secrets round-trip configured:true with valid key; woo_base_url write
rejected 400; no secret leak in GET. Gates: check_requirements + check_coverage exit 0;
pytest 13 passed. Deploy+build on ubudual5060 (never Windows).
2026-09-19 15:48:42 -07:00
grokbot_ty 03d3fcf460 fix(photos): preserve tapered tips and dark live edge (#73 #74)
Lower the axial body threshold while retaining fringe rejection, minimize length-only tip inset, and make sheet sensitivity curves testable and conservative at zero.
2026-09-16 15:18:06 +00:00
grokbot_ty 037a4b870a fix(photos): shared EXIF-oriented decode for axis/overlay space (#72)
PhotoStage used new Image() while mask worker and calculate used bare
createImageBitmap; Orientation-8 matched-set JPEGs can disagree across
those paths so the approved pink axis is not the overlay (ux,uy).

One decodeOrientedBitmap helper always requests imageOrientation
from-image (with bare fallback), wired into PhotoStage, mask.worker,
and session calculate/siblings.
2026-09-16 05:08:13 +00:00
grokbot_ty b8e69b5239 test(photos): refresh e2e evidence after tip/label fix (#71) 2026-09-16 04:55:19 +00:00
grokbot_ty 495f179c2f fix(photos): width-aware tips, stroke inset, smaller in-bounds labels (#71)
Root cause: 0.2%/99.8% axial percentiles still included dense fringe past
the visible body; round line caps and scale-tied font overshot edges and
clipped width labels. Use across-width tip stations, inset length/width
ends for stroke/arrowhead, butt caps, inventory-capped font, axis-right
length label, clamp labels to PNG.
2026-09-16 04:54:01 +00:00
grokbot_ty b386ab9670 test(photos): overlay tips on footprint + labels in-bounds (#71)
Failing UAT proofs: length span must exclude fringe whiskers past solid
body; width endpoints inset for stroke/arrowhead; smaller font; labels
clamped inside inventory PNG.
2026-09-16 04:46:48 +00:00
grokbot_ty 2be2e7d615 test(photos): refresh e2e evidence after axis parallel fix (#70) 2026-09-16 04:36:59 +00:00
grokbot_ty 6d182db81b fix(photos): length dim parallel to approved axis (#70) 2026-09-16 04:35:53 +00:00
grokbot_ty dc19cffcc0 test(photos): length/width dims follow approved axis (#70) 2026-09-16 04:35:38 +00:00
grokbot_ty 05447c92fc fix(photos): length dim snaps to wood footprint (#69) 2026-09-16 02:29:05 +00:00
grokbot_ty 415158d4d3 test(photos): length dim snaps to wood tips (#69) 2026-09-16 02:17:45 +00:00
grokbot_ty 9bc35f7edd fix(photos): overlay UAT nits + 90% crop fill (#67,#68)
Length dim already mask tip-to-tip (locked by test). Width labels move
axis-left off the wood via labelAt; font steps down to max(22, scale*1.0).
Inventory/overlay crop FILL 0.8→0.9; CAPTURE/PHOTOS docs match.
2026-09-15 21:46:58 +00:00
grokbot_ty 15895cae0c test(photos): overlay UAT nits + 90% fill expect (#67,#68)
Lock length dim to mask tip-to-tip (not oversized bbox), require width
labelAt on axis-left off the wood, smaller font contract, and 90% crop
fill extent. Red before green.
2026-09-15 21:46:27 +00:00
grokbot_ty 2be400e455 test(e2e): Playwright photos-ready flow + visual evidence (#65)
Drive batch → pick calibration → Mask (Black) → Axis → Measure Calculate →
Photos ready against set1-48x2 fixtures on local vite preview. Capture
screenshots under tests/e2e/artifacts/; commit README + small evidence JPEGs.
2026-09-15 21:38:45 +00:00
grokbot_ty 2c9a50ddf9 feat(photos): Photos-ready lineup after Calculate (#64)
Add buildPhotosReadyLineup (clean cal → overlay → batch inventory PNGs
in capture order, plus empty-mask/floor error slots). MeasurePanel
switches to a Photos ready end screen with Milestone 1 numbers and the
inventory PNG lineup; Back still returns to Length axis.
2026-09-15 21:33:37 +00:00
grokbot_ty 4c3c8b49eb test(photos): Photos-ready lineup order + error slots (#64)
Failing TDD for M2 Photos-ready end screen: pure buildPhotosReadyLineup
orders clean calibration inventory PNG → dimension-overlay copy → other
batch inventory PNGs in capture order (skipping cal null), surfaces
batchApply empty-mask/floor errors, and MeasurePanel must show Photos
ready with Milestone 1 numbers (not source thumbs).
2026-09-15 21:33:00 +00:00
grokbot_ty a22525023a feat(photos): burn axis-oriented dimension overlay PNG (#63)
Add pure station/axis math and canvas burn-in for a second calibration
inventory PNG (results.overlayPng). Width stations start 6 in from the
EXIF-oriented photo bottom along the approved length axis (6, 18, … ≤
L−6); length graphic sits on axis-right outside the slab. Clean
inventoryPng and batchApply[cal]=null stay unchanged.
2026-09-15 21:31:22 +00:00
grokbot_ty c674931f5c test(photos): dimension-overlay PNG stations + results.overlayPng (#63)
Failing TDD for M2 dimension-overlay copy of the calibration inventory PNG:
width stations every 12 in from EXIF bottom (6, 18, … ≤ L−6), axis-right
length graphic, and Calculate storing overlayPng without touching batchApply[cal].
2026-09-15 21:29:59 +00:00
grokbot_ty 3936c575b2 feat(photos): apply frozen mask to batch siblings after Calculate (#62)
After calibration Calculate, reuse approved sheet/sensitivity on non-calibration
photos via the mask worker + inventory compose. Persist per-photo ok/error on
measure.batchApply and the device draft for #64; leave Mask UI unopened and keep
scale/widths/sqft/bdft on the calibration results only.
2026-09-15 21:24:10 +00:00
grokbot_ty 9404d0130c test(photos): batch apply approved mask after Calculate (#62)
Prove frozen calibration mask reuse, per-photo errors, inventory PNG results, calibration-only measures, draft persistence, and no Mask re-prompting for batch siblings.
2026-09-15 21:20:35 +00:00
grokbot_ty 8ff008f85c feat(photos): keep M1 mask/axis/measure on calibration pick (#61)
Document single-source M1 path after pickCalibration: Mask/Axis/Measure/
Calculate use the chosen Source only; batch siblings stay draft-untouched
until #62 apply. Tag encode-fallback inventory Blob as image/png (CAPTURE.md).
2026-09-15 21:14:16 +00:00
grokbot_ty d2ffa4cad2 test(photos): M1 mask/axis/measure on calibration pick (#61)
Proving tests: after pick of index N≠0, Mask→Axis→Measure operate on that
blob only; Calculate yields inventory PNG + M1 numbers; other batch photos
stay in draft.batch untouched; resume through measure restores calibration
source. FakeWorker drives the path in Node. No production change yet (TDD).
2026-09-15 21:14:00 +00:00
grokbot_ty d18b96c10b feat(photos): pick calibration photo before Mask (#60)
Add pickCalibration state/UI over batch thumbs. Continue from batch opens
pick (including single-photo confirm). User pick enters M1 Mask on that
Source only; draft stores calibrationIndex. Removes first-photo proxy.
2026-09-15 21:11:07 +00:00
grokbot_ty 4fa8126471 test(photos): pick calibration before Mask (#60)
Failing session/UI tests for M2-3: Continue → pickCalibration (not
first-photo Mask proxy), user pick enters Mask on that Source, draft
remembers calibrationIndex, Resume restores. No production change yet (TDD).
2026-09-15 21:10:14 +00:00
grokbot_ty ff4bb0f31e feat(photos): multi-photo batch + device draft (#59)
Replace single Add photo with Batch: accumulate 1–N photos, Continue
when ≥1 (M1 bridge: mask on first), persist batch blobs in the device
draft, Resume Continue restores. Keeps Mask/Axis/Measure intact.
2026-09-15 21:08:44 +00:00
grokbot_ty e919fe9a35 test(photos): batch add + draft restore before green (#59)
Failing session/UI tests for M2-2: multi-photo batch state, Continue
when ≥1, device-draft persistence of batch blobs, Resume Continue
restores. No production change yet (TDD).
2026-09-15 21:07:17 +00:00
grokbot_ty 2eb825c84e test(fixtures): add Ty matched slab photo sets for M2
Set1 L=48in thickness=2in (2 faces); set2 L=31in thickness=2.5in
(2 faces). Black-sheet samples for Playwright and calibration TDD.
2026-09-15 20:54:51 +00:00
grokbot_ty eaedcb53dd fix(capture): drop first and last 6in from width samples
sampleWidths used floor(L/6)+1 stations from t=0, so tapered tips
dominated min/max. Sample only interior stations at 6, 12, … ≤ L−6.
When L≤12 there is no interior span: every6In stays empty and
min/max/avg stay 0. CAPTURE.md Widths bullet matches.

sqft, bdft, scale, and inventory PNG are unchanged.
2026-09-15 03:20:28 +00:00
grokbot_ty 11c8124674 test(capture): expect tip-trimmed every-6in width stations
Ty UAT: width min/max must describe bulk, not tips. Pin L=48 tapered
case (7 interior stations, no tip endpoints), uniform mid-board count,
and L≤12 empty/zero fallback before the sampleWidths change lands.
2026-09-15 03:20:17 +00:00
grokbot_ty 44d428b6f4 fix(capture): lower inventory PNG floor to 800px
Product call for the WooCommerce redo. Calculate accepts
sources whose post-crop shorter side is at least 800px.
Warn copy and CAPTURE.md match. No upscale still holds.
2026-09-14 23:40:02 +00:00
grokbot_ty f20d84c543 test(capture): expect 800px inventory PNG floor
Ty UAT rejected phone photos under the 1600px bar.
Lock the mid-band case (shorter side 1000) and floor
geometry at 800×1067 / 0.8×800=640 before the code change.
2026-09-14 23:40:01 +00:00
grokbot_ty fbcd05169f fix(capture): apply approved mask alpha in inventory PNG
Why
CAPTURE.md Crop keeps slab pixels and makes removed pixels transparent.
encodeInventoryPng only drawImage-cropped the AABB from the raw photo.

Scope
composeInventoryFrame samples the source through approvedMask.bitset.
Non-slab pixels stay alpha 0. Drop the 8% MARGIN; fill 80% of mask
extents in a 3:4 frame. Floor rules unchanged.

Verification
cd frontend && npm test
12 pass, including alpha contract (no sheet green in opaque pixels).
2026-09-14 19:11:52 +00:00
grokbot_ty b3a9fdf330 test(capture): require mask alpha in inventory PNG compose
Why
Calculate showed accurate numbers but the inventory PNG kept the sheet
background. Measure already uses approvedMask.bitset; encode did not.

Scope
Add composeInventoryFrame and inventory.test.ts alpha/crop proofs.
Wire encodeInventoryPng({ bitmap, mask, source }) at calculate().
This commit still paints the slabBounds AABB without the bitset gate so
the alpha contract fails until the follow-up fix.

Verification
node --import tsx --test src/png/inventory.test.ts
alpha contract fails (sheet green leaks); 3:4 / 80% / floor pass.
2026-09-14 19:11:52 +00:00