Skip to content

Remove day bounds from the manual timetable flow

1. Problem distillation

  • DayBounds models 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-bounds module: 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 (checkOutsideDayBounds short-circuits on an empty set).
  • This work deletes the concept end-to-end: the Prisma model, the /day-bounds API, 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 from Tenant, Department, Curriculum, Grade.

Migration shape

  • Destructive. npx prisma migrate dev will emit DROP 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.sql against the ch12 hazard checklist; confirm it only drops day_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 on day_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 / ApiDeleteDayBounds decorators are deleted with day-bounds.swagger.ts; the OpenAPI doc loses the /day-bounds tag automatically.
  • The DAY_BOUNDS_INVALID_RANGE error example is removed from error-examples.ts, so it no longer appears in any Swagger error oneOf.

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; memory project_timetable_activities_v1) has ScheduledActivity carry audienceScopes and participate in OUTSIDE_DAY_BOUNDS. Removing day bounds moots those lines. Not edited now (the feature isn't built). Follow-up: when timetable-activities is implemented, drop the audienceScopes / OUTSIDE_DAY_BOUNDS references 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.configuration READ 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 the OUTSIDE_DAY_BOUNDS cases; remove dayBounds/boundsScopes/audienceScopes from fixtures; add/keep an assertion that the engine never emits OUTSIDE_DAY_BOUNDS.
  • src/timetables/timetables.queries.spec.ts — remove day-bounds snapshot assertions and boundsScopes/audienceScopes fixture expectations.
  • src/timetables/timetables.service.spec.ts — remove the boundsScopes fixture field (:66).
  • src/timetables/diagnostics-cards.spec.ts — remove OUTSIDE_DAY_BOUNDS card assertions.
  • diagnostics-cards.drift.spec.ts — expected category set drops OUTSIDE_DAY_BOUNDS; spec passes (also scans solver/app/model.py, which never referenced this category — no solver change).
  • src/timetables/generation/generation.service.spec.ts — remove the dayBounds: [] field (:174) now that DiagnosticsSnapshot has no such field.
  • src/timetables/day-bounds/day-bounds.service.spec.tsdeleted with the module.
  • E2E specs:
  • test/timetables-diagnostics.e2e-spec.ts — remove OUTSIDE_DAY_BOUNDS scenarios + any /day-bounds setup calls.
  • test/timetables.e2e-spec.ts — remove day-bounds references/cleanup (deleteMany etc.).
  • No dedicated day-bounds.e2e-spec.ts exists (confirmed — day-bounds was unit-tested only).
  • Drift e2e: rls-coverage + tenanted-models drift specs pass after entry removal; rbac-grants.db-sync unaffected.
  • Manual verification (per verify skill discipline): npx prisma migrate devnpm run build (regenerates client without DayBounds) → on a seeded draft that previously reported OUTSIDE_DAY_BOUNDS, GET /timetables/:id/diagnostics no longer lists it and canPublish flips true (assuming no other ERROR); GET /day-bounds returns 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.