Goal
Per R3 research (#49) — workstream C, FIRST before any UI: the learner's evidence log must survive reload and replay into the same projection and same claim.
Contract
users/{uid}/vnext_events/{eventId} — doc id = EvidenceEvent.id.
- CREATE + READ only — update AND delete denied (stricter than
lesson_events): evidence is immutable, no client can rewrite history.
owner_id pins path uid; learner_id must equal uid — evidence for another learner is a forgery.
- Fields:
id, owner_id, learner_id, capability_id, task_id, task_revision, event_type, modality, occurred_at (number), recorded_at (timestamp), schema_version=1, policy_version, mission_run_id, context, attempt, support, feedback, evaluation, binding.
occurred_at (client-observed, drives lag) vs recorded_at (server arrival) are never conflated.
Adapter — src/vnext/persist.js
appendVnextEvents(fs, uid, events, {missionRunId, policyVersion}) — per-event transaction: absent → create; identical content → dedupe (lost-ack retry is a logical no-op); different content under same id → hard conflict, never overwrite.
loadVnextEvents(fs, uid) — canonical order (occurredAt, id).
- Same
fs surface as cloud-compat → emulator, fake, or real SDK.
Tests
tests/vnext-persist.test.mjs (fake fs): round-trip provenance, append→reload→replay parity (projection + claim identical), exactly-once retry, id-conflict throws, out-of-order canonicalization, learner isolation.
tests/firestore-vnext-emulator.test.mjs (real rules): owner CRUD, update/delete denied for owner too, cross-owner+anon denied, learner_id≠uid denied, malformed docs denied, e2e persist→reload→replay→identical claim.
Research basis: issue #49 round 3 — "persistence before UI; if offline/retry can duplicate learning evidence, UI can mint INDEPENDENT by accident."
Goal
Per R3 research (#49) — workstream C, FIRST before any UI: the learner's evidence log must survive reload and replay into the same projection and same claim.
Contract
users/{uid}/vnext_events/{eventId}— doc id =EvidenceEvent.id.lesson_events): evidence is immutable, no client can rewrite history.owner_idpins path uid;learner_idmust equal uid — evidence for another learner is a forgery.id, owner_id, learner_id, capability_id, task_id, task_revision, event_type, modality, occurred_at (number), recorded_at (timestamp), schema_version=1, policy_version, mission_run_id, context, attempt, support, feedback, evaluation, binding.occurred_at(client-observed, drives lag) vsrecorded_at(server arrival) are never conflated.Adapter —
src/vnext/persist.jsappendVnextEvents(fs, uid, events, {missionRunId, policyVersion})— per-event transaction: absent → create; identical content → dedupe (lost-ack retry is a logical no-op); different content under same id → hard conflict, never overwrite.loadVnextEvents(fs, uid)— canonical order (occurredAt, id).fssurface as cloud-compat → emulator, fake, or real SDK.Tests
tests/vnext-persist.test.mjs(fake fs): round-trip provenance, append→reload→replay parity (projection + claim identical), exactly-once retry, id-conflict throws, out-of-order canonicalization, learner isolation.tests/firestore-vnext-emulator.test.mjs(real rules): owner CRUD, update/delete denied for owner too, cross-owner+anon denied, learner_id≠uid denied, malformed docs denied, e2e persist→reload→replay→identical claim.Research basis: issue #49 round 3 — "persistence before UI; if offline/retry can duplicate learning evidence, UI can mint INDEPENDENT by accident."