# Education OS — Design Spec

*Drafted 2026-07-13 (silicon-zack, orchestrating). The teacher operating system: a
permanent `vita.education.*` capability domain + a first-class UI page. Sits ON TOP of
the curriculum graph (data/education/curriculum/, lib scripts/education/curriculum_lib.py).
bio-zack greenlit staged build: capability domain FIRST (the spine), then the page.
Registration/UI patterns pending two scout agents; this spec pins the DESIGN (my job).*

## Why this exists (the two maps)

The curriculum graph is the KID's map (where she is, what's next). This is the TEACHER's
operating system (what HE does, what's in flight, is he ahead). The graph tracks the
kid's MASTERY; the OS adds the teacher's WORKFLOW STATE + the action queue + the build/
coverage view. Permanent, first-class — not a workshop page (per bio-zack: workshop is an
incubator for impermanent capability UIs; this is load-bearing for years).

## The teacher loop (what the OS must support)

1. **Choose** — with the kid, pick her next node from the frontier (the agency lever).
2. **Deliver** — generate a packet (ed-packet) / assign a gumball module, workbook chapter, or activity.
3. **Administer** — some nodes need the teacher to RUN something: a timed fluency sheet, the Writing Speed Test, an oral-reading check, an L3 spaced-retention re-check.
4. **Score → record** — `set_level` → mastery advances → frontier recomputes.
5. **Communicate** — celebrate mastery (the non-cash currency), hand over the next choice.

## Workflow-state layer (the NEW data — the gap the graph doesn't cover)

Mastery (ledger) = the kid's achievement. Workflow status = the teacher's process state.
KEEP THEM SEPARATE. New store: `data/education/workflow/<kid>.yaml` (atomic writes).

Per (kid, node) entry — only for nodes NOT in the default `available` state:
```yaml
kid: penelope
entries:
  - node: math.addsub.make-a-ten-strategy
    status: assigned            # see states below
    material: "packet:make-a-ten-2026-07-14"   # what was delivered (packet slug / gumball gen / chapter / activity / test)
    since: 2026-07-13
    note: "she picked this one"
```

**States** (lean; a node with no entry = `available`):
- `available` — in frontier, nothing started (default, no entry)
- `chosen` — kid picked it as next; not yet delivered
- `assigned` — material delivered; she's working it
- `needs_scoring` — she finished; awaiting teacher score/administer
- `done` — scored; mastery updated; exits the active queue (entry kept for history)
- `parked` — deliberately set aside (optional)

The queue is DERIVED from (frontier × workflow-status), not stored:
- **"needs you now"**: `needs_scoring` + `chosen`-not-yet-`assigned` + any node whose next gate is an administer-a-test action (fluency/Writing-Speed-Test/oral-reading/L3 re-check).
- **"in flight"**: `assigned`.
- **"ready to choose"**: `available` frontier nodes (constrained first; free-roam summarized).

## Capability surface — `vita.education.*`

READ:
- `vita.education.kids` → roster + per-kid summary (position label, frontier counts, queue counts, last-activity).
- `vita.education.frontier` {kid} → constrained + free_roam (wraps curriculum_lib.frontier).
- `vita.education.node` {id, kid?} → full node detail (gates, fluency_aim, resources, sources, requires/helps) + kid's level + workflow status.
- `vita.education.queue` {kid} → the DERIVED teacher action queue (needs-you-now / in-flight / ready-to-choose), each item with the concrete next action ("generate packet", "administer Writing Speed Test", "score the money packet"). **This is the Q2 answer — his to-do list.**
- `vita.education.map` → OUR build state: per-domain node counts + fill status (math dense / 3 skeletons), instrument_gap count (the gumball backlog), roadmap items (from ORCHESTRATION.md / panel-adjudication.md open items). **This is the Q1 answer — am-I-ahead.**
- `vita.education.progress` {kid, since?} → recent mastery changes (from ledger evidence history) for reporting.

WRITE:
- `vita.education.set_level` {kid, node, level, evidence, as_of, rate?} → wraps curriculum_lib.set_level (records mastery; frontier recomputes).
- `vita.education.set_status` {kid, node, status, material?, note?, as_of} → workflow-state write.
- `vita.education.choose` {kid, node} → convenience: status→chosen (the agency action).
- `vita.education.score` {kid, node, level, evidence, as_of} → convenience: set_level + status→done in one call (the common "she finished, here's how she did" action).

LATER (stub/link for v1):
- `vita.education.generate_packet` {node, kid} → hook to the ed-packet skill (v1 can return the suggested `ed-packet` invocation + the node's teaching content, rather than auto-run).

Conventions: follow the scouted `vita.*` registration/handler/validation/error pattern
exactly (the `not_found` convention for unknown kid/node; idempotency/provenance if the
layer wants it). Doc at `docs/api/capabilities/vita.education.md`. Tests alongside.

## Build patterns (scouted 2026-07-13 — agents follow these exactly)

**Capability domain** (`scripts/capabilities/`): create `scripts/capabilities/namespaces/education.py`
with `@capability(name="vita.education.<verb>", description=, side_effect=SideEffect.READ|WRITE,
input_model=<pydantic>, output_model=<pydantic>, source=, examples=[...])` handlers
(`def _impl(inp, ctx) -> OutModel`; lazy-import curriculum_lib/workflow inside the body).
Register by appending `"namespaces.education"` to `NAMESPACE_MODULES` in
`scripts/capabilities/core.py`. Name regex `^vita\.[a-z_]+\.[a-z_]+$` (3 segments) — all our
verbs fit. Errors: `raise CapabilityError("not_found"|"invalid_input"|"conflict", msg)`.
Local store env override `VITA_EDUCATION_DATA_DIR` (add to the `_isolated_store` test fixture).
Then `./run gen-capabilities` (docs auto-generate to `docs/api/capabilities/vita.education.md`
— NEVER hand-write) and add cases to `tests/test_capabilities.py` (the drift test
`test_spec_artifacts_are_current` FAILS if gen is skipped). Reference namespaces:
`namespaces/memory.py` (simple READ), `workshop.py` (WRITE+invalid_input), `people.py`
(not_found/conflict), `calendar.py` (`ctx.set_freshness`).

**ARCHITECTURE DECISION: capabilities are the SINGLE backend.** No separate REST router.
The `/education` page calls the existing generic capability HTTP transport directly:
`POST /api/v1/capabilities/call/vita.education.<verb>` with header `X-Vita-Transport: js`
(js transport can call READ/WRITE — education has no SEND/SPAWN, so all verbs are reachable).
One surface for CLI + agents + Sunday review + the page. Responses are v1 envelopes
`{ok, data, error, meta}` — the TS hook unwraps.

## UI page — first-class `/education` (scouted)

Stack: Vite SPA on :33801 (`web/`, React 19 + TanStack Router file-based + TanStack Query +
Tailwind v4); FastAPI on :33800 (`api/`). Model on the LIVING BRIEFING. Touchpoints:
- Route: `web/src/routes/education/index.tsx` → `createFileRoute('/education/')` (routeTree.gen.ts
  auto-regens — do NOT hand-edit).
- Hook: `web/src/hooks/useEducation.ts` — TanStack Query, `api` from `@/api`, calls
  `api.post('/api/v1/capabilities/call/vita.education.<verb>', payload, {headers:{'X-Vita-Transport':'js'}})`,
  unwraps the envelope. (Mirror `useBriefing.ts`.)
- Nav: one `NavItem` in `web/src/lib/nav-config.ts` (`to:"/education"`, lucide `GraduationCap`)
  — command palette derives automatically.
- Styling: Tailwind inline, `@/components/ui/card`, lucide-react, recharts if charts. Page-specific
  bits under `web/src/components/education/`.
- Access is internal-only automatically (DomainGuard excludes `/education` + `/api/v1/capabilities`
  from the public allowlist) — no auth code. Backend is NOT hot-reload → `./run api restart` after
  api edits; frontend IS (`./run web`). Verify: `http://localhost:33801/education`, then
  `cd web && npm run typecheck && npm run lint`.

### Original 3-tab plan

Three tabs, fed entirely by the capabilities above, WRITABLE:
1. **Console (per kid)** — her position, the action queue (needs-you-now / in-flight / ready-to-choose), each with a one-click action (choose / mark delivered / score → set_level). The daily driver.
2. **Our map** — build state + roadmap (from `vita.education.map`): domains filled vs skeleton, the 88 instrument-gap nodes, what's next to build. Keeps him ahead.
3. **The graph** — the existing cytoscape viewer (reuse `viewer/index.html`), for structure.

Peer of the living-briefing / feed pages (same API backend + `ui/`), NOT /workshop. The
current `viewer/index.html` + the published report are the throwaway DESIGN SKETCH — keep
as reference/export, don't orphan.

## Artifact ingestion & assessment (added 2026-07-13, bio-zack) — the INPUT side of the loop

The ledger is digital; her work is PAPER (paper-first directive). The system can track
"ready-for" + "assigned" but can't see what she PRODUCED. A photo bridges it — and it's
the general input for the whole SCORE step: completed packet page, timed fluency sheet,
Writing Speed Test, freewrite. Generalizes the existing spelling-photo pipeline
(photo → read misspellings → word bank) into a first-class channel.

- **Store** = raw data source: `data/education/artifacts/<kid>/<YYYY-MM-DD>-<slug>.<ext>`
  + metadata index (`data/education/artifacts/index.jsonl`): {id, kid, date, kind
  (freewrite|packet|timed-sheet|misc), prompt/context, linked_node?, path, extracted_read,
  proposed_levels?}. Durable, git-tracked, re-readable. NB: a freewrite (e.g. "what does
  your life look like at 25") is ALSO identity data — a window into who she is, valuable
  beyond assessment.
- **Two ingest paths, telegram PRIMARY** (phone in hand, snap into the Learning topic →
  learning agent stores + reads). The `/education` page = upload button + per-kid gallery
  (secondary).
- **Assessment: extract → PROPOSE → bio-zack confirms.** Vision-read the image → extract
  by kind (freewrite: sentence structure / spelling errors / ideas / handwriting; timed
  sheet: rate; packet: score) → propose set_level updates; teacher approves (assessment is
  his call). Auto-apply ONLY low-stakes (harvest misspelled words into the gumball bank).
- **Capability**: `vita.education.submit_artifact` (WRITE — store img+meta), `.artifacts`
  (READ — list/gallery), and the extract step as an agent/vision pass (learning agent or a
  flow; a SPAWN cap if formalized). Next stage after the page.

## From Rick's LATTICE (rickgorman/leverage#658, assessed 2026-07-13)

Rick independently built the same architecture (node=skill, prereq DAG, height=depth,
domain, standards) — convergence validates the design. His fill: LLM-swarm, 3,378 math
nodes ages 4→23. Ours: hand-curated, 4 domains, 2 kids. His is static graph data; our
value is the mastery + teacher-OS layer he lacks. Takeaways:
- **ADOPT — S/G/C/N representational "rungs"** (Symbolic / Graphical / Concrete-manipulative
  / Narrative-real-world), each a teaching *move* + *check* question + optional down-prereq.
  ORTHOGONAL to our L1→L4 ladder (ours = how well she holds it; his = how to teach/enter it
  4 ways). **This IS Concrete-Pictorial-Abstract + Narrative — the Singapore/Dimensions spine
  Penelope's curriculum already runs on.** Add as an optional per-node `representations` field
  → feeds ed-packet generation (lead with concrete, rotate to symbolic as she consolidates)
  AND the "administer" check questions. Node-schema addition + ed-packet enhancement. bio-zack
  to greenlight.
- **BOOKMARK — his AUDIT.md verification rig** (adversarial dual-auditor refute-prompting +
  Clopper–Pearson bounds; finding: auditors flagged the human gold-standard edges WORSE than
  generated ones = most "errors" are strictness not fabrication). The template for when we
  LLM-fill the 3 skeleton domains. Matches our adversarial-panel ethos.
- **EVALUATE — Marble open taxonomy** (withmarbleapp/os-taxonomy, ODbL/CC-BY-SA): free,
  standards-tagged elementary-math backbone to CROSS-CHECK math.yaml (license = share-alike
  attribution; cross-check, don't bulk-copy).
- **IGNORE** the 3,378-node bulk import (95% irrelevant to a 5/7yo; would bury the tight graph).

## Build stages
1. **Workflow-state lib** (`scripts/education/workflow.py`) — store + queue-derivation logic + tests. Pure, testable, no capability-layer dep.
2. **Capability domain** `vita.education.*` — register per scout A; thin handlers over curriculum_lib + workflow.py; docs + tests. THE SPINE — delivers value to CLI + Sunday review immediately.
3. **First-class UI page** — per scout B; the 3-tab console.
4. **Wire the Sunday Learning review** to `vita.education.queue`/`.frontier` (retire ad-hoc gap-picking).
5. **Teacher playbook** — the loop written down (short), living in the OS.

## Non-goals (v1)
- No auto-packet-generation (link/suggest only).
- No gumball instrument-building (that's the separate 88-node backlog — surfaced in Our Map, not built here).
- No multi-teacher / sharing (single teacher = bio-zack).
