Skip to content

Teacher record visibility and policy-scoped filter facets — iteration 2


1. Problem distillation

  • A simple teacher's student reach is already assignment-derived, but the canonical predicate omits students covered by an assigned lunch/break duty. The duty model and audience selectors exist; record access simply does not consume them.
  • Homeroom visibility independently re-derives only part of the teacher/student relationship. It therefore misses combined-course, supervised-activity, and break-duty students, while a visible homeroom detail currently returns its entire roster and can expose unrelated classmates.
  • Curriculum Coordinators (curriculum_coordinator) and Department Principals (department_head) must receive personal teacher reach ∪ managed curriculum/department reach in both the combined session and an active management-role view. Current role-key composition loses the personal leg in the active view, while a staff-profile manager can become tenant-wide because staff is a read-policy pass-through.
  • The same union-composed policies currently fence reads and writes. In a mixed teacher+manager session, a WRITE grant from the management assignment can borrow the teacher read branch and reach personally taught rows outside the managed parameters.
  • Structural filter endpoints deliberately return a tenant-wide catalog for school-side sessions, and their family-only exception is based on forward-head student columns rather than the effective/display facts used by the tables. Every filter option must instead be projected from the policy-visible rows of the table surface it serves.

Success criteria (observable behavior that proves this works):

  • A simple teacher sees exactly the dated union of: students in a directly taught subject group; students in a group effectively combined with a directly taught group; students in a tutored homeroom; students in the audience of a supervised scheduled activity; and students in the audience of an assigned scheduled lunch/break duty.
  • The activity and duty relationships are week-long while their timetable version governs schoolToday: weekday is deliberately not a record-access dimension. Expired/future timetable versions and expired/future memberships grant no early or lingering access.
  • A teacher sees an empty homeroom they tutor and every homeroom containing at least one currently visible student. Seeing a homeroom through one student does not expose unrelated roster students, pending student changes, or unrelated linked-course rows.
  • A teacher sees their own effective subject groups. Existing combined-class presentation behavior remains: teaching one effective member admits the combined presentation and its related-student roster.
  • A Curriculum Coordinator sees personal teacher reach ∪ rows in assigned curricula; a Department Principal sees personal teacher reach ∪ rows in assigned departments. The result is identical in the combined session and in the active management-role view.
  • A staff-profile management holder receives only their curriculum/department parameter reach. The dormant staff policy branch can no longer short-circuit to the tenant base when another role supplies the route grant.
  • Tenant-wide roles (admin, hr, secretary, Director/principal, and platform admin where already admitted) retain their current read reach. Field-level RBAC remains the authority for which student blocks are returned.
  • Read widening propagates consistently to the existing student-related read consumers: referent resolution, disciplinary-note rows, curriculum-selection reads, attendance-justification file reads, and communications recipient reach. Communication reach is symmetric: a duty teacher can address duty students/referents, and an eligible student/referent can resolve that duty teacher under the existing tier rules.
  • Student, homeroom, and subject-group writes use write-only record policies plus qualified role slices. A manager who personally teaches outside their assigned management parameters may read those rows but cannot update/delete/recompose them on the strength of the management WRITE grant.
  • Every /filters/* request names surface=students|homerooms|subject-groups. Returned values are projections of rows admitted by that surface's read policy and the endpoint's existing upstream cascade filters. Missing/invalid/unsupported surface values return 400; there is no tenant-wide fallback.
  • The existing filter algebra remains unchanged: selected values within one dimension union; different dimensions intersect. Pagination and response envelopes do not change.

Non-goals (in-scope-shaped things this iteration is explicitly not doing):

  • No change to teacher, coordinator, Department Principal, staff, or Director scope/action grants; this is row reach, not field reach.
  • No new historical/as-of API. All authorization decisions use the one captured schoolToday already required by the affected policies.
  • No weekday-scoped profile visibility. Duties and activities follow the existing week-long supervised-activity precedent while the timetable version governs the date.
  • No change to attendance authority: breaks carry no attendance register, and being on break duty does not create a take/justify permission.
  • No change to grade-book reach. Duty supervision is a student relationship, not grading authority; grades.visibility.ts remains anchored on teaching/tutoring rules.
  • No fully self-excluding faceted-search protocol. Existing left-to-right cascade parameters remain; this iteration makes their candidate values policy- and surface-honest.
  • No narrowing of the tenant-wide identity-only teacher directory outside a surface-specific /filters/teachers projection.

2. Patterns survey

Analogous module/spec What we'd borrow What doesn't fit
docs/superpowers/specs/2026-08-02-teacher-people-visibility-narrowing-design.md + src/students/students.policy.ts The canonical dated teacher→student relation, including direct SG, combined sibling, tutor, supervised-activity, governing-timetable, and fail-closed schoolToday conventions This iteration adds break duty, renames the helper to reflect relationship rather than only teaching, and makes homeroom/filter consumers derive from it
src/timetables/timetables.queries.ts (resolveBreakAudiences) + src/attendance/attendance-day.queries.ts (loadTeacherDutiesOn) Existing ScheduledBreakAudience union semantics (department/curriculum/grade/homeroom), duty-teacher relation, and governing timetable data Those are imperative display/agenda readers; authorization needs a pure Prisma WHERE using strict effective facts and no weekday filter
src/teachers/teachers.policy.ts (TeachersPolicy, TeachersDocumentsReadPolicy, TeachersWritePolicy) Explicit read/write policy split preventing a read-directory branch from lending rows to a manager's WRITE grant Students have per-scope PATCH blocks and referent writes, so their write path additionally needs qualified slices per mutated scope
src/permissions/permissions.service.ts (resolveAuthorizedRoleSlice) Existing primitive that retains only role assignments independently satisfying one scope/action requirement and their own parameters A student PATCH can touch several scopes; the service must resolve and apply one slice per mutated scope rather than one request-global slice
src/homerooms/homerooms.policy.ts + src/subject-groups/subject-groups.policy.ts (narrowRosterToVisibleStudents) Current policy-composed roster intersection and dated assignment patterns The short-circuit currently treats every non-family role as full-roster; reads must always intersect the student read policy, while write-side reloads with no access context remain complete
docs/superpowers/specs/2026-07-13-table-lists-and-filters-iteration-2-design.md + src/filters/ Existing route set, multi-value DTO convention, left-to-right cascade, OR-within/AND-across algebra, sorting, and response envelopes The current target-catalog queries do not know which table they serve; this iteration adds a required surface and projects from policy-visible source rows
src/subject-groups/subject-groups.queries.ts (buildSubjectGroupsTableWhere, buildCombinedClassesTableWhere) Single source for plain-course and combined-presentation filter semantics A subject-group facet source must project from both sources; projecting only directly visible SG rows would omit values present on a visible combined presentation
docs/superpowers/specs/2026-08-04-session-view-switch-iteration-2-design.md Active-role narrowing semantics: active management view intentionally drops the teacher role key Personal teaching reach must therefore be data-derived inside the parameter branch, not conditional on ctx.roles containing teacher
src/communications/recipient-policy.ts + src/communications/recipient-policy.queries.ts Forward teacher→student relationship reuse and inverse teachersTeachingStudentsOn parity The inverse query cannot import a Student WHERE verbatim; it must add the equivalent break-duty leg and remain locked by symmetry tests

On-axis/off-axis assessment (ch16 §4–§5): the access changes are on-axis: EntityAccessPolicy, OR-composed role/parameter branches, dated relation predicates, and qualified role slices are all paved primitives. The filter surface discriminator is a new API contract but stays local to src/filters/: a typed enum plus an exhaustive source-query switch, not a new authorization framework or cross-module service dependency. None of ch16 §5's off-axis write-coordination, fan-out, cascade, or aggregate-response problems is introduced.


3. Architecture mapping

Primitive Apply? How Justify
Tenant scope yes Every read/write policy keeps { tenantId: ctx.tenantId }; inner Teacher, timetable, duty, audience, membership, and facet queries also pin tenant directly or through a tenant-scoped parent. RLS remains defense-in-depth. No cross-tenant relation may establish visibility
Academic-year scope yes Surface services resolve the requested/active AY as today; membership and selection predicates use the captured schoolToday; homeroom/SG/filter source rows retain academicYearId. Prevents an old/future cohort from lending current access
RBAC entity key existing only Reuse STUDENTS, HOMEROOMS, SUBJECT_GROUPS, TEACHERS, REFERENTS, DISCIPLINARY_NOTES, COMMUNICATIONS; no entity-keys.ts delta. Row-policy iteration only
Scopes existing Reuse each route's current read/write scope, including student block scopes, homerooms.composition, and subject_groups.composition. Field access is deliberately unchanged
Actions existing Reuse students.delete, homerooms.create/delete, subject_groups.create/delete, and communications.send; qualified slices include the action where the route is action-gated. Prevent action authority from borrowing an unrelated row branch
Service base mixed StudentsService remains on BaseTenantedCrudService but explicitly selects read vs write policy in access-context entry points; homeroom/SG/filter/communications services remain custom. No new service abstraction is needed
queries.ts shape named functions + pure WHERE builders Rename the shared relation builder to studentRelatedToTeacherOn; add the break-duty EXISTS legs; add inverse communication duty query; add typed filter-surface projection functions reusing table WHERE builders. Keeps policy fragments pure and query ownership local
Error codes existing Invalid/missing/unsupported surface is class-validator 400; inaccessible rows remain policy-composed 404; grant failures remain 403. No domain error is needed
DTO conventions list-query DTO Add required surface to every filter query DTO using a shared string enum/constant and endpoint-specific @IsIn values; existing id/search/page fields stay unchanged. Explicit, Swagger-visible source context
File-backed sub-resources n/a — existing routes only Student document reads use StudentsReadPolicy; writes use the documents WRITE-qualified slice plus StudentsWritePolicy. Attendance justification attachment reads inherit the canonical relation. No storage model change
Custom fields no Existing scope masking and platform-wide custom-field posture remain unchanged. Row reach only
Profile completeness no No completion-required field or command-center contract changes. n/a

4. Data model plan

Schema deltas

  • None. ScheduledBreak, ScheduledBreakAudience, and ScheduledBreakDuty already represent all required duty relationships.
  • No new RLS-covered or strictly-tenanted model.

Migration shape

  • Additive / destructive / renaming: n/a — no migration.
  • Data backfill: none.
  • Hazards from chapter 12 checklist: none; Prisma schema and generated client are unchanged.

Indexes and uniqueness

  • None added. The duty lookup uses existing ScheduledBreakDuty.teacherId, ScheduledBreak.timetableId, audience breakId, and effective membership/selection FK indexes.
  • Audience reference columns mirror the already-shipped scheduled-activity query posture. Revisit only with measured slow-query evidence; this iteration does not introduce speculative indexes.

5. API surface

No response DTO changes and no new routes. Entity GET/PATCH/DELETE paths change record behavior only. The filter request contract is intentionally breaking because surface becomes required.

Verb Path Decorators Request DTO Response DTO
GET /filters/departments existing @ProtectedResource, @AggregateResponse existing fields + required surface (students, homerooms, subject-groups) unchanged DepartmentFiltersResponseDto
GET /filters/grades existing existing fields + required surface (all three) unchanged GradeFiltersResponseDto
GET /filters/curricula existing existing fields + required surface (all three) unchanged CurriculumFiltersResponseDto
GET /filters/tracks existing existing fields + required surface (students, subject-groups) unchanged TrackFiltersResponseDto
GET /filters/option-blocks existing existing fields + required surface=subject-groups unchanged OptionBlockFiltersResponseDto
GET /filters/subjects existing existing fields + required surface=subject-groups unchanged SubjectFiltersResponseDto
GET /filters/homerooms existing existing fields + required surface (all three) unchanged HomeroomFiltersResponseDto
GET /filters/teachers existing @RequireScopes(TEACHERS,'read'), teacher-policy dimension gate existing search/page/limit + required surface (homerooms, subject-groups) unchanged paginated TeacherFiltersResponseDto
GET /filters/students existing @RequireScopes(STUDENTS,'read'), student-policy dimension gate existing search/dept/grade/page/limit + required surface (students, homerooms) unchanged paginated StudentFiltersResponseDto
GET /students*, /homerooms*, /subject-groups* swap to the corresponding *ReadPolicy unchanged unchanged shapes, narrowed rows/nested collections
PATCH/DELETE/POST composition existing student/homeroom/SG paths swap write routes to *WritePolicy; service resolves qualified role slice(s) unchanged unchanged; out-of-management-scope rows now 404

Filter-surface contract

surface identifies the result-set family whose rows the options must be able to match:

  • students: start from StudentsReadPolicy.where(datedCtx) and the student list's display/effective filter helpers. Project display department/grade, effective curriculum/track, and effective homeroom; never read raw forward-head department/grade as the facet fact.
  • homerooms: start from HomeroomsReadPolicy.where(datedCtx). Project direct structural fields and tutor. Student options are the effective roster intersected with StudentsReadPolicy; one related student does not admit their classmates.
  • subject-groups: start from the plain-SG and combined-presentation sources used by the table. Admission is driven by SubjectGroupsReadPolicy; projection reuses buildSubjectGroupsTableWhere / buildCombinedClassesTableWhere so values present on a visible combined row are not lost. Teachers are effective group teachers; homerooms are effective roster overlap, matching the table's derived filter.

For every endpoint:

  1. Resolve AY and schoolToday once.
  2. Build the selected surface's policy-visible base.
  3. Apply that endpoint's existing upstream cascade parameters with the same helper used by the surface table.
  4. Project distinct target ids from the surviving rows, then load/order the existing response shape.
  5. For people endpoints, additionally intersect the projected ids with TeachersPolicy or StudentsReadPolicy, and use the same WHERE for data and count.

familyDropdownScope and its narrowIds set arithmetic are deleted. Platform/admin callers naturally receive the full matching catalog because the surface read policy returns its tenant base; no role-specific filter branch remains.

Swagger considerations

  • Every filter DTO documents surface as required and explains that it selects the table whose policy-visible rows source the options.
  • Each endpoint advertises only its supported surface enum values; missing/unsupported values are validation 400s.
  • Filter route JSDoc drops the tenant-wide structural-catalog promise and states the projection rule in client-visible language.
  • No JSDoc mentions Prisma, role-slice internals, policy names, or transactions.
  • A breaking FE guide must give the exact route/surface matrix and update every existing request example to send surface.

6. RBAC seed plan

Seed file Delta
PermissionScope (rbac-catalogue.ts) none
PermissionAction (rbac-catalogue.ts) none
ScopeFieldMapping (rbac-catalogue.ts) none
Role grants (roles.ts) none
*_SCOPES runtime constant none

Canonical teacher/student relation

Rename studentTaughtByCallerOn to studentRelatedToTeacherOn(ctx, schoolToday). Update every internal consumer in the same change; do not retain a misleading alias.

Keep the four existing relation families and add the duty family. Let DUTY_BREAK mean a ScheduledBreak whose duties.some.teacher matches { userId, tenantId } and whose timetable has effectiveFrom <= schoolToday < effectiveUntil|null. The new Student OR legs are:

  • effective placement → department → breakAudiences.some.scheduledBreak = DUTY_BREAK;
  • effective placement → grade → matching break audience;
  • effective curriculum selection → curriculum → matching break audience;
  • effective curriculum selection → track → matching break audience;
  • effective homeroom assignment → homeroom → matching break audience;
  • direct student → matching break audience.

Break audiences have no EVERYONE selector. The authorization predicate uses strict effective placements/selections/assignments, not display fallback or forward-head metadata. Like activity supervision, it deliberately ignores ScheduledBreak.weekday.

Consumers updated to the renamed canonical relation:

  • StudentsReadPolicy;
  • ReferentsPolicy;
  • DisciplinaryNotesPolicy;
  • CurriculumSelectionReadPolicy;
  • communications/recipient-policy.ts;
  • attendance/attendance-justification-files.service.ts;
  • homeroom read visibility and every roster/pending-change intersection.

communications/recipient-policy.queries.ts adds the inverse duty leg to teachersTeachingStudentsOn, using the same six selector families and temporal rules. Forward and inverse duty reach are locked together by unit/e2e scenarios.

Read policies

  • StudentsReadPolicy:
  • tenant-wide branches: platform admin, admin, hr, secretary, Director/principal (unchanged);
  • delete the staff pass-through;
  • teacher: studentRelatedToTeacherOn;
  • referent and student: unchanged linked/self branches;
  • DEPARTMENT parameter: studentRelatedToTeacherOn OR studentPlacedInDepartmentsOn(parameterIds);
  • CURRICULUM parameter: studentRelatedToTeacherOn OR studentSelectedCurriculaOn(parameterIds).

The personal limb is intentionally data-derived by ctx.userId, not role-keyed, so it survives active management-role narrowing. A non-teacher staff-profile manager simply has no matching Teacher relation and receives only the parameter limb.

  • HomeroomsReadPolicy:
  • same tenant-wide branch set and staff removal;
  • teacher/personal limb: direct tutor OR an effective homeroom assignment whose student matches studentRelatedToTeacherOn;
  • DEPARTMENT/CURRICULUM limbs: personal limb OR direct homeroom containment in the assigned ids;
  • referent/student branches remain based on their effective linked/self student.

  • SubjectGroupsReadPolicy:

  • same tenant-wide branch set and staff removal;
  • teacher/personal limb: caller has an effective SubjectGroupTeacher episode;
  • DEPARTMENT/CURRICULUM limbs: personal limb OR existing subject→curriculum containment;
  • referent/student branches remain effective roster-derived.

All read controllers, grouped boards, eligibility/picker surfaces, nested loaders, and filter surfaces import the explicit read policies.

Nested collection safety

  • Remove role-name-based rosterRequiresNarrowing. On every access-context read, narrowRosterToVisibleStudents resolves the roster id set through StudentsReadPolicy, even for a tenant-wide caller; the policy itself is the only pass-through authority. Write-side internal reloads with ctx = null remain complete.
  • Apply the same intersection to homeroom and subject-group pendingChanges[].
  • Homeroom linkedCourses[] intersects SubjectGroupsReadPolicy; it does not become an indirect course-directory bypass.
  • Counts that intentionally describe the container (_count/class size) remain full counts and disclose no student identity. If product later wants “visible student count,” that is a separate response-contract change.

Write policies and qualified slices

  • StudentsWritePolicy contains only admin/secretary tenant-wide branches, the existing referent-linked branch, and DEPARTMENT/CURRICULUM parameter branches. It has no teacher, staff, HR, or Director read branch and no personal-teaching fallback.
  • HomeroomsWritePolicy and SubjectGroupsWritePolicy contain only roles/dimensions that can author the existing composition scope: tenant-wide admin/Front Office branches plus DEPARTMENT/CURRICULUM containment. They have no teacher-personal or staff branch.
  • Student update resolves one resolveAuthorizedRoleSlice per mutated top-level scope with requiredAccess=WRITE; the target must match StudentsWritePolicy.where(slice.context) for every mutated scope. The existing assignment target fence and referent canWrite gate remain additional checks.
  • Student delete resolves { entity: STUDENTS, scopeKey: identity, requiredAccess: WRITE, action: delete }. Student document writes resolve the documents WRITE slice. Read-only document paths use StudentsReadPolicy.
  • Homeroom/SG create, update, delete, and composition operations resolve the composition WRITE slice, including create/delete action where applicable, and apply the corresponding write policy with slice.context before mutation.
  • Row-scope mismatch remains a 404 anti-oracle. Scope/action absence remains a guard-level 403.

7. Divergence ledger

Pattern We diverge by Reason Tradeoff accepted
One StudentsPolicy / HomeroomsPolicy / SubjectGroupsPolicy object for reads and writes Split each into explicit read and write policies OR-composed read reach is intentionally broader than management write reach; one object permits authority lending More policy objects and route imports, offset by clear semantics and the existing Teachers precedent
Structural /filters/* routes return the tenant-wide AY catalog for school-side sessions Require a surface and project only from policy-visible source rows A generic catalog cannot guarantee that an option can match a row the caller may see Breaking query parameter on every filter request; FE must state intent
resolveBreakAudiences uses display cohort resolution for agenda/diagnostics Record authorization uses strict effective placement/selection/membership A planned future placement must not grant profile access early Pre-year agenda can name a future audience before record access begins; deliberate security boundary
Roster narrowing skips all non-family roles Every access-context read intersects StudentsReadPolicy A visible homeroom may be admitted by one related student; role-name shortcuts expose unrelated classmates and fail for mixed/custom roles One extra small id query on tenant-wide detail reads unless later folded into the include query with measured need
Active management view contains only the active role key Parameter branches also OR a data-derived personal teacher relation Product requires management reach to be additive to personal assignments in every view A parameter branch has a larger WHERE; non-teacher managers pay a no-match teacher EXISTS leg
Activity/duty slots occur on selected weekdays Relationship-based record visibility is week-long while the version governs Matches the existing supervised-activity contract and avoids profiles appearing/disappearing by weekday A Monday duty grants the related-student row for the whole governing week/date range

8. Pushback log

US says Conflicts with Proposed instead Status
Teachers see students from homerooms, courses, and lunch/break duties Existing canonical set additionally includes combined-class siblings and supervised scheduled activities Preserve the two existing legs and add break duty; they remain necessary for current attendance/supervision behavior Resolved — approved 2026-08-26
Teachers see every homeroom linked to any related student Existing homeroom detail returns the complete roster once the header is visible Admit the homeroom header, but independently intersect roster/pending changes with student visibility and linked courses with course visibility Resolved — approved 2026-08-26
Coordinators/Department Principals get teacher reach plus managed scope Active-role sessions remove the teacher key; combined staff+manager sessions can short-circuit through staff Data-derived personal fallback in parameter read branches; remove staff pass-through; keep write policies management-only Resolved — approved 2026-08-26
Filter values follow the same visibility docs/REFERENCE.md, ch14, filter tests, and FE guides explicitly lock a tenant-wide school-side structural catalog Replace the catalog rule with required surface-aware policy projections; update canonical docs/tests/guides in the implementation Resolved — approved 2026-08-26
Reuse the canonical student relation everywhere Communications are an action surface, not merely a read Include duty students/referents in send reach and add inverse duty-teacher resolution, as explicitly approved; field/action gates remain independent Resolved — approved 2026-08-26

9. Deferrals

  • Full self-excluding facets that accept every sibling table filter on every /filters/* route — current left-to-right cascade remains sufficient; revisit only with an explicit FE request for dynamic zero-result suppression.
  • Historical/as-of record visibility — this iteration is schoolToday only; follow-up requires a separate audit/event-history contract.
  • Day-of-week-limited profile visibility for activities/duties — deliberately week-long; revisit only if product reverses the existing supervised-activity rule.
  • Grade-book/attendance-take authority from break duty — explicitly not implied by student row reach; future changes belong to their domain policies.
  • Query-plan-driven audience reference indexes — no speculative migration; revisit from measured slow-query evidence.

10. Open questions

None — surface-aware filters, supervised-activity retention, read-consumer propagation, communication reach, nested roster narrowing, and read/write policy separation were all approved by Fabio in chat 2026-08-26.


11. Verification plan

  • Unit specs:
  • src/students/students.policy.spec.ts: rename snapshots to studentRelatedToTeacherOn; assert all six break audience selectors, governing/future/expired timetable windows, strict effective placements/selections/assignments, no weekday predicate, management personal∪parameter reach, active-role-equivalent shape, staff fail-close, and broad-role preservation.
  • src/homerooms/homerooms.policy.spec.ts: direct empty tutor; homeroom via direct/combined/activity/duty student; parameter scope ∪ personal reach; no unrelated homeroom; no staff pass-through.
  • src/subject-groups/subject-groups.policy.spec.ts: own effective group; management scope ∪ own group; no staff pass-through; broad/referent/student branches unchanged.
  • New/updated write-policy specs for students/homerooms/subject groups: no teacher-personal limb, qualified parameter containment, referent-linked student writes, and mixed teacher+manager context cannot widen write WHEREs.
  • src/subject-groups/subject-groups.policy.spec.ts plus homeroom/SG service specs: all read rosters and pending changes intersect StudentsReadPolicy; broad roles still receive all roster ids through the policy result; homeroom linked courses intersect SubjectGroupsReadPolicy.
  • src/filters/filters.service.spec.ts and filters.queries.spec.ts: required/supported surface matrix; each surface starts from its read policy; left-to-right cascade preserved; student facets use display/effective facts rather than forward heads; homeroom student options exclude unrelated classmates; subject-group facets include values from visible combined presentations; data/count share the same WHERE.
  • Existing consumer specs (referents.policy.spec.ts, disciplinary-notes.policy.spec.ts, curriculum-selection.policies.spec.ts, attendance justification file specs) update for the renamed canonical relation and include a representative duty case where behavior is not already proven centrally.
  • src/communications/recipient-policy.spec.ts and recipient-policy.queries.spec.ts: duty student/referent forward reach and inverse duty-teacher reach stay symmetric under the same governing window and audience selectors.
  • E2E specs:
  • Extend test/teacher-people-visibility.e2e-spec.ts: create a governing break/lunch duty and all six audience shapes; duty student visible in list/detail/filter, unrelated student 404; future/expired duty version excluded; supervised-activity behavior remains.
  • Homeroom e2e: a duty/course/activity-related student admits every effective homeroom linked to that student; detail exposes only visible roster/pending students and visible linked courses; directly tutored empty homeroom remains visible.
  • Department Principal/Curriculum Coordinator e2e (test/dept-head.e2e-spec.ts plus coordinator coverage): combined and active management views both return personal∪managed reads; staff-profile holder is not tenant-wide.
  • Write-fence e2e: a manager who teaches outside assigned parameters can read but gets 404 for student PATCH/delete/document write and homeroom/SG composition outside scope; in-scope writes remain green.
  • test/filters.e2e-spec.ts: missing/invalid/unsupported surface 400; simple teacher, coordinator, Department Principal, family, and broad admin option sets; duty-related early-years structure appears; unrelated catalog values disappear; OR-within/AND-across regressions stay green.
  • Communications e2e: duty teacher can select/send to duty student/referent and the inverse eligible recipient path resolves the teacher; unrelated recipients remain absent.
  • Manual verification: exercise the three table surfaces in Scalar with identical role sessions and confirm every returned option can match at least one policy-visible base row. Repeat once in combined and active management views.

Documentation delivered with implementation: update docs/04-rbac.md policy inventory and active-view explanation; docs/14-homerooms-subject-groups.md homeroom/roster/filter contract; docs/REFERENCE.md teacher visibility and the 2026-08-26 tenant-wide-filter correction; docs/18-timetables.md duty-as-record-relationship note; the authoritative teacher visibility FE guide; the cascading-filter guide; and a breaking filter-surface FE guide with the route matrix.


12. Sign-off

  • Approved by: Fabio
  • Date: 2026-08-26
  • Chat reference: “approved everything, craft a spec if no open issues remains” after the record-policy audit and walkthrough of surface-aware filters, supervised-activity retention, communications propagation, nested roster narrowing, and read/write policy separation.

Until this section is filled, no implementation code is written. When you fill it, flip the frontmatter status: to Approved in the same edit.