{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://teacher-dashboard/schema/course.schema.json",
  "title": "Teacher Dashboard — course data",
  "description": "One JSON file is a teacher's entire course: the thing the site renders from, and the backup they'd download. Meant to be edited directly in a text editor, not through any in-app or AI-assisted tool. Grounded in a real 45-lesson Grade 8 ELA course audited 2026-09-14 (see ROADMAP.md's Phase 0 entry for the full findings this schema is built from). HARD RULE for anyone producing example/demo data against this schema: dashboard-level operational values (real class-platform URLs, gradebook/grading-system links, admin portal links, real school-year-specific dates) must never be real — obviously synthetic placeholders only. Lesson and unit/section CONTENT (objectives, activities, curriculum titles) is the demo-safe part and may be real.",
  "type": "object",
  "required": ["version", "teacher", "schoolYear", "classes", "linkGroups", "units", "calendar"],
  "properties": {
    "version": { "const": 1 },
    "teacher": { "type": "string" },
    "schoolYear": { "type": "string", "description": "Display label, e.g. \"2031-32\" — not used for date math; the calendar block carries real dates." },
    "classes": {
      "type": "array",
      "items": { "$ref": "#/definitions/classSection" }
    },
    "linkGroups": {
      "type": "array",
      "description": "The shortcuts panel. Generic on purpose — every tool-specific link (a class platform, a gradebook, an admin portal) is just a {label, href} pair in a named group, not a hardcoded field, so the schema never needs to change when a teacher's specific toolset does.",
      "items": { "$ref": "#/definitions/linkGroup" }
    },
    "units": {
      "type": "array",
      "description": "Course -> units -> sections -> lessons. A section is valid with only its skeleton fields and zero authored lessons — most real courses have far more planned sections than written lessons.",
      "items": { "$ref": "#/definitions/unit" }
    },
    "calendar": { "$ref": "#/definitions/calendar" }
  },
  "definitions": {
    "classSection": {
      "type": "object",
      "required": ["key", "name", "meets"],
      "additionalProperties": false,
      "properties": {
        "key": { "type": "string", "description": "Stable short code, e.g. \"P1\"." },
        "name": { "type": "string", "description": "Display name, e.g. \"Period 1 — Grade 8 ELA\"." },
        "meets": {
          "type": "array",
          "items": { "type": "integer", "minimum": 0, "maximum": 6 },
          "description": "Weekday indices this section meets, 0=Sunday."
        },
        "doublePeriodOn": {
          "type": ["integer", "null"],
          "minimum": 0,
          "maximum": 6,
          "description": "The one weekday (if any) this section gets a double period instead of a single — real courses can have a section meet twice in one day's slot."
        },
        "rosterSize": { "type": "integer", "minimum": 0 }
      }
    },
    "linkGroup": {
      "type": "object",
      "required": ["label", "links"],
      "additionalProperties": false,
      "properties": {
        "label": { "type": "string", "description": "e.g. \"Communication\", \"Records\"." },
        "links": {
          "type": "array",
          "items": { "$ref": "#/definitions/link" }
        }
      }
    },
    "link": {
      "type": "object",
      "required": ["label", "href"],
      "additionalProperties": false,
      "properties": {
        "label": { "type": "string" },
        "href": { "type": "string", "description": "Empty string renders as a visibly disabled control in the UI, never a dead link." }
      }
    },
    "unit": {
      "type": "object",
      "required": ["id", "title", "sections"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string" },
        "title": { "type": "string", "description": "Real curriculum content, tied to whatever textbook/program a course uses — demo-safe like lesson objectives." },
        "sections": {
          "type": "array",
          "items": { "$ref": "#/definitions/section" }
        }
      }
    },
    "section": {
      "type": "object",
      "required": ["code", "title", "lessonCount"],
      "additionalProperties": false,
      "properties": {
        "code": { "type": "string" },
        "title": { "type": "string" },
        "lessonCount": { "type": "integer", "minimum": 0 },
        "studentEditionPages": { "type": "string" },
        "lessons": {
          "type": "array",
          "description": "May be shorter than lessonCount, or omitted entirely, for a section whose individual lessons haven't been authored yet.",
          "items": { "$ref": "#/definitions/lesson" }
        }
      }
    },
    "lesson": {
      "type": "object",
      "description": "Required fields here are the six sections confirmed present in effectively every real lesson audited (44-45 of 45). teacherEditionGuidance, generalNote, print.context, and reference.carriesForward are genuinely optional — present in a meaningful fraction of real lessons but not most, and the schema must allow their absence, not just tolerate it.",
      "required": [
        "n", "title", "objectives", "beforeClass", "timeline", "beats",
        "print", "assessment", "ifYouRunShort", "doNotCut", "reference"
      ],
      "additionalProperties": false,
      "properties": {
        "n": { "type": "integer", "minimum": 1 },
        "title": { "type": "string" },
        "durationMinutes": { "type": "integer", "minimum": 1 },
        "studentEditionPages": { "type": "string" },
        "generalNote": { "type": "string", "description": "Optional pre-Objectives heads-up context for the lesson as a whole — distinct from a per-beat note." },
        "links": {
          "type": "array",
          "description": "The 'Open before class' bar — lesson-specific shortcuts (that day's Classroom assignment, a specific slide deck), distinct from the course-level linkGroups shortcuts panel. Omit to render no bar for this lesson rather than falling back to linkGroups — the two are different things, not tiers of the same list.",
          "items": { "$ref": "#/definitions/link" }
        },
        "objectives": {
          "type": "array",
          "items": { "type": "string" },
          "minItems": 1
        },
        "beforeClass": {
          "type": "object",
          "required": ["checklist"],
          "additionalProperties": false,
          "properties": {
            "checklist": { "type": "array", "items": { "type": "string" } },
            "onTheBoard": { "type": "string", "description": "The \"write this on the board before they walk in\" line — a distinct field, not just another checklist item." }
          }
        },
        "timeline": {
          "type": "array",
          "description": "The at-a-glance overview table, not the detailed beats below.",
          "items": {
            "type": "object",
            "required": ["time", "beat"],
            "additionalProperties": false,
            "properties": {
              "time": { "type": "string" },
              "beat": { "type": "string" },
              "links": { "type": "array", "items": { "$ref": "#/definitions/link" } }
            }
          }
        },
        "beats": {
          "type": "array",
          "description": "A flat, ordered list — real lessons nest headings inconsistently, so the schema deliberately doesn't mirror any one file's heading hierarchy. sectionTitle is an optional label for grouping/collapsing display only.",
          "items": {
            "type": "object",
            "required": ["startTime", "endTime", "title", "body"],
            "additionalProperties": false,
            "properties": {
              "startTime": { "type": "string" },
              "endTime": { "type": "string" },
              "title": { "type": "string" },
              "sectionTitle": { "type": "string" },
              "body": { "type": "array", "items": { "type": "string" } },
              "say": {
                "type": "array",
                "items": { "type": "string" },
                "description": "Verbatim teacher dialogue. Legitimately empty for many real lessons — that's a register difference (scripted vs. plain instructional prose), not a data gap."
              },
              "notes": { "type": "array", "items": { "type": "string" } },
              "links": { "type": "array", "items": { "$ref": "#/definitions/link" } }
            }
          }
        },
        "print": {
          "type": "object",
          "required": ["studentCount", "items"],
          "additionalProperties": false,
          "properties": {
            "studentCount": { "type": "integer", "minimum": 0 },
            "items": {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["copies", "item"],
                "additionalProperties": false,
                "properties": {
                  "copies": { "type": "integer", "minimum": 0 },
                  "item": { "type": "string" },
                  "href": { "type": "string" },
                  "note": { "type": "string" }
                }
              }
            },
            "context": { "type": "string" }
          }
        },
        "teacherEditionGuidance": { "type": "array", "items": { "type": "string" } },
        "assessment": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["type", "evidence"],
            "additionalProperties": false,
            "properties": {
              "type": { "type": "string" },
              "evidence": { "type": "string" }
            }
          }
        },
        "ifYouRunShort": { "type": "array", "items": { "type": "string" } },
        "doNotCut": {
          "type": "array",
          "items": { "type": "string" },
          "minItems": 1,
          "description": "An array, not a single string — a real lesson can have more than one."
        },
        "reference": {
          "type": "object",
          "required": ["nextLesson"],
          "additionalProperties": false,
          "properties": {
            "carriesForward": { "type": "string" },
            "nextLesson": { "type": "string" }
          }
        }
      }
    },
    "calendar": {
      "type": "object",
      "required": ["weeks", "holidays", "flexDays", "breaks"],
      "additionalProperties": false,
      "properties": {
        "weeks": {
          "type": "array",
          "description": "One entry per teaching week, in order — the calendar is generated by walking every school day against each section's weekly meeting pattern, not stored as a literal per-lesson date list.",
          "items": {
            "type": "object",
            "required": ["mondayDate", "quarter"],
            "additionalProperties": false,
            "properties": {
              "mondayDate": { "type": "string", "format": "date" },
              "quarter": { "type": "integer", "minimum": 1 }
            }
          }
        },
        "holidays": {
          "type": "array",
          "description": "A single non-teaching date with no substitute activity.",
          "items": {
            "type": "object",
            "required": ["date", "label"],
            "additionalProperties": false,
            "properties": {
              "date": { "type": "string", "format": "date" },
              "label": { "type": "string" }
            }
          }
        },
        "flexDays": {
          "type": "array",
          "description": "A single date where something other than a normal lesson happens — distinct from a holiday because there IS an activity, just not the regular lesson sequence.",
          "items": {
            "type": "object",
            "required": ["date", "label"],
            "additionalProperties": false,
            "properties": {
              "date": { "type": "string", "format": "date" },
              "label": { "type": "string" },
              "description": { "type": "string" }
            }
          }
        },
        "breaks": {
          "type": "array",
          "description": "A named multi-day range, e.g. a holiday week — distinct from a single-date holiday.",
          "items": {
            "type": "object",
            "required": ["label", "startDate", "endDate"],
            "additionalProperties": false,
            "properties": {
              "label": { "type": "string" },
              "startDate": { "type": "string", "format": "date" },
              "endDate": { "type": "string", "format": "date" }
            }
          }
        }
      }
    }
  }
}
