Skip to content

Per-subject period durations — envelope templates + reflow timetables

AMENDED 2026-07-08 (adversarial architecture review, ratified in chat): the coordinate model in §4 is now the absolute start tick (5-minute steps from midnight) — two refinement steps past the original "sequence position" text (see §4 for the full rationale chain) — and the column is renamed periodOrdinalPositionstartTick (start_tick) on ScheduledLesson + AttendanceRecord (plan rename addendum, ratified 2026-07-07: the old name is an ordinal lie once the value is a time offset). SEQUENCE_MISALIGNMENT is dropped (impossible by construction), the ORDER break-invalid reason is dropped (order derives from startTime), and the whole-minutes ε is fixed at 0.01 minute. All other sections stand as approved.

1. Problem distillation

  • Some schools do not have one universal lesson length: languages run 40', content subjects 60', labs 90'. Today a day template is a fixed ordered slot list, so every subject placed in slot k gets slot k's duration — these schools cannot be represented at all.
  • Period duration must become a property of the subject (with sensible defaults), resolved through the same optional-inherited cascade the curriculum grid already uses for evaluation scales: curriculum → curriculum-grade → subject → (subject, grade) cell, falling through to a template-level default.
  • The day template stops enumerating slots and becomes an envelope: start time, end time, fixed break/lunch bands, and one default period duration. A class's bell sequence is derived from what is placed in it (lessons of possibly-different lengths pack one after another between the fixed anchors) — the "reflow grid".
  • weeklyHours on the grid cell means teaching time (clock time), not a period count. The number of weekly lessons derives: time ÷ effective duration.
  • Feasibility feedback must arrive at the earliest surface that can know about it: curriculum authoring → template authoring → template assignment (the demand-vs-supply reconciliation point) → generation pre-checks → diagnostics.

Success criteria (observable behavior that proves this works): - A tenant that never sets any periodDuration gets today's behavior: uniform grids derived from the template default, identical diagnostics, identical generation results (modulo solver encoding). - A curriculum with English at 40' and Math at 60' in the same grade can be authored, generated (FEASIBLE where a solution exists), manually edited, and published; each homeroom's read view shows its own derived bell times, aligned at the template's break/lunch anchors. - A 90' lab subject is one 90' lesson — no double-period workaround. - GET /timetable-assignments reports per-grade demand-vs-supply mismatches (minutes identity, divisibility, band tileability) before any timetable exists. - Attendance PERIOD-mode keys keep functioning (column renamed start_tick; rows preserved); existing attendance history is untouched at the storage level (rows are self-contained snapshots with label + wall times). Caveat (2026-07-08): pre-migration rows store slot-index ordinals that will never match tick-frame recordable cells for past dates — those registers become read-only history. Accepted (greenfield).

