Vegman
Design · API and Data Contract

API and Data Contract

A proposal, not a decision. Every entity and relationship below is open to challenge; the open decisions are listed at the end.

Version 0.3, 6 September 2026 (0.1 and 0.2 earlier the same day; changes listed at the end). The single agreement between the engine track (backend, solver) and the experience track (UI, flows). Both build against this document. Any change to a shape or endpoint is made here first, by whoever needs it, with a version bump, and the other track is told.

Overview

The contract has two halves: the JSON shapes of the domain objects, taken from the domain model proposal, and the HTTP endpoints through which a client reads and writes them and asks for a timetable. The engine implements the endpoints; the experience track stubs them with the same shapes until integration.

Summary

Bottom line

Version 0.1 covers what Mevo Hagalil needs: staff with availability, classes and student groups, subjects, a curriculum, events with staff slots, constraints traceable to requirement IDs, and timetables with violations and pinning. Rooms and students are present in the shapes but optional everywhere.

Detailed explanation

Shapes

{
  "School": {
    "id": "mevo-hagalil", "name": "Mevo Hagalil", "nameHe": "מבוא הגליל",
    "sector": "state", "stage": "primary",
    "timeGrid": {
      "days": ["sun", "mon", "tue", "wed", "thu"],
      "periodsPerDay": {"sun": 7, "mon": 7, "tue": 5, "wed": 7, "thu": 7},
      "longDayPeriod": 7,
      "clockTimes": [{"period": 1, "start": "08:00", "end": "08:45"}],
      "breaks": [{"afterPeriod": 2, "start": "09:25", "end": "09:55", "kind": "break", "name": "Breakfast and break", "nameHe": "ארוחת בוקר והפסקה"}]
    }
  },
  "RoomType": {"id": "science", "schoolId": "mevo-hagalil", "name": "Science room", "nameHe": "חדר מדעים"},
  "Room": {"id": "r-science", "schoolId": "mevo-hagalil", "roomTypeId": "science", "name": "Science room", "nameHe": "חדר מדעים", "ownerStaffIds": ["s-keren-s", "s-shirly"]},
  "Grade": {"id": "g3", "schoolId": "mevo-hagalil", "level": 3, "name": "Grade 3", "nameHe": "מחזור ג'"},
  "Class": {"id": "c-3-nesher", "schoolId": "mevo-hagalil", "gradeId": "g3", "name": "3 Nesher", "nameHe": "ג' נשר", "homeroomStaffId": "s-idit"},
  "StudentGroup": {
    "id": "sg-3-nesher", "schoolId": "mevo-hagalil",
    "kind": "class",            
    "classIds": ["c-3-nesher"],
    "name": "3 Nesher", "nameHe": "ג' נשר",
    "studentIds": []
  },
  "Staff": {
    "id": "s-idit", "schoolId": "mevo-hagalil",
    "name": "Idit Tamir", "nameHe": "תמיר עידית", "shortNameHe": "עידית",
    "role": "teacher",          
    "contract": "ofek-hadash",
    "hours": {"frontal": 23, "individual": 4, "staying": 9, "derived": true},
    "subjectIds": ["math", "hebrew"],
    "homeroomClassId": "c-3-nesher",
    "availability": [
      {"day": "tue", "period": null, "state": "unavailable", "reason": "part-time"},
      {"day": "mon", "period": 5, "state": "unavailable", "reason": "meeting: grade 3 team"}
    ]
  },
  "Subject": {"id": "math", "schoolId": "mevo-hagalil", "name": "Mathematics", "nameHe": "חשבון", "isCore": true, "roomTypeId": null},
  "CurriculumLine": {
    "id": "cl-g3-math", "schoolId": "mevo-hagalil", "gradeId": "g3", "subjectId": "math",
    "weeklyPeriods": 5, "pattern": [1, 1, 1, 1, 1],
    "coTeaching": {"required": true, "withHomeroom": true}
  },
  "Event": {
    "id": "ev-3-nesher-math-1", "schoolId": "mevo-hagalil",
    "kind": "lesson",           
    "subjectId": "math",
    "duration": 1,
    "studentGroupIds": ["sg-3-nesher"],
    "staffSlots": [
      {"role": "lead", "staffId": "s-idit"},
      {"role": "co", "staffId": null, "subjectId": "math"}
    ],
    "roomTypeId": null,
    "curriculumLineId": "cl-g3-math",
    "eventGroupId": "eg-3-nesher-math",
    "fixedAt": null,            
    "tags": []
  },
  "EventGroup": {"id": "eg-3-nesher-math", "schoolId": "mevo-hagalil", "kind": "same-course", "eventIds": ["ev-3-nesher-math-1"]},
  "Constraint": {
    "id": "k-r06", "schoolId": "mevo-hagalil",
    "type": "homeroom-starts-first-period",
    "enabled": true,
    "hard": false, "weight": 10,
    "params": {"minDays": 3},
    "appliesTo": {"staffIds": [], "classIds": [], "subjectIds": [], "all": true},
    "implements": ["R-06"],
    "name": "Homeroom starts the day", "nameHe": "מחנכת פותחת את היום"
  },
  "Timetable": {
    "id": "tt-2026-09-03", "schoolId": "mevo-hagalil",
    "version": 3, "status": "draft",
    "basedOnId": "tt-2026-08-30",
    "createdAt": "2026-09-06T10:00:00Z",
    "score": {"hard": 0, "soft": 42},
    "assignments": [
      {"eventId": "ev-3-nesher-math-1", "slots": [{"day": "sun", "period": 2}], "roomId": null, "staffIds": ["s-idit", "s-sapir-h"], "pinned": false}
    ],
    "violations": [
      {"constraintId": "k-r06", "cost": 10, "entityIds": ["s-mor"], "eventIds": [], "slots": [{"day": "tue", "period": 1}], "message": "Mor starts at period 1 on 2 days; the rule asks for 3.", "messageHe": "מור מתחילה בשיעור 1 ב-2 ימים; הכלל דורש 3."}
    ]
  },
  "Job": {"id": "job-123", "kind": "solve", "status": "running", "progress": {"phase": "improving", "elapsedSeconds": 12, "hard": 0, "soft": 57}, "resultTimetableId": null, "error": null}
}

