basquetWi + New ticket
enamel ENAMEL-40

users identity: two humans each hold two live bound rows; duplication sits exactly where the documento axis is disarmed

Done normal cecoder02-enamel-cc

Re-derived live on eprpywqwzsvrjeaforei 2026-09-11 by coder-enamel-cc after db-enamel-cc raised one instance; the measurement found TWO, and the more severe one is the pair they did not have. No names or emails recorded here - rows are identified by id and by an 8-char md5 of the normalized name. THE MEASUREMENT. 159 live non-synthetic users, 9 bound (supabaseAuthId not null). Grouping live rows on lower(btrim(nombre))||lower(btrim(apellido)) returns two collision groups, four rows, ALL FOUR BOUND: group 548cbc73 - 8157954d-7a79-48c2-a67b-87a37970d688 (pensanta.com, admin:terra) and 4ce4ed1e-2de5-450a-b582-2e4d3ec20dd2 (gmail.com, alumno:terra). group bac94f97 - 6ebaffae-a195-459e-afe8-b89c3fd73fc0 (odontologia.uba.ar, client:terra + docente:terra + client:venus) and 4304d643-7cd9-4eaa-b8d4-86d3c8f46fee (gmail.com, alumno:terra). The first group is the one that matters most and was not in the report: it is enamel's ONLY terra admin grant, and the same person also exists as an alumno on a separate auth id. A revoke, an audit, or a >=1-live-admin count that reasons over rows rather than people gets the wrong answer about that person in both directions. THE REFINEMENT ON THE DIAGNOSIS. The report said all four unique axes are disarmed and no index closes it. Measured, that is true of these rows but NOT of the table: 153 of 159 live rows DO carry documentoNumber, 16 carry legajo, and only 6 rows have documento NULL. Four of those six are these duplicates. So the documento axis is NOT absent - it is absent exactly on the hand-created login accounts, which is a different and more actionable statement: the rows created to make someone able to log in bypass the identity axis the Excel-imported registry already has. Every one of the 149 unbound+documented rows is covered. WHY IT IS NOT A UNIQUE INDEX. nombre+apellido cannot be a key - homonyms are legitimate in a 159-row professional registry and would become legitimate at scale - and both rows carry a real distinct email and a real distinct auth id, so neither email axis nor auth axis is violated. There is nothing malformed here to constrain away. This is a MERGE/LINK problem, not a constraint problem. WHAT IT DOES TO ENAMEL-22. An admin list keyed on grants shows one person twice in two roles with no signal they are the same human, and an exact-email lookup starting from either row can never reach the other. The UI must therefore either (a) surface a same-person candidate set, or (b) state explicitly that it lists ACCOUNTS and not PEOPLE, and never present a per-person role summary it cannot compute. Deciding which is a product call. OPEN, NOT DECIDED HERE: whether the two pairs should be merged (one person, one row, two auth ids) or kept as two accounts by design. That needs a users-to-auth cardinality decision - today the schema says ONE supabaseAuthId per users row, so merging requires a second linkage table, not an UPDATE. Related: [[ENAMEL-1]] Ley 25.326, since duplicate live records of one data subject bear on rectification and access rights. Depends on ENAMEL-22 for the surface and on db-enamel-cc for any schema answer. ALSO CORRECTS TWO STALE NUMBERS carried in fleet memory and in earlier ENAMEL-22 events: live users is 159 not 157, and bound (login-capable) is 9 not 3.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 5w ago
  • wi-cli-venus priorityChanged · 5w ago
    2
  • wi-cli-venus assigned · 5w ago
    coder-enamel-cc
  • wi-cli-venus audit-finding · 5w ago
    MEASURED LIVE 2026-09-11, and it sharpens this row from a description into a discriminator. 159 users rows. 6 have documentoNumber NULL, all 6 live (deletedAt null). Grouping live rows by lower(trim(nombre||' '||apellido)) returns exactly two groups with more than one row: 'elazar pimentel' (2 rows, 8157954d / 4ce4ed1e) and 'sebastian puia' (2 rows, 6ebaffae / 4304d643). ALL FOUR duplicate rows are documentoNumber NULL.\n\nSO THE TWO FACTS ARE THE SAME FACT: 100% of the duplication is documento-NULL, and 4 of the 6 documento-NULL rows ARE the duplication. The documento axis is not merely unused on these rows - it is disarmed precisely where it would have prevented the problem, which is why 'add a uniqueness constraint on documento' was never going to be the fix on its own: there is nothing to be unique ON.\n\nCONSEQUENCE FOR enamel_055, sent to coder02-enamel-cc and db-enamel-cc rather than left for them to hit. The proposed CHECK (documentoNumber is not null or documentoAbsenceReason is not null) NOT VALID grandfathers the 6 rows at add time, because NOT VALID skips the back-scan - but the constraint IS enforced on every subsequent INSERT AND UPDATE. From the moment it applies, any UPDATE to one of those 6 rows fails unless the same statement sets one of the two columns, including an edit unrelated to documents such as a phone number. enamel_045's unconditional BEFORE UPDATE updatedAt trigger does not rescue it: it touches neither column.\n\nNOT RAISED AS AN OBJECTION. For the 4 duplicate rows it is arguably the forcing function we want - you cannot touch a duplicate without declaring its identity. It has to be a decision rather than a surprise: either accept that the 6 are uneditable until someone supplies a value, or have the edit form always submit both columns. FLAGGED AGAINST THE SECOND OPTION: if the form defaults documentoAbsenceReason to a placeholder like 'not provided', the disarmed state acquires a compliant shape and the 4 duplicates become permanently invisible to the constraint added to surface them.
  • wi-cli-venus assigned · 5w ago
    coder02-enamel-cc
  • wi-cli-venus decision · 5w ago
    PRODUCT RULING, given as GO on Elazar's behalf under the standing grant (Elazar 2026-09-11 09:22/09:27), so this row is no longer parked on a product call. The description closes on a fork - (a) surface a same-person candidate set, or (b) state explicitly that the surface lists ACCOUNTS and not PEOPLE and never present a per-person role summary it cannot compute. TAKE (b). Reasoning, so it is re-derivable and not just an instruction: (b) is the only option that is CORRECT rather than approximately correct. The row's own measurement establishes there is nothing malformed to constrain away - two real distinct emails, two real distinct auth ids, homonyms legitimate at 159 rows and more legitimate at scale - so any same-person set is a HEURISTIC, and a heuristic presented in an admin surface that governs a revoke is a new failure mode rather than a fix: it will both miss pairs and assert pairs that are not the same human, and the admin cannot tell which from the UI. (b) costs a label and a deletion, ships now, and removes the specific wrong answer named in the row, which is an admin list showing one person twice with no signal. (a) is not refused, it is SEQUENCED - file it as its own row if and when someone wants merge/link, because it is a real feature with a real design and it does not belong smuggled into a labelling fix. ACCEPTANCE, and note it asserts an ABSENCE, which needs naming rather than sampling: the surface says ACCOUNTS in the UI text, and there is no per-person aggregate anywhere on it. Name the routes/components inspected and say what was NOT exercised - do not write verified without that. SCOPE BOUNDARY: this row does not touch users rows, does not merge, does not add an index, and does not change any grant. ENAMEL-34 is inProgress on resolveOrCreatePaciente - do not enter that function. ADJACENCY worth knowing before you touch anything: user 6ebaffae-a195-459e-afe8-b89c3fd73fc0 in group bac94f97 is the SAME principal the EVO-83 close-proof depends on (client:terra + client:venus), which is the portal you just shipped as 01c0845. If a labelling change alters how that identity renders anywhere in the portal, say so in the report - it would move the close-proof, and that is the one thing here that can reach outside enamel.
  • wi-cli-venus completed · 5w ago
    SHIPPED ffde6e5 (app v0.12.0), scoped to app/src/app/(protected)/usuarios/page.tsx. Took (b) per the ruling. Inspected readManagedUsers (actions.ts): already groups per users.id (per-account), appKeys scoped per-account - no per-person aggregate existed to remove. The defect was the visible caption calling rows "Personas" while the query lists accounts. Changed the caption to state CUENTAS explicitly, with a one-line note that two accounts can belong to the same person and the screen cannot tell. Reworded the summary line to avoid "cuenta" meaning two different things in one sentence. Left untouched: the [id] detail page's existing readSamePersonCandidates advisory marker (ENAMEL-22, already labelled "otras cuentas") - the sequenced (a) path, not built here. /usuarios/nuevo and person-forms.tsx keep "persona" wording for the create-a-row form (a different question, out of scope). NOT exercised: no browser render (tsc clean, 67/67 scoped + 237/237 full vitest, no visual check); no sweep of other surfaces for the same wording - only /usuarios per ENAMEL-22's own comment naming it as the surface in question. Adjacency: no /portal files touched, so the 6ebaffae/EVO-83 close-proof rendering is unaffected. Boundaries held: no users-row edits, no merge, no index, no grant change, did not enter resolveOrCreatePacienteForApp.
task
5w ago by wi-cli-venus
5w ago
2026-09-12 01:27