Non-goals (in-scope-shaped things this iteration is explicitly not doing): - Position-in-day preferences ("long lessons in the morning") — explicitly excluded by product; the solver guarantees feasibility, not placement taste. - Per-period passing time (a 5' interval between every lesson) — inexpressible under fixed bands + floating boundaries; stated limitation. - Floating (per-class) breaks/lunch — anchors are template-fixed; that is what keeps canteen/supervision logistics plannable. - Combined classes (HL/SL) — unblocked by this work (wall-clock co-location primitive), lands as its own resumed iteration. - Notifying users when a published timetable's derived times shift — same deferral as live-edit notifications (iteration-2 spec).


2. Patterns survey

Analogous module/spec What we'd borrow What doesn't fit
src/evaluation-scales/evaluation-scales.queries.ts (resolveEffectiveCellScale, SCALE_FK_REFERENCES) + spec 2026-05-18-evaluation-scales-design.md The 4-level optional-inherited cascade on the curriculum tree, the x + effectiveX dual read projection, the write-path visibility assert pattern Scales resolve to a curriculum-level root; durations fall through the curriculum to a template default that is only resolvable per (grade, AY) via week-template assignment — the cascade has an external tail
src/timetable-templates/ (timetable-templates.service.ts, day-templates.controller.ts, timetable-assignments.controller.ts) + spec 2026-05-25-timetable-templates-design.md Template CRUD conventions, the dept→grade assignment resolver, and the assignment-tree computed-alert surface (LUNCH_CAPACITY_EXCEEDED) that the new fit report extends The TimeSlot ordered-slot model is the thing being deleted; assertSequentialPositions on slots goes with it
src/timetables/timetables.diagnostics.ts + buildDiagnosticsSnapshot (timetables.queries.ts) + spec 2026-06-11-timetable-manual-management-iteration-2-design.md Pure engine over a plain snapshot, {category, severity, params} i18n violations, interval math (windowsOverlap, subtractIntervals) already used for teaching windows The ordinal→wall-clock resolver assumes a template slot list; it is reimplemented as the tick resolver (tick × 5 + cascade duration), and three checks change semantics (see the catalogue delta table below)
src/timetables/generation/ + solver/ + spec 2026-06-12-timetable-generation-design.md Pipeline shape (snapshot → pre-checks → solver → ingest → verify), assumption-literal → unsat-core violation mapping, SolverPort seam, pre-check conventions The CP-SAT model itself (x[(group, slot)] over a shared discrete grid) cannot express variable durations — full model rewrite (§ Solver)
spec 2026-07-07-curriculum-zero-hours-not-taught-design.md Cell presence ⟺ taught; weekly_hours > 0 CHECK; isTaughtCell write-path idiom Fits cleanly — column, CHECK, and idiom carry over unchanged
spec 2026-06-16-combined-classes-design.md (§13 addendum, PAUSED) Confirms wall-clock co-location is the primitive future consumers need — validates spending on it here Not implemented here; consumes this work later

On-axis / off-axis (ch16 §4–§5): the cascade columns, DTO deltas, and fit-report alerts are on paved paths. The tick coordinate semantics (absolute 5-minute start ticks, wall-clock derived, duration from the cascade) and the solver interval model are off-axis pattern inventions — they are called out as the two load-bearing novelties of this spec, and §4/§8 below carry the inventor's rationale (grid machinery preserved; packing is unavoidable once durations vary).


3. Architecture mapping

Primitive Apply? How Justify
Tenant scope yes All touched tables already carry tenantId (templates) or FK-chain to it (curriculum tree, lessons); RLS coverage unchanged No new tables
Academic-year scope yes Unchanged: curricula + timetables + assignments are AY-scoped; templates stay tenant-flat Same as today
RBAC entity key existing TIMETABLE_TEMPLATES, CURRICULA, TIMETABLES — no new keys Feature reshapes existing surfaces
Scopes existing timetable_templates.configuration, curricula.configuration, timetables.configuration No new scopes
Actions none Existing create/delete/assign/publish/generate cover everything No new verbs
Service base custom (existing) TimetableTemplatesService, CurriculumService, TimetablesService — none use BaseTenantedCrudService; unchanged
queries.ts shape existing files extended Tick resolver + snapshot deltas in timetables.queries.ts; fit-report loaders in timetable-templates.queries.ts; cascade resolution helper in curriculum.queries.ts (mirroring resolveEffectiveCellScale)
Error codes new CURRICULUM_HOURS_NOT_DIVISIBLE (params: subjectName, gradeName, weeklyHours, periodDuration); DAY_TEMPLATE_BREAK_INVALID (params: reason: OVERLAP \| OUT_OF_ENVELOPEORDER dropped 2026-07-08: order derives from startTime, unorderable by construction). Diagnostics categories (not ErrorCodes): new DAY_OVERFLOW; re-semanticized HOUR_BUDGET, HOMEROOM_GAP, OPTION_BLOCK_SYNC (SEQUENCE_MISALIGNMENT dropped 2026-07-08 — impossible under tick coordinates) i18n catalog entries added for both codes + categories
DTO conventions existing files reshaped dto/day-template.dto.ts (envelope shape), dto/time-slot.dto.tsdto/day-template-break.dto.ts, curriculum grid DTOs gain periodDuration/effectivePeriodDuration FE-breaking (template + view reshapes), accepted (greenfield)
File-backed sub-resources n/a No files involved
Custom fields no n/a — none of the touched entities take custom fields for these surfaces
Profile completeness no n/a — no person entities touched

4. Data model plan

Schema deltas

  • DayTemplate: keep startTime; add endTime String @db.VarChar(5) and periodDuration Int (minutes; validated multiple of 5, 20 ≤ d ≤ 240). Drop the slot list.
  • TimeSlotDayTemplateBreak (rename/remodel): { id, dayTemplateId, type BreakType, name, startTime VarChar(5), durationMinutes Int, lunchShiftId?, roomId? }. BreakType { INTERVAL, LUNCH } replaces SlotType { PERIOD, INTERVAL, LUNCH } — PERIOD rows cease to exist. lunchShiftId required iff LUNCH, roomId optional on INTERVAL (same Restrict FKs as today). Breaks are fixed wall-clock bands; service validates: inside [startTime, endTime], non-overlapping, startTime-ordered. ordinalPosition is dropped (order derives from startTime); the inline slot-contiguity check and the REORDER_OFFSET shift mechanism in timetable-templates.service.ts go with it (assertSequentialPositions itself is untouched — it belongs to departments/periods).
  • Cascade columnsperiodDuration Int? on Curriculum, CurriculumGrade, CurriculumSubject, CurriculumSubjectHours (four levels, mirroring the grading-scale columns). Resolution for a (subject, grade) cell: cell ?? subject ?? curriculumGrade ?? curriculum ?? <template default at placement time>. The first four levels are the curriculum-resolved duration; when all null, the lesson inherits the periodDuration of the day template it lands on (which may differ per weekday via the week template).
  • CurriculumSubjectHours.weeklyHours Float — unchanged (user decision: schools think and calculate in hours; no rename, no unit change). Semantics are clock-time (teaching-hours, not periods). Because divisible hour values at 40/45/50' durations are non-terminating decimals (⅚ h ≈ 1.6667), all duration arithmetic goes through one conversion helperweeklyHoursToMinutes(h) = Math.round(h × 60) — and every check (budget, identities, divisibility, fit report, solver payload) compares integer minutes produced by it. New DTO validation on the cell: weeklyHours × 60 must be within ε = 0.01 minute of an integer (whole minutes; ε fixed 2026-07-08 — wide enough for 4-decimal truncations of sixths like 1.6667 h → 100.002 min, tight enough to reject 1.234 h → 74.04 min) — plain class-validator 400, no new ErrorCode. The > 0 CHECK and the isTaughtCell write idiom are untouched.
  • ScheduledLesson: coordinate column renamed periodOrdinalPositionstartTick (start_tick; same rename on AttendanceRecord, metadata-only, rows preserved); uniqueness re-keyed as @@unique([timetableId, subjectGroupId, weekday, startTick]). Semantics change (below). Lesson duration is derived at read time from the effective duration — never stored — so a curriculum edit re-resolves every lesson exactly as a template edit does today.

Coordinate semantics — absolute start ticks (off-axis, the heart of the spec)

AMENDED 2026-07-08. The rationale chain, for the record: 1. Original spec text (superseded): ordinal = "position in the class's teaching sequence", wall-clock derived by accumulation (the reflow resolver). Unsound at solver ingest — two homerooms can reach the same wall-clock minute after a different number of lessons (45+45+45 = 45+90 = 135'), so a single stored sequence index cannot resolve to one time. Ratified out 2026-07-07. 2. First refinement (superseded): tick offset from the template's startTime. Sound, but gives every template a private frame: a template startTime edit silently re-means every stored coordinate (lessons shift while the wall-clock break bands stay put), and the combined-classes pseudo-group (approved consumer, cross-grade members possibly on different templates) would need per-(group, homeroom) frame translation inside the CP-SAT model — contradicting that spec's "no Python constraint changes" criterion. Ratified out 2026-07-08. 3. Final (this spec): absolute frame — see below.

startTick (né periodOrdinalPosition) = the lesson's start tick in 5-minute steps from midnight (96 = 08:00; DTO bounds 0..287). One global axis shared by coordinates, break bands, the diagnostics engine, and the solver:

  • Wall-clock start = tick × TICK_MINUTES; end = start + effective duration (cascade ?? that weekday's template default). Duration is never stored — a curriculum or template edit re-resolves every lesson.
  • Cross-template comparisons (co-location, room/teacher occupancy across grades) need no frame translation; a template startTime edit never re-means a stored coordinate — a lesson that no longer fits simply surfaces DAY_OVERFLOW.
  • A mid-day duration edit does not auto-shift later lessons: the resulting gap/overlap surfaces as diagnostics (the FE may offer a "shift rest of day" bulk edit later).
  • The empty grid renders from virtualPositions (default-duration tiling of the envelope's teaching bands); placed lessons merge with the virtual remainder in the sequence views.
  • SEQUENCE_MISALIGNMENT (from the superseded sequence model) is impossible by construction — one tick is one time; plain interval-overlap checks cover everything it policed.

Structural floor (divergence, logged in §7): in-tenant FKs, coordinate uniqueness, effective-template existence for the group's grade + weekday, and start-within-envelope (envelope.start ≤ tick×5 < envelope.end). Band fit (break straddle / envelope exit) and overlaps move to diagnostics (DAY_OVERFLOW). Consistent with "DRAFT authoring never blocks".

Migration shape

  • Destructive + renaming. In order:
  • time_slots: DELETE PERIOD rows; rename table/model to day_template_breaks; convert ordinalPosition to a backfilled start_time (computed by walking the old slot list from DayTemplate.startTime); swap enum to BreakType.
  • day_templates: add end_time (backfill = old startTime + Σ old slot durations) and period_duration (backfill = modal PERIOD duration of the old slot list; fallback 60).
  • Add the four nullable period_duration cascade columns.
  • Wipe scheduled_lessons + timetables — old coordinates index the deleted slot list (PERIOD ordinals were non-contiguous around INTERVAL/LUNCH positions) and cannot be mapped onto tick coordinates. Attendance survives by design (soft-FK snapshots); PERIOD-mode recording pauses until a timetable is re-published.
  • Rename period_ordinal_positionstart_tick on scheduled_lessons (rows already wiped) and attendance_records (metadata-only; historical values keep their old slot-list meaning regardless of the name — noted in the migration header).
  • Hazards (ch12): enum value removal (PERIOD) — requires the delete-first ordering above; table rename with backfilled start_time — expand/contract not needed (dropable DBs, breaking accepted). Fold-check: an uncommitted migration (20260707120000_curriculum_hours_positive_not_taught) exists in the working tree — ask before generating whether to fold or sequence after it.
  • Seed/fixtures: tier-2 fixture builders that seed day templates / slots and any timetable e2e fixtures move to the envelope shape.

Indexes and uniqueness

  • day_template_breaks: keep @@index([dayTemplateId]), FK Restrict on lunchShiftId/roomId; replace @@unique([dayTemplateId, ordinalPosition]) with @@unique([dayTemplateId, startTime]).
  • ScheduledLesson unique/coordinate indexes carry over, re-keyed on the renamed startTick column.
  • No new indexes on the cascade columns (resolved in-memory per curriculum load, as scales are).

5. API surface

No new routes. Reshaped contracts (all FE-breaking, accepted):

Verb Path Decorators Request DTO Response DTO
POST/PATCH /day-templates(/:id) existing (timetable_templates.configuration + actions) CreateDayTemplateDto{ name, startTime, endTime, periodDuration, breaks[] } template + warnings[] (non-blocking: teaching-band minutes not divisible by own periodDuration)
GET /day-templates existing envelope shape; slot list gone
PATCH /curricula/:id existing family-sync grid arrays gain optional periodDuration at all four levels; cell weeklyHours unchanged (+ whole-minutes validation) levels expose periodDuration + effectivePeriodDuration (curriculum-resolved; null = template default), mirroring gradingScale/effectiveGradingScale
GET /timetable-assignments existing tree gains fit-report alerts per grade node (below), beside LUNCH_CAPACITY_EXCEEDED
GET /timetables/:id/lessons?view=… existing lessons carry resolved startTime/endTime + durationMinutes; slotGrids → derived sequence grids (virtual default-duration rows merged with placed durations) for class views; time-axis union for room/teacher/student views
POST/PATCH/DELETE …/lessons existing unchanged coordinate fields unchanged envelope { lesson?, diagnostics }

Fit-report alert categories (computed, aggregate — same pattern as LUNCH_CAPACITY_EXCEEDED; params carry curriculumId since a grade can host several): - TEMPLATE_MINUTES_MISMATCH — Σ cell minutes (weeklyHoursToMinutes) for (curriculum, grade) ≠ Σ teaching minutes over the assigned week template's operative days. - HOURS_NOT_DIVISIBLE — a cascade-silent cell whose converted minutes are not expressible with the assigned templates' default durations (single-duration week: plain divisibility; mixed-duration week: non-negative integer combination check). - BAND_NOT_TILEABLE — some teaching band of an assigned day template cannot be exactly filled by any non-negative combination of the grade's duration set (cheap coin-problem DP).

Swagger considerations

  • New error examples for CURRICULUM_HOURS_NOT_DIVISIBLE, DAY_TEMPLATE_BREAK_INVALID; diagnostics-category enum in the timetables swagger doc gains DAY_OVERFLOW.
  • slotGrids reshape + day-template envelope reshape called out in the changelog for FE; JSDoc on the affected controller methods updated as public copy (per the swagger discipline).

6. RBAC seed plan

Seed file Delta
PermissionScope (rbac-catalogue.ts) none
PermissionAction (rbac-catalogue.ts) none
ScopeFieldMapping (rbac-catalogue.ts) none — touched entities are flat-DTO (FLAT_DTO_ENTITIES) or already-mapped curriculum surfaces with no new scoped fields
Role grants (roles.ts) none
*_SCOPES runtime constant none

7. Divergence ledger

Pattern We diverge by Reason Tradeoff accepted
Coordinate = template slot index (manual-mgmt spec §1) Coordinate = absolute 5-minute start tick (from midnight); wall-clock = tick × 5, duration cascade-derived (2026-07-08 amendment; see §4 rationale chain) Keeps the grid machinery (schema, uniqueness, attendance keys, FE grid) while durations vary per subject; one global frame for cross-template comparisons and the solver A mid-day duration edit doesn't auto-shift later lessons — gaps/overlaps surface as diagnostics (FE bulk-shift affordance possible later)
Structural floor rejects invalid coordinates at write Band-fit validity moves to diagnostics (DAY_OVERFLOW) Fit depends on the lesson's resolved duration, not just the coordinate A DRAFT can hold an overflowing day; publish gate still blocks
Eval-scales cascade roots at Curriculum Duration cascade tails out to the day template default Product model: template owns the school-day base; curriculum owns pedagogical exceptions Effective value not fully resolvable at curriculum-write time → divisibility gate is layered (write-time only when curriculum-resolved; else fit report / pre-check)
Ordered sets use 0-based ordinalPosition (REFERENCE §5) DayTemplateBreak ordering derives from startTime; day-template slot ordinals deleted (inline contiguity check + REORDER_OFFSET mechanism removed) Breaks are wall-clock bands, not an ordered list REFERENCE §5 ordinals bullet updated (day-template slot check removed from the enforcement list)
HOUR_BUDGET = placed-lesson count ≠ cell Σ placed lesson minutesweeklyHoursToMinutes(weeklyHours) Count is ill-defined when the template default varies per weekday Retires the HOURS_FRACTIONAL skipped-groups wart (fractional hours were never placeable; 1.5 h now is)

8. Pushback log

US says Conflicts with Proposed instead Status
Cascade on three levels: cv, (cv, grade), (cv, grade, subject) Eval-scales precedent is four levels (subject-spanning level included) Four levels — "chemistry is 90' in every grade" is one value; resolver shape already exists Resolved (product doc approved with four levels)
"How many periods per subject = hours weekly ÷ effective period" Count is ill-defined with per-weekday template defaults; Float-hours arithmetic inexact at 40/45/50' Minutes-sum identity: Σ placed durations == weeklyHoursToMinutes(weeklyHours); count emerges. Storage stays weeklyHours Float — user decision 2026-07-07: schools think and calculate in hours; exactness handled by the rounded conversion helper + whole-minutes DTO validation Resolved
Day template "just gains a periodDuration field" Nothing would define day end or break positions once slots are gone Envelope: explicit endTime + breaks as fixed wall-clock bands Resolved (user confirmed envelope: "start/end, lunch/break intervals and a single number")
— (raised by us) position-in-day affinity for rhythm schools Solver scope Explicitly excluded: feasibility + domain fidelity only Resolved (product decision)

9. Deferrals

  • Combined classes (HL/SL) — consumes wall-clock co-location; resumes as its own iteration once this lands — follow-up: project_combined_classes_design memory + spec §13 addendum.
  • Per-period passing time / floating breaks — inexpressible under the fixed-anchor model; stated product limitation — revisit only on real demand.
  • Curriculum presets carrying periodDuration — presets are platform-global; duration is school-operational. Presets stay duration-silent — revisit with backoffice if a preset family needs it.
  • Solver soft-objective retuning — v1 targets parity of the existing warning-mirror + quality terms under the new encoding; weight re-tuning is follow-up — revisit after first real-school generation.
  • Published-timetable change notifications — unchanged deferral from iteration 2.
  • FE hours-input affordance (type hours vs minutes vs pick-a-count) — UX decision, BE stores hours either way.
  • Solver-driver hardening — surfaced by the 2026-07-08 post-implementation review (no correctness bug; enhancements only): (1) _minimize_core in solver/app/solve.py re-solves with the soft objective still attached, so feasible core-shrink re-solves run to OPTIMAL and, under a tight per-iteration time limit, time out to UNKNOWN → the loop conservatively keeps the candidate, yielding a larger-than-minimal unsat core (worse "why infeasible" messages). Fix: clear the objective or set stop_after_first_solution for the minimization re-solves. (2) The main-solve floor max(1.0, budget × 0.75) hands 100 % of a ≤1 s budget to the main solve, contradicting the documented 75 % split. (3) The tick grid assumes 5-minute-aligned band/duration boundaries (currently guaranteed by the DTO validators, not asserted in the solver) — a cheap defensive assert would fail loud if a future DTO change loosens the constraint. (4) Determinism is guaranteed only on OPTIMAL/INFEASIBLE termination, not on wall-clock timeout — the determinism test can't catch a near-timeout regression on a real instance. Revisit alongside the soft-objective retuning after the first real-school generation.
  • Migration header comment fix (cosmetic)20260707140000_day_template_envelope/migration.sql still describes coordinates as "tick offsets from the template startTime" (superseded relative frame); the code shipped the absolute frame. Fold the one-line comment fix in at the next dev-DB reset (editing an applied migration otherwise drifts its checksum).

Solver rewrite & diagnostics delta (sized honestly — the largest work item)

The x[(group, slot)] discrete-assignment model cannot express variable durations. Rewrite of solver/app/model.py + objective.py + solve.py + schemas.py, keeping the pipeline, SolverPort seam, and assumption-literal → unsat-core machinery.

  • Recommended encoding (not locked — implementation plan decides): 5-minute micro-ticks on the absolute axis (ticks from midnight — same frame as the stored coordinate, so cross-template groups and the combined-classes pseudo-group need no frame translation). x[(group, day, startTick)] BoolVars; occupancy sums per resource per tick replace slot-equality conflicts; teaching bands bound the tick domain; breaks are excluded ticks; per (homeroom, operative day): Σ placed durations == teaching minutes (which, with no-overlap + band containment, yields exact tiling ⇒ gap-freeness). Alternative: chained-sequence IntVars per homeroom-day. Micro-tick is closer to the current model and reuses the constraint-family structure.
  • Hard families: the existing 7 map over (HOUR_BUDGET → minutes identity; HOMEROOM_COVERAGE → per-day fill; conflicts → tick occupancy; OPTION_BLOCK_SYNC → equal start tick + equal duration; CLASH → tick overlap).
  • Ingest: solver returns wall-clock start minutes; ingest stores startMinute / TICK_MINUTES directly (a non-integer division indicates solver drift → marker error). Post-ingest computeDiagnostics assert stays the drift safety-net.
  • Pre-check deltas (generation.prechecks.ts): SUBJECT_HOURS_INVALID → the cell's converted minutes (weeklyHoursToMinutes) not expressible with the group's applicable durations; HOMEROOM_HOUR_IDENTITY → minutes identity per segment; TEACHER_CAPACITY → available minutes (not slot counts); OPTION_BLOCK_CHILD_MISMATCH gains equal-effective-duration; new BAND_NOT_TILEABLE mirror of the fit-report check.
  • Expect solve-time regression vs the slot model (≈9–12× more placement positions); budget/timeout defaults revisited during implementation; fixed seed + budget split unchanged.

Diagnostics catalogue delta (chapter 18 table)

Category Change
TEACHER/ROOM/STUDENT/CLASS_CONFLICT, ROOM_CAPACITY, ROOM_NOT_IN_SUBJECT_SET, SUBJECT_GROUP_NOT_IN_BASE_ROOM, TEACHER_AVAILABILITY, NO_EFFECTIVE_TEMPLATE, 3 WARNINGs unchanged (already wall-clock / non-positional)
HOUR_BUDGET Σ placed minutes ≠ weeklyHoursToMinutes(weeklyHours)
HOMEROOM_GAP uncovered teaching-band minutes (reuses subtractIntervals; teaching-window span logic retained)
OPTION_BLOCK_SYNC co-located = identical start tick and identical effective duration across children
DAY_OVERFLOW new ERROR — a lesson straddles a break band or exits the envelope (placeLesson(...).fitsInBand === false)

Attendance: the coordinate snapshot column is renamed start_tick (metadata-only; rows preserved); only the recordable-cell derivation consumes the tick resolver for wall-clock enrichment. No index change on attendance_records.


10. Open questions

None — all product-level forks were resolved in chat 2026-07-07 (see §8 Pushback log). One implementation-time procedural note, not a design blocker: the working tree carries an uncommitted migration (20260707120000_curriculum_hours_positive_not_taught) — ask at migrate dev time whether to fold or sequence after it (ch12 procedure).


11. Verification plan

  • Unit specs:
  • timetables.diagnostics.spec.ts — tick resolution (absolute tick → wall-clock, duration cascade fallback, virtual positions); new DAY_OVERFLOW (break-straddle + envelope-exit); re-semanticized HOUR_BUDGET/HOMEROOM_GAP/OPTION_BLOCK_SYNC; the "all-defaults tenant degenerates to today's behavior" property.
  • curriculum specs — cascade resolution (4 levels + null tail), CURRICULUM_HOURS_NOT_DIVISIBLE write gate (fires only when curriculum-resolved), the weeklyHoursToMinutes helper (rounding + whole-minutes validation) through the family-sync.
  • timetable-templates specs — envelope validation (DAY_TEMPLATE_BREAK_INVALID reasons), divisibility warning, fit-report categories incl. the coin-problem DP.
  • generation.prechecks.spec.ts — minutes-based identities, duration mismatch on sync blocks.
  • solver/ pytest — encoding correctness per hard family, exact-tiling property, ordinal ingest round-trip.
  • E2E specs: timetables.e2e-spec.ts — mixed-duration authoring → diagnostics → publish flow; generation FEASIBLE on a small mixed-duration fixture; curricula.e2e-spec.ts — cascade write/read + divisibility 422; template envelope CRUD.
  • Manual verification: seed a 40'/60' curriculum on the dev tenant, generate, inspect per-homeroom bell times in the grade view, take PERIOD attendance on a published day. (Run only when the user asks, per project norms.)

12. Sign-off

  • Approved by: Fabio Barbieri
  • Date: 2026-07-07
  • Chat reference: approved in chat 2026-07-07 after the needs-matrix walkthrough + one revision (weeklyHours stays Float hours — no minutes storage)

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