Enumerations:

Constraint types, version 0.3

Each type has fixed params. The engine validates them. The list grows as requirements are confirmed.

typeparamsimplements
no-clashnone; always hard. Half groups of the same class do not clash with each other; open slots (staffId: null) never clashstructural
availabilitynone; reads Staff.availabilityR-09, R-10, S-05
no-gapsedgesAreGaps: false, tuesdayStayingDeduction: 2, lunchDuties: 2, lunchDutiesByStaff: {staffId: n}; budget = individual + staying − deductions − non-frontal hours already placedR-05, S-11, S-12
homeroom-starts-first-periodminDays, ownClass: trueR-06
ends-long-daydays, comparison, minWorkingDays: 4, perStaffDays: {staffId: n}, partTimeRule: "proportional" or "exempt"R-07
subject-per-daysubjectId, min, max (null = no max), halvesCountOnce: trueR-08
block-at-periodseventGroupId, firstPeriod, lastPeriod, excludeDaysR-11
spread-over-dayseventGroupIds, distinctDays: boolR-11
double-periodcurriculumLineIdR-12
avoid-last-periodsubjectIds, period (7 for Mevo Hagalil)R-14
fixed-timereads Event.fixedAt; always hardS-05, S-06, S-07, S-15
room-shared-ownersroomId, maxSimultaneous: 1, exemptCoTeaching: bool; softS-13
room-owner-whole-classroomId; hard: when one owner co-teaches, the other owner's simultaneous lesson must be a whole classS-13

Endpoints

Base path /api/v1. All collections support GET (list), POST (create), and GET, PUT, DELETE on /{id}. Lists return {"items": [...], "total": n}.

