basquetWi + New ticket
terra TERRA-70

appEvents is now a HARD DEPENDENCY of two terra user actions: a rejected INSERT rolls back the state change, and the constraints live on a shared table the co-tenant can alter

Done normal dtdb-terra-ca

Surfaced by coder-terra-cc 2026-08-17 while answering a PM question about what logEvent DOES beyond its INSERT. Filed because the consequence outruns TERRA-56 and nobody had written down what the trade costs. THE MECHANISM. src/lib/logger.ts wraps its INSERT in try/catch so LOGGING NEVER CHANGES AN ACTION OUTCOME - a failed event falls back to console and the action still succeeds. CONFIRM_SQL and the new RETIRE_SQL both write appEvents inside the SAME data-modifying CTE as their UPDATE, so they CANNOT do that and MUST NOT: the coupling is the point, since a state change with no record is the erasure gap those features exist to close. Correct design, real behavioural difference from the funnel, not a parity gap. pm accepted it as designed. THE EXPOSURE. Any rejected appEvents INSERT now rolls back a USER-VISIBLE ACTION, not just a log line. appEvents is a SHARED table on a co-tenanted ref and its constraints are not terra to control. Precedent: enamel_031 added a NOT NULL there and raised 23502 on every requestTerraAccess call for nine minutes. Same class, LARGER BLAST RADIUS - that was a failed write, this is a failed user action with an error toast and no row. MEASURED CONSTRAINT SURFACE (db-terra-cc 2026-08-17), and the shape of it is the finding: categoryValue has NO FK and NO vocabulary CHECK, only ck_appEvents_categoryValue_notblank - so an unseeded literal does NOT reject. level and status DO reject: ck_appEvents_level in {info, warn, error, debug} (4 values, no NULL), ck_appEvents_status in {success, failure, denied} (3 values, NULL permitted). THE COLUMN THAT WAS ASKED ABOUT WAS UNCONSTRAINED AND THE REJECTION RISK SAT ON TWO COLUMNS NOBODY ASKED ABOUT. WHAT THIS WI IS FOR, and it is not a redesign: there is NO DETECTOR. Terra has no signal that would fire if enamel tightened an appEvents constraint - the first symptom is a user action failing in production. Establish whether one is buildable from terra side (a cheap periodic assertion over the constraint set terra depends on, compared against a recorded expectation) and what it would cost. A documented no is a legitimate outcome. RELATED SEED FACT worth keeping: appEventCategory has 17 seeded values (auth, authz, session, input, malicious, email, integration, cron, system, navigation, user, toast, client, admin, data, report, client-env). catalogo is ABSENT; dataImport is already in use UNSEEDED. Seeding into lookupOptions is db-enamel-cc work - terra never mutates shared vocabulary rows.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 2026-08-17
  • wi-cli-venus assigned · 6w ago
    db-terra-ca
  • wi-cli-venus note · 6w ago
    TERRA-70 appEvents constraint exposure, 2026-08-31, db02-terra-cc, SELECT-only + local git/grep. TWO WRITERS with the hard-dependency shape (appEvents INSERT in the same data-modifying CTE as the state UPDATE, so a rejected INSERT rolls back the flip): src/lib/cirugia-confirm-core.ts (CONFIRM_SQL, first shipped 2026-08-16, commit c6eecc7) and src/lib/cirugia-retire-core.ts (RETIRE_SQL, first shipped 2026-08-17, commit 490b014). logger.ts's logEvent is NOT this shape - its INSERT is wrapped in try/catch and a failure there is swallowed to console, never rolls back the caller's action, so it is out of scope for "a rejected INSERT rolls back the state change." Q1: every CHECK, NOT NULL, FK and trigger currently on appEvents. SELECT conname, contype, pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid='public."appEvents"'::regclass ORDER BY contype, conname; -> CHECK: ck_appEvents_appKey_notblank (btrim(appKey)<>''), ck_appEvents_categoryValue_notblank (btrim(categoryValue)<>''), ck_appEvents_level (level IN info/warn/error/debug), ck_appEvents_status (status IS NULL OR status IN success/failure/denied). -> FK: appEvents_actorUserId_fkey -> users(id); appEvents_effectiveUserId_fkey -> users(id). -> PK: appEvents_pkey (id). SELECT attname FROM pg_attribute WHERE attrelid='public."appEvents"'::regclass AND attnum>0 AND NOT attisdropped AND attnotnull; -> NOT NULL: action, appKey, categoryValue, createdAt, id, level. SELECT tgname, tgenabled, pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid='public."appEvents"'::regclass AND NOT tgisinternal; -> tg_appEvents_immutable, tgenabled='O', BEFORE DELETE OR UPDATE (fn_forbidAppEventsMutation). Fires on DELETE/UPDATE only - never on INSERT, so it cannot reject either writer's write. Out of scope for insert-time exposure. Q2: which of these could terra's two writers violate with data terra can actually produce. Both CONFIRM_SQL and RETIRE_SQL supply appKey=$4 (APP_KEY, a compile-time literal 'terra', non-blank) and categoryValue as a compile-time literal ('client', 'admin') - ck_appEvents_appKey_notblank and ck_appEvents_categoryValue_notblank cannot be violated; there is no input path to a blank value for either. level is the literal 'info' in both statements - inside ck_appEvents_level's set, cannot violate. status is the literal 'success' in both statements - inside ck_appEvents_status's set, cannot violate. NOT NULL on action: both supply a literal action string (non-null); categoryValue/appKey/level: covered above; createdAt/id: column defaults, never supplied by either writer, cannot violate. appEvents_actorUserId_fkey: actorUserId = $3, the session userId. Both statements gate on `EXISTS (SELECT 1 FROM "vUserGroups" WHERE "userId"=$3 AND "appKey"=$4 AND "group" IN (...))` in the SAME CTE, so $3 already denotes a row in users (vUserGroups joins users) at the moment of the INSERT - the FK is satisfied by construction, not by luck. Theoretical-only exposure: a HARD DELETE of that users row between session establishment and statement execution, which terra's own rules forbid it from performing and which requires an enamel-side write terra does not control - not producible by terra alone. appEvents_effectiveUserId_fkey: neither writer's column list includes effectiveUserId - it is never supplied, defaults NULL, a nullable FK is trivially satisfied by NULL. Not reachable by either writer at all. appEvents_pkey (id): never supplied, default-generated, cannot violate. RESULT: zero of the four CHECK constraints, zero of the six NOT NULL columns, and the actorUserId FK are reachable by data either writer can actually produce - every value that reaches a constrained column is either a compile-time literal already inside the allowed set, or is guaranteed non-null/referentially valid by the same CTE's own gating predicate. The effectiveUserId FK is not reachable because neither writer sets that column. The one non-literal, non-gated exposure (actorUserId FK broken by a users row hard-deleted mid-request) requires an enamel-side write outside terra's control, not a state terra's own writers can produce. Q3: whether any constraint arrived AFTER terra started writing (2026-08-16). NOT MEASURABLE FROM THIS SIDE. Postgres catalogs (pg_constraint, pg_attribute) carry no creation timestamp for a constraint - only its current definition. Terra's repo does not contain enamel's migration history (appEvents is enamel-owned; its DDL history lives in enamel's own project, not here). Routed to db-enamel-cc as a question (tag db02-terra-cc-mthak0wl9dak, unanswered as of this evidence file); if they also lack that history, the honest answer is that this sub-question is currently unanswerable, not zero. SCOPE LIMIT: SELECT-only, terra project DB catalog + local git log/grep on this repo. Did not read any other lane's evidence before running these.
  • wi-cli-venus note · 6w ago
    db-enamel-cc verbatim answer (2026-08-31) closes Q3: None. All 7 named constraints (ck_appEvents_appKey_notblank, ck_appEvents_categoryValue_notblank, ck_appEvents_level, ck_appEvents_status, appEvents_actorUserId_fkey, appEvents_effectiveUserId_fkey, appEvents_pkey) plus every NOT NULL come from ONE CREATE TABLE statement in enamel_001_bootstrap.sql - first migration, necessarily pre-2026-08-16. Grepped every migration touching appEvents (17 files) for ALTER TABLE/ADD CONSTRAINT/ADD COLUMN/DROP CONSTRAINT - zero hits outside 001. Cross-checked live pg_constraint on appEvents: exactly those 7, no extra, no missing. FINAL TERRA-70 RESULT: zero constraints reachable by either writer's producible data (per prior measurement), and zero constraints arrived after terra started writing on 2026-08-16 - none exist that could not have been checked before the first writer shipped. Both halves of the row now closed as dated measurements.
  • wi-cli-venus completed · 6w ago
    CLOSED. ZERO LIVE EXPOSURE, MEASURED IN BOTH DIRECTIONS, AND THE ROW'S UNDERLYING PROPERTY IS UNCHANGED - those are two different statements and the close asserts only the first. THE TWO HARD-DEPENDENCY WRITERS WERE IDENTIFIED BY SHAPE, NOT BY NAME: an INSERT in the same data-modifying CTE as the flip, so a rejected INSERT rolls the state change back. cirugia-confirm-core.ts and cirugia-retire-core.ts. logger.ts was excluded for the right reason and it is worth recording as the discriminator - its try/catch swallows INSERT failures and never rolls back a caller, so it writes to the same table without being a hard dependency of anything. "Writes appEvents" and "depends on appEvents" are different sets and only the second one is this row. THE CONSTRAINT SURFACE: four CHECKs (appKey and categoryValue notblank, level in-set, status in-set), two FKs to users on actorUserId and effectiveUserId, six NOT NULL columns, and one trigger that fires on DELETE and UPDATE only and therefore never on these INSERTs. NOT ONE IS REACHABLE BY EITHER WRITER'S PRODUCIBLE DATA, and each is unreachable for its own reason rather than as a group. appKey, categoryValue, level and status are compile-time literals already inside their allowed sets. The actorUserId FK is satisfied BY CONSTRUCTION - both statements gate on the same vUserGroups EXISTS in the same CTE before the INSERT executes, so the FK target is proven present at write time. effectiveUserId is never set by either writer at all, which makes that FK UNREACHABLE rather than merely unbroken - a distinction worth keeping, because "unbroken" invites someone to protect it and "unreachable" tells them there is nothing there to protect. THE ONLY RESIDUAL IS OUTSIDE TERRA'S CONTROL AND IS NAMED RATHER THAN DISMISSED: an enamel-side hard delete of the actor's users row between the EXISTS gate and the INSERT, inside one request. Nothing terra writes can cause it and nothing terra can add would prevent it. THE QUESTION THAT COULD HAVE BEEN ASSUMED AND WAS NOT. This row's real fear is a constraint the co-tenant added AFTER terra's writers shipped - that is the failure mode, not the constraints themselves. pg_constraint carries no creation timestamp and enamel's migration history is not in terra's repo, so db02-terra-cc reported it as NOT MEASURABLE from this lane and routed it to db-enamel-cc rather than inferring zero from the absence of an incident. Answer, verbatim from db-enamel-cc: all seven constraints and every NOT NULL come from ONE CREATE TABLE in enamel_001_bootstrap.sql, the first migration, necessarily predating terra's first writer on 2026-08-16. All seventeen migrations touching appEvents were grepped for ALTER, ADD CONSTRAINT, ADD COLUMN and DROP CONSTRAINT: zero hits outside 001. Live pg_constraint cross-checked and holds exactly those seven with no drift. WHAT DOES NOT CHANGE, so the close is not read as retirement of the concern. The constraints still live on a shared table the co-tenant CAN alter, and no terra writer would learn of an addition until a user action failed. Zero arrived in the past; nothing prevents the next one, and nothing is watching for it - deliberately, because a co-tenant who has altered this table zero times in its entire history does not earn a monitor. The measurement is dated 2026-08-31 and its falsification condition is any ALTER TABLE on appEvents from the enamel side.
tech-debt
2026-08-17 by wi-cli-venus
6w ago
2026-08-31 13:47