basquetWi + New ticket
terra TERRA-140

Operador selects the ayudante so one load records the whole team quirurgico - client gates go-live on this

Done high ctcoder-terra-ca

THE CLIENT GATES GO-LIVE ON THIS, in his own words: "Si logramos esto esta para arrancar." Nothing else in the returned consultas document or the thread carries that weight, so this is the item to sequence first among the new ones. His request, 2026-08-30 11:29, twice in identical form: "COMO FUNCION DE LA APP ( yo solo propondria): una cosa a ver si se puede resolver: aparece la opcion rol en la Cirugia... Pero podriamos hacer que el q opera pueda seleccionar al ayudante ( asi uno solo realiza la carga y figura todo el team quirurgico????? Como hacemos en las de cirugia." WHAT HE IS ACTUALLY ASKING FOR, and it is a data-model change dressed as a form field. Today "rol en la practica" records the ROLE OF THE PERSON LOADING - operador, ayudante or observador - so a surgery with three participants requires three separate practica loads, each person recording only themselves, and nothing in the data says they were the same surgery. He wants ONE load by the operador that names the rest of the team. That turns a practica from a per-person record into a per-surgery record with participants, which is a different shape, and "como hacemos en las de cirugia" says Maxilo already does it - so there is a reference implementation to read rather than a design to invent. READ MAXILO FIRST. This is the second item pointing at the same place: consulta 3 came back "es igual en todo", and TERRA-139 plus this one both hinge on how Maxilo models participants. Do the Maxilo read once and let both items use it. THE HARD QUESTIONS ARE NOT UI, and they need answering before any form work. Does an ayudante named by someone else get a practica that counts as THEIRS for their own totals and for Informes - almost certainly yes, since the point is that the team is recorded. If so, a docente or alumno can now create a record attributed to another person, which is a write on someone else's behalf and terra has no such path today. What stops A naming B on a surgery B was not at? Can B see it, and can B contest it? Is the operador the only one who may name others, or may an ayudante too? These are the client's decisions, not ours, and they are cheaper to ask now than to retrofit. DB SHAPE IS NOT A FREE CHOICE HERE. terra enforces cardinality and vocabulary rules in the DATABASE, not in the Node action - see the standing rule and TERRA-130, where docente-controlador membership is checked ONLY in the action and is therefore not a guarantee. A participants table with a role vocabulary and a uniqueness rule per (practica, person) belongs in db-terra-ca's lane and lands as a migration, with the app enforcing the same rule for the message only. Do not ship the form against a permissive table and call the invariant done - that is exactly the TERRA-136 shape already recorded on this project. Blocked pending Elazar's scope call and the client answers above. Filed at priority 1 because the client gates go-live on it, not because it is ready to start.