PathNotes
/schools
/schools/{schoolId}/grades
/schools/{schoolId}/classes
/schools/{schoolId}/student-groups
/schools/{schoolId}/staffavailability is embedded
/schools/{schoolId}/subjects
/schools/{schoolId}/curriculumCurriculumLine
/schools/{schoolId}/room-types, /schools/{schoolId}/roomsRoom has ownerStaffIds
/schools/{schoolId}/eventsPOST /generate derives events from the curriculum for every class; existing generated events are replaced, fixed and tagged ones kept
/schools/{schoolId}/event-groups
/schools/{schoolId}/constraintsGET /types lists constraint types and their params
/schools/{schoolId}/timetablesGET /{id} includes assignments and violations; POST /{id}/publish; POST /{id}/fork creates a new draft based on it; POST /{id}/pin with {"eventIds": [...], "pinned": true}. A published timetable is immutable: PUT on it returns 409; fork first.
/schools/{schoolId}/solvebody {"basedOnId"?: string, "timeLimitSeconds"?: number, "pinnedEventIds"?: [...]}; returns a Job
/schools/{schoolId}/scorebody: a Timetable; returns score and violations without solving
/jobs/{jobId}DELETE cancels
/schools/{schoolId}/import/tiktakmultipart upload of the Tik-Tak export; returns created objects and a draft timetable
/schools/{schoolId}/export/xhsttXHSTT XML of the school and its published timetable

Conventions for the stub

The experience track ships a stub that implements every endpoint above in memory, seeded with a fixture derived from Mevo Hagalil (data/mevo-hagalil/). POST /solve in the stub returns a Job that moves queued, running, done over a few seconds and produces a timetable equal to the fixture with a handful of synthetic violations, so the review and repair flows can be designed against realistic output.

Open items

Changes in 0.2

Requested by the experience track after building the prototype; the engine track adopts them in its next iteration.

Changes in 0.3

From Vegman's answers of 6 September (requirements v0.3) and both tracks' implementations.

תכנון · חוזה API ונתונים

חוזה API ונתונים

הצעה, לא החלטה. כל ישות וקשר להלן פתוחים לערעור; ההחלטות הפתוחות מפורטות בסוף.

גרסה 0.2, 6 בספטמבר 2026 (0.1 מוקדם יותר באותו יום; השינויים מפורטים בסוף). ההסכם היחיד בין מסלול המנוע (צד שרת, פותר) לבין מסלול החוויה (ממשק משתמש, תהליכים). שני המסלולים בונים על פי מסמך זה. כל שינוי בצורה או בנקודת קצה נעשה כאן תחילה, על ידי מי שזקוק לו, עם העלאת גרסה, והמסלול השני מעודכן.

> גרסה 0.3 (6 בספטמבר 2026): נוספו הפסקות ברשת הזמן, חדרים עם בעלים, סימון שעות משוערות, ושני סוגי אילוצי חדרים. הפירוט המלא בגרסה האנגלית, בסעיף "Changes in 0.3".

סקירה

לחוזה שני חצאים: צורות ה-JSON של אובייקטי התחום, הלקוחות מהצעת מודל התחום, ונקודות הקצה של HTTP שדרכן לקוח קורא וכותב אותן ומבקש מערכת שעות. המנוע מממש את נקודות הקצה; מסלול החוויה מדמה אותן (stub) באותן צורות עד לאינטגרציה.

תקציר

שורה תחתונה

גרסה 0.1 מכסה את מה שמבוא הגליל צריך: צוות עם זמינות, כיתות וקבוצות תלמידים, מקצועות, תוכנית לימודים, אירועים עם משבצות צוות, אילוצים הניתנים למעקב אל מזהי דרישות, ומערכות שעות עם הפרות ונעיצה. חדרים ותלמידים קיימים בצורות אך אופציונליים בכל מקום.

הסבר מפורט

צורות

