# The LightMySky pack format

A pack is a portable collection of teaching topics, each with an optional
lesson, an optional video, a question pool, and a place in a dependency
graph. One pack is one JSON file. The file is self-contained: importing it
into any LightMySky account recreates the pack exactly, with fresh ids.

File extension by convention: `.lightmysky-pack.json`.

## Top level

```json
{
  "format": "lightmysky-pack",   // required, exactly this string
  "version": 1,                   // required, exactly 1
  "name": "Chess basics",        // required, 1-60 chars
  "description": "First steps",  // optional, up to 160 chars
  "downloadable": true,           // optional, default false (see below)
  "library": { },                 // optional, ignored on import (see below)
  "topics": [ ]                   // required, 1-200 topic objects
}
```

`downloadable` is the author's permission for other people to take the file
away. It changes nothing about using the pack inside LightMySky: a public
pack can always be switched on for a learner. Leave it out and the file stays
yours, offered to nobody as a download.

`library` is a glossary written by the exporter: it maps every built-in
library topic id referenced in `after` to its human-readable name, e.g.
`{ "mt_WcfaSfVT33": "One-to-one counting" }`. Importers ignore it; it
exists so a reader knows what the ids mean without looking them up.

## Topic

```json
{
  "key": "how-pieces-move",      // recommended; lowercase/digits/dashes, unique in file
  "subject": "Chess",            // required, 1-40 chars; existing subject or a new one
  "name": "How pieces move",     // required, 1-80 chars
  "description": "One line",     // optional, up to 200 chars
  "ages": [6, 10],                // optional [from, to], integers 4-14; default [6, 8]
  "after": ["#basics", "mt_WcfaSfVT33", "One-to-one counting"],
  "lesson": "…",                  // optional, up to 10000 chars, format below
  "videoUrl": "https://…",       // optional http(s) URL; YouTube links play inline
  "questions": [ ]                // optional, up to 50, format below
}
```

`subject` may name one of the built-in subjects (Science, Mathematics,
English, History, Geography, Civics & Economics, Arts & Music,
Design & Technology, Languages, Personal & Social Development, Life Skills,
Computing, Learning to Learn) to extend it, or any new name to create a
subject of your own.

A topic with no questions is never served in learning sessions; its lesson
is still readable. Give every topic at least one question if you want it
practiced.

## The dependency graph ("after")

`after` lists what must be learned before this topic unlocks. Each entry
is a string in exactly one of three forms, checked in this order:

1. `"#<key>"` — another topic **in this file**, by its `key`. This is the
   only way to reference custom topics, because their database ids are
   assigned at import and differ per account. Never reference a custom
   topic by a `ct_…` id in a pack file.
2. `"mt_…"` — a built-in library topic by its stable id. Ids are stable
   across accounts and versions; prefer them for library references.
   Discover ids via `GET /api/v1/topics?q=<search>` (no auth needed).
3. Any other string — an **exact, case-insensitive name** of a library
   topic, e.g. `"One-to-one counting"`. Convenient to write by hand, but
   it must match exactly one library topic or the import is rejected.

Rules the importer enforces:
- every `#key` must exist in the file; a topic cannot come after itself
- `#key` edges must not form a cycle
- unknown ids or names are errors (the message tells you which entry)
- at most 20 entries per topic

Semantics after import: a learner sees the topic only once every entry in
`after` is known. A library prerequisite below the learner's configured
teaching floor counts as known. Prerequisites pointing at deleted topics
are waived rather than blocking.

## Lesson text

Plain text. Blank lines split it into sections. A section's first word can
mark its kind:

- `tip:` renders as a "good to know" callout
- `example:` (or `try:`) renders as a try-it-together activity
- `recap:` becomes the one-line summary shown with a star; write at most one
- anything else is a plain teaching paragraph

Example:

```
Every chess piece has its own way of moving.

tip: The knight is the only piece that can jump over others.

example: Put a rook on an empty board and count its squares.

recap: Each piece moves its own way, and the knight jumps.
```

## Questions

Three types. Every question needs `prompt` and `explanation` (shown after
answering). `difficulty` is 1 (easy), 2 (medium, default), or 3 (hard);
the session picks difficulty to match the learner's current strength.

```json
{ "type": "mcq", "prompt": "Which piece can jump?",
  "choices": ["Knight", "Rook"], "answerIndex": 0,
  "explanation": "Only the knight jumps.", "difficulty": 1 }

{ "type": "true_false", "prompt": "Bishops move on diagonals.",
  "answer": true, "explanation": "They never leave their color." }

{ "type": "numeric", "prompt": "How many squares on a chessboard?",
  "answer": 64, "explanation": "8 rows of 8." }
```

`choices` holds 2-4 strings and `answerIndex` is 0-based.

## Import semantics

- Importing always creates a **new private pack** owned by the importer;
  nothing merges with or overwrites existing packs.
- All topics get fresh `ct_…` ids; `#key` references are rewired to them.
- Sharing, per-learner enablement, and learner progress never travel in the
  file; only content does.
- Validation is all-or-nothing: any error rejects the whole file, and every
  error is reported with its JSON path, e.g.
  `topics[2].after[0]: no topic in this file has key "basics"`.

## Endpoints

- `GET /api/v1/packs/:id/export` (API key) — download a pack as this format
- `POST /api/v1/packs/import` (API key) — body is the file; returns the new pack
- `GET /api/v1/packs/format` (no auth) — this document
- `GET /learn/packs/:id/file` (no auth) — download a pack from the public gallery
- On the website: Download on a pack's page, Upload on the Packs page.

## Complete example

```json
{
  "format": "lightmysky-pack",
  "version": 1,
  "name": "Chess basics",
  "description": "Board, pieces, and first mates",
  "library": { "mt_WcfaSfVT33": "One-to-one counting" },
  "topics": [
    {
      "key": "how-pieces-move",
      "subject": "Chess",
      "name": "How pieces move",
      "ages": [6, 10],
      "after": ["mt_WcfaSfVT33"],
      "lesson": "Every piece has its own way of moving.\n\ntip: The knight jumps.\n\nrecap: Each piece moves its own way.",
      "videoUrl": "https://www.youtube.com/watch?v=fKxG8KjH1Qg",
      "questions": [
        { "type": "mcq", "prompt": "Which piece can jump over other pieces?",
          "choices": ["Knight", "Rook"], "answerIndex": 0,
          "explanation": "Only the knight jumps.", "difficulty": 1 }
      ]
    },
    {
      "key": "simple-checkmates",
      "subject": "Chess",
      "name": "Simple checkmates",
      "ages": [7, 11],
      "after": ["#how-pieces-move"],
      "questions": [
        { "type": "true_false", "prompt": "A king can be captured.",
          "answer": false, "explanation": "The game ends at checkmate, before capture." }
      ]
    }
  ]
}
```
