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
periodOrdinalPosition→startTick(start_tick) onScheduledLesson+AttendanceRecord(plan rename addendum, ratified 2026-07-07: the old name is an ordinal lie once the value is a time offset).SEQUENCE_MISALIGNMENTis dropped (impossible by construction), theORDERbreak-invalid reason is dropped (order derives fromstartTime), 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".
weeklyHourson 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_ENVELOPE — ORDER 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.ts → dto/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: keepstartTime; addendTime String @db.VarChar(5)andperiodDuration Int(minutes; validated multiple of 5,20 ≤ d ≤ 240). Drop the slot list.TimeSlot→DayTemplateBreak(rename/remodel):{ id, dayTemplateId, type BreakType, name, startTime VarChar(5), durationMinutes Int, lunchShiftId?, roomId? }.BreakType { INTERVAL, LUNCH }replacesSlotType { PERIOD, INTERVAL, LUNCH }— PERIOD rows cease to exist.lunchShiftIdrequired iff LUNCH,roomIdoptional on INTERVAL (same Restrict FKs as today). Breaks are fixed wall-clock bands; service validates: inside[startTime, endTime], non-overlapping,startTime-ordered.ordinalPositionis dropped (order derives fromstartTime); the inline slot-contiguity check and theREORDER_OFFSETshift mechanism intimetable-templates.service.tsgo with it (assertSequentialPositionsitself is untouched — it belongs to departments/periods).- Cascade columns —
periodDuration Int?onCurriculum,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 theperiodDurationof 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 helper —weeklyHoursToMinutes(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 × 60must 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> 0CHECK and theisTaughtCellwrite idiom are untouched.ScheduledLesson: coordinate column renamedperiodOrdinalPosition→startTick(start_tick; same rename onAttendanceRecord, 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 templatestartTimeedit 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
startTimeedit never re-means a stored coordinate — a lesson that no longer fits simply surfacesDAY_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 today_template_breaks; convertordinalPositionto a backfilledstart_time(computed by walking the old slot list fromDayTemplate.startTime); swap enum toBreakType.day_templates: addend_time(backfill = oldstartTime+ Σ old slot durations) andperiod_duration(backfill = modal PERIOD duration of the old slot list; fallback 60).- Add the four nullable
period_durationcascade 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_position→start_tickonscheduled_lessons(rows already wiped) andattendance_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 onlunchShiftId/roomId; replace@@unique([dayTemplateId, ordinalPosition])with@@unique([dayTemplateId, startTime]).ScheduledLessonunique/coordinate indexes carry over, re-keyed on the renamedstartTickcolumn.- 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 gainsDAY_OVERFLOW. slotGridsreshape + 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 minutes ≠ weeklyHoursToMinutes(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_designmemory + 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_coreinsolver/app/solve.pyre-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 setstop_after_first_solutionfor the minimization re-solves. (2) The main-solve floormax(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.sqlstill 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;
ingeststoresstartMinute / TICK_MINUTESdirectly (a non-integer division indicates solver drift → marker error). Post-ingestcomputeDiagnosticsassert 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_MISMATCHgains equal-effective-duration; newBAND_NOT_TILEABLEmirror 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); newDAY_OVERFLOW(break-straddle + envelope-exit); re-semanticizedHOUR_BUDGET/HOMEROOM_GAP/OPTION_BLOCK_SYNC; the "all-defaults tenant degenerates to today's behavior" property.curriculumspecs — cascade resolution (4 levels + null tail),CURRICULUM_HOURS_NOT_DIVISIBLEwrite gate (fires only when curriculum-resolved), theweeklyHoursToMinuteshelper (rounding + whole-minutes validation) through the family-sync.timetable-templatesspecs — envelope validation (DAY_TEMPLATE_BREAK_INVALIDreasons), 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.