Remove day bounds from the manual timetable flow¶
1. Problem distillation¶
DayBoundsmodels the school day's{startMinute, endMinute}per department/curriculum/grade (optionally per weekday). Its only functional consumer is one soft diagnostic,OUTSIDE_DAY_BOUNDS(ERROR tier), which fires when a placed lesson/break falls outside the resolved window and thereby gates publish.- The manual flow is a free 5-minute grid the admin drags lessons onto (chapter 18 §1–§2). On a manually-placed timetable the admin already owns the day's shape; a configurable "day bound" that then blocks publish is friction with no pedagogical payoff — it re-introduces the exact "day-shape gate" that iteration-3 otherwise tore out with the templates.
- Day bounds add a whole config surface (
/day-boundsmodule: model + 4 routes + cascade resolver + DTOs + error code + i18n cards + drift entries) whose value never materialized. With zero rows configured — the default — the concept already influences nothing (checkOutsideDayBoundsshort-circuits on an empty set). - This work deletes the concept end-to-end: the Prisma model, the
/day-boundsAPI, the diagnostics category, and every drift/i18n/doc artifact keyed to it.
Success criteria (observable behavior that proves this works):
- No /day-bounds* route exists; hitting any returns 404.
- The day_bounds table is dropped; DayBounds is absent from the Prisma schema and generated client.
- OUTSIDE_DAY_BOUNDS is never computed, never rendered as a card, and never contributes to canPublish. A draft that previously reported it now publishes (assuming no other ERROR).
- The diagnostics engine catalogue is 12 ERROR + 5 WARNING (down from 13 ERROR).
- All drift specs pass with day-bounds entries removed: diagnostics-cards.drift.spec.ts (engine↔registry↔catalog lockstep), rls-coverage, tenanted-models.
- Automatic generation is behaviorally unchanged (it consumes template day-envelopes, not DayBounds).
Non-goals (in-scope-shaped things this iteration is explicitly not doing):
- Not touching the generation-only template day-envelope machinery (src/common/utils/day-envelope.ts, generation/effective-templates.ts) — different concept, stays.
- Not changing how the FE renders the grid's visible time range (that was always FE-owned; day bounds never drove it).
- Not implementing teacher published-timetable reads. The teacher timetables.configuration READ grant is kept (per sign-off decision) as a forward-looking inert grant, not wired to any new route here.
- Not editing the committed-but-unimplemented timetable-activities v1 plan now (see §9).
2. Patterns survey¶
| Analogous module/spec | What we'd borrow | What doesn't fit |
|---|---|---|
docs/superpowers/specs/2026-07-09-timetable-manual-management-iteration-3-design.md — deleted the DAY_OVERFLOW + NO_EFFECTIVE_TEMPLATE diagnostic categories |
The exact removal sequence for a diagnostics category: drop the check* fn + its call in computeDiagnostics, drop the card renderer + catalog keys, update the drift spec's expected set |
Those categories were template-era in-memory only; here we additionally drop a DB model + a whole module + routes + an error code |
docs/18-timetables.md "Combined classes" — iter-3 deleted the entire v1 co-location machinery (COMBINED_SHARED_MISCOUNT, COMBINED_CLASS_ROOM_SPLIT, exemptions) |
Precedent that removing diagnostic categories with no successor check is an accepted, clean move (not every signal must be replaced) | Those had no dedicated routes/DTOs/DB table to unwind |
src/prisma/rls-coverage.ts + src/prisma/tenanted-models.ts + docs/12-migrations.md "New tenant-bearing model checklist" |
Run the tenant-model checklist in reverse: destructive DROP TABLE (policy drops with the table) + remove both drift-registry entries so the lists still match the live schema |
The chapter checklist is written for adding a model; we invert every step |
src/timetables/diagnostics-cards.ts + src/common/i18n/diagnostic-messages.catalog.ts + diagnostics-cards.drift.spec.ts |
The renderer↔catalog↔engine lockstep contract: a category must be removed from all three (renderer row, .title/base/.break catalog keys, DIAGNOSTIC_TEXT_PARAMS bijection) in one change or the drift spec fails |
Fits cleanly — this is the governing contract for the diagnostics-side deletion |
3. Architecture mapping¶
| Primitive | Apply? | How | Justify |
|---|---|---|---|
| Tenant scope | yes (being removed) | DayBounds was tenant-scoped (RLS-covered + in tenanted-models.ts). Removal drops the table (policy drops with it) and both drift-registry entries |
Keeps the RLS-coverage / tenanted-models drift specs matching the live schema |
| Academic-year scope | no | DayBounds was (tenant)-scoped only, never AY-scoped |
n/a — nothing to unwind |
| RBAC entity key | existing EntityKey.TIMETABLES (unchanged) |
Day-bounds routes rode TIMETABLES; deleting the routes removes no entity key |
n/a — no key added or removed |
| Scopes | timetables.configuration (reused, unchanged) |
No scope key removed. The teacher READ grant on this scope is kept (§6) | Forward-looking; matches feedback_rbac_drift_prefer_admit |
| Actions | none | Day-bounds used only read/write (implicit + descriptor); no dedicated action existed |
n/a — nothing to remove |
| Service base | custom (being deleted) | DayBoundsService was a hand-written service, not BaseTenantedCrudService. Deleted with the module |
n/a |
queries.ts shape |
delete + prune | Delete src/timetables/day-bounds/day-bounds.queries.ts; remove loadDayBoundsRows + the dayBounds snapshot field + boundsScopes/audienceScopes snapshot fields from timetables.queries.ts |
Those snapshot fields exist solely for checkOutsideDayBounds (verified: no other check reads them) |
| Error codes | remove DAY_BOUNDS_INVALID_RANGE |
Drop from error-codes.ts enum (:239), the message catalog (:760), error-examples.ts, and the DIAGNOSTIC_TEXT_PARAMS-style params bijection (error-codes.ts:950) |
Only the day-bounds upsert threw it |
| DTO conventions | delete | Delete src/timetables/day-bounds/dto/day-bounds.dto.ts and day-bounds.swagger.ts |
n/a |
| File-backed sub-resources | no | n/a — day bounds are scalar rows | n/a |
| Custom fields | no | n/a | n/a |
| Profile completeness | no | n/a — not a person entity | n/a |
Removal inventory (complete file list)¶
Delete outright
- src/timetables/day-bounds/ — entire folder: day-bounds.controller.ts, day-bounds.service.ts, day-bounds.queries.ts, day-bounds.resolve.ts, day-bounds.swagger.ts, dto/day-bounds.dto.ts, day-bounds.service.spec.ts.
Edit
- src/timetables/timetables.module.ts — drop DayBoundsController from controllers, DayBoundsService from providers + imports.
- src/timetables/timetables.diagnostics.ts — remove checkOutsideDayBounds, its call in computeDiagnostics, and the resolveDayBounds import.
- src/timetables/timetables.queries.ts — remove loadDayBoundsRows, the dayBounds field on DiagnosticsSnapshot, the boundsScopes field on SnapshotLesson, the audienceScopes field on SnapshotBreak, audienceScopeOf, and the BoundsScope/DayBoundsSourceRow imports + their population sites.
- src/timetables/diagnostics-cards.ts — remove the OUTSIDE_DAY_BOUNDS renderer row (:398).
- src/common/i18n/diagnostic-messages.catalog.ts — remove the 5 keys (.title, base, .break, and the two item-line entries at :201, :205, :211, :478, :486).
- src/common/constants/error-codes.ts — remove DAY_BOUNDS_INVALID_RANGE (enum + message + params bijection).
- src/common/i18n/error-messages.catalog.ts, src/common/constants/error-examples.ts — remove DAY_BOUNDS_INVALID_RANGE entries.
- src/prisma/tenanted-models.ts — remove 'DayBounds' (:49).
- src/prisma/rls-coverage.ts — remove 'day_bounds' (:76).
- prisma/schema.prisma — remove model DayBounds and the dayBounds DayBounds[] back-relations on Tenant, Department, Curriculum, Grade.
- prisma/seed/roles.ts — keep the teacher grant; re-comment the timetables.configuration justification (:42–46) so it no longer claims to "ride the day-bounds read".
- src/timetables/generation/effective-templates.ts — update the comment (:18) that references the "day-bounds cascade" as the manual-flow day-shape source.
- Specs (see §11): timetables.diagnostics.spec.ts, timetables.queries.spec.ts, timetables.service.spec.ts, diagnostics-cards.spec.ts, generation.service.spec.ts, test/timetables-diagnostics.e2e-spec.ts, test/timetables.e2e-spec.ts.
- Docs: docs/18-timetables.md (§1 model list → 4 models; §2; §3 catalogue header → 12 ERROR + 5 WARNING and drop the OUTSIDE_DAY_BOUNDS row + its variant mentions; §6 day-bounds paragraph; §7 teacher carve-out; §9 key files), docs/REFERENCE.md (:138 day-shape sentence + §6 file-index row), docs/fe-guides/2026-07-10-timetable-manual-flow.md, docs/fe-guides/2026-07-12-timetable-diagnostics-cards.md.
4. Data model plan¶
Schema deltas¶
- Drop
model DayBounds(schema.prisma:2513). - Remove the
dayBounds DayBounds[]back-relation field fromTenant,Department,Curriculum,Grade.
Migration shape¶
- Destructive.
npx prisma migrate devwill emitDROP TABLE "day_bounds"(its FK constraints and the tenant RLS policy drop with the table). - Data backfill: none (breaking changes acceptable; dev DB droppable —
feedback_breaking_changes_acceptable). - Hazards from chapter 12 checklist:
- Uncommitted schema/migration at implementation start — the working tree already shows
M prisma/schema.prisma. Before generating this migration, check whether that pending edit should be folded/committed first (ch12 "check for an uncommitted migration and ask whether to fold"). - Removing a table is the safe direction of a destructive change (no dependents survive: all four FK owners cascade into
day_bounds, nothing references it outbound). - Post-generation: audit
migration.sqlagainst the ch12 hazard checklist; confirm it only dropsday_bounds(+ its policy) and touches nothing else. - Drift-registry removals (
tenanted-models.ts,rls-coverage.ts) must land in the same change as the migration or the drift specs fail.
Indexes and uniqueness¶
- The
@@index([tenantId])and the partial-unique behavior lived onday_bounds; both vanish with the table. No index changes elsewhere.
5. API surface¶
All four routes are removed (no replacement):
| Verb | Path | Decorators (removed) | Request DTO | Response DTO |
|---|---|---|---|---|
| GET | /day-bounds |
@RequireScopes(TIMETABLES,'read'), @RequireRoles('admin','teacher') |
— | ~~DayBoundsListDto~~ |
| GET | /day-bounds/effective |
@RequireScopes(TIMETABLES,'read'), @RequireRoles('admin','teacher') |
~~EffectiveDayBoundsQueryDto~~ |
~~EffectiveDayBoundsDto~~ |
| PUT | /day-bounds |
@RequireScopes(TIMETABLES,'write'), @AppliesPolicy(TimetablesPolicy) |
~~UpsertDayBoundsDto~~ |
~~DayBoundsDto~~ |
| DELETE | /day-bounds |
@RequireScopes(TIMETABLES,'write'), @AppliesPolicy(TimetablesPolicy) |
~~DeleteDayBoundsQueryDto~~ |
204 |
Swagger considerations¶
- The
ApiDayBoundsController/ApiListDayBounds/ApiEffectiveDayBounds/ApiUpsertDayBounds/ApiDeleteDayBoundsdecorators are deleted withday-bounds.swagger.ts; the OpenAPI doc loses the/day-boundstag automatically. - The
DAY_BOUNDS_INVALID_RANGEerror example is removed fromerror-examples.ts, so it no longer appears in any Swagger erroroneOf.
6. RBAC seed plan¶
| Seed file | Delta |
|---|---|
PermissionScope (rbac-catalogue.ts) |
none |
PermissionAction (rbac-catalogue.ts) |
none |
ScopeFieldMapping (rbac-catalogue.ts) |
none |
Role grants (prisma/seed/roles.ts) |
No grant removed. Teacher keeps timetables.configuration READ (sign-off decision). Only the explanatory comment (:42–46) is rewritten — it must no longer say the grant "rides the day-bounds read"; new wording: reserved for imminent teacher published-timetable reads, day-bounds removed 2026-07-13. |
*_SCOPES runtime constant |
none — TIMETABLES scope set is unchanged |
The rbac-grants.db-sync drift mirror (expectedPresetGrants) is unaffected because no grant changes.
7. Divergence ledger¶
| Pattern | We diverge by | Reason | Tradeoff accepted |
|---|---|---|---|
| "Remove dead grants when their sole consumer is deleted" | Keeping the teacher timetables.configuration READ grant even though its only route (the day-bounds GETs) is gone |
feedback_rbac_drift_prefer_admit (widen over drop) + teachers will need published-timetable reads imminently; re-adding is churn |
An inert grant lingers until teacher timetable reads land. Not a drift failure (drift checks scope-constant↔seed parity, not route usage). |
| Diagnostic-category removal (iter-3 precedent) | None — follows the engine→card→catalog→drift removal sequence exactly | — | — |
| Tenant-model removal (ch12 checklist, reversed) | None — destructive DROP + both drift-registry entries removed together | — | — |
8. Pushback log¶
| US says | Conflicts with | Proposed instead | Status |
|---|---|---|---|
| None — engineering-initiated simplification, no US/Gherkin to push back on | — | — | Resolved |
9. Deferrals¶
- timetable-activities v1 plan coupling — the committed-but-unimplemented plan (
docs/superpowers/plans/2026-07-13-timetable-activities.md:430,458; memoryproject_timetable_activities_v1) hasScheduledActivitycarryaudienceScopesand participate inOUTSIDE_DAY_BOUNDS. Removing day bounds moots those lines. Not edited now (the feature isn't built). Follow-up: when timetable-activities is implemented, drop theaudienceScopes/OUTSIDE_DAY_BOUNDSreferences from its plan (they simply disappear — no replacement). - Teacher published-timetable reads — the kept teacher grant is forward-looking only; wiring actual teacher-facing timetable read routes is a separate future iteration. Follow-up: revisit when teacher timetable UX is specced.
10. Open questions¶
- Teacher
timetables.configurationREAD grant: keep or drop? → Resolved: keep (Fabio, chat 2026-07-13).
None remaining.
11. Verification plan¶
- Unit specs:
src/timetables/timetables.diagnostics.spec.ts— delete theOUTSIDE_DAY_BOUNDScases; removedayBounds/boundsScopes/audienceScopesfrom fixtures; add/keep an assertion that the engine never emitsOUTSIDE_DAY_BOUNDS.src/timetables/timetables.queries.spec.ts— remove day-bounds snapshot assertions andboundsScopes/audienceScopesfixture expectations.src/timetables/timetables.service.spec.ts— remove theboundsScopesfixture field (:66).src/timetables/diagnostics-cards.spec.ts— removeOUTSIDE_DAY_BOUNDScard assertions.diagnostics-cards.drift.spec.ts— expected category set dropsOUTSIDE_DAY_BOUNDS; spec passes (also scanssolver/app/model.py, which never referenced this category — no solver change).src/timetables/generation/generation.service.spec.ts— remove thedayBounds: []field (:174) now thatDiagnosticsSnapshothas no such field.src/timetables/day-bounds/day-bounds.service.spec.ts— deleted with the module.- E2E specs:
test/timetables-diagnostics.e2e-spec.ts— removeOUTSIDE_DAY_BOUNDSscenarios + any/day-boundssetup calls.test/timetables.e2e-spec.ts— remove day-bounds references/cleanup (deleteManyetc.).- No dedicated
day-bounds.e2e-spec.tsexists (confirmed — day-bounds was unit-tested only). - Drift e2e:
rls-coverage+tenanted-modelsdrift specs pass after entry removal;rbac-grants.db-syncunaffected. - Manual verification (per
verifyskill discipline):npx prisma migrate dev→npm run build(regenerates client withoutDayBounds) → on a seeded draft that previously reportedOUTSIDE_DAY_BOUNDS,GET /timetables/:id/diagnosticsno longer lists it andcanPublishflips true (assuming no other ERROR);GET /day-boundsreturns 404.
Patterns: chapter 09 (testing), feedback_e2e_isolation_patterns.md.
12. Sign-off¶
- Approved by: Fabio Barbieri
- Date: 2026-07-13
- Chat reference: approved by Fabio in chat 2026-07-13 after walkthrough of the removal inventory + teacher-grant decision (keep)
Until this section is filled, no implementation code is written. When you fill it, flip the frontmatter status: to Approved in the same edit.