Activity

  • wi-cli-venus created · 6w ago
  • wi-cli-venus note · 6w ago
    THIS ITEM IS MUCH SMALLER THAN FILED, and I was wrong to describe it as a data-model change. Measured in the source rather than assumed, after db-terra-ca reported the table already exists. `terraPracticasParticipantes` is present, populated and rendered TODAY. It carries practicaId, userId and rolEnPractica with soft-delete and role/vocabulary guards, `practica-actions.ts:555` INSERTs into it on every save, `/practicas/[id]` renders a Participantes section, and `/mi-cursada` LEFT JOINs it. So the per-surgery-with-participants shape the client is asking for is ALREADY THE SHAPE. It is not a migration. THE ACTUAL GAP IS THE INPUT, AND IT IS TWO PEOPLE WIDE. `buildParticipantes` in practica-participantes-core.ts takes exactly `authorId`, `rolPropio` and `docenteControladorId` - so a save can name the author in one rol and one docente controlador, and nobody else. The client wants the operador to add ayudantes. That is a picker plus a third input to an existing function, not a new table. THE FILE ALREADY MODELS THE DISTINCTION THE CLIENT IS ASKING FOR, in a comment written before he asked: SELF_ROLES is "the roles a person may claim for THEMSELVES", and docente/supervisor are deliberately excluded because they are "roles OTHER people hold on your practica". So naming another person in a rol is an established idea in this code, currently implemented for exactly one case - the docente controlador - and the request is to generalise it. WHAT DOES NOT SHRINK, and it is the whole risk. The docente-controlador precedent is ALSO the cautionary one: TERRA-130 records that its membership is validated only in the Node action, so the DB does not guarantee it. Generalising that path multiplies an unguaranteed write rather than adding one. The authorization question is unchanged and still the client's to answer - what stops A naming B on a surgery B was not at, does B see it, may B contest it, and does a practica named by someone else count as B's for their own totals and in Informes. db-terra-ca's framing is the right one: the design must separate record AUTHOR from participant ATTRIBUTION, and the DB must validate the target's Terra membership and the operator cardinality rather than trusting Node. Also note buildParticipantes deliberately writes TWO ROWS for one person holding two roles and calls that correct, not a duplicate - a docente who operated on a practica they also supervised. Any picker must not collapse that. The Maxilo read is still worth doing for the UX of naming people, but it is no longer a prerequisite for knowing the data shape. Re-briefing coder-terra-ca accordingly.
  • wi-cli-venus statusChanged · 6w ago
    Implementation started: extend existing participant-entry UI/action while preserving multi-role rows and null=no-row; add unit coverage. TERRA-139 roster-picker gate will be covered separately under its own item.
  • wi-cli-venus statusChanged · 6w ago
    Dispatched to coder-terra-ca 2026-08-31 with the scope correction that matters: this is NOT a data-model change. terraPracticasParticipantes already exists, is INSERTed at src/lib/practica-actions.ts:555 and is already rendered, and practica-participantes-core.ts already exposes SELF_ROLES and buildParticipantes. Form plus a second participant row. Two invariants that would ship green if missed: two rows for one person holding two roles is CORRECT and must not be collapsed, and a null rol writes NO ROW.
  • wi-cli-venus spec · 6w ago
    DB SIDE ANSWERED, AND THE ANSWER IS THAT NO MIGRATION IS NEEDED. db-terra-ca, read-only: 1. VOCABULARY ALREADY HAS IT. Active terra rolEnPractica values: operador (10), ayudante (20), observador (30), docente (40), supervisor (50). Seeded in db/migrations/applied/terra_1_seed_vocabulary.sql under the shared rolEnPractica category, enforced live by fn_terraParticipantesVocabGuard via resolveScopedVocabulary('terra','rolEnPractica'). No seed migration. 2. UNIQUENESS PERMITS THE FEATURE. Live partial unique index is uq_terraPracticasParticipantes_live (practicaId, userId, rolEnPractica) WHERE deletedAt IS NULL. So one person MAY hold operador AND ayudante on one practica, and two different people MAY both be ayudante. It rejects only same person + same role + same live practica. 3. NO CARDINALITY LIMIT. Only a primary key and two FKs; zero live check constraints. N ayudantes are allowed today - two, five, more. The FORM is the only limiter, which means the cardinality decision is a UI decision and not a schema one. So the go-live gate is PURE APP WORK. That is worth stating plainly because the row has been sitting as though it needed database design. THE REAL DESIGN PROBLEM IS A DISCLOSURE ONE, AND IT IS THE SAME CLASS AS TERRA-139. To credit an ayudante the form must write a USER ID, not a typed name - a free-text name records nothing in that person's own history, which defeats the entire point ("one load records the whole team"). So the author needs a PEOPLE PICKER. And the author of a practica can be an alumno: PRACTICA_ROLES includes alumno. So shipping this hands every alumno a picker listing other people. MY DECISION, and I am recording the reasoning because it is the part that has to survive: THE PICKER IS ALLOWED FOR ALUMNOS, NAMES ONLY. A cátedra is a teaching cohort whose members already know each other by name and work in the same room; a list of names is not a disclosure to a peer in the way it would be to an outsider. What stays staff-only is the /alumnos ROSTER VIEW - legajo, practica counts, per-person records - which is an administrative view of other people's work and is what ROSTER_ROLES actually protects. The distinction is NAME versus RECORD, not list versus no-list. CONSTRAINTS THAT FOLLOW FROM THAT, and they are not optional: - The picker returns id + display name ONLY. No legajo, no email, no counts, no practica history. If it reuses getAlumnos (lookups.ts:265) note that query already returns legajo as a FALLBACK label when nombre/apellido are empty - that is a leak into this surface and needs a separate query or a label that degrades to something non-identifying. - It lists cátedra members, not all users. Scope by appKey exactly as getAlumnos does. - Do NOT reuse the TERRA-139 alumno picker component or its gate. Same shape, different authorisation: that one is gated ROSTER_ROLES because it filters other people's records; this one is open to PRACTICA_ROLES because it names a teammate. Two pickers that look identical and are gated differently is precisely the pair a later editor collapses - comment both. OPEN, NOT DECIDED BY ME: whether the named ayudante should be able to REMOVE themselves from a practica they did not create. Somebody can be named without their knowledge, and the record then asserts they were present at a surgery. That is a real question about a clinical record and it is Elazar's or Puia's, not mine. Filing it rather than answering it - the feature ships without it, because today nobody can name anybody at all, so the status quo is not made worse.
  • wi-cli-venus audit-finding · 6w ago
    IMPLEMENTED, AUDITED, HELD ON A COMMENT. 417/417 on my own full run, not taken on report. CORRECT AND VERIFIED IN SOURCE: - getPracticaTeammates (lookups.ts:251-267): id + name only, appKey-scoped, nameless fallback is "Miembro de la catedra sin nombre registrado" and NOT legajo. That was the specific leak I flagged - getAlumnos falls back to legajo as a label - and it is closed. Its comment states the NAME versus RECORD ruling and points at getAlumnos, so the two pickers name each other. - Membership validation (practica-actions.ts:561-572) runs INSIDE the transaction against vUserGroups scoped to APP_KEY. A tampered payload naming an arbitrary enamel account is refused. - buildParticipantes extended with ayudanteId; no migration needed, as db-terra-ca established. THE HOLD, AND IT IS NOT UNTIDINESS. practica-actions.ts:521-538 - inherited, predating this work - asserts that terraPracticasParticipantes "has NOTHING ... No CHECK on rolEnPractica, no unique, no vocabulary guard" (measured 2026-08-23) and that "db-terra-cc IS ADDING the partial unique and the vocabulary guard (TERRA-115)". Both shipped: terra_44 is applied and I read it - unique index at :29, fn_terraParticipantesVocabGuard at :33, trigger bound at :72 - and db-terra-ca re-measured it live today. WHY A FALSE COMMENT IS AN ACTIVE HAZARD HERE RATHER THAN A STALE NOTE. The same block says "a Node check against a DB that also enforces is redundant" and "Delete this only if it is the trigger that replaces it." THE TRIGGER NOW EXISTS, so a reader checking that condition finds it satisfied and deletes the block. But the two guarantees are different: - fn_terraParticipantesVocabGuard enforces the ROL VOCABULARY - that 'ayudante' is a legal code. - The Node check enforces MEMBERSHIP, and NOTHING in the DB does. The FK only proves users.id exists, and users is SHARED WITH ENAMEL. Deleting the Node block on the grounds that a guard landed silently reopens the exact hole this feature would otherwise create: any enamel account credited as ayudante on a terra clinical record, with a perfectly valid rol that the vocabulary guard has no reason to reject. A COMMENT CARRYING A DELETION CONDITION IS A LOADED INSTRUMENT: when the condition becomes true the comment starts instructing rather than describing, and nothing re-checks whether the reasoning behind it still holds. This one was written when the DB had no guard at all, so "the DB will enforce it" was true of the ROL and was never true of MEMBERSHIP - the distinction did not matter when neither existed and decides the outcome now that one does. THIRD INSTANCE OF THIS CLASS TODAY in one file family: pool-identity-classify.ts (three rows vs five), the lookups.ts quotation carried forward as current fact, and now this. A fact repeated in two places has two expiry dates and one correction. Push held until the comment is corrected: state the guard as LIVE, separate DB-owns-ROL from Node-owns-MEMBERSHIP, and remove the deletion condition.
  • wi-cli-venus note · 6w ago
    SHIPPED AND VERIFIED LIVE, not on the pusher's word. coder-terra-ca reported main 34f3167ea3ebbf452295067542688c94061e71a2 at 08:14 ART; I confirmed it independently through Vercel, which is the only read path this lane has since manual git is banned here. Production deployment dpl_HwABk6oy4bi6YXPAe84piaxfB6is is READY on target production, githubCommitRef main, githubCommitSha 34f3167ea3ebbf452295067542688c94061e71a2, repoPushedAt 1788174857000. So the commit exists on the remote default branch AND the build that came out of it is serving. That is stronger evidence than a git log line, which would only have proven the local ref. WHAT SHIPPED: the named-ayudante attribution path. src/lib/practica-participantes-core.ts extended with ayudanteId; src/lib/lookups.ts gained getPracticaTeammates, id and name only, appKey-scoped, falling back to a generic catedra-member string and deliberately NOT to legajo the way getAlumnos does, because a teammate picker discloses a NAME to a cohort peer while an administrative roster discloses a RECORD. 417 tests, typecheck, 15 audited files staged. THE HOLD THAT WAS CLEARED FIRST: practica-actions.ts:521-538 carried a comment whose deletion condition had come true. It said the Node membership check was redundant once db-terra-cc landed terra_44's unique index and vocabulary guard, and terra_44 IS applied. Deleting the block on that reading would have readmitted any enamel account as a credited ayudante on a clinical record. The two guarantees are not the same guarantee: fn_terraParticipantesVocabGuard proves the ROL code is legal, and nothing in the database proves the PERSON belongs to terra, because the FK only proves users.id exists and users is shared with enamel. The comment now states that split and the membership query stays, scoped to APP_KEY inside the transaction. ONE DEVIATION WORTH RECORDING RATHER THAN LETTING IT PASS. The commit subject is a bare conventional-commits line with no work-item reference and no Agent trailer, unlike every other commit from that lane this week, which all carry a timestamp prefix, the TERRA number and the agent name. The consequence is not cosmetic: the only artifact tying this deployment to this row is a hub message, and a hub message lives in one recipient's context window until it is compacted away. Anyone reading the repository six months out sees a deployment with no route back to the reasoning above. Recorded here so the link survives in the place that is actually durable. STILL OWED: nothing on this row. Post-push behavioural verification of the picker itself needs an authenticated session, which this lane does not have.
  • wi-cli-venus completed · 6w ago
    Named-ayudante attribution shipped on main 34f3167 and live in production (Vercel dpl_HwABk6oy4bi6YXPAe84piaxfB6is READY). Verified independently of the pusher. The authorization questions the client still owes - who may name whom, whether the named person sees or contests it, whether it counts toward their totals - are TERRA-146 and are not part of this row.
task
6w ago by wi-cli-venus
6w ago
2026-08-31 11:21