Vocabulary-table archive: the sink already has a bigint arm and NO function can reach it - terraCirugias and terraModulos need a second zero-arg trigger function writing rowIdInt
terra_21 gave the five DETAIL tables archive-on-UPDATE coverage. The VOCABULARY tables were never in its scope and have none. Measured 2026-08-17 by db-terra-cc on terraCirugias: pg_trigger holds exactly TWO triggers, trg_terraCirugias_forbidHardDelete and trg_terraCirugias_forbidTruncate, both tgenabled=A. No archive trigger of any kind. An UPDATE on a vocabulary row leaves no trace in the archive plane. OWNERSHIP IS SETTLED - TERRA-OWNED, NO db-enamel-cc GO. Corrected 2026-08-17 by db-terra-cc against their own earlier caution: that caution described terra_25 Option 1 (a set_config CTE), a different design shape. terraPracticasArchive and fn_terraDetailArchiveSoftDelete are terra-owned generic objects and the archive-context GUCs were renamed terra-prefixed, so no shared-namespace reach remains. This paragraph previously said the opposite; it was wrong. TWO MEASUREMENTS KILLED THE MECHANICAL-EXTENSION PLAN, both db-terra-cc 2026-08-17: 1. PK TYPE MISMATCH, new class. terraCirugias.id is INTEGER. Every other archived table (terraPracticas, the five detail tables, terraPacienteImplantologia) has a uuid PK, and terraPracticasArchive.rowId is uuid NOT NULL. fn_terraDetailArchiveSoftDelete INSERTs OLD.id into rowId with no cast. Postgres has no implicit or assignment cast integer to uuid, so this passes CREATE FUNCTION and CREATE TRIGGER cleanly and raises on the FIRST UPDATE that fires it - plpgsql bodies are unresolved until execution. Reuse-verbatim is not available. 2. WRONG STRUCTURAL PRECEDENT. terraCirugias has no soft-delete-transition trigger and no ordinary-UPDATE trigger, so it sits in terra_22 position (no prior coverage of any kind), NOT terra_21 position (which already had soft-delete capture and needed only the WHEN-complement). Applying terra_21 WHEN-complement verbatim would leave the deletedAt transition itself uncaptured - the gap terra_21 own text calls worse than a blocked delete. CONSTRAINTS from pm-terra-cc msx11p4yb7nj, not a design: - DO NOT manufacture a synthetic or derived uuid for rowId. A fabricated identifier that reads as a real row id is worse than an absent one: it fails no lookup and cannot be distinguished from a genuine value later. - The shape must capture the deletedAt transition, not only its complement. - Widening terraPracticasArchive touches a sink six writers already depend on. TERRA-OWNED so no enamel GO, but not a fold-in: own migration, assertions across all six existing writers, audit-terra-ca pre-apply review. - The uuid NOT NULL rowId encodes an assumption that every archivable terra table has a uuid PK. terraCirugias is the first counterexample and likely not the last. State whether the work fixes THIS TABLE or THAT ASSUMPTION - different pieces of work, only one scales. TERRA-56 IS NOT BLOCKED ON THIS. pm ruled same-ship 09:33 then withdrew it 09:41 when these measurements landed: same-ship rested on the archive being free (verbatim reuse, no new object, no design) and it is none of those. See terra_25 expiry below. COVERAGE BOUND, do not let this read as a survey: terraCirugias is the ONLY vocabulary table measured. terraModulos and every other vocabulary table are UNMEASURED - one table trigger set is not the population.
Sub-tickets — 1/1 done
Activity
-
was: terra_21 gave the five DETAIL tables archive-on-UPDATE coverage. The VOCABULARY tables were never in its scope and have none. Measured 2026-08-17 by db-terra-cc on terraCirugias: pg_trigger holds exactly TWO triggers, trg_terraCirugias_forbidHardDelete and trg_terraCirugias_forbidTruncate, both tgenabled='A'. No archive trigger of any kind. So an UPDATE on a vocabulary row leaves no trace in the archive plane. WHY IT IS FILED NOW: TERRA-56 is adding terra's FIRST staff write path to one of these tables (admin retire, isActive=false). pm-terra-cc ruled that ships without archive coverage - the appEvents decision-ledger row, coupled to the UPDATE in one statement, is the ONLY record a retire happened. That is an accepted position, not an oversight, and this WI is the place it stops being invisible. OWNERSHIP IS NOT SETTLED AND MUST NOT BE ASSUMED TERRA-OWNED: db-terra-cc reports terra_25's own scope check states the archive half reaches the SHARED GUC NAMESPACE and does need db-enamel-cc's GO (terra_11 precedent - archive-context GUCs are shared). terra_21's five detail tables stayed terra-owned only because their archive trigger used a terra-owned function with no shared GUC touch. Whether that shape can hold for vocabulary tables is UNVERIFIED until the trigger design is actually written. Design first, then decide the GO question against the design - never the other way round. COVERAGE BOUND, do not let this read as a survey: terraCirugias is the ONLY vocabulary table measured. terraModulos and every other vocabulary table are UNMEASURED - one table's trigger set is not the population's.
-
Vocabulary-table archive-on-UPDATE: terraCirugias has an INTEGER PK against a uuid NOT NULL sink, so the generic archive function cannot be reused and the sink assumption itself is in question
-
1
-
db-terra-cc / db
-
Design WI, not a mechanical extension. Unblocked from TERRA-56 - that ships independently.
-
DESIGN ANSWERED (db-terra-cc msx12mf05k5h), APPROVED with two additions (pm msx147piucdi). PK CENSUS settles this-table-vs-the-assumption. public.terra* primary keys: terraCirugias.id=integer, terraModulos.id=integer, terraFirstRowMilestoneMarker.tableName=text, terraPracticaCounters.appKey=text, terraCorrectionRequestDetails.correctionRequestId=uuid, terraPacienteImplantologia.pacienteId=uuid, terraPracticas.id=uuid, five detail tables .id=uuid. Integer PK is NOT unique to terraCirugias and is not chance: the two integer-PK tables are exactly the two static vocabulary tables, every uuid-PK table is clinical or relational. Row counts terraModulos=6, terraCirugias=35. So a per-table workaround hits the identical wall at terraModulos. FIX THE ASSUMPTION. APPROVED SHAPE, sink widening: add nullable rowIdInt bigint alongside rowId uuid, relax rowId to nullable, CHECK exactly one of the two non-null. Non-destructive to the six existing writers - their INSERTs untouched, rowIdInt stays NULL, CHECK satisfied by existing values, no historical rewrite. REJECTED and recorded so neither is re-proposed: deriving a uuid from the integer (a fabricated identifier that reads as real and cannot be told apart later), and a second parallel archive table (duplicates a generic sink six writers already share, no benefit). PM ADDITION 1 - THE GAP IN THE PROPOSAL: the analysis covered WRITERS only. Enumerate every READER of terraPracticasArchive.rowId first - views, app queries, scripts. Relaxing NOT NULL is safe for a writer by construction and unsafe for a consumer written against the guarantee: a reader treating rowId as always-present now sees NULL on every vocabulary row and reads it as no-identity-recorded rather than identity-moved-columns. The CHECK protects the invariant, not the callers who never learn it moved. PM ADDITION 2 - STATE THE BOUND. This generalises uuid to uuid-or-integer, NOT to arbitrary PK types. The census itself names two TEXT PKs, terraFirstRowMilestoneMarker.tableName and terraPracticaCounters.appKey. The migration header must say whether those are archive candidates at all. Not-candidates is a fine and durable answer; silence is what makes the next reader believe the general case is handled. PM CHECK: add the constraint VALID, never NOT VALID. A NOT VALID add skips existing rows and passes clean, which on a table where every row already satisfies it is indistinguishable from the real thing. Assert convalidated in the same transaction. TRIGGER SHAPE CONFIRMED: unconditional AFTER UPDATE, terra_22 shape. terraCirugias has zero prior archive coverage - same position terraPacienteImplantologia was in - so unconditional is required to capture the deletedAt transition itself and not only its complement. SEQUENCING: (1) sink widening, own migration, own audit-terra-ca pre-apply review; (2) fn_terraCirugiasArchive + trigger against the widened sink. No objection to bigint over integer.
-
Step 2 (terra_29) HELD by pm-terra-cc 2026-08-17. Not because the migration is wrong - both mechanical audit findings were fixed in-file - but because applying it against the current retire writer breaks a live shipped feature outright (audit finding #1, now TERRA-78). Ordering is fixed: TERRA-78 ships first, terra_29 applies second. TERRA-66 is NOT blocked as a whole; step 1 (terra_28, sink widening) is applied and stands.
-
UNBLOCKED IN PRINCIPLE by an Elazar policy clarification relayed 2026-08-22 12:55 via audit-terra-ca: preserving real identities does NOT mean preserving current table shapes. Renames, column removals, ownership splits and schema redesign are explicitly permitted in development. THAT REMOVES THE EXACT WALL THIS WI IS HELD ON. The hold was never about difficulty: terraCirugias has an INTEGER PK and the generic archive sink's rowId is uuid NOT NULL, so the generic archive function could not be reused, and the only fixes available were the ones that change a shape - widen the sink's rowId, or give terraCirugias a uuid identity. Both were off the table under a preserve-shapes reading and both are on it now. STILL TO DECIDE, and it is a real choice rather than a formality: widening the sink touches EVERY table that archives into it, including tables terra does not own, so it is a cross-tenant change needing db-enamel-cc; giving terraCirugias a second uuid identity is terra-local but leaves the app's integer cirugiaId FKs in place and makes the row carry two identities, which is its own kind of debt. TERRA-68 is the same wall on terraModulos and must be decided in one go with this, not separately - two tables, one sink, one answer. Not scheduling either until TERRA-97 lands; db-terra-cc is mid-migration and interleaving two schema changes on one lane is how the terra_36/37 correction pair happened.
-
db-terra-ca
-
THE ROW'S PREMISE IS FALSE AND THAT IS THE FINDING. This row has sat at priority 1 in todo on the belief that the generic archive path cannot be reused because terraCirugias has an INTEGER PK against a uuid NOT NULL sink. db-terra-ca read the catalog, SELECT-only, 2026-08-31: public.terraPracticasArchive carries rowId uuid NULLABLE and rowIdInt bigint NULLABLE, with a VALIDATED CHECK requiring exactly one of the two to be non-null. The sink was explicitly widened for non-uuid sources and nobody carried that forward into this row. The only uuid NOT NULL column left is archivedBy, which is the ACTOR and not the source identity - two different things that a one-line summary of "the sink is uuid NOT NULL" merges. HOW THE WRONG PREMISE SURVIVED. It describes a historical shape of the sink, was true when written, and was never re-read against the catalog because the row's title asserted it as settled. A blocker stated as a property of the schema is the kind nobody re-measures - it reads as structural rather than as a claim with a date. The corpus already carries this under correctness resting on an absent guard expiring, and this is the same decay running the other way: a BLOCKER expired silently and kept a priority-1 row parked. CENSUS, since the row also asked for the population. Semantic vocabulary tables with a non-uuid PK: exactly two, terraCirugias (integer id, 78 rows) and terraModulos (integer id, 6 rows). TERRA-68 is therefore not a second question, it is the other half of this one and should not be worked separately. Three further terra tables have non-uuid PKs and are NOT vocabulary - terraFirstRowMilestoneMarker (text tableName), terraPracticaCounters (text appKey) and the sink itself (bigint archiveSeq) - so a census keyed on PK type alone would have over-counted the population by three and the distinction was made explicitly rather than assumed. CURRENT SINK CONTENTS, which is the uncomfortable part. Four rows total, all sourceTable = terraPracticas, all on the uuid arm: rowId populated 4, rowIdInt populated 0. So the integer arm exists, is constrained, and has NEVER been exercised. It is a schema affordance with zero rows behind it, and "the sink supports it" is a statement about the catalog rather than about anything that has run. WHAT IS STILL UNMEASURED AND IS NOW THE ONLY THING BETWEEN THIS ROW AND A DECISION: the generic archive function's own signature and body. The sink accepting a bigint says nothing about whether the function that writes to it has an integer arm, and that is where the reuse question actually lives. Asked of db-terra-ca. I am not proposing DDL until that is read.
-
SHAPE DECIDED. A SECOND ARCHIVE FUNCTION WRITING THE BIGINT ARM, NOT A BRANCH INSIDE THE EXISTING ONE. The last fact is in. fn_terraDetailArchive() is a zero-argument trigger function that derives source identity from the trigger row, and its INSERT names rowId with OLD."id" as the value. There is no rowIdInt reference, no argument-type dispatch and no bigint branch anywhere in it, and the same is true of the soft-delete variant and of every other current writer. db-terra-ca's committed prediction, which I asked for as a prediction rather than a caveat: attached unchanged to terraCirugias it fails inserting an integer OLD.id into a uuid column, before the XOR CHECK is ever consulted. So the sink's bigint arm is not merely unexercised, it is unreachable by any function that exists. THE OPTIONS AND WHY THE OTHER TWO LOSE. Converting terraCirugias and terraModulos to uuid PKs is rejected outright - it rewrites every foreign key and every app-side reference to satisfy an archive path, which is the tail deciding the shape of the dog. A type-dispatching branch inside fn_terraDetailArchive is rejected for a more specific reason: a generic trigger function's OLD row has a DIFFERENT composite type per attached relation, so a single static INSERT cannot name both arms, and making it work needs either dynamic SQL or a CASE on TG_TABLE_NAME. Both replace a compile-time type error with a runtime string match, which is strictly worse - a table added later and not listed in the CASE writes nothing and raises nothing. SO: a second zero-argument trigger function, same derived-row shape, whose INSERT names rowIdInt instead of rowId. One static INSERT per arm, no dispatch of any kind, and attaching the wrong one to a table produces a hard type error rather than a wrong value. Both terraCirugias and terraModulos get it in ONE migration, because the census established they are one population of two and TERRA-68 is the other half of this row rather than a separate question. THE COST I AM ACCEPTING, NAMED SO IT IS NOT DISCOVERED LATER AS A SURPRISE. This is a fork, and the corpus is explicit that a forked copy freezes and silently un-fixes its own history. The divergence here is exactly one column name and it is forced by the type system rather than chosen for convenience, which is the only version of a fork I will take. Both function headers must state that they are a PAIR and that a change to either is a change to both - the next person to fix a bug in the uuid writer is the person who will not know the bigint one exists. AND THE FAILURE MODE THAT MAKES THIS DANGEROUS TO GET WRONG. A plpgsql body is unresolved until execution, so attaching the WRONG function of the pair installs green - CREATE TRIGGER succeeds, the migration passes, and the type error arrives at the first archive write, which is the first time anybody edits or soft-deletes a vocabulary row. That is potentially weeks after apply and will present as an unrelated failure. The acceptance arms must therefore fire an actual archive write on each of the two tables and assert a row lands with rowIdInt populated and rowId null - not merely that the trigger exists. A trigger-existence assertion is exactly the coverage that would let this ship broken. DRAFTING GOES TO db-terra-ca, GATE TO db-enamel-cc as always. I am not writing DDL and neither is anyone until that draft is read. STILL SEPARATE: TERRA-131. It is a cardinality check on pieza-or-region, not an archive question, and it does not unblock from this decision.
-
Shape decided, drafting dispatched to db-terra-ca. TERRA-68 folds into the same migration.
-
Vocabulary-table archive: the sink already has a bigint arm and NO function can reach it - terraCirugias and terraModulos need a second zero-arg trigger function writing rowIdInt
-
DRAFT AT db/migrations/draft/terra_29_vocabulary_archive_bigint_arm.sql, SHA-256 4a2234c386afb0d27f8a0094a3bebae2b9a49acd2e819fc03953920e42a2facc, routed to audit-terra-ca for the source gate. Two revisions, both on my BLOCK, both improvements. REVISION ONE FIXED AN ATTRIBUTION DEFECT I WOULD RATE THE MOST SERIOUS THING IN THE FIRST DRAFT. Its acceptance block SELECTed a live actor and performed value-preserving UPDATEs, landing two permanent rows in an append-only record saying that person edited a surgery and a module. Value-preserving in the SOURCE table is not the point - the ARCHIVE row is the artifact, it is permanent by design, and rowBefore equal to rowAfter is something a reader has to notice and interpret rather than a label. Nothing distinguished it from a real edit. Resolved with a reserved nil-UUID in archivedBy plus a mandatory archiveReason string naming it as a terra_29 acceptance probe. The column is uuid NOT NULL with no FK and format-only validation, so it already admits an unresolvable value; making that explicit beats borrowing a clinician's identity. I REQUIRED THE SENTINEL BE ASSERTED, NOT REASONED. The whole safety of the design is that the nil UUID is not a person, which was resting on it looking obviously synthetic. It is now an in-transaction EXISTS check against public.users that raises on a hit. That reintroduces a read of a shared table and I took that trade deliberately: a READ is not a TOUCH, terra_28 set exactly this precedent for its own file, and the header now states the distinction so the terra-owned scope claim rests on a named table rather than on nobody objecting. PERSISTENCE IS FORCED, NOT CHOSEN, AND THAT IS NOW MEASURED RATHER THAN ASSUMED. I asked because two permanent rows are acceptable when the record forbids DELETE and unnecessary when it does not - and if it does not, deleting them in the same transaction beats any label. The catalog shows an ENABLE ALWAYS append-only trigger on the sink plus a forbidTruncate, and the migration asserts both by exact tgrelid and tgfoid before it creates the probes. So the labelled rows win outright, and nobody is disabling a guard to tidy up. THE SENTINEL IS A CONVENTION AND CONVENTIONS NEED A HOME. It will be used exactly twice and nothing else in terra knows it exists, so the next person needing a non-human actor would either invent a second sentinel or reuse this one for a different meaning. The header now states in one sentence that a nil UUID in archivedBy means "no human actor" and that any future reuse means exactly that - discoverable from the only artifact that will still be read. TWO CATALOG FACTS I ASKED TO BE STATED RATHER THAN INFERRED, both confirmed: terraCirugias carries only forbidHardDelete and forbidTruncate, terraModulos only forbidTruncate, and the updatedAt trigger count is zero on both - so the probe UPDATE moves no source timestamp and cannot turn "somebody opened the catalog" into "this surgery changed today". THE terra_28 OVERRULE IS MINE AND THE HEADER SAYS SO. terra_28's closing statement sequenced terraModulos separately and explicitly barred it from riding along; I folded it in because pair drift across separately applied files is the dominant risk here, and the header records the divergence as deliberate, with its reason, attributed to this lane rather than to the drafter. Separately recorded: the unconditional AFTER UPDATE shape CONVERGES with terra_28's own prescription - terra_22's shape, not terra_21's WHEN-complement - reached independently from the double-archive argument, so a reader knows that paragraph was not missed.
-
TERRA-29 independent post-apply verification, 2026-08-31, db02-terra-cc, SELECT-only. All numbers below measured BEFORE opening db-terra-ca's evidence file or the migration file. 1. Triggers. SELECT t.tgname, t.tgenabled, c.relname, p.proname FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid JOIN pg_proc p ON p.oid=t.tgfoid WHERE t.tgname IN ('trg_terraCirugias_archiveOnUpdate','trg_terraModulos_archiveOnUpdate') AND NOT t.tgisinternal; -> trg_terraCirugias_archiveOnUpdate: tgenabled='A', relation=terraCirugias, function=fn_terraVocabularyArchive. -> trg_terraModulos_archiveOnUpdate: tgenabled='A', relation=terraModulos, function=fn_terraVocabularyArchive. Both A, both on the intended table, both resolve to the same function. 2. Function body (right one of the pair). SELECT prosrc, proconfig, proacl::text FROM pg_proc WHERE proname='fn_terraVocabularyArchive'; prosrc INSERTs into terraPracticasArchive with column list ("archiveOp","sourceTable","archivedBy","archiveReason","rowIdInt","rowBefore","rowAfter") and value OLD."id" bound to rowIdInt. rowId is NOT in the column list - implicit NULL. Confirms bigint arm, not the uuid arm (fn_terraDetailArchiveSoftDelete is the paired uuid writer, named in the function's own header comment, not touched here). proconfig = ["search_path=public, pg_temp"]. proacl_raw = "{postgres=X/postgres,service_role=X/postgres}" - no PUBLIC entry, matches the standard revoke pattern. 3. Acceptance rows. SELECT "archiveSeq","sourceTable","rowId","rowIdInt","archivedBy","archiveReason","rowBefore"="rowAfter" FROM public."terraPracticasArchive" WHERE "archiveSeq" IN (48,49); archiveSeq 48: sourceTable=terraCirugias, rowId=NULL, rowIdInt=3, archivedBy=00000000-0000-0000-0000-000000000000, archiveReason="system:terra_29-acceptance-probe; value-preserving bigint-arm control", rowBefore=rowAfter TRUE. archiveSeq 49: sourceTable=terraModulos, rowId=NULL, rowIdInt=1, archivedBy=00000000-0000-0000-0000-000000000000, archiveReason="system:terra_29-acceptance-probe; value-preserving bigint-arm control", rowBefore=rowAfter TRUE. AGREE: archiveSeq 48/49 exist, sourceTable/rowIdInt values (terraCirugias/3, terraModulos/1), rowId NULL on both, nil-UUID sentinel in archivedBy on both, rowBefore=rowAfter TRUE on both. DISAGREE: the task's claim was archiveReason "of exactly system:terra_29". The measured value is "system:terra_29-acceptance-probe; value-preserving bigint-arm control" on both rows - NOT an exact match to "system:terra_29". It does carry "system:terra_29" as a PREFIX/substring, so this reads as an imprecise paraphrase in the task description rather than a defect in the applied migration, but it is reported as a disagreement per instruction since the task asked for exact-field confirmation and the string does not match. 4. Sentinel non-collision. SELECT count(*) FROM public.users WHERE id = '00000000-0000-0000-0000-000000000000'::uuid; -> 0. Confirmed: the nil UUID matches no real user. 5. XOR constraint, checked against the rows not just the catalog. Constraint definition: CHECK ((("rowId" IS NOT NULL)::integer + ("rowIdInt" IS NOT NULL)::integer) = 1). SELECT count(*) FROM public."terraPracticasArchive" WHERE "archiveSeq" IN (48,49) AND NOT ((("rowId" IS NOT NULL) AND ("rowIdInt" IS NULL)) OR (("rowId" IS NULL) AND ("rowIdInt" IS NOT NULL))); -> 0 rows violate. Both rows satisfy the xor shape (rowIdInt set, rowId null) - not merely "the constraint exists," the actual rows conform. Read db-terra-ca's evidence (2026-08-31-terra_29-post-apply-ptd.result.txt) AFTER recording all of the above. Their archiveReason string is the SAME "system:terra_29-acceptance-probe; value-preserving bigint-arm control" I independently measured - so the earlier "disagreement" is against coder02's summary of the row in the verification request ("exactly system:terra_29"), not against db-terra-ca's own artifact. Two lanes agree with each other; the paraphrase in the task text is what was imprecise. SUMMARY: independent measurement agrees with db-terra-ca on every field: triggers (tgenabled=A, correct relations, both resolve to fn_terraVocabularyArchive), function identity/body/proconfig/proacl, both acceptance rows' sourceTable/rowIdInt/rowId/archivedBy/archiveReason/rowBefore=rowAfter. No substantive disagreement with the applying lane's evidence. GAP, not a disagreement: db-terra-ca's file does not show the sentinel non-collision check (item 4) or the xor constraint checked against the actual rows rather than just its catalog presence (item 5) - it asserts "the rowId XOR contract" was exercised without a query proving it against these two rows, and never checks the nil UUID against public.users. My independent measurement supplies both: sentinel_collision_count=0, 0 rows violate the xor shape. Recommend their file be amended to carry these two checks explicitly, same class of gap as the proacl-vs-derived-boolean finding on TERRA-130's post-apply file. SCOPE LIMIT: SELECT-only, terra project DB. Did not read db-terra-ca's evidence file or the migration file before recording the above.
-
CLOSED. APPLIED, PUSHED, AND MEASURED TWICE BY LANES THAT DID NOT READ EACH OTHER. terra_29_vocabulary_archive_bigint_arm.sql, Supabase migration 20260831135235, reviewed SHA ecea26b70378fc0dc25b780bb0591048793e66d161490bc50cf536e81e1fc4cd. fn_terraVocabularyArchive plus unconditional AFTER UPDATE triggers on terraCirugias and terraModulos. THE ROW'S OWN PREMISE WAS FALSE WHEN I INHERITED IT AND THAT IS THE FIRST THING WORTH RECORDING. It sat at priority 1 for weeks stating a blocker in terms of a uuid NOT NULL sink that terra_28 had already falsified by adding rowIdInt and dropping rowId's NOT NULL. Nobody re-measured it because a blocker stated as a schema property is the one claim people take on trust - its function is to stop anyone looking. What terra_28 actually left behind was different and more interesting: the bigint arm was POSSIBLE and UNREACHABLE, because every writer in existence was the uuid-shaped fn_terraDetailArchive family. A column nothing can write to for a month. TERRA-68 CLOSES INSIDE THIS ROW RATHER THAN AFTER IT, and that was a deliberate overrule of terra_28's own sequencing note, recorded in the file rather than left implicit. terra_28 sequenced terraModulos as a separate later item. Six rows, identical integer-PK zero-coverage shape, and one function serving both - splitting it across two migrations makes the second copy easy to miss once the first is history. Pair drift was the dominant risk, so both tables ship in one pair-preserving file. WHY A SECOND FUNCTION AND NOT A BRANCH: a generic trigger function's OLD row is a different composite type per attached relation, so one static INSERT cannot serve both PK types. A CASE or TG_TABLE_NAME dispatch trades a compile-time type error for a runtime string match, which moves the failure from apply time to the first real write. The file forbids that merge explicitly - an unlisted future table must fail loudly by type rather than silently skip archival. THREE THINGS I BLOCKED BEFORE THIS COULD APPLY, none of which were caught by tests because none of them are testable from source alone. (1) The acceptance block attributed two fabricated edits to a REAL CLINICIAN in an append-only archive; it now writes the nil UUID with a labelled archiveReason, and the sentinel's non-collision against public.users is asserted rather than assumed. (2) The pair declaration named fn_terraDetailArchive as its uuid counterpart when the true counterpart is fn_terraDetailArchiveSoftDelete - the two arms differ only in rowId versus rowIdInt and both write OLD and NEW json, while fn_terraDetailArchive leaves rowAfter NULL. A pair comment naming the wrong partner is a citation nothing checks. audit-terra-ca found that one. (3) The header claimed no shared object was read while the file SELECTs public.users; that SELECT is the entire reason it needed db-enamel-cc's GO. I RESOLVED THE GATE QUESTION WRONGLY AND THE RIGHT ANSWER CAME FROM AUDIT FIRST. I told db-terra-ca a shared-table read does not trigger the enamel gate, citing terra_28 as precedent - and terra_28 reads no shared table at all, so it is not a precedent about reading one. audit-terra-ca BLOCKED on the ground that reading public.users IS a shared-object touch, which is correct, and the gate's owner has since confirmed it: touch is the test, a read counts, "terra-owned" is never the test. TERRA-153. TWO INDEPENDENT MEASUREMENTS, and they were kept independent on purpose - db02-terra-cc was told not to read db-terra-ca's evidence before measuring, because two lanes agreeing after one has seen the other's answer is one measurement wearing two names. Both triggers exist on the right relations with tgenabled A and resolve to fn_terraVocabularyArchive. prosrc read directly and confirmed as the BIGINT arm: writes rowIdInt from OLD.id with rowId absent from the column list, so implicitly null - this is the specific way the migration could have installed green and wrong, because a plpgsql body is unresolved until it executes and the uuid twin would have errored at the first real write, possibly weeks later. archiveSeq 48 (terraCirugias, rowIdInt 3) and 49 (terraModulos, rowIdInt 1), both rowId NULL, both carrying the nil sentinel and the acceptance label, rowBefore equal to rowAfter. Zero users match the nil UUID. Zero rows violate the xor constraint, checked against the two rows themselves rather than the catalog's mere possession of the constraint. I CHECKED THE COPY PROCEDURE MYSELF rather than on the applying lane's word: draft sha256 is ecea26b7 and the comment-and-blank-stripped diff of draft against applied is empty. COVERAGE BOUNDARY, kept verbatim because the PASS tag does not carry it: audit-terra-ca reviewed SOURCE AND HASH ONLY. Every live-catalog, ACL and by-effect claim rests on the two DB lanes, which is why there are two of them. TERRA-133 is the row for the audit lane holding no database connection. ONE OPEN AMENDMENT, not blocking: db02 notes db-terra-ca's evidence file asserts the sentinel non-collision and the xor check as conclusions without showing the queries - same gap class as TERRA-130's proacl finding. Amendment to that evidence file, not to the applied migration, which is never patched. Pushed as part of the terra_29 commit with the authorization block added to the applied header - the pre-push hook correctly refused the file until it cited the tags it was actually authorized by.
-
completed