PLUTO-77 ·
plutoF-6/voice-note: jefe-de-TP cross-linking via ayudante role drags wrong roster. Real teacher (Ale): sees his own ~8-10 alumnos OK, but ALSO sees ALL alumnos of ANOTHER jefa from a different group/day, because he is ayudante of that jefa on Wednesday -> the vinculation links him to the Wed jefa and surfaces her students in his correction view (~20+ total -> confusion). PROD real user. Same class as PLUTO-70 (resolveTeacherFilterIds dual-role); investigate the jtpAyudantes->jefa linking path - is this a PLUTO-70 gap (ayudante path uncovered) or new. He CAN evaluate his own; the bug is the extra roster bleed-in.
- Ref
PLUTO-77(#977)- Project
pluto- Status
- done
- Priority
- high
- Type
- task
- Assigned
- coder-pluto-cc
- Created by
- wi-cli-venus
- Created
- 2026-06-13T05:24:00.876Z
- Updated
- 2026-06-13T07:43:38.728Z
- Closed
- 2026-06-13T07:43:38.728Z
Questions
No questions.
Event log
-
JOINT DESIGN (option B, expand/contract). Root: fn_resolveDataAccessScope effective CTE unions parent-jefe bare jtpId for ayudantes, losing comision gate. SCOPE EXPANSION (coder catch): effectiveJtpIds ALSO gates write/authz paths practica-actions.ts:162 (student-scope validation),:438 (getPracticas grading list), export-actions.ts:74 — display-only fix = ayudante can GRADE/EXPORT parent's other-comision students = fraud hole. Write paths IN-SCOPE. DESIGN: Phase-1 db additive fn field effectiveJtpScopes = jsonb [{jtpId,comisionId}] (comisionId null=ungated own-jefe; one entry per jtpAyudantes (parentJtpId,comisionId) = ayudante gated to assist-comision); existing fields UNCHANGED so old TS safe. Phase-2 coder: access-scope.ts derive jtpIds[]/gateComisionIds[]; replace flat ANY() predicate with pair-aware unnest(jtp[],gate[]) EXISTS on 6 roster sites + the 3 write/authz sites; ungated callers pass all-null=backward-compat (no PLUTO-70 regression). ROLLOUT: fn migration live BEFORE TS deploy, consistent every step, no contract phase. VERIFY Mastricchio: effectiveJtpScopes=[{self,null},{Silvia,WedComision}] -> own Viernes + Silvia Miercoles-only, Silvia Viernes excluded. GATES: audit design-ping + Elazar semantic (read keep-Wed + write grade-vs-viewonly) before implement.
-
DESIGN FINAL (held per Elazar before implement). db co-signed: additive effectiveJtpScopes [{jtpId,comisionId,via:'jefe'|'ayudante'}]; jefe entry {self,null,'jefe'} ungated, ayudante entries from jtpAyudantes (carry specific comisionId), via discriminator makes view-only a one-bool write-scope filter. fn STABLE/read-only no archive ctx. Rollout: fn migration 019 live BEFORE TS deploy, additive=no half-broken window. File split clean (db: 019+schema.md; coder: access-scope.ts+role-view-scope.ts+6 queries.ts roster sites+practica-actions.ts:162/438+export-actions.ts:74). FOLLOW-UP (data-hygiene, separate WI candidate): coder found 1 orphan studentAssignments row in active periodo (jtpId->jefe but comisionId NOT in that jefe's comisionJtps) — validates the null-comisionId jefe branch (concrete pairs would have dropped the orphan student); the anomaly itself = separate data-hygiene note. OPEN (audit deciding): accessibleComisionIds metadata bleed in/out of scope.
-
AUDIT VERDICT (banked, held): CHANGES — cleaner root fix than Option B. ROOT single-sourced: fn ayu_com CTE DISCARDS jtpAyudantes.comisionId (NOT NULL, already correct) and re-derives from PARENT comisionJtps -> parent's FULL comision set => BOTH vectors. VECTOR 2 (write-authz+metadata) closes FN-ONLY, zero consumer edits: set ayudanteComisionIds := array_agg(jtpAyudantes.comisionId), drop ayu_com parent join. NOT just metadata — gates 5 WRITE sites (comentario-actions:74, practica-actions:477/1068/1126, practicas/[id]/page:72) + 5 visible filters. Shipping B without this = LIVE grading-authz hole (Mastricchio edits/grades Silvia Viernes) -> must fold in. VECTOR 1 (effectiveJtpScopes pair-predicate) needs consumer migration, list WIDER than stated: also +practicas/[id]/page:71 (authz grant via effectiveJtpIds.includes — miss=practica-level access leak), nueva:40, page:156, alumnos/page:57, alumnos/[id]:54, informes:104/173, role-view-scope:10-23. via-refinement: surface TWO named lists readScopes(all)/writeScopes(via=jefe unless ayudanteCanGrade); write sites key on writeScopes. ROLLOUT: fn fix closes write-authz+metadata INSTANTLY on migration (before TS); residual TS-window = display-only over-LIST, strictly improving, no half-applied over-grant. RESUME PLAN: fn fix = priority half (fast safe close of the live hole) then Vector-1 consumer migration.
-
ELAZAR SEMANTIC DECIDED. VIEW: (a) MAIN list = user's OWN students only (his jefe roster), always, unmixed — kills the volume-confusion. (b) SECONDARY list behind a TOGGLE 'Ver alumnos de comisión (día)', shown only for docentes who have a secondary (ayudante) relationship to a comisión-day; toggle ON -> shows the FULL roster of that comisión-day (Silvia Miércoles), ALL-OR-NONE because Pluto has NO per-student ayudante field on práctica (can't tell which students he assisted). Separate from main. GRADE/WRITE: ONLY the JTP set ON THE PRÁCTICA may grade/edit/comment (present + legally-responsible dr). Gate = practica.jtpId == currentUser (PER-RECORD identity), NOT roster/comisión-membership. Ayudantes never grade (for now; future: allow when JTP unavailable, deferred). => write gates at comentario:74 + practica-actions:477/1068/1126 must switch from canAccessComision/roster-membership to practica-JTP-identity = closes the live hole fully (Mastricchio cannot grade ANY Silvia student, Wed or Fri). fn ayu_com SSOT fix still needed so the secondary toggle shows the CORRECT comisión-day. IMPL NOTES: verify practica row carries authoritative jtpId for grade gate; handle pure-ayudante edge (empty own-roster -> comisión-day list is their primary).
-
Elazar GO (2026-06-13). View taxonomy FINAL: pure-jefe = own-roster only, no toggle. pure-ayudante (empty own-roster) = the comision-day-roster they are ayudante of IS their MAIN list (read students+practicas), no toggle. mixed (jefe AND ayudante elsewhere, e.g. Mastricchio) = own-roster main + toggle 'Ver alumnos de comision (dia)' showing the secondary day-roster all-or-none. GRADE gate in ALL cases = practica.jtpId==currentUser (per-record identity), not roster/comision-membership; ayudante sees-but-cannot-grade; delegation deferred. Hold LIFTED, dispatching coder+db+audit.
-
Gate B UNBLOCKED (coder verified). Authoritative grade column = practicas.assignmentJtpId (FK->users.id, snapshot col), 100% populated live (53/53, 0 null incl. all non-demo). Grade gate B = practica.assignmentJtpId==currentUser — NO ungradeable-null hole. NOTE column name is assignmentJtpId (NOT bare jtpId) — sibling snapshot cols assignmentComisionId/assignmentAdjuntoId present if adjunto path needed. All 3 agents picked up; coder holding TS until db 019 live (expand/contract); audit owns pre-apply(019 sql)+pre-push(diff)+PTD gates.
-
DB HALF DONE+VERIFIED LIVE. Migration 019 SHA f87fab4 v1.68.1: effectiveJtpScopes added + ayu_com SSOT fix (single-sources comision gate from jtpAyudantes.comisionId, drops parent-comisionJtps join). Audit PASSed pre-apply; PTD PASS:f87fab4 (fn live, jtpAyudantes.comisionId NOT NULL verified). Write-authz hole closed fn-only as of this SHA. Coder building TS half (A pair-predicate / B grade gate assignmentJtpId / C view taxonomy), ETA ~30-40min, audit pre-reviews diff before push.
-
LIVE-STATE CONFLICT (caught by coder, pre-deploy). f87fab4 committed the migration FILE+schema.md but coder's dump from DATABASE_URL_DIRECT shows the prod-primary fn is STILL the flat pre-fix version: NO effectiveJtpScopes, ayu_com still over-broad, single overload position 0. Contradicts db+audit PASS:f87fab4 (which read a pooler surface, not the primary). DDL did NOT persist to primary. Expand/contract held BECAUSE coder verified before building. Ordered: db to re-run 019 CREATE OR REPLACE via DATABASE_URL_DIRECT + paste pg_get_functiondef proof; audit to re-pull from primary; coder holds (tree clean, 0 edits) until verified-direct dump confirms. Write-authz hole NOT closed until then.
-
CONFLICT RESOLVED — fn IS live on prod primary. Audit direct-DSN proof: host db.fdwjmzjwurbpkxersigg.supabase.co:5432 (primary, not 6543 pooler), oid 43519 single overload, effectiveJtpScopes in live body L102+L112-113, ayu_com reads jtpAyudantes.comisionId (SSOT fix). Root cause of coder's contradicting dump = STALE connection predating db's 03:39 CREATE OR REPLACE commit (or pre-commit conn reuse), NOT a primary/pooler split or two-DB problem; position() test was also a false-negative vs LIKE. Gate-1 GREEN + PASS:f87fab4 stand. Coder re-running on fresh connection then proceeds A/B/C; audit gate-2 (TS diff) next. Write-authz hole CONFIRMED closed fn-only.
-
FALSE-GREEN ROOT CAUSE (caught by coder app-path invocation; audit retracted PASS:f87fab4 semantic). Migration 019 wrote CREATE OR REPLACE FUNCTION public."fn_resolveDataAccessScope" DOUBLE-QUOTED. Postgres treats quoted identifiers as case-sensitive -> created a NEW distinct fn (oid 43519, camelCase, WITH the fix). The app (access-scope.ts:25) calls it UNQUOTED -> Postgres folds to lowercase -> binds the ORIGINAL fn_resolvedataaccessscope (oid 42915, pre-fix, ayu_com bleed intact, no effectiveJtpScopes). So 019's fix landed on an orphan the app never calls; the write-authz bleed was NEVER closed. PASS:f87fab4 = valid DEPLOY fact but FALSE semantic. Both args identical (uuid,uuid,text[]) -> NOT an overload split, a NAME-CASING split. FIX = migration 020: CREATE OR REPLACE the UNQUOTED name (replace 42915 in place) + DROP FUNCTION public."fn_resolveDataAccessScope"(uuid,uuid,text[]) (the quoted orphan 43519); commit as 020+schema.md+db:export+push (NOT a live-only hotfix; 019 file superseded-by-020). GATE: invoke via the UNQUOTED app call path, assert effectiveJtpScopes present + exactly one resolveDataAccessScope fn remains. PROCESS FIX adopted: fn PTD verifies by INVOKING through the app's exact call, never definition-grep alone.
-
FN FIX GENUINELY LIVE. Corrective migration 020 pushed SHA 9743ea9. Gate proof (app call path): exactly ONE fn fn_resolvedataaccessscope(uuid,uuid,text[]) oid 42915 (orphan camelCase 43519 dropped); live UNQUOTED invocation returns key set incl. effectiveJtpScopes + accessibleComisionIds + ayudanteComisionIds (SSOT). 019 file kept superseded-by-020 (immutable). Write-authz/metadata bleed now closed at fn level on the surface the app actually binds. Coder unblocked for TS half (A pair-predicate / B assignmentJtpId grade gate / C view taxonomy). Pending: audit PTD on 9743ea9 (Vercel build+runtime+app-version curl) + retro-confirm 020 SQL; then coder TS diff -> audit pre-push -> coder push -> audit PTD -> FINISHED.
-
DB HALF DONE + PROPERLY VERIFIED. audit PASS:9743ea9 (verified the right way = live unquoted invocation, not body grep): (1) 020 .sql = DROP quoted orphan 43519 + CREATE OR REPLACE unquoted (replaces 42915 in place), body byte-identical to approved 019; (2) live DB: exactly ONE fn oid 42915 lowercase has_scopes=true, orphan gone, app's exact SELECT fn_resolveDataAccessScope($1,$2,$3) returns effectiveJtpScopes+accessibleComisionIds; (3) Vercel 9743ea9 READY, runtime clean, live /api/app-version=1.68.2==package.json. Write-authz/metadata bleed CLOSED fn-only on the app-bound fn. Coder building TS half (ETA ~40-50min from 03:54), audit on gate-2 (TS diff pre-push) next.
-
TS DESIGN LOCKED + greenlit (coder). A: access-scope.ts adds effectiveJtpScopes:JtpScope[]{jtpId,comisionId|null,via} + helpers jefeScopes/ayudanteScopes/isMixedTeacher/scopePairArrays; parallel $gate array beside $jtp in 7 queries.ts roster fns; predicate CASE null-gate=ANY($jtp) [admin path preserved] ELSE unnest($jtp,$gate) EXISTS pair-match [kills cross-day bleed]. B: grade gate comentario-actions:74 + practica-actions:477/1068/1126 -> assignmentJtpId==currentUser.id (toggle-independent). C: mixed-teacher toggle ?verComision=1 searchParam default OFF (jefeScopes only = secure default; secondary roster never shipped to browser unless toggled); pure-jefe/pure-ayudante hide toggle; PM refinements: toggle only if isMixedTeacher(), secondary list visually separate+labeled 'Alumnos de comisión (día)', secondary rows render NO grade/edit affordances (sees-but-cannot-grade). Diff->audit pre-push (fraud-sensitive).
-
L1 SQL-core PASS (audit, PASS-L1-CORE). queries.ts pair-gate helpers + all gated roster/práctica fns (incl getPracticas:317 over-fill list + getStudentRanking/getOpAsistBalance/getInactiveStudents) + access-scope.ts + role-view-scope.ts + getTeacherContext inline gate. Audit independent grep: nothing missed in gated set; param-index parallelism + assignmentJtpId/assignmentComisionId snapshot cols + 3-way taxonomy correct. LIVE-verified: 0 active practicas have assignmentJtpId outside their comisión jefe set => pair-gate ≡ comisión-filter, so coder's drop of redundant ayudanteComisionIds OR-term (a real toggle-bypass hole coder self-caught) is safe. 4 carry-forward (non-blocking): (1) DELETE dead resolveTeacherJtpIds (0 callers, ungated footgun); (2) getStudentPracticasAuditLog ungated - verify alumnos/[id]+practicas student-access gate pair-correct or add jtpGates; (3) getJtpPracticasAuditLog exempt (global-only caller); (4) getComisionesForReport moot (no jtpIds). Next: coder L1-pages ({jtpIds,jtpGates} derivation + OR-term drops in informes/alumnos) + L2 grade gate; audit re-checks 1+2 on page diff then final consolidated pass gates push.
-
L2 grade gate DONE: shared canGradePractica(scope,userId,assignmentJtpId,assignmentComisionId) = global OR practica-jefe OR adjunto-of-comision; ayudante excluded (Elazar (ii)). Adjunto line = single swappable clause. Applied at 4 write sites: comentario-actions:74, updateEstadoRevision (grade write), updateNumeroHC, canWriteTeacherNoteForPractica; replaced canAccessComision (granted ayudante). Latent bug fixed: updateNumeroHC never SELECTed assignmentComisionId so old canAccessComision was always false. REQ#2 (who graded) already satisfied: updateEstadoRevision setArchiveContext(user.id) -> practicasArchive.archivedBy -> vPracticasAuditEntries byName -> Historial de cambios shows actor + before/after. Next: L3 toggle UI.
-
Grade gate = (ii) strip-ayudante-keep-adjunto (Elazar). L3 design = (A) two-fetch separate read-only 'Alumnos de comision (dia)' section, toggle+mixed-gated, list views /alumnos+/practicas only (informes no toggle) (PM). OPEN FORKS: (1) CREATE-policy escalated to Elazar — may pure-ayudante CREATE practicas (includeComisionDay on 2 create paths) or jefe/adjunto-only; PM lean=allow. (2) numeroHC unblock — coder's SELECT fix enables a previously always-denied teacher write; coder investigating whether teacher-set was intended/how numeroHC is set today; that site held until resolved. Consolidated audit pass held on both + tsc verComision fix + L3.
-
Elazar: ayudante CREATE is ALLOWED. Keep includeComisionDay=true on the 2 create paths (/practicas/nueva + createPractica). Ayudante records practicas for assisted-comision-day students; grading still blocked (canGradePractica excludes ayudante). Create-policy fork CLOSED. Consolidated pass now gates only on L3 diff-stable.
-
L3 DONE. Full diff (L1+L2+L3) diff-stable, tsc exit 0, shared-tree clean (13 PLUTO-77 files + new comision-day-toggle.tsx, no db-pluto files). L3: toggle (mixed-only ?verComision); main=jefe-only always; secondary labeled read-only 'de comision (dia)' section on /alumnos+/practicas (canApprove=false + L2 server gate); /informes no toggle. Both forks closed (grade=(ii), ayudante-create allowed). Audit pinged for consolidated pass; push gates only on audit PASS.
-
Audit consolidated PASS (14-file diff): L1 pair-gating complete + bypass dropped + footguns deleted; L2 canGradePractica at 3 docente-write sites with assignmentJtpId in each SELECT (jefe-identity fires, no silent lockout), ayudante excluded, participant/fullAccess preserved; updateNumeroHC reverted to exact prior (no unblock); L3 main jefe-only+toggle-independent, secondary mixed-only ayudante-scoped read-only (canApprove=false+server gate), informes toggle-free; tsc exit 0. Create-policy honored. Coder cleared to push --minor; audit runs post-push PTD (build+runtime+live-version + live unquoted fn sanity) -> PASS:<sha>/BLOCK.
-
Pushed beac03d v1.69.0 (L1+L2+L3 one push, 16 files 607/139). Awaiting audit post-push PTD PASS:beac03d before FINISHED.
-
PASS:beac03d — PTD fully green: Vercel dpl READY (67s build), sha match, alias bound, ZERO runtime errors, live /api/app-version=1.69.0==package.json + deploymentId match, DB fn sanity 1 copy returns effectiveJtpScopes. LIVE.
-
L1+L2+L3 shipped beac03d v1.69.0, audit PASS, live + PTD green