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
This commit is contained in:
Ty
2026-09-20 23:36:31 -07:00
parent 41401a7266
commit 693719e85f
8 changed files with 329 additions and 170 deletions
+13
View File
@@ -39,6 +39,8 @@ export interface Slab {
short_description: string | null;
description: string | null;
content_source: string | null;
// #89: set true once the owner has locked the taxonomy (deterministic copy built).
metadata_locked: boolean;
species_confidence: number | null;
figure_confidence: number | null;
grade_confidence: number | null;
@@ -220,6 +222,8 @@ export interface ReviewResult {
price: number | null;
price_suggestion: number | null;
primary_wood_category_id: number | null;
// #89: whether the metadata is locked (deterministic copy built/persisted).
locked: boolean;
}
export interface ReviewInput {
@@ -231,6 +235,8 @@ export interface ReviewInput {
description?: string;
price_source?: string;
content_source?: string;
// #89: 'lock' triggers the explicit metadata-lock + deterministic copy build.
action?: 'lock';
}
export function reviewSlab(slabId: string, payload: ReviewInput = {}): Promise<ReviewResult> {
@@ -241,6 +247,13 @@ export function reviewSlab(slabId: string, payload: ReviewInput = {}): Promise<R
}).then((r) => parse<ReviewResult>(r));
}
// #89: explicit lock of the confirmed taxonomy. The server builds and persists
// the deterministic title/short_description from the locked set (owner prose
// already on the slab wins). Distinct from reviewSlab() which only reports.
export function lockSlab(slabId: string, payload: ReviewInput = {}): Promise<ReviewResult> {
return reviewSlab(slabId, { ...payload, action: 'lock' });
}
// ---------------------------------------------------------------------------
// Publish (PUBLISH-*)
// ---------------------------------------------------------------------------