TODO — near-term tasks & working-tree situation¶
What this file is. A living tracker of current near-term work and the state of the working tree — the "what's in flight right now" view that
REFERENCE.md(the static project map) deliberately doesn't carry. Read it at the start of a session to know what's uncommitted, what's verified, and what's next. Keep it updated as tasks land — and note the convention: this file carries no "done" marks. A finished item is deleted entirely, not checked off or moved to a "done" list (durable record of what shipped lives in commits + the spec/plan docs + memory, not here). Bump the "Last updated" line on each edit. It is the session-to-session handoff for situational state; durable decisions still live in the spec/plan docs it links to.
Last updated: 2026-07-06
Situation¶
RLS is at FULL COVERAGE in tree (uncommitted): Wave-4 Phases 2–3 (8 PII tables) + the same-day full-coverage iteration (all 58 tenant-bearing tables, boot guards, tripwire, bypass fence). Audit #1 is structurally closed and its follow-ups resolved. App runs as non-superuser app_user; Wave-2a observe guard deleted. See docs/02-multitenancy.md §4; the closure record lives in the Wave-1→4 specs under docs/superpowers/specs/ + project memory (the point-in-time architecture-audit files were folded into docs/reviews/2026-07-03_audit.md and deleted).
Working tree (uncommitted)¶
-
Student import: grade/department error disambiguation (bugfix) 2026-07-06. In
validateStudentRows(students.service.ts) the "(grade, department) pair doesn't resolve" branch used to unconditionally emitdepartment:invalid— so a row naming a valid department that simply doesn't offer the grade (grade names are unique per-department, not global) got blamed on the department and shown the full department list as allowed values. Now three-way: grade unknown everywhere →grade:invalid; grade known + department unknown →department:invalid; grade known + department valid but doesn't offer it → newgrade:not_in_department(flags the GRADE column, grade allowedValues). New reason key registered inSTUDENT_EXTRA_REGISTRY(→FIELD_INVALID) + dedicated catalog messageimport.FIELD_INVALID.not_in_department(en/it, no placeholders → noERROR_TEXT_PARAMSentry needed). No schema/RBAC/migration change. Touched:students.service.ts,constants/import-schema.ts,error-messages.catalog.ts,students-import.spec.ts(old "reject when department does not contain the grade" test split into GRADE-flagged + DEPARTMENT-flagged cases), ch07 §3 hook example. Verify pending: user runsnpm test(students-import.spec.ts,error-messages.drift.spec.ts) + build/lint + commits. -
Option-block num-choices no longer validated against actual subject count (bugfix) 2026-07-06.
assertMinMaxSelections(curriculum.validation.ts) dropped itsmax > subjectCountcheck (wasSETUP_VALIDATION_FAILED / max_exceeds_subjects, i.e. "maxSelections exceeds number of alternatives") — an admin builds blocks incrementally, so declared min/max must be independent of how many alternatives are present right now. Kept themin > maxself-consistency check (not count-based). Selection-time (selection-consistency.ts) + assignment-time (subject-group-eligibility.ts) checks — which count a student's actual choices — are unchanged.subjectCountparam + caller arg removed;max_exceeds_subjectsreason retired fromerror-messages.coverage.ts,error-messages.catalog.ts(en/it), and the ch06 §validation-reasons table; the validation spec's "rejects max > alternatives" test inverted to assert acceptance. No schema/migration/RBAC change (no DB CHECK enforced it). Verify pending: user runs build/test/lint + commits. -
Curriculum READY gate for classes — REVERTED 2026-07-06 (reversal uncommitted; the gate itself had been committed
d7c6b77). Product decided classes must be creatable while a curriculum is still DRAFT, so the just-landed READY gate was undone. Removed both create gates (POST /homeroomsresolveCurriculumForWizard,POST /subject-groupsanchor check) —Curriculum.statusno longer gates class creation, only referent selection eligibility (RUS-4) + the selection-window prereq. Removed the READY→DRAFT demote classes guard (assertNoClassesForCurriculum+countClassesForCurriculum) fromswitchCurriculumStatus; demote is now refused only on selections (CURRICULUM_IN_USE_BY_SELECTIONS) and any attached homerooms/SGs carry over into DRAFT untouched (the deadthrowIfSubjectGroupInUsecatch was kept — still live inupdateCurriculum). Grouped boards (grouped-homerooms/grouped-courses) now enumerate DRAFT and READY curricula (dropped thestatus:'READY'skeleton filter). Both ErrorCodesCLASS_REQUIRES_READY_CURRICULUM+CURRICULUM_IN_USE_BY_CLASSESremoved cleanly (enum + params +ERROR_TEXT_PARAMS+ catalog en/it + examples; both drift specs stay balanced since every side removed together). The hard-delete block (removeCurriculum→guardAndClearSubjectGroupsForCurriculum, "no courses with students") is the only class↔curriculum constraint left — unchanged, as is the subject-removal grid guard. No schema/migration/RBAC change. Touched:homerooms.{service,service.spec,swagger}.ts,subject-groups.{service,service.spec,swagger}.ts,curriculum.{queries,service,service.spec,swagger}.ts,error-codes.ts,error-messages.catalog.ts,error-examples.ts,test/curricula.e2e-spec.ts, ch14 §1.3, REFERENCE rows (curriculum/homerooms/subject-groups). The original gate's spec+plan (docs/superpowers/specs|plans/2026-07-06-curriculum-ready-gate-for-classes*) are left as-is (point-in-time record of the reverted gate). Verify pending: user runs build/test/e2e/lint + commits. -
Grouped-courses iteration 3 (grade level + per-subject coverage) —
GET /subject-groups/grouped-coursesgrows agradenode (dept → curriculum → grade, mirroring grouped-homerooms): SG leaves move onto the grade node, which also carries uniform plan-coverage (numStudents,numStudentsWithMissingAssignments) + teacher-coverage (numSubjectGroups,numSubjectGroupsWithoutTeacher) counts and a catalog-complete per-subjectsubjects[](numStudentsExpected/numStudentsPlaced,placed ≤ expected). Curriculum-node counts are now Σ grades;numMinRequiredSubjectGroupsunchanged. Dropped the optional/mandatory split from the original FE ask (uniform: a course is a course). Data: +curriculumSelection.choiceson the student projection (studentCoverageCountSelect), +findBoardSubjectsForCurricula(one catalog query); no schema/RBAC/migration change; FE-breaking (course list relocates under grade). Touched:grouped-courses-response.dto.ts,subject-groups.{service,queries,swagger}.ts,subject-groups.service.spec.ts,test/subject-groups-grouped.e2e-spec.ts, ch14 §5.1, REFERENCE rows. Spec+plan committed74d188e/e7de119; impl in tree, verify pending: user runs build/test/e2e/lint + commits. -
Evaluation-scale
inUseflag → per-tenant (bugfix) — the RLS commit0b5f0abrouted the DISPLAYinUseflag throughadminPrisma(cross-tenant), so any preset referenced by ANY tenant readinUse:truefor EVERY tenant (symptom: "all scales in use no matter what"). Fixed by pointingcomputeInUseMap(list) +getScaleById(detail) back atthis.prisma(RLS-scoped → per-tenant at all 6 FK grains; the child-table RLS policies filter via join tocurricula.tenant_id, verified). The write/delete FK guards (referenceCounts, the tx-locked guard inwriteScaleValuesAndName,existsInUsefor the@NoTenantTxpreset-update response) stay cross-tenant onadminPrisma— "would editing/deleting break ANY tenant's FK?" is correctly global. Constructor + helper doc comments rewritten to explain the display=per-tenant / guard=cross-tenant split. Added regression blockEvaluationScalesService — inUse is per-tenant (RLS split)with distinct prisma/adminPrisma mocks (existing blocks alias both to one mock and can't tell them apart). Mechanism DB-verified via rolled-back simulation onsis_dev. No schema/RBAC/migration change. Copy-on-use for presets was considered and explicitly dropped (user decision 2026-07-03): it isn't needed for per-tenantinUse— the RLS-client split already gives per-tenant display + cross-tenant edit/delete guards with no schema change. Verify pending: user runsnpm test(esp.evaluation-scales.service.spec.ts) + build/lint + commits. -
Curriculum coordinator role + HR/Secretary split — second parametric dimension
ParameterDim.CURRICULUM+ newcurriculum_coordinatorpreset (dept-head-shaped, WRITE only on curricula/homerooms/SGs, no curricula create/delete, students READ-via-selection, teachers role-keyed pass-through) and the combinedhrsplit into personnel-onlyhr(relabeled "HR"; teachers/staff + invitations) + newsecretary(students/config/composition/invitations; no personnel health). Migration20260702123500_curriculum_coordinator_hr_secretary_split(applied to localsis_devBEFORE the DML was appended — fold-edited after apply, so runmigrate reset/docker:resetbefore the nextmigrate devor e2e run; enum ADD VALUE + hr relabel + stale hr action-grant DELETEs). NewAppliesPolicyDimensionsdecorator (metadata-only — boards/pickers keep their tight@RequireRoleswhile RolesGuard dimension-overlap admits parametric managers); homeroom-create curriculum write gate;userRoleParameterCURRICULUM cascade on curriculum delete; Tier-1backfillMissingPresetRoles(additive-only preset propagation to existing tenants);expectedPresetGrantsmirror + e2e fixtures (secretary@/coordinator@e2e.dev, Carla = Teacher-profile trick) + newtest/{curriculum-coordinator,hr-secretary-split}.e2e-spec.ts+ rewritten hr block inrbac-presets.e2e-spec.ts; ~124 specParametricContextliterals extended. Specb329e33+ planf4d89df. Verify pending: user runs build/test/e2e/lint (after DB reset) + commits. -
e2e repair (post-commit breakage): base-room migration + RLS P2002-redaction fix + setup.e2e tenant-name restore — three fixes: (1) commit
d725cecshippedSubjectGroup.baseRoomIdin the schema/client without a migration, so every SG read 500'd (P2022 missing column) — now generated+applied as20260702115115_add_subject_group_base_room(nullable column + index + FKSET NULL; ch12 hazard audit clean). Note: its UTC timestamp sorts BEFORE the hand-authored future-stamped2026070213/14/150000trio — content-independent,migrate deployapplies unapplied regardless of order. (2) On RLS-enabled tables Postgres redacts the 23505 DETAIL line forapp_user, so the pg adapter emits P2002 with noconstraint.fields→mapP2002targetStr was''→ typed 409s (e.g.SUBJECT_GROUP_NAME_CONFLICT) degraded to genericCONFLICT. Fixed inmapP2002(fallback: parse constraint name fromoriginalMessage) + unit spec + ch02 §4.0.3 — new-handler rule: match index-name tokens, not just column lists. (3)setup.e2ewalks the wizard SCHOOL step, which now syncsoperationalName→Tenant.name— thetesttenant stayed renamed andauth.e2e(multi-tenant picker asserts'Test School') failed downstream; fixed by snapshotting the seeded name inbeforeAll+ restoring it in the existing symmetricafterAll. Next: user runs the full gate + commits (migration +prisma-p2002.{ts,spec.ts}+setup.e2e-spec.ts+ ch02 + this entry). -
Grouped boards iteration 2 (
grouped-homerooms/grouped-courses) — IMPL in tree (spec24f62c8+ plan7e51856, all 8 tasks done). Replaced bothplacement-boardendpoints wholesale: catalog-driven skeleton (visible depts viaDepartmentsPolicy→ READY curricula viafindGroupedBoardCurricula→ covered grades; empty nodes present; defensive union for rows on demoted curricula), roster-free counts (homeroom placement per grade/dept +numStudentsWithoutCurriculum; SG counts:numMinRequiredSubjectGroupsstatic floor via newcountMinRequiredSubjectGroups, missing-assignment viaresolveTargetPlan, teacherless), class leaves kept (bound SGs now included on the courses board, leaf gainshomeroomref),groupBoardTreeDELETED, e2e suites renamed (test/{homerooms,subject-groups}-grouped.e2e-spec.ts; old routes now 400 via:idcapture), coordinator e2e route updated, docs (ch.14 §5.1 + recipe, ch.05 §route-merge, REFERENCE rows) rewritten, 2026-06-15 spec marked Superseded. No schema/RBAC change; FE-breaking renames. Verify pending: user runs build/test/e2e/lint + commits. -
Setup wizard INVITATIONS step removed — product resolved the long-standing "does setup own credential-sending?" doubt: it doesn't. Wizard order is now
…STUDENTS → ROLLOVER → COMPLETE; the standaloneinvitationsgroup is gone (two groups, both required); handler + spec +SetupInvitationsStateDto+ module wiring deleted; the next-steps to-do keeps its current behavior unchanged (itsINVITATIONSCTA enum is the unrelated RBAC/CTA key).SetupStep.INVITATIONSstays as a second orphaned PG enum value (CURRICULUM precedent); data migration20260702160000_remove_invitations_setup_step(hand-authored, NOT applied) rehomes any tenant parked on it to ROLLOVER — required becauseparseSetupStepnow throws on the orphan. Specs updated:setup-steps.spec,setup.service.spec(INVITATIONS→ROLLOVER terminal advance, 2-group overview),setup.controller.spec,test/setup.e2e-spec.ts. Ch08 rewritten (state machine, groups, archetypes, §6 → "Invitations are post-setup"). Verify pending: user runs build/test/e2e/lint, applies the migration, commits. -
maxAbsenceHours→minPresentHoursPercentage— Departmentconfigurationfield reshaped from an absolute absence-hours cap to a minimum-present-hours percentage (integer 1-100, nullable;@Min(1) @Max(100),null= no minimum). Inverted semantics → no data conversion: migration20260702150000_replace_max_absence_hours_with_min_present_hours_percentage(hand-authored drop+add nullable column, NOT applied), mirrors the20260611123000precedent. Renamed across schema, both DTO files (department-configuration.dto.ts+ setupdepartments-step.dto.ts—Maximport added to each), 8departments.service.tsmap/type sites, thescope-fields.tsconfigurationallowlist, and thedepartments.service.spec.tsfixture; the REFERENCE.mddepartments/row reworded. Config-only — presence computation / notifications stay deferred (unchanged). No RBAC /ErrorCodechange. FE-breaking (OpenAPI field rename on theconfigurationscope + setup DEPARTMENTS step). Spec+plandocs/superpowers/specs|plans/2026-07-02-min-present-hours-percentage*. Verify pending: user runsprisma generate+ build/test/lint, applies the migration, commits. - Remove
codefrom curriculum tracks — droppedCurriculumTrack.code+ its unique index (migration20260702140000_drop_curriculum_track_code, hand-authored, NOT applied);name(already@@unique([curriculumId,name])) is the sole track identity → sync keys tracks by name (trackIdByName), dormantselection-carrymatches by name, validation keeps only name-uniqueness. Swept DTOs/interfaces/validation/sync/service/students-read-chain/selection-carry + all their specs +test/curricula.e2e-spec.ts(rename test flipped to name-change delete+create). Bundled (option a): seed-source presets can now express code-free tracks —CurriculumPresetDefinition.tracks(newPresetTrackTemplate) +expandPresetToGridemitstracks+ seed builder threads them (no preset data added; DB/backoffice/runtime already handled tracks). Backoffice aligned (5 files:types.ts,PresetEditor,TrackMatrix,curriculum-port,curriculum-import— removedslugCode/code-dedup → name-dedup + LLM prompt copy). FE-breaking. NOT the retainedCurriculumSubject.code. Spec+plandocs/superpowers/specs|plans/2026-07-02-remove-curriculum-track-code*. Verify pending: user runsprisma generate+ build/test/e2e/lint, applies the migration, commits. - SubjectGroup base room (
baseRoomId) — additive nullableSubjectGroup.baseRoomId(Room?,onDelete: SetNull,@@index) mirroringHomeroom.baseRoomId; rides the existingsubject_groups.compositionscope (field added toSUBJECT_GROUP_SCOPES.composition, seed FIELD_MAPPINGS is single-sourced so no seed edit; no new RBAC/ErrorCode). Editable on both standalone and homeroom-bound SGs (write-time FK validation in-tenant/same-AY →VALIDATION_FAILED). Solver hard-forces the room structurally:toSolverRequestcollapses a base-room SG'scompatibleRoomIdsto[baseRoomId](no new hard family); strictSUBJECT_GROUP_BASE_ROOM_INCOMPATIBLEpre-check (8th) rejects a base room outside the capacity+subject-set compatible set; manual path gets a 13th ERROR diagnosticSUBJECT_GROUP_NOT_IN_BASE_ROOM(blocks publish). Solver objective:departmentRoomPreferenceflipped to favorNULL-dept rooms (penalize only different-dept) globally; newsubjectGroupRoomStabilityquality term (per-SG one-room); both skip base-room-pinned SGs (+ homeroom stability excludes them). Spec/plandocs/superpowers/specs|plans/2026-07-02-subject-group-base-room*. Migration generated 2026-07-02 as20260702115115_add_subject_group_base_room(see the e2e-500-repair entry above — the commitd725cechad shipped without it). Verify pending: unit specs added (subject-groups.service,generation.prechecks,solver.port,timetables.diagnostics) + solver pytest (test_objective) + generation e2e (base-room collapse); user runs build/test/e2e/pytest + commits. - Next-steps: done-heuristics removed (product rollback) —
GET /dashboard/next-stepsno longer infers a card's status from tenant data; a step is done only when the admin explicitly marks it (NextStepCompletion). Deletedevaluate()+ all done-inference queries (findDepartmentsWithReadyCurriculum,findCurriculumAuthorCoveredDepartmentIds,hasAnySubjectGroupAndHomeroom,hasPublishedTimetable,findReferent/StudentInviteCoverage,COVERS_ALL,InviteCoverage). Card DTO reshaped to{ key, completed, optional, blockedBy, order, cta }(droppedstatus/detected/manuallyCompleted/progress; deletedNextStepStatusenum). Kept: the manual complete/reopen routes + table, adminNextStepsPolicy, andhasCurriculumChoices(applicability gate forRESOLVE_STUDENT_CHOICESonly). No migration / RBAC change (schema comment reworded only). Also fixed a latent bug inrecompute(force-materialize path). Spec+plandocs/superpowers/specs|plans/2026-07-02-next-steps-remove-heuristics*. Verify pending: unit specs rewritten (next-steps.{queries,service}.spec.ts) + e2e rewritten (test/next-steps.e2e-spec.ts, adds anti-heuristic assertion); user runs the gate + commits. - RLS Phases 2–3 (plan Tasks 8–13): migration
20260702082621_enable_rls_high_value_pii(applied to localsis_dev+sis_e2e);app_userprovisioning (prisma/sql/provision-app-role.{sql,cjs},db:provision-app-role, docker/e2e hooks);APP_DATABASE_URLsplit (.env/.env.template);AdminPrismaService;withTenantGuc+ pre-auth fixes (login profile resolution, invitation verify/accept,fetchTeacherDepartmentIds); CLS tx handoff (TENANT_TX_CLS_KEY— the injected-TransactionHost route was broken, spec Q1 fallback made primary); Wave-2a guard deleted (registry →src/prisma/tenanted-models.ts); e2e fixture access →getAdminPrisma()(32 spec files);test/rls-isolation.e2e-spec.ts+db-constraintspolicy assertions; docs (ch02/ch10/ch12/REFERENCE/audit). - Verified: targeted units green (auth, invitations, permissions, interceptor, prisma resolver); the full
test:e2esuite runs green with RLS ON asapp_user— 64/64 suites, 771 passed, 285s. The earlier full-run Node OOM is fixed:test:e2eno longer uses--runInBand(which accumulated heap across all ~64 boots, worsened by the second per-app Prisma clientAdminPrismaService); it now runs serial via--maxWorkers=1 --workerIdleMemoryLimit=512MB, so the single worker recycles and the working set stays bounded. Seedocs/09-testing.md§Database isolation. -
Next: user runs the full gate (
npm run build+npm test+npm run test:e2e+npm run lint) and commits. Then on Railway dev/stage/prod: runprisma/sql/provision-app-role.sqlonce per DB, set a strong password +APP_DATABASE_URL(until then the app falls back to the superuser URL — boots fine, RLS bypassed). NOTE: once the full-coverage item below is included, the fallback is gone — hosted deploys FAIL AT BOOT until each env is provisioned. Provision BEFORE deploying this tree. -
Backend error i18n (localized
messages{en_US,it_IT}) — Phase 1 + Phase 2 + Phase 3 (safe half) IMPL in tree 2026-07-03 (uncommitted); design+plan committedb67f0fe/d982ff8, Phase-2 plan…-phase2.md, Phase-3 plan…-phase3.md. Move error message translation from FE to BE: each error resolves to amessages{en_US,it_IT}object rendered + interpolated server-side inAllExceptionsFilter, keyed by code + discriminators (CODE.<reason|kind>) +validation.<rule>+import.<CODE>. Phase 1 (additive): newsrc/common/i18n/(supported-langs,format-interpolation-value[date→fixed"Wed 7 January"UTC, noIntl],render-error-messages[most-specific-key resolution,{{placeholder}}interpolation,fieldpromotion, params partition, per-leaf enrichment for VALIDATION_FAILED/IMPORT_VALIDATION_FAILED/SELECTION_VALIDATION_FAILED],error-messages.catalog.ts~240 en+it — IT is a first pass, needs native review,error-messages.coverage.tsassertCatalogCoverageboot-throw, drift spec, barrel);ERROR_PARAM_KEYS;LocalizedMessageDto; filter emitsmessages/field/reorganizeddataalongside legacymessage/params; 4 error DTOs reshaped;assertCatalogCoverage()inmain.ts. Phase 2 (this session):AppExceptionconstructor →(code, statusCode?, options?)(message arg dropped; 3 subclasses updated); 447new AppException(...)sites across 77 files codemodded via a TS-compiler-API text-splice (AST-verified: 0 retain a string arg; arities 144×3-arg + 303×4-arg);main.tsValidationPipe factory updated by the same codemod;renderErrorMessageslegacymessagenow =messages.en(mirror for lagging FE). Divergence from the Phase-1 outline: the render fallback is RETAINED (not removed) — it's the only path for non-catalog codes (HTTP_<status>, unmapped Prisma), which the boot assertion doesn't cover; Prisma/HTTP-fallback branches keep their inline English. Codemod fallout cleaned: ~9 forwarding helpers lost deadmessage/label/notFoundLabelparams → removed + all callers/specs updated (assertCallerIsSelfOrAdmin,reorderInScope,curriculum.validationfail/failWindow,attendancefail, +PersonDocumentConfig.notFoundLabelfield) incl. two cascades (assertMinMaxSelections.blockLabel,assertValidRules.label+ruleLabel); dead locals/consts removed (ENTITY_LABELS,describeStorageError,offendingValues,ELIGIBLE_POOL_MESSAGE,NOT_ELIGIBLE_MESSAGE); ~20 spec assertions that read the now-gone message re-pointed tocode/params/status(filter, custom-fields, reorder, assert-caller, people-duplicate, run-bulk-sync, curriculum-structure-sync, base-tenanted, grades, departments, rooms, scope.guard specs). Verified:npx tsc -p tsconfig.build.json(source, excl. specs) exit 0;src/common/i18n+all-exceptions.filter.spec= 52 tests green; the 12 touched service/util/guard specs = 160+ tests green; my unused-var fallout lint-clean; codemodded filesprettier --write-formatted. NOT run: fullnpm test,npm run test:e2e, whole-treenpm run lint(its--fixnormalizes remaining codemod formatting). Semantic-loss follow-up (params enrichment, deferred): sites whose detail lived only in the free-text message now go generic (reorder missing-id, people identity/email match, curriculum offending grade, attendance field/rule, files storage reason, custom-fields offending values) — accept generic per the approved design; surface as structuredparamslater. Pre-existing tree debt (NOT this work): unused imports (PersonProfileKey/ImportErrorCode/evaluationScaleRefSelect/PersonEntity/CurriculumGridLike) +_ctx/_dtopolicy params + spec type errors (makeCtx()arity,count,departmentIds/RecordAccessContext) come from other unverified in-tree features and will show in a whole-tree gate. Phase 3 (safe half, this session — user chose "ship the safe half only"): dropped the redundant envelopemessage(puremessages.enmirror) + interpolation-only leafparams(validation + import leaves) from the wire, DTOs (ErrorResponseDto.message,ValidationFieldErrorDto.params,ImportValidationErrorDto.params), renderer output (RenderedError.message), and filter emit. KEPT (deferred, FE-gated): the envelopeparamsobject + leafrule/importreason/code— they still carry load-bearing structure (discriminatorsreason/kind/outcome, ids, counts) that ~15 E2E specs and the FE branch on; the params-partition treats those primitives as interpolation-only and would silently drop them, so the rawparamsecho stays until the FE migrates and that structure is relocated todata.NormalizedError.messagestays (internal-only, feeds the renderer's non-catalogHTTP_<status>fallback). Touched:render-error-messages.{ts,spec},all-exceptions.filter.{ts,spec}(every envelopemessage:assertion → catalog-sourcedmessages:), the 3 error DTOs,test/{error-i18n,students,setup,referents}.e2e-spec.ts(res.body.message→res.body.messages.en_US); ch06 §2 + examples;error-examples.tsheader flags the deferred example refresh. Nores.body.paramsE2E touched (params kept). Locale keys renameden/it→en_US/it_IT(POSIX/underscore, user's call over BCP-47 hyphen) across the catalog (588 keys),SUPPORTED_LANGS,LocalizedMessageDto, the renderer/filter/supported-langs specs, e2e, and ch06. Deferred: (a) the risky Phase-3 half — drop envelopeparams+ leafrule/reason— blocked on FE migration + params-enrichment (relocate the load-bearing primitives todata); (b)ERROR_EXAMPLESrefresh tomessages{en_US,it_IT}(~90 examples, needs Italian); (c) native IT review of the catalog. Message-completeness iteration (this session, IMPL in tree uncommitted): inverted the "messages stay generic" convention — code-level messages now interpolate their text-informative params so the FE rendersmessages[lang]verbatim (goals: FE reads onlymessages, messages fully explainable, no per-code FE logic).ERROR_PARAM_KEYS→ERROR_TEXT_PARAMS(full-catalog-key-addressable); drift spec now enforces a bijection (⊆ no orphan placeholders + ⊇ every declared text param interpolated in both langs) → build fails on an incomplete message.enrichCollections' 3 hardcoded branches → declarativeCOLLECTION_ENRICHMENTSregistry + 4th codeSELECTION_WINDOW_PREREQ_FAILED(per-gap.gapleaf; throw site already carriesdepartmentName/gradeName/activeStudentCount). Catalog rewrite by shape: ~22 Shape-1 (counts/dates/values already inparams— 0 throw-site change, compiler-guaranteed byErrorParamsMap), ~13 Shape-3 (names already inparams:*_NAME_TAKEN/*_NAME_CONFLICT/presetkey/subjectNamegrid errors incl. the 4CURRICULUM_HOURS_WINDOW_INVALID.*subkeys), 5 Shape-2 leaf sharpening (SELECTION_VALIDATION_FAILED.{below_min,above_max,rule_*}→{{expected}}/{{actual}}, verified always-set inselection-consistency.ts:166-217), 1 Shape-4 (REFERENT_IS_LAST_FOR_STUDENTS+count— throw site +ErrorParamsMap+ Swagger example updated). Skipped by design: 0-basedordinalPosition(UX wart),*_IN_USEreferences{}(breakdown stays indata),NOT_FOUND/CONFLICTentity (generic DB fallbacks, risk of raw model names),ROLLOVER_INVALID_STUDENT_IDScount (discriminator subkeys already explanatory).render-error-messages.spec+prereq test;error-i18n.e2e-spec+no-{{-leak invariant. ch06 §2 "Messages are self-contained" added + catalog header convention inverted. NOT run: the gate (per convention). Deferred (unchanged): dropparams/leafrule/reasonfrom wire (FE-gated);ERROR_EXAMPLESrefresh; native IT; enum-value→label polish (weekday/status/step/target render raw). Spec…-message-completeness-design.md(Approved 2026-07-03); plan…-message-completeness.md.
Next: user runs the full gate + commits. Spec …-backend-error-i18n-design.md; plans …-backend-error-i18n.md (P1) + …-phase2.md (P2) + …-phase3.md (P3).
- RLS full coverage — IMPLEMENTED in tree 2026-07-02 (spec+plan same date, all 12 tasks done). RLS 8 → 58/66 tables via migration 20260702130000_extend_rls_full_coverage (applied to local sis_dev + sis_e2e; generated from the new src/prisma/rls-coverage.ts registry, partition drift-guarded); APP_DATABASE_URL required on hosted + pg_roles role probe (both boot-throw); log-only misroute tripwire in the PrismaService proxy; AdminPrismaService grep-guard fence (admin-prisma.usage.drift.spec.ts, 5 consumers: auth token-hash flows, invitations token/webhook, + rooms/subject-levels/evaluation-scales preset usage guards — the Task-7 audit found those counting Class-S tables GUC-less, silently 0 = guard defeated → fixed on the admin client); guard-stack permission reads (fetchUserRoles/fetchParametricAssignments/getActiveRoleKeys) wrapped in withTenantGuc; login/refresh/logout/invitation flows rerouted (admin pre-reads + GUC txs); found+fixed a Phase-1 regression (switch-profile replay revocation rolled back with the ambient tx). Deviations recorded in the spec's status banner. Verified: 733 units green across touched modules; e2e green: db-constraints (58 policies+FORCE), rls-isolation (+ 4-class matrix), 9 rerouted-flow suites (117 tests) as app_user. Next: user gate + commit (single commit with the Phases 2–3 tree — deploy-coupled).
Near-term tasks (prioritized)¶
Architecture security backlog — remaining after #1 closed 2026-07-02. Source of truth: docs/reviews/2026-07-03_audit.md Part 1 (only open finding now is #5; R4 + R6 landed 2026-07-06 — see below).
- #5 canary — resolve the Draft spec's Q1/Q2 → sign-off → ship the curriculum-edit scaling canary e2e.
R4 + R6 landed (2026-07-06, uncommitted — verify pending). Spec docs/superpowers/specs/2026-07-02-school-relationship-flags-ay-create-contract-design.md (Approved). R4: school-relationship flags are now derived live at read in enrichSchoolRelationshipPeople (lock-overridden), materialization plumbing deleted (school-relationships.recompute.ts + spec, dead query fns, referents recompute triggers, seed reconcile call). R6: contract recorded as a TODO(rollover) at the base-tenanted-crud.service.ts create seam (reject-on-mismatch deferred) + create-echo assertion added to test/students.e2e-spec.ts. Verify pending: user runs npm run build + npm test + npm run test:e2e + npm run lint and commits.
RLS follow-ups: consolidated into the RLS full-coverage item above (the users auth-path story is resolved there — spec §10 Q2; the findUnique lint ban is DROPPED as structurally superseded — spec §9).