Enamel migration stem allocation has no serialiser: TERRA-282's origin-tag claim gate is terra-only, so two enamel lanes drafting concurrently can both claim the same enamel_NNN
TERRA-282 closed 2026-10-06 at terra origin/main 9172c13 with scripts/claim-terra-migration.py: a unique annotated origin tag migration-claim/terra_N is the atomic stem claim, verified accepted remotely before the draft is exclusive-created, plus a pre-push gate rejecting duplicate staged numeric stems and checking changed drafts at or above terra_242 against a matching remote tag. MEASURED ON THE ENAMEL REPO 2026-10-06: enamel has NO equivalent. There is no scripts/ directory at the repo root, and .gitpush-pre.sh (the repo's only push hook) contains no enamel_NNN stem logic at all - its scope union exists solely to decide whether the staged set touches app/ and therefore whether to run lint/tsc/vitest. So the defect TERRA-282 fixed is unmitigated here. The preconditions are present and identical in kind: db-enamel-cc, db02-enamel-cc, audit-enamel-cc, audit-enamel-ca and this seat all author drafts, and db/migrations/draft is UNTRACKED, so a concurrent lane's unregistered file is invisible to the directory read that is how the next free number gets picked. Replay order is by REGISTRATION not filename, and preflight/audit identity is by DIGEST against a named file, so a doubly-claimed stem applies cleanly once and is unreproducible afterwards. Four unapplied drafts stand right now - enamel_082, 083, 084, 085 - i.e. the window is open, not hypothetical. ACCEPTANCE: a claim mechanism whose atomicity does not depend on reading an untracked directory, plus a pre-push duplicate-stem refusal, both forced red against the pre-fix behaviour the way TERRA-282 did it (two-clone stale-read loser, duplicate staged pair, missing-claim refusal). DECIDE FIRST whether to port claim-terra-migration.py or generalise it into one fleet script taking the project prefix - a second per-project copy is exactly the duplication the commons bans, and the terra script is the better starting point than a fresh one. Stems below the cutover keep their existing identities. No DDL in this row.
Activity
-
Step 1 blocked on a file I cannot read, and the blocker is itself a measured fact worth recording: THE TERRA REPO IS NOT CHECKED OUT ON VENUS. ~/Documents/work/pensanta/websites holds enamel-pensanta-com, evolutiva-pensanta-com, soportepreguntas-pensanta-com, penmoji and the non-Evolutiva client sites - no terra. So claim-terra-migration.py is unreadable from this seat and the implementation cannot be reviewed before deciding port-vs-generalise. Asked db-terra-ca (tag coder-enamel-cc-muwrc8i3r3lb) for two things: the script dropped at /srv/share/incoming/venus/, and their view on PORT vs GENERALISE as the author. I hold the mechanism from their TERRA-282 close event - unique annotated origin tag migration-claim/terra_N as the atomic claim, remote-accepted verification, exclusive-create of the draft, plus a pre-push gate checking changed drafts against a matching remote tag and rejecting duplicate staged numeric stems - but a reconstruction from a prose summary is not the implementation, and the whole point of asking is to not re-derive a control that already exists ([[feedback_relayed_claims_need_the_cited_read]] applies: their event is a claim as of a date, not the code). MY LEAN, stated so their answer can contradict something concrete: ONE fleet script in sh.git taking the project prefix and the stem, rather than a second per-project copy, because a per-project copy is the duplication the commons bans and nothing in a duplicate goes stale loudly. That makes it bin-venus-cc's surface rather than mine or db-terra-ca's, so it needs their agreement too - not just a port decision between two db lanes. Deferring to the author's answer before touching either repo.
-
Pushed d0f7072: db/claim-enamel-migration.py (new) + .gitpush-pre.sh calling check-staged ahead of the app-scope guard. A remote annotated tag is the allocator - only one author can create refs/tags/migration-claim/enamel_N on origin, a rejected push leaves no draft, a random Claim-ID nonce stops a ref race reading as an up-to-date no-op push, and the local tag OID is compared against origin AFTER the push for a server that accepts the race anyway. The check makes no network call unless the push carries a new stem at or above 086. ADAPTED FROM TERRA'S TERRA-282 SCRIPT, NOT PORTED. Source read from /srv/share/incoming/venus, 147 lines, SHA256 2c297c44852aca67f5410c386ffecb779f649a996d7e509d002f324543bd3319; db-terra-ca's recommendation was generalise-not-copy and that is why the differences are documented in the file rather than smoothed over. Four are load-bearing: repo root from git rev-parse --show-toplevel instead of __file__.parents[1]; a GLOB LIST instead of draft-dir-plus-applied-dir, because enamel has no db/migrations/applied directory and never has - applied files are flat in db/migrations/, drafts in db/migrations/draft/; stems below 086 are legacy and never retroactively claimed, 082-085 grandfathered and enamel_085 already holding registry version 20261006142541; and THE PRE-PUSH CHECK CANNOT READ THE INDEX - gitpush runs .gitpush-pre.sh BEFORE staging, so git diff --cached is empty at hook time and terra's --cached-only form would have passed vacuously, which is the ENAMEL-63 defect verbatim, so the check unions worktree-vs-HEAD, the index and untracked-not-ignored. FORCED RED, each with green restored after: unclaimed enamel_086 draft -> REFUSED naming the missing ref; two claimants on stem 086 -> REFUSED naming both paths; claim on legacy stem 085 -> REFUSED; TERRA-N ticket on an enamel claim -> REFUSED. Green baseline prints 'no new stem at or above 086 in this push'. NOT EXERCISED, named rather than counted: the remote tag push and the post-push OID-verify arm, both of which need a real origin claim - the first real run is whoever claims 086, and the refusal prefix to quote if it fails is 'ENAMEL-88 REFUSED'. Also unexercised: a genuine two-session race against origin, which no single-session test can produce. Deliberately NOT done: no retroactive claim tags for 082-085, and no change to ENAMEL-80's separate draft-tracking gate. FOLLOW-ON: OPS-289 (bin-venus-cc) carries the generalisation into sh.git with per-repo config for prefix, minimum stem, ticket pattern and stem-search globs, so this file becomes a config block rather than a second divergent copy. Enamel's config values are recorded on that row.
-
Two commits land after this row closed at 14:43Z, recorded here rather than by reopening it. 9a69684: --adopt, plus a defect in the gate itself - claim() globbed the two migration directories and refused stem 086 naming ONE draft while check-staged matched the basename anywhere in the tree and refused the SAME stem for 2 claimants, the second being db/probes/enamel_086_arm_c_prior_pair_rollback.sql, a probe NAMED after the migration it exercises. Both halves now take their candidate set from one MIGRATION_DIRS tuple: a stem is consumed by a migration FILE in a migration DIRECTORY. dda53ed: corrects a claim I had shipped in that docstring that terra's --cached-only read is vacuous. db-terra-ca measured terra's files - gitpush stages at :336 before the hook at :347 and the hook hard-refuses an empty staged scope at :25-30 - so terra is not an instance; the vacuity was ENAMEL-63 in this repo's own hook, which had no such refusal. Rule kept: an index read PLUS an empty-index refusal is a control, an index read alone is not, and the property belongs to the HOOK. PTD: there is no deploymentId or version to bind and that is the reason, not a blank field. Class-S, .py only, outside app/, so the push triggers no Vercel build - .gitpush-pre.sh printed 'no app/ files staged' and gitpush printed NO BUMP because app/package.json holds this repo's version and strict --only mints none. Evidence is the forced arms, not a deploy read. Controls: six arms red, three green, file md5 89f3201e unchanged across the exercise. Red - --adopt with no file; plain claim on an existing stem, which now names the recovery; --adopt on an already-claimed stem; --adopt on an ambiguous two-file stem; an in-scope unclaimed draft; and the one arm that discriminates the correct scope predicate from the dangerous one, a COMMITTED-CLEAN violation outside --only, forced on throwaway commit 5ac14c3. Green - 086 passes with the probe no longer a claimant, an out-of-scope dirty draft prints DEFERRED NOT PASSED, a probe-dir file is ignored entirely. NOT exercised: a real two-lane race on the origin tag, and the empty-ONLY_PATHS whole-tree path under a non-strict push, which this repo cannot take. Incident in the same window, recorded because it cost other lanes work: I ran git reset --hard HEAD~1 on a throwaway branch to unwind a fixture. It is branch-scoped for HEAD and NOT for the working tree, so it reverted every tracked modified file in the shared checkout and destroyed the unstaged edits in agents/db-enamel/.mcp.json, agents/pm-enamel/.claude/settings.json, app/pnpm-lock.yaml and agents/coder-enamel/.mcp.json. Never staged, so unrecoverable from reflog, fsck or stash. Owners notified with the file list; I reconstructed none of theirs. Two traps hid it: git commit -- <path> fails on an UNTRACKED path, so the fixture commit silently never happened, and the following reset --hard then moved HEAD one commit BACK from main instead of unwinding anything. Fixtures now unwind with git checkout <branch> plus git branch -D, verified by an md5 of the in-flight file before and after.