First module of Phase 6 (Research Capture). A thin projection over SM_Game's existing session metadata. Aggregates identity, timing, progression, and pipeline log counts into a structured blob for export by M21 DATA_Recorder and consumption by post-hoc analysis scripts.
createdAt, endedAt, endReason,
sessionId, and gameState on its session shape. M20 reads these fields
directly. No synchronisation bugs possible because there is no second place to synchronise to.
This is the discipline that should propagate to M21–M25.
| # | Assertion | Result | Detail |
|---|
Drive the session lifecycle on the left; the telemetry blob on the right updates after every action. Fields whose values just changed are momentarily highlighted in amber to make the projection visible.
| Field | Type | Source | Description |
|---|---|---|---|
| Identity | |||
| sessionId | string | null | SM_Game.session.sessionId | UUID generated by SM_Game at createSession() |
| conditionVariant | 'adaptive' | 'static' | null | SM_Game.session.conditionVariant | RQ1b experimental treatment assignment |
| insiderAssigned | string | null | SM_Game.session.insiderAssigned | Which peer is the insider (arthur / liam / sarah / dave) |
| Timing | |||
| createdAt | ISO 8601 UTC | null | SM_Game.session.createdAt | Session start timestamp |
| endedAt | ISO 8601 UTC | null | SM_Game.session.endedAt | Session end timestamp; null if still active |
| durationSeconds | number | null | computed: end - start | Total session duration; null if not yet ended |
| Progression | |||
| gameState | 'active' | 'ended' | null | SM_Game.session.gameState | Whether the session is in flight or finished |
| currentPhase | 1 | 2 | 3 | 4 | null | PM_Phase.getCurrentPhase() | Active phase; null if no session |
| decisionsSubmitted | number | SM_Game.getDecisionHistory().length | Count of decisions committed so far |
| decisionsTotal | number | DATA_Decisions.getIds().length | Total decisions in the architecture (16) |
| percentComplete | number [0..1] | computed: submitted / total | Session progress fraction |
| Pipeline log counts | |||
| logCounts.decisionHistory | number | SM_Game.getDecisionHistory().length | Decisions submitted (mirror of decisionsSubmitted) |
| logCounts.consequenceTags | number | SM_Game.getConsequenceTags().length | Story-flag effects recorded by M11 |
| logCounts.events | number | SM_Game.getEventLog().length | Narrative events fired by M14 |
| logCounts.confusion | number | SM_Game.getConfusionLog().length | Confusion classifications by M15 |
| logCounts.reasoning | number | SM_Game.getReasoningLog().length | Reasoning classifications by M16 |
| logCounts.morality | number | SM_Game.getMoralityLog().length | Morality classifications by M17 (PRIMARY DV) |
| logCounts.trigger | number | SM_Game.getTriggerLog().length | Trigger decisions by M18 |
| logCounts.condition | number | SM_Game.getConditionLog().length | Gating decisions by M19 |
| Termination | |||
| terminationReason | 'completed' | 'in_progress' | 'incomplete' | 'no_session' | computed: gameState + decisions | Standardised classification for analysis filters (ADR-055) |
| endReason | string | null | SM_Game.session.endReason | Free-text reason from endSession() call (e.g. 'player_accused', 'time_expired') |
| Provenance | |||
| telemetryVersion | string | module constant | Schema version — increment for breaking changes (ADR-056) |
| computedAt | ISO 8601 UTC | computed: now | When this telemetry blob was generated |
M20 has the smallest public API of any module so far — three entries.