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 becausestaffis 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 seespersonal 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
staffpolicy 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 namessurface=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
schoolTodayalready 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.tsremains 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/teachersprojection.
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, andScheduledBreakDutyalready 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, audiencebreakId, 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 fromStudentsReadPolicy.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 fromHomeroomsReadPolicy.where(datedCtx). Project direct structural fields and tutor. Student options are the effective roster intersected withStudentsReadPolicy; 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 bySubjectGroupsReadPolicy; projection reusesbuildSubjectGroupsTableWhere/buildCombinedClassesTableWhereso 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:
- Resolve AY and
schoolTodayonce. - Build the selected surface's policy-visible base.
- Apply that endpoint's existing upstream cascade parameters with the same helper used by the surface table.
- Project distinct target ids from the surviving rows, then load/order the existing response shape.
- For people endpoints, additionally intersect the projected ids with
TeachersPolicyorStudentsReadPolicy, and use the same WHERE fordataandcount.
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
surfaceas 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
staffpass-through; teacher:studentRelatedToTeacherOn;referentandstudent: 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
staffremoval; - 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
staffremoval; - teacher/personal limb: caller has an effective
SubjectGroupTeacherepisode; - 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,narrowRosterToVisibleStudentsresolves the roster id set throughStudentsReadPolicy, even for a tenant-wide caller; the policy itself is the only pass-through authority. Write-side internal reloads withctx = nullremain complete. - Apply the same intersection to homeroom and subject-group
pendingChanges[]. - Homeroom
linkedCourses[]intersectsSubjectGroupsReadPolicy; 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¶
StudentsWritePolicycontains onlyadmin/secretarytenant-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.HomeroomsWritePolicyandSubjectGroupsWritePolicycontain 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
resolveAuthorizedRoleSliceper mutated top-level scope withrequiredAccess=WRITE; the target must matchStudentsWritePolicy.where(slice.context)for every mutated scope. The existing assignment target fence and referentcanWritegate remain additional checks. - Student delete resolves
{ entity: STUDENTS, scopeKey: identity, requiredAccess: WRITE, action: delete }. Student document writes resolve thedocumentsWRITE slice. Read-only document paths useStudentsReadPolicy. - Homeroom/SG create, update, delete, and composition operations resolve the
compositionWRITE slice, includingcreate/deleteaction where applicable, and apply the corresponding write policy withslice.contextbefore 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
schoolTodayonly; 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 tostudentRelatedToTeacherOn; 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,stafffail-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.tsplus homeroom/SG service specs: all read rosters and pending changes intersectStudentsReadPolicy; broad roles still receive all roster ids through the policy result; homeroom linked courses intersectSubjectGroupsReadPolicy.src/filters/filters.service.spec.tsandfilters.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.tsandrecipient-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.tsplus 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.