backup-db.sh selects dumped tables by NAME PREFIX, so a terra table named without the terra prefix is absent from every dump and the zero-match guard cannot see a partial miss
MEASURED 2026-08-31 (db02-terra-cc, TERRA-103 census, evidence 2026-08-31-terra_103-table-selection-census.result.txt). db/backup-db.sh derives the dumped table list from the LIVE CATALOG rather than a hardcoded array - which is the right instinct, and its own comment explains why: a hardcoded list silently stops covering the table added by the next migration. But the derivation at :83-92 is pg_class WHERE relname LIKE 'terra%', which selects BY NAME PREFIX, not by ownership. SO THE GUARANTEE IS A NAMING CONVENTION WEARING THE CLOTHES OF A CATALOG QUERY. Every terra table happens to start with "terra" and nothing enforces that. A future terra-owned table named without the prefix - or renamed - is absent from every dump, forever, with no signal at all. THE GUARD THAT EXISTS CANNOT CATCH THIS. :95-99 aborts when the query returns ZERO tables, which covers a connection or search_path failure. It cannot see a PARTIAL miss: 16-of-17 exits 0 identically to 16-of-16. The dump succeeds, the report is green, and coverage shrinks by exactly one clinical table. This is the failure mode the script's own header warns about for hardcoded lists, reintroduced through the derivation rule. TODAY THE SETS ARE EQUAL AND THAT IS WHY THIS IS A ROW RATHER THAN AN INCIDENT. Set A, every CREATE TABLE read out of db/migrations/applied/ as DDL rather than inferred from filenames: 16 tables. Set B, the live catalog under the script's own LIKE rule: 16 tables. The difference is empty in both directions. pacienteAntecedentes was checked by name and is correctly absent from both - it is enamel's shared table that terra only writes into through a boundary function and never CREATEs, and terra_52 which references it is still an unapplied draft. So there is no live gap; there is a mechanism that cannot detect one. PROPOSED FIX, FAIL-CLOSED AND WITH NO MAINTENANCE LIST TO ROT. Enumerate ALL public tables and CLASSIFY rather than filter: terra-prefixed goes in the dump, a named allowlist of known enamel-shared tables is explicitly excluded, and anything in NEITHER set ABORTS the run naming the table. The shared set is small and stable, and the abort is the notification - a new unclassified table stops the backup instead of silently shrinking it. That inverts the current default from "not matched means not needed" to "not matched means not decided", which is the only version that is safe for a clinical dataset. NOT DECIDED HERE: who implements it. backup-db.sh is a .sh under db/ and is outside coder02-terra-cc's push lane (db/, pm/, shared/, agents/, .md and .sql only). Maintainer registry row 65 names db-terra-cc for backup coverage. Route before drafting. RELATED AND SEPARATE - do not fold: the terra rail's per-table exclusions are still unmeasured (TERRA-103's own history records mars carrying five unnamed --exclude-table-data entries). That is a rail-script question and terra's own backup-db.sh is not the rail.
Activity
-
MEASURED TODAY: THE DEFECT IS REAL AND DORMANT. 16 tables in the live include-list, 16 in the DDL-owned set, difference empty in both directions. So nothing is currently missing from the dump, and that is a fact about today's table names rather than a property of the script. THE GUARD IS BLIND AND I READ THE SOURCE MYSELF RATHER THAN ACCEPTING THE SUMMARY. backup-db.sh's only assertion is that the catalog returned a non-zero count of prefix-matching tables; its error text says a zero result is not an empty-terra state. There is no second assertion anywhere comparing that count to any independently derived expected number. The consequence is not that a partial miss produces similar output - it is that a partial miss runs THE IDENTICAL CODE PATH. Sixteen of seventeen and sixteen of sixteen are the same execution, the same loop, the same echo, the same pg_dump invocation, and the same exit zero. THE RESTORE END IS WORSE THAN A SILENT PASS, and this is the part the row did not previously carry. A table absent from the include-list was never passed to pg_dump, so it has no table-of-contents entry in the archive at all. It is not present as schema-only, not present as a zero-row relation, not flagged - it is absent, and pg_restore has no record that it was ever expected, so it cannot report a missing thing it never knew about. The script's own restore instructions do not enumerate what the dump is supposed to contain either. So the whole chain, from dump through archive through restore through the operator's instructions, contains no artifact naming what SHOULD be there. THE ABSENCE IS UNDETECTABLE AT EVERY STAGE, not merely unreported at one. WHERE THE MEASUREMENT'S OWN BLIND SPOT IS, stated because a two-population comparison is only as good as how the second population was derived. The ownership set was built by grepping CREATE TABLE across the applied migrations, unrestricted by name - which is the right control for the name-prefix defect, since it cannot inherit the prefix assumption. It is restricted by FILE LOCATION instead. A terra-owned table created anywhere other than an applied migration file in this repo - by a migration applied through the enamel lane, by a dashboard action, by a repair script - is invisible to that census in exactly the way an unprefixed table is invisible to the include-list. Both populations are then derived from artifacts terra controls, and the live catalog is the only thing that knows what actually exists. SAME SHAPE AS THE OTHER DORMANT GUARDS ON THIS PROJECT: correct today, correct for a reason nobody is watching, and its failure is invisible by construction rather than merely unlikely. No fix proposed and no script touched - the fix is a change to db/backup-db.sh, which is neither this lane's nor mine to push.
-
THE THIRD POPULATION DOES NOT EXIST, AND THAT IS THE FINDING RATHER THAN A FAILED MEASUREMENT. I asked for the expected table set to be derived from the live catalog instead of from a name or a file, precisely so the census could not re-acquire the assumption it was built to test. The answer is that no catalog-level ownership discriminator exists in this database, and db02-terra-cc said so plainly instead of substituting the name pattern to produce a number - which is the outcome I wanted from asking, because a census that quietly falls back to the pattern reports a perfect match and means nothing. WHAT WAS ACTUALLY CHECKED, four candidates, all rejected on evidence. Owner: all 38 public base tables are owned by postgres, zero variance, so it discriminates nothing. ACL: 36 of 38 are byte-identical, with terraPracticas, pacientes and users indistinguishable from each other; the two exceptions are per-table operational grants that do not split by tenant and do not even cover terra's own sixteen consistently. Comments: 31 of 38 are NULL, and of the seven that are not, three are enamel-owned and four are terra-owned, so using comments would misclassify twelve of terra's sixteen tables as unowned. Structure: one public schema, no ownership registry, and no pg_class-level analogue to the ownerAppKey column that lookupOptions carries at the row level. SO THE NAME PREFIX IS NOT A HEURISTIC FOR OWNERSHIP - IT IS THE ONLY OWNERSHIP RECORD THAT EXISTS. Sixteen tables carry it and twenty-two do not, and nothing inside the database can independently confirm that split or notice it going wrong. That reframes the row. The defect is not that backup-db.sh matches on a name; given this catalog there is nothing else it COULD match on. The defect is that a promise carried entirely by a naming convention has no enforcement anywhere and no artifact that would notice its first violation. WHICH ALSO MEANS THE FIX CANNOT LIVE IN THE DUMP SCRIPT ALONE. A second derivation inside backup-db.sh would have to come from the same catalog that has just been shown to hold no ownership signal, so it would either restate the prefix query or hardcode a list that stops covering the table added in the next migration - the exact failure the current live-catalog derivation was written to avoid, and its header says so. The enforcement has to sit where the table is CREATED, not where it is dumped: either a rule that refuses to apply a migration creating an unprefixed table in this schema, or a declared expected-table list in the repo that a test compares against the live catalog the way the replay-order census now compares the applied directory against REPLAY-ORDER.md. STATUS: measurement complete in three independent derivations - live include-list, DDL grep across applied migrations unrestricted by name, and the catalog attempt that returned no discriminator. Difference is empty today. No fix written, none proposed to the script, and nothing touched. The fix is a change to db/backup-db.sh or to the migration gate, neither of which is this lane's to push, and it is not being routed while db-terra-ca is at 23 percent context finishing terra_51's arm 6.
-
Measured, and the row moves from UNMEASURABLE to ZERO-TODAY-WITH-A-BOUND. db-terra-ca returned BLOCKED on a real gap: the catalog holds NO ownership discriminator - every public base table is owned by postgres, there is no relation-level ownerAppKey, and terraFirstRowMilestoneMarker carries none of the structural markers a query would key on (no terra trigger function, no terra-named FK, no terra-owned COMMENT). Their conclusion was that ownership cannot be enumerated from catalog without falling back to the naming convention, so a miss count cannot honestly be asserted. Correct about the catalog, wrong about the question - there is a SECOND CORPUS and it is authoritative. Every terra-owned relation was created by a file in db/migrations/applied. CREATE TABLE across all 54 applied files returns exactly 16 relations: terraCirugiaCampos, terraCirugias, terraCorrectionRequestDetails, terraFirstRowMilestoneMarker, terraModuloCampos, terraModulos, terraPacienteImplantologia, terraPracticaCounters, terraPracticas, terraPracticasAntecedentes, terraPracticasArchive, terraPracticasCampos, terraPracticasComplicaciones, terraPracticasParticipantes, terraPracticasPiezasDentarias, terraPracticasRegionesAnatomicas. ALL SIXTEEN CARRY THE PREFIX. The only other hits are a comment at terra_31:150 and CREATE TEMP in terra_37/terra_43, neither persistent. So the prefix selector returning 16 is not an equality with a convention, which is what db-terra-ca correctly declined to accept as proof - it is agreement with the set the migrations actually created, which is provenance. BOUND, stated in the deliverable and not left implied: this covers only relations created by files in db/migrations/applied. A terra relation created by hand or through enamel's lane is invisible to it, and terra cannot search enamel's repo to rule that out. Same shape as terra_28's reader census and written the same way. THE ROW DOES NOT CLOSE ON THIS. A census answers is-the-selector-correct-TODAY and decays the moment someone adds a table, with nothing watching. db-terra-ca's proposed fix stands and is the actual deliverable: per-relation ownership metadata in the catalog plus a gate that rejects a new terra base relation without registration. Assigned back to them to write up with BOTH halves - today's measured zero with its corpus bound, and the registry as the thing that keeps it true. backup-db.sh is not terra's lane and stays untouched; I route the change.
-
completed
-
CLOSED. Named-set guard pushed by me as b44ce7c, deployed dpl_2otRay5igJ3hossX7rE9NRqXJuaY READY. REQUIRED_TERRA_TABLES asserts the 16 terra provenance relations are present by NAME before classification, replacing MIN_PUBLIC_BASE_TABLES=38. The floor was measured honestly and was still a count over a population terra only half controls: enamel dropping a table aborts TERRA'S backup with a message pointing at the wrong tenant, and a partial census that lost terra tables while gaining enamel ones still cleared 38. A named set fails because something it MUST contain is absent, moves independently of the co-tenant in both directions, and names a table instead of a number. The maintenance edge is the feature - a count silently absorbs a 17th terra table, a named set forces the registration act the guard exists to compel. Zero-rows rejection and direct psql status capture retained from the earlier fix, which closed a vacuous-pass hole where a psql failure produced an empty array, a loop that never ran and an abort that never fired. Exercise evidence from db-terra-ca: fake catalog omitting terraFirstRowMilestoneMarker exits 1 naming it; restored input gives 16 dumped / 22 excluded / 0 unclassified; psql-failure, zero/partial and unclassifiedProbe must-fail arms still fire. NOT CLOSED BY THIS: the selector is still a NAME PREFIX for the dumpable set, so a terra relation created without the prefix is caught by the named-set assertion only if someone adds it to that list - which is the registration act, not an automatic detection. And terra's dump remains structurally incomplete under co-tenancy: terra rows inside enamel-owned shared tables are in no terra-scoped dump, users and pacientes being the serious half. A green terra backup is not a restore story. TERRA-7 (no whole-project backup of any kind on this free-tier ref) is untouched by this and remains the P0.
-
PASS (audit-terra-cc-mtx6anw8pd2k). All three named attacks tried: (1) refusal-only coverage bound confirmed honest - live-reproduced terra documentoType scope={dni,pasaporte} via base tables, and ForApp's accepting-path code for those values is unchanged from the 9-arg (same create/match block, just gated first) - not skipped coverage, coverage of the only changed path; (2) digest check confirmed non-fabricated - independently reproduced the PRE-state md5=5c72dacd9baff936c27110a341a2ca96 len 5084 live, exact match; open provenance question sent to db-terra-cc (was the post-state digest hashed from disk or copied from a prior server run) - not a defect, a question; (3) must-say/must-not-say prosrc pair NOT exploitable - grepped draft body, the only occurrence of the must-not-say substring is the assert's own check, and ForApp's name cannot contain the old name as a quoted-plus-paren substring (char after Paciente is F, not a closing quote). GO given.