Skip to content

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 emit department: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 → new grade:not_in_department (flags the GRADE column, grade allowedValues). New reason key registered in STUDENT_EXTRA_REGISTRY (→ FIELD_INVALID) + dedicated catalog message import.FIELD_INVALID.not_in_department (en/it, no placeholders → no ERROR_TEXT_PARAMS entry 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 runs npm 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 its max > subjectCount check (was SETUP_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 the min > max self-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. subjectCount param + caller arg removed; max_exceeds_subjects reason retired from error-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 /homerooms resolveCurriculumForWizard, POST /subject-groups anchor check) — Curriculum.status no longer gates class creation, only referent selection eligibility (RUS-4) + the selection-window prereq. Removed the READY→DRAFT demote classes guard (assertNoClassesForCurriculum + countClassesForCurriculum) from switchCurriculumStatus; demote is now refused only on selections (CURRICULUM_IN_USE_BY_SELECTIONS) and any attached homerooms/SGs carry over into DRAFT untouched (the dead throwIfSubjectGroupInUse catch was kept — still live in updateCurriculum). Grouped boards (grouped-homerooms/grouped-courses) now enumerate DRAFT and READY curricula (dropped the status:'READY' skeleton filter). Both ErrorCodes CLASS_REQUIRES_READY_CURRICULUM + CURRICULUM_IN_USE_BY_CLASSES removed cleanly (enum + params + ERROR_TEXT_PARAMS + catalog en/it + examples; both drift specs stay balanced since every side removed together). The hard-delete block (removeCurriculumguardAndClearSubjectGroupsForCurriculum, "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-courses grows a grade node (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-subject subjects[] (numStudentsExpected/numStudentsPlaced, placed ≤ expected). Curriculum-node counts are now Σ grades; numMinRequiredSubjectGroups unchanged. Dropped the optional/mandatory split from the original FE ask (uniform: a course is a course). Data: +curriculumSelection.choices on 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 committed 74d188e/e7de119; impl in tree, verify pending: user runs build/test/e2e/lint + commits.

  • Evaluation-scale inUse flag → per-tenant (bugfix) — the RLS commit 0b5f0ab routed the DISPLAY inUse flag through adminPrisma (cross-tenant), so any preset referenced by ANY tenant read inUse:true for EVERY tenant (symptom: "all scales in use no matter what"). Fixed by pointing computeInUseMap (list) + getScaleById (detail) back at this.prisma (RLS-scoped → per-tenant at all 6 FK grains; the child-table RLS policies filter via join to curricula.tenant_id, verified). The write/delete FK guards (referenceCounts, the tx-locked guard in writeScaleValuesAndName, existsInUse for the @NoTenantTx preset-update response) stay cross-tenant on adminPrisma — "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 block EvaluationScalesService — 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 on sis_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-tenant inUse — the RLS-client split already gives per-tenant display + cross-tenant edit/delete guards with no schema change. Verify pending: user runs npm test (esp. evaluation-scales.service.spec.ts) + build/lint + commits.

  • Curriculum coordinator role + HR/Secretary split — second parametric dimension ParameterDim.CURRICULUM + new curriculum_coordinator preset (dept-head-shaped, WRITE only on curricula/homerooms/SGs, no curricula create/delete, students READ-via-selection, teachers role-keyed pass-through) and the combined hr split into personnel-only hr (relabeled "HR"; teachers/staff + invitations) + new secretary (students/config/composition/invitations; no personnel health). Migration 20260702123500_curriculum_coordinator_hr_secretary_split (applied to local sis_dev BEFORE the DML was appended — fold-edited after apply, so run migrate reset/docker:reset before the next migrate dev or e2e run; enum ADD VALUE + hr relabel + stale hr action-grant DELETEs). New AppliesPolicyDimensions decorator (metadata-only — boards/pickers keep their tight @RequireRoles while RolesGuard dimension-overlap admits parametric managers); homeroom-create curriculum write gate; userRoleParameter CURRICULUM cascade on curriculum delete; Tier-1 backfillMissingPresetRoles (additive-only preset propagation to existing tenants); expectedPresetGrants mirror + e2e fixtures (secretary@/coordinator@e2e.dev, Carla = Teacher-profile trick) + new test/{curriculum-coordinator,hr-secretary-split}.e2e-spec.ts + rewritten hr block in rbac-presets.e2e-spec.ts; ~124 spec ParametricContext literals extended. Spec b329e33 + plan f4d89df. 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 d725cec shipped SubjectGroup.baseRoomId in the schema/client without a migration, so every SG read 500'd (P2022 missing column) — now generated+applied as 20260702115115_add_subject_group_base_room (nullable column + index + FK SET NULL; ch12 hazard audit clean). Note: its UTC timestamp sorts BEFORE the hand-authored future-stamped 2026070213/14/150000 trio — content-independent, migrate deploy applies unapplied regardless of order. (2) On RLS-enabled tables Postgres redacts the 23505 DETAIL line for app_user, so the pg adapter emits P2002 with no constraint.fieldsmapP2002 targetStr was '' → typed 409s (e.g. SUBJECT_GROUP_NAME_CONFLICT) degraded to generic CONFLICT. Fixed in mapP2002 (fallback: parse constraint name from originalMessage) + unit spec + ch02 §4.0.3 — new-handler rule: match index-name tokens, not just column lists. (3) setup.e2e walks the wizard SCHOOL step, which now syncs operationalNameTenant.name — the test tenant stayed renamed and auth.e2e (multi-tenant picker asserts 'Test School') failed downstream; fixed by snapshotting the seeded name in beforeAll + restoring it in the existing symmetric afterAll. 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 (spec 24f62c8 + plan 7e51856, all 8 tasks done). Replaced both placement-board endpoints wholesale: catalog-driven skeleton (visible depts via DepartmentsPolicy → READY curricula via findGroupedBoardCurricula → covered grades; empty nodes present; defensive union for rows on demoted curricula), roster-free counts (homeroom placement per grade/dept + numStudentsWithoutCurriculum; SG counts: numMinRequiredSubjectGroups static floor via new countMinRequiredSubjectGroups, missing-assignment via resolveTargetPlan, teacherless), class leaves kept (bound SGs now included on the courses board, leaf gains homeroom ref), groupBoardTree DELETED, e2e suites renamed (test/{homerooms,subject-groups}-grouped.e2e-spec.ts; old routes now 400 via :id capture), 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 standalone invitations group is gone (two groups, both required); handler + spec + SetupInvitationsStateDto + module wiring deleted; the next-steps to-do keeps its current behavior unchanged (its INVITATIONS CTA enum is the unrelated RBAC/CTA key). SetupStep.INVITATIONS stays as a second orphaned PG enum value (CURRICULUM precedent); data migration 20260702160000_remove_invitations_setup_step (hand-authored, NOT applied) rehomes any tenant parked on it to ROLLOVER — required because parseSetupStep now 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.

  • maxAbsenceHoursminPresentHoursPercentage — Department configuration field 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: migration 20260702150000_replace_max_absence_hours_with_min_present_hours_percentage (hand-authored drop+add nullable column, NOT applied), mirrors the 20260611123000 precedent. Renamed across schema, both DTO files (department-configuration.dto.ts + setup departments-step.dto.tsMax import added to each), 8 departments.service.ts map/type sites, the scope-fields.ts configuration allowlist, and the departments.service.spec.ts fixture; the REFERENCE.md departments/ row reworded. Config-only — presence computation / notifications stay deferred (unchanged). No RBAC / ErrorCode change. FE-breaking (OpenAPI field rename on the configuration scope + setup DEPARTMENTS step). Spec+plan docs/superpowers/specs|plans/2026-07-02-min-present-hours-percentage*. Verify pending: user runs prisma generate + build/test/lint, applies the migration, commits.

  • Remove code from curriculum tracks — dropped CurriculumTrack.code + its unique index (migration 20260702140000_drop_curriculum_track_code, hand-authored, NOT applied); name (already @@unique([curriculumId,name])) is the sole track identity → sync keys tracks by name (trackIdByName), dormant selection-carry matches 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 (new PresetTrackTemplate) + expandPresetToGrid emits tracks + 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 — removed slugCode/code-dedup → name-dedup + LLM prompt copy). FE-breaking. NOT the retained CurriculumSubject.code. Spec+plan docs/superpowers/specs|plans/2026-07-02-remove-curriculum-track-code*. Verify pending: user runs prisma generate + build/test/e2e/lint, applies the migration, commits.
  • SubjectGroup base room (baseRoomId) — additive nullable SubjectGroup.baseRoomId (Room?, onDelete: SetNull, @@index) mirroring Homeroom.baseRoomId; rides the existing subject_groups.composition scope (field added to SUBJECT_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: toSolverRequest collapses a base-room SG's compatibleRoomIds to [baseRoomId] (no new hard family); strict SUBJECT_GROUP_BASE_ROOM_INCOMPATIBLE pre-check (8th) rejects a base room outside the capacity+subject-set compatible set; manual path gets a 13th ERROR diagnostic SUBJECT_GROUP_NOT_IN_BASE_ROOM (blocks publish). Solver objective: departmentRoomPreference flipped to favor NULL-dept rooms (penalize only different-dept) globally; new subjectGroupRoomStability quality term (per-SG one-room); both skip base-room-pinned SGs (+ homeroom stability excludes them). Spec/plan docs/superpowers/specs|plans/2026-07-02-subject-group-base-room*. Migration generated 2026-07-02 as 20260702115115_add_subject_group_base_room (see the e2e-500-repair entry above — the commit d725cec had 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-steps no longer infers a card's status from tenant data; a step is done only when the admin explicitly marks it (NextStepCompletion). Deleted evaluate() + all done-inference queries (findDepartmentsWithReadyCurriculum, findCurriculumAuthorCoveredDepartmentIds, hasAnySubjectGroupAndHomeroom, hasPublishedTimetable, findReferent/StudentInviteCoverage, COVERS_ALL, InviteCoverage). Card DTO reshaped to { key, completed, optional, blockedBy, order, cta } (dropped status/detected/manuallyCompleted/progress; deleted NextStepStatus enum). Kept: the manual complete/reopen routes + table, admin NextStepsPolicy, and hasCurriculumChoices (applicability gate for RESOLVE_STUDENT_CHOICES only). No migration / RBAC change (schema comment reworded only). Also fixed a latent bug in recompute (force-materialize path). Spec+plan docs/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 local sis_dev + sis_e2e); app_user provisioning (prisma/sql/provision-app-role.{sql,cjs}, db:provision-app-role, docker/e2e hooks); APP_DATABASE_URL split (.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-constraints policy assertions; docs (ch02/ch10/ch12/REFERENCE/audit).
  • Verified: targeted units green (auth, invitations, permissions, interceptor, prisma resolver); the full test:e2e suite runs green with RLS ON as app_user — 64/64 suites, 771 passed, 285s. The earlier full-run Node OOM is fixed: test:e2e no longer uses --runInBand (which accumulated heap across all ~64 boots, worsened by the second per-app Prisma client AdminPrismaService); it now runs serial via --maxWorkers=1 --workerIdleMemoryLimit=512MB, so the single worker recycles and the working set stays bounded. See docs/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: run prisma/sql/provision-app-role.sql once 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 committed b67f0fe/d982ff8, Phase-2 plan …-phase2.md, Phase-3 plan …-phase3.md. Move error message translation from FE to BE: each error resolves to a messages{en_US,it_IT} object rendered + interpolated server-side in AllExceptionsFilter, keyed by code + discriminators (CODE.<reason|kind>) + validation.<rule> + import.<CODE>. Phase 1 (additive): new src/common/i18n/ (supported-langs, format-interpolation-value [date→fixed "Wed 7 January" UTC, no Intl], render-error-messages [most-specific-key resolution, {{placeholder}} interpolation, field promotion, 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.ts assertCatalogCoverage boot-throw, drift spec, barrel); ERROR_PARAM_KEYS; LocalizedMessageDto; filter emits messages/field/reorganized data alongside legacy message/params; 4 error DTOs reshaped; assertCatalogCoverage() in main.ts. Phase 2 (this session): AppException constructor → (code, statusCode?, options?) (message arg dropped; 3 subclasses updated); 447 new 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.ts ValidationPipe factory updated by the same codemod; renderErrorMessages legacy message now = 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 dead message/label/notFoundLabel params → removed + all callers/specs updated (assertCallerIsSelfOrAdmin, reorderInScope, curriculum.validation fail/failWindow, attendance fail, + PersonDocumentConfig.notFoundLabel field) 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 to code/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 files prettier --write-formatted. NOT run: full npm test, npm run test:e2e, whole-tree npm run lint (its --fix normalizes 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 structured params later. Pre-existing tree debt (NOT this work): unused imports (PersonProfileKey/ImportErrorCode/evaluationScaleRefSelect/PersonEntity/CurriculumGridLike) + _ctx/_dto policy 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 envelope message (pure messages.en mirror) + interpolation-only leaf params (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 envelope params object + leaf rule/import reason/code — they still carry load-bearing structure (discriminators reason/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 raw params echo stays until the FE migrates and that structure is relocated to data. NormalizedError.message stays (internal-only, feeds the renderer's non-catalog HTTP_<status> fallback). Touched: render-error-messages.{ts,spec}, all-exceptions.filter.{ts,spec} (every envelope message: assertion → catalog-sourced messages:), the 3 error DTOs, test/{error-i18n,students,setup,referents}.e2e-spec.ts (res.body.messageres.body.messages.en_US); ch06 §2 + examples; error-examples.ts header flags the deferred example refresh. No res.body.params E2E touched (params kept). Locale keys renamed en/iten_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 envelope params + leaf rule/reason — blocked on FE migration + params-enrichment (relocate the load-bearing primitives to data); (b) ERROR_EXAMPLES refresh to messages{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 renders messages[lang] verbatim (goals: FE reads only messages, messages fully explainable, no per-code FE logic). ERROR_PARAM_KEYSERROR_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 → declarative COLLECTION_ENRICHMENTS registry + 4th code SELECTION_WINDOW_PREREQ_FAILED (per-gap .gap leaf; throw site already carries departmentName/gradeName/activeStudentCount). Catalog rewrite by shape: ~22 Shape-1 (counts/dates/values already in params — 0 throw-site change, compiler-guaranteed by ErrorParamsMap), ~13 Shape-3 (names already in params: *_NAME_TAKEN/*_NAME_CONFLICT/preset key/subjectName grid errors incl. the 4 CURRICULUM_HOURS_WINDOW_INVALID.* subkeys), 5 Shape-2 leaf sharpening (SELECTION_VALIDATION_FAILED.{below_min,above_max,rule_*}{{expected}}/{{actual}}, verified always-set in selection-consistency.ts:166-217), 1 Shape-4 (REFERENT_IS_LAST_FOR_STUDENTS +count — throw site + ErrorParamsMap + Swagger example updated). Skipped by design: 0-based ordinalPosition (UX wart), *_IN_USE references{} (breakdown stays in data), NOT_FOUND/CONFLICT entity (generic DB fallbacks, risk of raw model names), ROLLOVER_INVALID_STUDENT_IDS count (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): drop params/leaf rule/reason from wire (FE-gated); ERROR_EXAMPLES refresh; 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).