{
  "School": {
    "id": "mevo-hagalil", "name": "Mevo Hagalil", "nameHe": "מבוא הגליל",
    "sector": "state", "stage": "primary",
    "timeGrid": {
      "days": ["sun", "mon", "tue", "wed", "thu"],
      "periodsPerDay": {"sun": 7, "mon": 7, "tue": 5, "wed": 7, "thu": 7},
      "longDayPeriod": 7,
      "clockTimes": [{"period": 1, "start": "08:00", "end": "08:45"}]
    }
  },
  "Grade": {"id": "g3", "schoolId": "mevo-hagalil", "level": 3, "name": "Grade 3", "nameHe": "מחזור ג'"},
  "Class": {"id": "c-3-nesher", "schoolId": "mevo-hagalil", "gradeId": "g3", "name": "3 Nesher", "nameHe": "ג' נשר", "homeroomStaffId": "s-idit"},
  "StudentGroup": {
    "id": "sg-3-nesher", "schoolId": "mevo-hagalil",
    "kind": "class",            
    "classIds": ["c-3-nesher"],
    "name": "3 Nesher", "nameHe": "ג' נשר",
    "studentIds": []
  },
  "Staff": {
    "id": "s-idit", "schoolId": "mevo-hagalil",
    "name": "Idit Tamir", "nameHe": "תמיר עידית", "shortNameHe": "עידית",
    "role": "teacher",          
    "contract": "ofek-hadash",
    "hours": {"frontal": 23, "individual": 4, "staying": 9},
    "subjectIds": ["math", "hebrew"],
    "homeroomClassId": "c-3-nesher",
    "availability": [
      {"day": "tue", "period": null, "state": "unavailable", "reason": "part-time"},
      {"day": "mon", "period": 5, "state": "unavailable", "reason": "meeting: grade 3 team"}
    ]
  },
  "Subject": {"id": "math", "schoolId": "mevo-hagalil", "name": "Mathematics", "nameHe": "חשבון", "isCore": true, "roomTypeId": null},
  "CurriculumLine": {
    "id": "cl-g3-math", "schoolId": "mevo-hagalil", "gradeId": "g3", "subjectId": "math",
    "weeklyPeriods": 5, "pattern": [1, 1, 1, 1, 1],
    "coTeaching": {"required": true, "withHomeroom": true}
  },
  "Event": {
    "id": "ev-3-nesher-math-1", "schoolId": "mevo-hagalil",
    "kind": "lesson",           
    "subjectId": "math",
    "duration": 1,
    "studentGroupIds": ["sg-3-nesher"],
    "staffSlots": [
      {"role": "lead", "staffId": "s-idit"},
      {"role": "co", "staffId": null, "subjectId": "math"}
    ],
    "roomTypeId": null,
    "curriculumLineId": "cl-g3-math",
    "eventGroupId": "eg-3-nesher-math",
    "fixedAt": null,            
    "tags": []
  },
  "EventGroup": {"id": "eg-3-nesher-math", "schoolId": "mevo-hagalil", "kind": "same-course", "eventIds": ["ev-3-nesher-math-1"]},
  "Constraint": {
    "id": "k-r06", "schoolId": "mevo-hagalil",
    "type": "homeroom-starts-first-period",
    "enabled": true,
    "hard": false, "weight": 10,
    "params": {"minDays": 3},
    "appliesTo": {"staffIds": [], "classIds": [], "subjectIds": [], "all": true},
    "implements": ["R-06"],
    "name": "Homeroom starts the day", "nameHe": "מחנכת פותחת את היום"
  },
  "Timetable": {
    "id": "tt-2026-09-03", "schoolId": "mevo-hagalil",
    "version": 3, "status": "draft",
    "basedOnId": "tt-2026-08-30",
    "createdAt": "2026-09-06T10:00:00Z",
    "score": {"hard": 0, "soft": 42},
    "assignments": [
      {"eventId": "ev-3-nesher-math-1", "slots": [{"day": "sun", "period": 2}], "roomId": null, "staffIds": ["s-idit", "s-sapir-h"], "pinned": false}
    ],
    "violations": [
      {"constraintId": "k-r06", "cost": 10, "entityIds": ["s-mor"], "eventIds": [], "slots": [{"day": "tue", "period": 1}], "message": "Mor starts at period 1 on 2 days; the rule asks for 3.", "messageHe": "מור מתחילה בשיעור 1 ב-2 ימים; הכלל דורש 3."}
    ]
  },
  "Job": {"id": "job-123", "kind": "solve", "status": "running", "progress": {"phase": "improving", "elapsedSeconds": 12, "hard": 0, "soft": 57}, "resultTimetableId": null, "error": null}
}

