Every one of 159 live shared users holds a Terra membership, so terra appKey scoping currently selects everyone and three checks that depend on it are unfalsifiable
A fixture question returned a number that is a claim about the whole co-tenancy model, and nothing was watching for it. db-terra-ca, sizing a negative fixture for terra_53, measured the shared users table: 159 live users, 159 holding a live Terra vUserGroups membership, zero without. The fixture answer is that terra_53's negative case is unreachable in this database. The larger answer is that terra's appKey scope may currently select EVERYONE. That matters because appKey scoping is load-bearing in both layers. src/lib/auth.ts treats vUserGroups filtered by appKey as the sanctioned membership read; practica-actions.ts reads it on the transaction connection before inserting participantes; terra_53 adds a third consumer as a BEFORE ROW trigger. If every live user holds a terra membership, then every one of those checks passes for every user, and NOTHING WOULD NOTICE IF THE PREDICATE WERE WRONG - a filter with one reachable outcome is not being tested by production traffic, it is being carried by it. Three shapes to distinguish, and they have different consequences. (a) Every live user holds memberships in every app in vUserGroups: co-tenancy is nominal, appKey has never discriminated anyone, and terra_53 is joining a set of unfalsifiable checks. (b) Users hold differing app sets and terra merely happens to be universal today: the checks are real and the current population is a coincidence that will end with the first enamel-only account. (c) The 159 came from a query that had already filtered to terra, and the number is an artefact of its own join. Rule out (c) first. A count that agrees with a convenient story deserves the same suspicion as one that does not, and discovering the query was self-selecting is a better outcome than building on it. Required output: distinct appKey values in vUserGroups with a live-user count each, live users grouped by the SET of appKeys they hold, and the verbatim SQL. SELECT ONLY. No writes to users or userGroups under any circumstance - those are enamel-owned, and the standing GO covers reading vUserGroups, not writing the co-tenant's tables. Close condition: the three counts above are recorded with their SQL, and the shape is named as (a), (b) or (c). If (a), this row spawns a follow-up on what to do about three unfalsifiable checks; if (b) or (c) it closes on the measurement.
Activity
-
ownerAppKey finding (larger of the two): all 6 live groups (admin/alumno/bedel/client/docente/jtp) have ownerAppKey = NULL. vUserGroups predicate (g."ownerAppKey" IS NULL OR g."ownerAppKey" = ug."appKey") is vacuously true for every group that exists today - group-level app-scoping is currently a no-op, independent of the population question. SQL: SELECT g.value, g.label, g."ownerAppKey", g."isActive", count(ug.id) FILTER (WHERE ug."deletedAt" IS NULL AND ug."toAt" IS NULL) AS live_holders FROM groups g LEFT JOIN "userGroups" ug ON ug."groupId" = g.id WHERE g."deletedAt" IS NULL GROUP BY g.id, g.value, g.label, g."ownerAppKey", g."isActive" ORDER BY g.value. Population measurement. (c) ruled out: query is LEFT JOIN from users, no pre-filter to terra. Distinct appKey in vUserGroups with live-user count: SELECT "appKey", count(DISTINCT "userId") FROM "vUserGroups" GROUP BY "appKey" -> terra=159, venus=1. No enamel appKey row exists in userGroups, ever. Live users by appKey SET: SELECT appkeys, count(*) FROM (SELECT u.id, COALESCE(array_agg(DISTINCT vg."appKey" ORDER BY vg."appKey") FILTER (WHERE vg."appKey" IS NOT NULL), ARRAY[]::text[]) AS appkeys FROM users u LEFT JOIN "vUserGroups" vg ON vg."userId"=u.id WHERE u."deletedAt" IS NULL AND u."isSynthetic" IS FALSE GROUP BY u.id) s GROUP BY appkeys -> {terra}=158, {terra,venus}=1, sum=159=live_users_total (verified separately, not a typo). Zero users hold no membership row. Shape: neither (a) nor (b) as posed. Enamel is not represented in userGroups/groups at all - zero rows, so 'co-tenancy implies a non-terra population' is untestable against this table; enamel's authorization does not run through it. Whether enamel enforces per-app membership through a different mechanism, or not at all, is unanswerable from this side.
-
Census of every terra-membership gate (predicate on userGroups.appKey='terra' / vUserGroups), requested by coder02. The row named THREE consumers; the actual count is at least SEVEN distinct predicates of this shape, of which five are strictly unfalsifiable under the current 159/0 population and none are producible by a terra-owned write (users/userGroups are enamel-owned; terra holds no DDL/DML there - confirmed, not assumed). Row's three, re-examined: 1. auth.ts requireAppUserPage/Action: user.roles.length===0 -> noTerraAccess -> /sin-acceso (auth.ts:388). Falsifiable only by a live users row with zero live terra memberships. Population today: 0/159. UNFALSIFIABLE. Not producible from terra. 2. practica-actions.ts pre-insert: this is actually TWO predicates on the transaction connection (practica-actions.ts:580-603), not one - docenteControladorId (WHERE appKey=terra AND group IN (docente,client) AND userId=$2) and ayudanteId (WHERE appKey=terra AND userId=$2, no group filter). ayudanteId's refusal is the pure membership gate and is UNFALSIFIABLE (0 non-members exist to trigger it legitimately). docenteControladorId's refusal IS falsifiable today via a terra member with the wrong role (e.g. an alumno-only actor, 141 available) - its non-member-specific sub-case is unfalsifiable but its role-mismatch sub-case is exercised routinely. 3. terra_53 trigger (fn_terraParticipanteRequireTerraMembership): DRAFT, not yet applied. Refusal unfalsifiable via real data; exercised only via the approved random-UUID fixture in the rolled-back dry-run (see 2026-08-31-terra_53-dryrun-full.result.txt). Not named by the row, same predicate shape, found by grepping every public.* function body for vUserGroups/appKey='terra': 4. createTerraPractica: RAISE 'actor % has no live terra membership'. UNFALSIFIABLE. 5. lookupTerraPaciente: same RAISE text. UNFALSIFIABLE. 6. setTerraUserTelefono: v_held check -> notMember refusal. UNFALSIFIABLE. 7. requestTerraAccess: INVERTED shape - refuses if the actor ALREADY holds a live terra membership (trivially falsifiable, 159/159 satisfy it). But its intended success path - a genuine non-member requesting access and getting a ticket - is itself unexercised by live data for the identical population-zero reason, just on the opposite branch. Not counted here: changeTerraMembership and createVenusSupportTicket also read vUserGroups, but their predicates are ROLE-scoped (admin/bedel, client) not bare membership, and are falsifiable today via role variety in the live population - different question than TERRA-147 asks. Answer to the second deliverable: for every UNFALSIFIABLE predicate above (1, 2's ayudanteId half, 3, 4, 5, 6, and 7's inverted refusal-is-trivial/success-is-unfalsifiable pair), the state that would make it falsifiable is a live public.users row with zero live terra vUserGroups memberships (or a live user holding only a non-terra appKey). Confirmed, not assumed: users and userGroups are both enamel-owned shared tables; terra's standing GO covers reading vUserGroups, not writing users or userGroups. No terra-owned write path can create that state. The finding stands as coder02 expected: these checks cannot be exercised from terra at all, and an unfalsifiable check is not the same as a passing one.
-
TERRA-147 membership census, 2026-08-31, db02-terra-cc, SELECT-only. Q1: per appKey, distinct live userIds. SELECT ug."appKey", count(DISTINCT ug."userId") AS distinct_users FROM public."userGroups" ug JOIN public.users u ON u.id = ug."userId" JOIN public.groups g ON g.id = ug."groupId" WHERE ug."toAt" IS NULL AND ug."deletedAt" IS NULL AND g."deletedAt" IS NULL AND u."deletedAt" IS NULL AND ug."isSynthetic" IS FALSE AND u."isSynthetic" IS FALSE GROUP BY ug."appKey" ORDER BY ug."appKey"; -> terra: 159, venus: 1 Q2: users holding memberships in more than one app. SELECT multi_app_count, count(*) AS users_with_that_many_apps FROM ( SELECT ug."userId", count(DISTINCT ug."appKey") AS multi_app_count FROM public."userGroups" ug JOIN public.users u ON u.id = ug."userId" JOIN public.groups g ON g.id = ug."groupId" WHERE ug."toAt" IS NULL AND ug."deletedAt" IS NULL AND g."deletedAt" IS NULL AND u."deletedAt" IS NULL AND ug."isSynthetic" IS FALSE AND u."isSynthetic" IS FALSE GROUP BY ug."userId" ) s GROUP BY multi_app_count ORDER BY multi_app_count; -> 1 app: 158 users, 2 apps: 1 user (the terra+venus user from Q1) Q3: live users with ZERO live memberships (any app). SELECT count(*) FROM public.users u WHERE u."deletedAt" IS NULL AND u."isSynthetic" IS FALSE AND NOT EXISTS ( SELECT 1 FROM public."userGroups" ug JOIN public.groups g ON g.id = ug."groupId" WHERE ug."userId" = u.id AND ug."toAt" IS NULL AND ug."deletedAt" IS NULL AND g."deletedAt" IS NULL AND ug."isSynthetic" IS FALSE ); -> 0 FINDING: 159/159 live shared users hold a live terra membership; 0 hold zero memberships of any kind. appKey='terra' currently selects the entire live user population. This is a MEASUREMENT dated 2026-08-31, not a permanent property - it can change the moment a non-Terra-only shared user is created by another app (venus did exactly this once, for 1 user). While it holds, the three dependent checks (auth.ts getUserRoles/noTerraAccess, practica-actions.ts docente/ayudante membership checks, terra_53's fn_terraParticipanteRequireTerraMembership) are unfalsifiable: no live fixture can produce their refusal branch without an out-of-scope enamel-owned write. See terra_53 dry-run evidence (2026-08-31-terra_53-dryrun-full.result.txt) for the same limit applied to one of the three. SCOPE LIMIT: SELECT-only, terra project DB. Did not read any other evidence file before running these queries.
-
CENSUS RUN, AND THE HEADLINE HOLDS WITH ONE CORRECTION TO ITS SHAPE. db02-terra-cc, SELECT-only, evidence in agents/db02-terra/evidence/2026-08-31-terra_147-membership-census.result.txt. Per appKey, distinct users holding a live membership: terra 159, venus 1. Distribution: 158 users in exactly one app, one user in two, and that one is in terra and venus both. Live users holding ZERO memberships of any kind: zero. So appKey = 'terra' selects the entire live population as of 2026-08-31, and every check filtered on it passes for reasons unrelated to the filter. THE CORRECTION IS THAT THE ROW SAID 159 OF 159 AND THE POPULATION IS NOT A SINGLE NUMBER. There is a second appKey in the data with exactly one user, and that user also holds terra. A census that had returned only the terra count would have looked identical to a census of a single-tenant database, and the two are different worlds for every predicate that scopes on appKey. One row is a small sample, not an absence. WHAT THIS MAKES UNFALSIFIABLE, said as a measurement rather than a defect. Any assertion of the form "this refuses a user without a Terra membership" cannot fail today, because no such user exists to present. That includes terra_53's refusal branch, which was dry-run against the live database and is therefore proven to REFUSE a synthetic non-member, but has never refused a real one and cannot until the population stops being uniform. The distinction matters: the branch is tested, the SELECTIVITY is not. Same family as the group-level ownerAppKey clause in TERRA-148, answered the same day - a predicate can be enforced, correct, and select the whole population at once. CLOSED AS A DATED MEASUREMENT, WITH NO WATCH ARMED. The uniformity is a property of today's data and will end the first time a shared user is created without a Terra membership - onboarding into another enamel tenant does exactly that. Nothing is monitoring for it and nothing should be: this is one measurement with a date, not a condition needing machinery. Anyone re-reading a check that depends on appKey scoping should re-run the census rather than trust this row's numbers, and the query text is in the evidence file so they can. METHOD NOTE THAT IS THE REASON THIS EVIDENCE IS USABLE. Raw counts and the query text were recorded, not a verdict computed from them. The terra_53 post-apply file recorded a derived boolean and could not be re-checked by anyone who had not run the query; this one can.