{
"id": "at.kotoba.session",
"defs": {
"card": {
"type": "object",
"required": [
"glyph",
"type",
"mode",
"correct",
"parseMs",
"totalMs",
"ts"
],
"properties": {
"ts": {
"type": "string",
"format": "datetime"
},
"mode": {
"type": "string",
"knownValues": [
"tap",
"type"
]
},
"type": {
"type": "string",
"description": "Open string; matches the pack item's type."
},
"glyph": {
"type": "string"
},
"answer": {
"type": "string",
"description": "What the user answered. Optional: may be absent on a skip/timeout (app-side `given` is nullable)."
},
"correct": {
"type": "boolean"
},
"parseMs": {
"type": "integer",
"description": "render -> first response/keystroke."
},
"totalMs": {
"type": "integer",
"description": "render -> submit (tap: == parseMs)."
},
"distractors": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tap mode only; the romaji offered. Absent for type."
}
},
"description": "One answered card. `answer` is what the user produced (the correct reading is derivable by joining packRef+glyph; not stored, per SPEC ยง3.2)."
},
"main": {
"key": "tid",
"type": "record",
"record": {
"type": "object",
"required": [
"startedAt",
"endedAt",
"cards",
"rollup"
],
"properties": {
"cards": {
"type": "array",
"items": {
"ref": "#card",
"type": "ref"
}
},
"config": {
"ref": "#config",
"type": "ref"
},
"rollup": {
"ref": "#rollup",
"type": "ref"
},
"endedAt": {
"type": "string",
"format": "datetime"
},
"packRef": {
"type": "string",
"format": "at-uri",
"description": "at:// URI of the lesson.pack record that drove this session."
},
"startedAt": {
"type": "string",
"format": "datetime"
}
}
},
"description": "One practice session: the full per-card interaction array, the config that drove it, and session-level rollups. Append-only stack; TID rkeys (time-ordered)."
},
"config": {
"type": "object",
"required": [
"length",
"focus",
"recall",
"cardStyle"
],
"properties": {
"focus": {
"type": "string",
"description": "Open. all | hiragana | katakana."
},
"length": {
"type": "integer",
"description": "10 | 20 | full-count."
},
"recall": {
"type": "string",
"description": "Open. tap | mixed | type."
},
"cardStyle": {
"type": "string",
"description": "Open. genko | hanko | ma."
}
},
"description": "The setup choices that drove the session (enables Drill-again + analytics slicing)."
},
"rollup": {
"type": "object",
"required": [
"n",
"correct",
"medianParseMs"
],
"properties": {
"n": {
"type": "integer"
},
"byType": {
"type": "unknown",
"description": "Map keyed by type string -> { n, correct }. `unknown` because lexicons have no dynamic-key map type (see README)."
},
"correct": {
"type": "integer"
},
"medianParseMs": {
"type": "integer"
}
},
"description": "Precomputed so range scans avoid touching the per-card array."
}
},
"$type": "com.atproto.lexicon.schema",
"lexicon": 1
}