ערכי ספירה:

סוגי אילוצים, גרסה 0.1

לכל סוג יש params קבועים. המנוע מאמת אותם. הרשימה גדלה ככל שדרישות מאושרות.

typeparamsimplements
no-clashnone; always hardמבני
availabilitynone; reads Staff.availabilityR-09, R-10, S-05
no-gapscountNonFrontalAsBusy: boolR-05
homeroom-starts-first-periodminDaysR-06
ends-long-daydays, comparison: "exact" or "min" or "max"R-07
subject-per-daysubjectId, min, maxR-08
block-at-periodseventGroupId, firstPeriod, lastPeriod, excludeDaysR-11
spread-over-dayseventGroupIds, distinctDays: boolR-11
double-periodcurriculumLineIdR-12
avoid-last-periodsubjectIdsR-14
fixed-timereads Event.fixedAt; always hardS-05, S-06, S-07

נקודות קצה

נתיב בסיס /api/v1. כל האוספים תומכים ב-GET (רשימה), POST (יצירה), וב-GET, PUT, DELETE על /{id}. רשימות מחזירות {"items": [...], "total": n}.

נתיבהערות
/schools
/schools/{schoolId}/grades
/schools/{schoolId}/classes
/schools/{schoolId}/student-groups
/schools/{schoolId}/staffהזמינות מוטמעת בתוך האובייקט
/schools/{schoolId}/subjects
/schools/{schoolId}/curriculumCurriculumLine
/schools/{schoolId}/eventsPOST /generate גוזר אירועים מתוכנית הלימודים עבור כל כיתה; אירועים שנוצרו קודם לכן מוחלפים, אירועים קבועים ומתויגים נשמרים
/schools/{schoolId}/event-groups
/schools/{schoolId}/constraintsGET /types מפרט את סוגי האילוצים ואת הפרמטרים שלהם
/schools/{schoolId}/timetablesGET /{id} כולל שיבוצים והפרות; POST /{id}/publish; POST /{id}/fork יוצר טיוטה חדשה המבוססת עליה; POST /{id}/pin עם {"eventIds": [...], "pinned": true}. מערכת שעות שפורסמה אינה ניתנת לשינוי: PUT עליה מחזיר 409; יש לבצע fork תחילה.
/schools/{schoolId}/solveגוף {"basedOnId"?: string, "timeLimitSeconds"?: number, "pinnedEventIds"?: [...]}; מחזיר Job
/schools/{schoolId}/scoreגוף: Timetable; מחזיר score ו-violations ללא פתרון
/jobs/{jobId}DELETE מבטל
/schools/{schoolId}/import/tiktakהעלאת multipart של ייצוא תיק-תק; מחזיר את האובייקטים שנוצרו ומערכת שעות טיוטה
/schools/{schoolId}/export/xhsttXML בתקן XHSTT של בית הספר ומערכת השעות שפורסמה שלו

מוסכמות לסטאב

מסלול החוויה מספק סטאב המממש בזיכרון כל נקודת קצה שלעיל, מאותחל בנתוני דוגמה (fixture) הנגזרים ממבוא הגליל (data/mevo-hagalil/). POST /solve בסטאב מחזיר Job העובר בין queued, running, done במשך כמה שניות ומפיק מערכת שעות זהה לנתוני הדוגמה עם קומץ הפרות סינתטיות, כך שניתן לעצב את תהליכי הסקירה והתיקון מול פלט מציאותי.

פריטים פתוחים

שינויים ב-0.2

התבקשו על ידי מסלול החוויה לאחר בניית אב-הטיפוס; מסלול המנוע מאמץ אותם באיטרציה הבאה שלו.