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:
@@ -64,6 +64,11 @@ def _encode(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
# #89: server-managed — only the explicit review lock action may set it. A plain
|
||||
# PUT must not flip the lock (the deterministic copy is built inside that action).
|
||||
CLIENT_FORBIDDEN_FIELDS = {"metadata_locked"}
|
||||
|
||||
|
||||
def _row_to_slab(row) -> Dict[str, Any]:
|
||||
d = dict(row)
|
||||
for f in JSON_ARRAY_FIELDS:
|
||||
@@ -72,6 +77,8 @@ def _row_to_slab(row) -> Dict[str, Any]:
|
||||
d[f] = json.loads(d[f])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
# #89: expose the lock flag to the client (0/None → False, 1 → True).
|
||||
d["metadata_locked"] = bool(d.get("metadata_locked"))
|
||||
return d
|
||||
|
||||
|
||||
@@ -155,6 +162,11 @@ async def get_slab(id: str) -> Dict[str, Any]:
|
||||
async def update_slab(id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if "client_rev" not in payload:
|
||||
return _err("validation", "client_rev required", status=400)
|
||||
# #89: the metadata lock is server-managed via the explicit review lock
|
||||
# action; a plain PUT may not flip it.
|
||||
for field in CLIENT_FORBIDDEN_FIELDS:
|
||||
if field in payload:
|
||||
return _err("validation", f"{field} is set by the review lock action, not by update", status=400)
|
||||
conn = get_connection()
|
||||
current = conn.execute("SELECT * FROM slabs WHERE id=? AND deleted_at IS NULL", (id,)).fetchone()
|
||||
if current is None:
|
||||
|
||||
Reference in New Issue
Block a user