# at.kotoba.session

> Published by [kotoba.at](https://lexicon.garden/identity/did:plc:o5f3cgna5sku4p6iitld6mxs)

✓ This is the authoritative definition for this NSID.

## Links

- [View on Lexicon Garden](https://lexicon.garden/lexicon/did:plc:o5f3cgna5sku4p6iitld6mxs/at.kotoba.session)
- [Documentation](https://lexicon.garden/lexicon/did:plc:o5f3cgna5sku4p6iitld6mxs/at.kotoba.session/docs)
- [Examples](https://lexicon.garden/lexicon/did:plc:o5f3cgna5sku4p6iitld6mxs/at.kotoba.session/examples)

## Definitions

### `at.kotoba.session#card`

**Type**: `object`

One answered card. `answer` is what the user produced (the correct reading is derivable by joining packRef+glyph; not stored, per SPEC §3.2).

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `ts` | `string` (datetime) | Yes |  |
| `mode` | `string` | Yes |  |
| `type` | `string` | Yes | Open string; matches the pack item's type. |
| `glyph` | `string` | Yes |  |
| `answer` | `string` | No | What the user answered. Optional: may be absent on a skip/timeout (app-side `given` is nullable). |
| `correct` | `boolean` | Yes |  |
| `parseMs` | `integer` | Yes | render -> first response/keystroke. |
| `totalMs` | `integer` | Yes | render -> submit (tap: == parseMs). |
| `distractors` | `array` | No | Tap mode only; the romaji offered. Absent for type. |

### `at.kotoba.session`

**Type**: `record`

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).

**Key**: `tid`

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `cards` | `array` | Yes |  |
| `config` | `ref` → `#config` | No |  |
| `rollup` | `ref` → `#rollup` | Yes |  |
| `endedAt` | `string` (datetime) | Yes |  |
| `packRef` | `string` (at-uri) | No | at:// URI of the lesson.pack record that drove this session. |
| `startedAt` | `string` (datetime) | Yes |  |

### `at.kotoba.session#config`

**Type**: `object`

The setup choices that drove the session (enables Drill-again + analytics slicing).

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `focus` | `string` | Yes | Open. all \| hiragana \| katakana. |
| `length` | `integer` | Yes | 10 \| 20 \| full-count. |
| `recall` | `string` | Yes | Open. tap \| mixed \| type. |
| `cardStyle` | `string` | Yes | Open. genko \| hanko \| ma. |

### `at.kotoba.session#rollup`

**Type**: `object`

Precomputed so range scans avoid touching the per-card array.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `n` | `integer` | Yes |  |
| `byType` | `unknown` | No | Map keyed by type string -> { n, correct }. `unknown` because lexicons have no dynamic-key map type (see README). |
| `correct` | `integer` | Yes |  |
| `medianParseMs` | `integer` | Yes |  |

## Raw Schema

```json
{
  "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
}
```
