qlm-measure 0.2.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,629 @@
1
+ const str = (v) => (v === undefined || v === null ? "" : String(v));
2
+ /** Same as index.ts sortKeysDeep (this module imports only types from there). */
3
+ function sortKeysDeep(value) {
4
+ if (value === null || value === undefined)
5
+ return value;
6
+ if (Array.isArray(value))
7
+ return value.map(sortKeysDeep);
8
+ if (typeof value === "object") {
9
+ const sorted = {};
10
+ for (const key of Object.keys(value).sort())
11
+ sorted[key] = sortKeysDeep(value[key]);
12
+ return sorted;
13
+ }
14
+ return value;
15
+ }
16
+ // ── play/emit-1 — qlm-games src/app/api/evidence/emit/route.ts ────────────
17
+ // computeChainHash(event, prevHash): sha256 of JSON.stringify({student_id,
18
+ // product, construct, signal, weight, prev_hash: prevHash || "genesis"}).
19
+ // Stored row: chain_hash / prev_hash ("genesis" on the first row).
20
+ export const PLAY_EMIT_1 = {
21
+ id: "play/emit-1", family: "play/emit", since: "2026-09-01",
22
+ hashField: "chain_hash", prevField: "prev_hash", genesis: "genesis",
23
+ applies: () => true,
24
+ canonical: (e) => JSON.stringify({
25
+ student_id: e.student_id, product: e.product, construct: e.construct, signal: e.signal, weight: e.weight,
26
+ prev_hash: e.prev_hash || "genesis",
27
+ }),
28
+ validate: (e) => {
29
+ const errors = [];
30
+ for (const f of ["student_id", "product", "construct", "signal"])
31
+ if (typeof e[f] !== "string" || !e[f])
32
+ errors.push(`missing ${f}`);
33
+ return errors;
34
+ },
35
+ };
36
+ // ── play/measure-session-1 — qlm-games src/app/api/measure/session/route.ts ─
37
+ // computeEventHash(studyId, sessionId, event): sha256 hex truncated to 32 of
38
+ // JSON.stringify({studyId, sessionId, studentId ?? 'unknown', timestamp ?? '',
39
+ // correct, domain ?? '', responseTimeMs ?? null, sequenceNumber ?? null}).
40
+ // A content hash used for idempotent replay; records carry no link.
41
+ // The stored row's study_id / session_id / student_id are the hash inputs.
42
+ export const PLAY_MEASURE_SESSION_1 = {
43
+ id: "play/measure-session-1", family: "play/measure-session", since: "2026-08-20",
44
+ hashField: "event_hash", prevField: null, genesis: null, link: "none", truncate: 32,
45
+ applies: () => true,
46
+ canonical: (e) => JSON.stringify({
47
+ studyId: e.studyId ?? e.study_id, sessionId: e.sessionId ?? e.session_id,
48
+ studentId: e.studentId ?? e.student_id ?? "unknown", timestamp: e.timestamp ?? "",
49
+ correct: e.correct, domain: e.domain ?? "", responseTimeMs: e.responseTimeMs ?? e.response_time_ms ?? null,
50
+ sequenceNumber: e.sequenceNumber ?? e.sequence_number ?? null,
51
+ }),
52
+ validate: (e) => {
53
+ const errors = [];
54
+ if (!(e.studyId ?? e.study_id))
55
+ errors.push("missing studyId");
56
+ if (!(e.sessionId ?? e.session_id))
57
+ errors.push("missing sessionId");
58
+ return errors;
59
+ },
60
+ };
61
+ // ── play/encounter-fnv64-1 — qlm-games src/lib/credentials/platform/evidence-schema.ts
62
+ // computeEventHash: hashSnapshotSync (FNV-1a 64 over UTF-16 units) of
63
+ // JSON.stringify({eventId, ts, encounterId, actor, source, type, payload,
64
+ // consentRef, prevHash}) in that insertion order. Fields: eventHash / prevHash
65
+ // ("" on the first event). It labels itself schemaVersion "clin-1.0", which is
66
+ // NOT the SHA-256 residency scheme play/clinical-clin-1.0.
67
+ export const PLAY_ENCOUNTER_FNV64_1 = {
68
+ id: "play/encounter-fnv64-1", family: "play/encounter", since: "2026-08-16",
69
+ hashField: "eventHash", prevField: "prevHash", genesis: "", digest: "fnv1a64",
70
+ applies: (e) => e.eventHash !== undefined || e.schemaVersion === "clin-1.0",
71
+ canonical: (e) => JSON.stringify({
72
+ eventId: e.eventId, ts: e.ts, encounterId: e.encounterId, actor: e.actor, source: e.source,
73
+ type: e.type, payload: e.payload, consentRef: e.consentRef, prevHash: e.prevHash,
74
+ }),
75
+ validate: (e) => {
76
+ const errors = [];
77
+ for (const f of ["eventId", "ts", "encounterId", "type"])
78
+ if (typeof e[f] !== "string" || !e[f])
79
+ errors.push(`missing ${f}`);
80
+ return errors;
81
+ },
82
+ };
83
+ // ── play/research-provenance-1 — qlm-games src/lib/research/evidence-provenance-chain.ts
84
+ // computeChainHash(chain): sha256 of the concatenation (no separators) of
85
+ // designLayer.blueprintId, JSON.stringify(designLayer.auditResult.scores),
86
+ // String(runtimeLayer.worldId)+String(eventCount)+String(evidenceChainIntegrity),
87
+ // then proofLayer.protocolCommitmentHash+bridgeCertificateHash if present, then
88
+ // lineageLayer.closureRate+allLineagesClosed if present. One record, no link.
89
+ export const PLAY_RESEARCH_PROVENANCE_1 = {
90
+ id: "play/research-provenance-1", family: "play/research-provenance", since: "2026-08-25",
91
+ hashField: "chainHash", prevField: null, genesis: null, link: "none",
92
+ applies: (e) => typeof e.designLayer === "object" && e.designLayer !== null,
93
+ canonical: (e) => {
94
+ const d = e.designLayer;
95
+ const r = e.runtimeLayer;
96
+ const p = e.proofLayer;
97
+ const l = e.lineageLayer;
98
+ const parts = [];
99
+ parts.push(str(d.blueprintId));
100
+ parts.push(JSON.stringify(d.auditResult.scores));
101
+ parts.push(String(r.worldId) + String(r.eventCount) + String(r.evidenceChainIntegrity));
102
+ if (p)
103
+ parts.push(String(p.protocolCommitmentHash) + String(p.bridgeCertificateHash));
104
+ if (l)
105
+ parts.push(String(l.closureRate) + String(l.allLineagesClosed));
106
+ return parts.join("");
107
+ },
108
+ validate: (e) => {
109
+ const errors = [];
110
+ if (!e.designLayer)
111
+ errors.push("missing designLayer");
112
+ if (!e.runtimeLayer)
113
+ errors.push("missing runtimeLayer");
114
+ return errors;
115
+ },
116
+ };
117
+ // ── qcore/qinverse-djb2-1 — qlm-games src/lib/q-core/qinverse/evidence-chain.ts
118
+ // (identical copy in crucible-teacher). computeEventHash: djb2 (32-bit) of
119
+ // JSON.stringify({eventId, type, data, prevHash, sequence, seed}); fields
120
+ // hash / prevHash ("genesis" first). djb2 is not collision resistant: the
121
+ // chain is order-evident, not tamper-evident. A SHA-256 successor scheme is
122
+ // the product's call; verification of what was sealed needs this one.
123
+ export const QCORE_QINVERSE_DJB2_1 = {
124
+ id: "qcore/qinverse-djb2-1", family: "qcore/qinverse", since: "2026-08-10",
125
+ hashField: "hash", prevField: "prevHash", genesis: "genesis", digest: "djb2-32",
126
+ applies: () => true,
127
+ canonical: (e) => JSON.stringify({ eventId: e.eventId, type: e.type, data: e.data, prevHash: e.prevHash, sequence: e.sequence, seed: e.seed }),
128
+ validate: (e) => {
129
+ const errors = [];
130
+ if (typeof e.eventId !== "string" || !e.eventId)
131
+ errors.push("missing eventId");
132
+ if (typeof e.sequence !== "number")
133
+ errors.push("missing sequence");
134
+ return errors;
135
+ },
136
+ };
137
+ // ── studio/loop-1 — qlm-studio src/lib/loop/evidence.ts ───────────────────
138
+ // hashEvent(prevHash, event): sha256 of canonical({prevHash, ...event}) where
139
+ // canonical = JSON.stringify(sortedKeys.map(k => [k, value])) over exactly
140
+ // {sessionId, participantId, stage, event, at, payload}; the stored `id` is
141
+ // not hashed. Fields hash / prevHash ("genesis" first).
142
+ const STUDIO_FIELDS = ["sessionId", "participantId", "stage", "event", "at", "payload"];
143
+ export const STUDIO_LOOP_1 = {
144
+ id: "studio/loop-1", family: "studio/loop", since: "2026-09-05",
145
+ hashField: "hash", prevField: "prevHash", genesis: "genesis",
146
+ applies: () => true,
147
+ canonical: (e) => {
148
+ const input = { prevHash: e.prevHash };
149
+ for (const f of STUDIO_FIELDS)
150
+ input[f] = e[f];
151
+ const keys = Object.keys(input).sort();
152
+ return JSON.stringify(keys.map((k) => [k, input[k]]));
153
+ },
154
+ validate: (e) => {
155
+ const errors = [];
156
+ for (const f of ["sessionId", "participantId", "stage", "event", "at"])
157
+ if (typeof e[f] !== "string" || !e[f])
158
+ errors.push(`missing ${f}`);
159
+ return errors;
160
+ },
161
+ };
162
+ // ── dp/ledger-v3 — art-of-kings src/lib/engine/v3/event-ledger.ts ────────
163
+ // computeEventHash(event, previousEventHash): sha256 over
164
+ // (previousEventHash ?? "") followed by canonicalize(event), where the event
165
+ // excludes eventHash, eventSignature, signatureKeyVersion and receivedAt, and
166
+ // canonicalize is the RFC 8785-style form below (sorted keys, JSON scalars,
167
+ // undefined → null). previousEventHash is absent on the first event.
168
+ // eventSignature (HMAC over the hash) is a tenant signature, not the chain
169
+ // link, and is outside this scheme.
170
+ export function canonicalizeRfc8785(value) {
171
+ if (value === undefined)
172
+ return "null";
173
+ if (value === null)
174
+ return "null";
175
+ if (typeof value === "boolean")
176
+ return value.toString();
177
+ if (typeof value === "number") {
178
+ if (!isFinite(value))
179
+ throw new Error("Non-finite number in canonical event");
180
+ return JSON.stringify(value);
181
+ }
182
+ if (typeof value === "string")
183
+ return JSON.stringify(value);
184
+ if (Array.isArray(value))
185
+ return "[" + value.map(canonicalizeRfc8785).join(",") + "]";
186
+ if (typeof value === "object") {
187
+ const sorted = Object.keys(value).sort()
188
+ .map((k) => JSON.stringify(k) + ":" + canonicalizeRfc8785(value[k]));
189
+ return "{" + sorted.join(",") + "}";
190
+ }
191
+ throw new Error(`Unserializable type: ${typeof value}`);
192
+ }
193
+ const DP_EXCLUDED = new Set(["eventHash", "eventSignature", "signatureKeyVersion", "receivedAt"]);
194
+ /** The envelope keys buildEvent always sets (undefined optionals canonicalize to null). */
195
+ const DP_FIELDS = [
196
+ "eventId", "tenantId", "sessionId", "learnerId", "actorUserId", "actorRole", "eventSeq", "eventType", "occurredAt",
197
+ "requestId", "idempotencyKey", "scenarioId", "scenarioVersion", "turnId", "checkpointId", "stateBeforeHash", "stateAfterHash",
198
+ "knowledgeSnapshotId", "payloadSchema", "payloadVersion", "payload", "previousEventHash",
199
+ ];
200
+ export const DP_LEDGER_V3 = {
201
+ id: "dp/ledger-v3", family: "dp/ledger", since: "2026-08-28",
202
+ hashField: "eventHash", prevField: "previousEventHash", genesis: null,
203
+ applies: () => true,
204
+ canonical: (e) => {
205
+ // The sealer canonicalized every envelope key (absent optionals as null),
206
+ // so a stored row that dropped them is canonicalized as the sealer saw it;
207
+ // any extra stored key outside the excluded four is hashed too, as the
208
+ // product's own verifier would.
209
+ const rest = {};
210
+ for (const k of DP_FIELDS)
211
+ rest[k] = e[k] === undefined ? null : e[k];
212
+ for (const k of Object.keys(e))
213
+ if (!DP_EXCLUDED.has(k) && !(k in rest))
214
+ rest[k] = e[k];
215
+ return str(e.previousEventHash) + canonicalizeRfc8785(rest);
216
+ },
217
+ validate: (e) => {
218
+ const errors = [];
219
+ for (const f of ["eventId", "tenantId", "sessionId", "eventType"])
220
+ if (typeof e[f] !== "string" || !e[f])
221
+ errors.push(`missing ${f}`);
222
+ if (typeof e.eventSeq !== "number")
223
+ errors.push("missing eventSeq");
224
+ return errors;
225
+ },
226
+ };
227
+ // ── tpc/dse-journal-1 — teachproof src/lib/tutor-lab/dse/journal.ts ──────
228
+ // computeEntryHash(prevHash, entry): sha256 of prevHash + JSON.stringify({seq,
229
+ // eventType, data}). Entries store `hash` only; the link is implicit in order
230
+ // (the previous entry's hash, "" for the first). `timestamp` is not hashed.
231
+ export const TPC_DSE_JOURNAL_1 = {
232
+ id: "tpc/dse-journal-1", family: "tpc/dse-journal", since: "2026-09-08",
233
+ hashField: "hash", prevField: null, genesis: "", link: "implicit",
234
+ applies: () => true,
235
+ canonical: (e, prev) => (prev ?? "") + JSON.stringify({ seq: e.seq, eventType: e.eventType, data: e.data }),
236
+ validate: (e) => {
237
+ const errors = [];
238
+ if (typeof e.seq !== "number")
239
+ errors.push("missing seq");
240
+ if (typeof e.eventType !== "string" || !e.eventType)
241
+ errors.push("missing eventType");
242
+ return errors;
243
+ },
244
+ };
245
+ // ── yardstick/spine-1 — yardstick/project_spine/models.py ResponseRecord ──
246
+ // compute_chain_hash(previous_hash): sha256 of the UTF-8 bytes of
247
+ // f"{previous_hash}:{enrollment_id}:{item_id}:{response}:{correct}" with
248
+ // Python's str() forms: UUIDs as text, booleans as True/False.
249
+ export const YARDSTICK_SPINE_1 = {
250
+ id: "yardstick/spine-1", family: "yardstick/spine", since: "2026-08-30",
251
+ hashField: "chain_hash", prevField: "previous_hash", genesis: "",
252
+ applies: () => true,
253
+ canonical: (e) => {
254
+ // Python's f-string forms: True/False for booleans, None for a missing value — including previous_hash.
255
+ const py = (v) => (v === true ? "True" : v === false ? "False" : v === null || v === undefined ? "None" : String(v));
256
+ return `${py(e.previous_hash)}:${py(e.enrollment_id)}:${py(e.item_id)}:${py(e.response)}:${py(e.correct)}`;
257
+ },
258
+ validate: (e) => {
259
+ const errors = [];
260
+ for (const f of ["enrollment_id", "item_id"])
261
+ if (typeof e[f] !== "string" || !e[f])
262
+ errors.push(`missing ${f}`);
263
+ return errors;
264
+ },
265
+ };
266
+ // ── labpath/learning-evidence-v1 — qlm-games src/lib/ecogenesis/labpath/learning-evidence-event.ts
267
+ // hashLearningEvidenceEvent(draft): sha256 of JSON.stringify(sortObject(draft))
268
+ // where draft is the whole event minus `hash` and sortObject orders keys at
269
+ // every depth with String.prototype.localeCompare. Fields hash / prev_hash
270
+ // (null on the first event).
271
+ export function sortObjectLocale(value) {
272
+ if (Array.isArray(value))
273
+ return value.map(sortObjectLocale);
274
+ if (!value || typeof value !== "object")
275
+ return value;
276
+ return Object.fromEntries(Object.entries(value)
277
+ .sort(([a], [b]) => a.localeCompare(b))
278
+ .map(([key, nested]) => [key, sortObjectLocale(nested)]));
279
+ }
280
+ export const LABPATH_LEARNING_EVIDENCE_V1 = {
281
+ id: "labpath/learning-evidence-v1", family: "labpath/learning-evidence", since: "2026-09-12",
282
+ hashField: "hash", prevField: "prev_hash", genesis: null,
283
+ applies: (e) => typeof e.event_id === "string",
284
+ canonical: (e) => {
285
+ const { hash: _h, ...draft } = e;
286
+ void _h;
287
+ return JSON.stringify(sortObjectLocale(draft));
288
+ },
289
+ validate: (e) => {
290
+ const errors = [];
291
+ for (const f of ["event_id", "learner_id", "ts", "world_id", "session_id"])
292
+ if (typeof e[f] !== "string" || !e[f])
293
+ errors.push(`missing ${f}`);
294
+ return errors;
295
+ },
296
+ };
297
+ // ── tpc/transcript-1 — teachproof src/app/api/clinical/evidence/process/route.ts
298
+ // Stage 3 seals an evidence item: sha256 of JSON.stringify({text, turns,
299
+ // sourceType, duration}) — the item's transcript column — and stores the
300
+ // previous item's hash as prev_hash (null on the learner's first item). The
301
+ // link is stored, not hashed; the verifier checks it separately.
302
+ export const TPC_TRANSCRIPT_1 = {
303
+ id: "tpc/transcript-1", family: "tpc/transcript", since: "2026-08-20",
304
+ hashField: "hash", prevField: "prev_hash", genesis: null,
305
+ applies: (e) => typeof e.transcript === "object" && e.transcript !== null,
306
+ canonical: (e) => {
307
+ const t = e.transcript;
308
+ return JSON.stringify({ text: t.text, turns: t.turns, sourceType: t.sourceType, duration: t.duration });
309
+ },
310
+ validate: (e) => (typeof e.transcript === "object" && e.transcript !== null ? [] : ["missing transcript"]),
311
+ };
312
+ // ── tpc/differential-commit-1 — teachproof src/lib/clinical/longitudinal/events/differential-commit.ts
313
+ // Commit and skip events are appended to the clinical evidence chain (same
314
+ // hash/prev_hash fields, so they coexist with tpc/clinical-v*). hashCommit:
315
+ // sha256 of JSON.stringify(canonical) where canonical is
316
+ // ["differential_commit", learner, encounter, checkpoint, rankedDifferential, whyText, nextAction, discipline, prev_hash]
317
+ // or ["commit_skipped", learner, encounter, checkpoint, reason, prev_hash]. `turn` is not hashed.
318
+ // 0.7.0: a record that carries commit_schema is never this scheme's, so a
319
+ // version 2 commit's added fields are always covered (version 2, below), and
320
+ // a commit that carries those fields without commit_schema fails validation
321
+ // rather than verifying with them unhashed. Skip records stay here, and
322
+ // acted_without_commit joins the reasons (TPC-SPEC-002 E6). The canonical
323
+ // form is unchanged.
324
+ const isCommitType = (e) => e.type === "differential_commit" || e.type === "commit_skipped";
325
+ const COMMIT_SKIP_REASONS = ["skip", "timeout", "not_reached", "acted_without_commit"];
326
+ /** The fields version 2 appends to version 1's list, in hash order. */
327
+ const COMMIT_V2_FIELDS = ["concepts", "statuses", "confidence", "evidenceLinks", "discriminator", "planLinks", "trigger", "captureSnapshot", "supersedes"];
328
+ const commitV1List = (e) => ["differential_commit", e.learner, e.encounter, e.checkpoint, e.rankedDifferential, e.whyText, e.nextAction, e.discipline, e.prev_hash];
329
+ function commitValidate(e) {
330
+ const errors = [];
331
+ for (const f of ["learner", "encounter", "checkpoint"])
332
+ if (typeof e[f] !== "string" || !e[f])
333
+ errors.push(`missing ${f}`);
334
+ if (e.type === "commit_skipped" && !COMMIT_SKIP_REASONS.includes(e.reason))
335
+ errors.push(`invalid reason: ${String(e.reason)}`);
336
+ return errors;
337
+ }
338
+ /**
339
+ * Version 1's checks. As the family's last scheme it also checks a commit
340
+ * record no scheme applies to, so it says why: a commit_schema that is not
341
+ * version 2's, or version 2's fields on a record without it.
342
+ */
343
+ function commitV1Validate(e) {
344
+ const errors = commitValidate(e);
345
+ if (e.commit_schema !== undefined) {
346
+ errors.push(e.type !== "differential_commit" ? `${String(e.type)} carries no commit_schema`
347
+ : e.commit_schema === 2 ? "a commit with commit_schema 2 is tpc/differential-commit-2's"
348
+ : `invalid commit_schema: ${String(e.commit_schema)}`);
349
+ }
350
+ else if (e.type === "differential_commit") {
351
+ const added = COMMIT_V2_FIELDS.filter((f) => e[f] !== undefined);
352
+ if (added.length > 0)
353
+ errors.push(`fields without commit_schema 2: ${added.join(", ")}`);
354
+ }
355
+ return errors;
356
+ }
357
+ export const TPC_DIFFERENTIAL_COMMIT_1 = {
358
+ id: "tpc/differential-commit-1", family: "tpc/clinical", since: "2026-09-14",
359
+ hashField: "hash", prevField: "prev_hash", genesis: "genesis",
360
+ applies: (e) => isCommitType(e) && e.commit_schema === undefined,
361
+ canonical: (e) => JSON.stringify(e.type === "differential_commit"
362
+ ? commitV1List(e)
363
+ : ["commit_skipped", e.learner, e.encounter, e.checkpoint, e.reason, e.prev_hash]),
364
+ validate: commitV1Validate,
365
+ coexists: ["tpc/clinical-v1", "tpc/clinical-v2", "tpc/clinical-v3", "tpc/clinical-v4", "tpc/differential-commit-2"],
366
+ };
367
+ // ── tpc/differential-commit-2 — TPC-SPEC-002 E4, the commit model ─────────
368
+ // A commit that carries commit_schema: 2. Version 1's list is hashed as it
369
+ // was, then commit_schema and the E4 fields append in this fixed order, each
370
+ // with its keys sorted at every depth (stored rows may reorder keys):
371
+ // [...version 1's list, commit_schema, concepts, statuses, confidence,
372
+ // evidenceLinks, discriminator, planLinks, trigger, captureSnapshot, supersedes]
373
+ // Missing fields hash as null and fail validation: every field is written,
374
+ // null where empty. The objects inside take only their named keys, so every
375
+ // valid record hashes the same in JavaScript and Python. Records sealed under
376
+ // version 1 keep verifying under it, and the two coexist in one chain. Its
377
+ // definition; there is no older sealer.
378
+ const COMMIT_CHECKPOINTS = ["pre_brief", "post_history", "on_evidence", "pre_close"];
379
+ const COMMIT_DISCIPLINES = ["OD", "RN", "GM"];
380
+ const COMMIT_STATUSES = ["leading", "active", "ruled_out"];
381
+ const DISCRIMINATOR_KINDS = ["question", "test", "finding", "time"];
382
+ const MAX_RANKED = 5;
383
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
384
+ const nonEmpty = (v) => typeof v === "string" && v !== "";
385
+ /** An object with no key but these (each still checked on its own). */
386
+ const onlyKeys = (o, keys) => Object.keys(o).every((k) => keys.includes(k));
387
+ /** A 1-based position in rankedDifferential. */
388
+ const isRank = (v, n) => typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= n;
389
+ function commitV2Validate(e) {
390
+ const errors = commitValidate(e);
391
+ if (e.commit_schema !== 2)
392
+ errors.push(`invalid commit_schema: ${String(e.commit_schema)}`);
393
+ if (nonEmpty(e.checkpoint) && !COMMIT_CHECKPOINTS.includes(e.checkpoint))
394
+ errors.push(`invalid checkpoint: ${e.checkpoint}`);
395
+ const ranked = e.rankedDifferential;
396
+ const n = Array.isArray(ranked) ? ranked.length : 0;
397
+ if (!Array.isArray(ranked) || n < 1 || n > MAX_RANKED || !ranked.every(nonEmpty))
398
+ errors.push(`rankedDifferential must be 1 to ${MAX_RANKED} non-empty strings`);
399
+ if (typeof e.whyText !== "string")
400
+ errors.push("whyText must be a string");
401
+ if (e.nextAction !== null && typeof e.nextAction !== "string")
402
+ errors.push("nextAction must be a string or null");
403
+ if (!COMMIT_DISCIPLINES.includes(e.discipline))
404
+ errors.push(`invalid discipline: ${String(e.discipline)}`);
405
+ const perEntry = (f, what, ok) => {
406
+ const a = e[f];
407
+ if (!Array.isArray(a) || a.length !== n || !a.every(ok))
408
+ errors.push(`${f} must hold one ${what} per ranked entry`);
409
+ };
410
+ perEntry("concepts", "concept id or null", (c) => c === null || nonEmpty(c));
411
+ perEntry("statuses", "of leading, active, ruled_out", (s) => COMMIT_STATUSES.includes(s));
412
+ perEntry("confidence", "number 0 to 100 or null", (c) => c === null || (typeof c === "number" && c >= 0 && c <= 100));
413
+ const snapshot = Array.isArray(e.captureSnapshot) && e.captureSnapshot.every(nonEmpty) ? e.captureSnapshot : null;
414
+ if (!snapshot)
415
+ errors.push("captureSnapshot must be an array of non-empty strings");
416
+ // E4: evidence links point only into the capture snapshot.
417
+ if (!Array.isArray(e.evidenceLinks))
418
+ errors.push("evidenceLinks must be an array");
419
+ else
420
+ e.evidenceLinks.forEach((l, i) => {
421
+ if (!isRecord(l) || !onlyKeys(l, ["rank", "ref", "direction"]) || !isRank(l.rank, n) || !nonEmpty(l.ref) || (l.direction !== "for" && l.direction !== "against")) {
422
+ errors.push(`invalid evidenceLinks[${i}]`);
423
+ }
424
+ else if (snapshot && !snapshot.includes(l.ref))
425
+ errors.push(`evidenceLinks[${i}] ref not in captureSnapshot: ${l.ref}`);
426
+ });
427
+ const d = e.discriminator;
428
+ if (d !== null && (!isRecord(d) || !onlyKeys(d, ["kind", "ref", "text"]) || !DISCRIMINATOR_KINDS.includes(d.kind)
429
+ || (d.ref !== null && !nonEmpty(d.ref)) || typeof d.text !== "string")) {
430
+ errors.push("invalid discriminator");
431
+ }
432
+ if (!Array.isArray(e.planLinks))
433
+ errors.push("planLinks must be an array");
434
+ else
435
+ e.planLinks.forEach((l, i) => {
436
+ if (!isRecord(l) || !onlyKeys(l, ["rank", "ref"]) || !isRank(l.rank, n) || !nonEmpty(l.ref))
437
+ errors.push(`invalid planLinks[${i}]`);
438
+ });
439
+ if (e.trigger !== null && !nonEmpty(e.trigger))
440
+ errors.push("trigger must be a non-empty string or null");
441
+ else if (e.checkpoint === "on_evidence" && e.trigger === null)
442
+ errors.push("on_evidence needs a trigger");
443
+ if (e.supersedes !== null && !nonEmpty(e.supersedes))
444
+ errors.push("supersedes must be a hash or null");
445
+ return errors;
446
+ }
447
+ export const TPC_DIFFERENTIAL_COMMIT_2 = {
448
+ id: "tpc/differential-commit-2", family: "tpc/clinical", since: "2026-09-28",
449
+ hashField: "hash", prevField: "prev_hash", genesis: "genesis",
450
+ applies: (e) => e.type === "differential_commit" && e.commit_schema === 2,
451
+ canonical: (e) => JSON.stringify([...commitV1List(e), e.commit_schema, ...COMMIT_V2_FIELDS.map((f) => sortKeysDeep(e[f] ?? null))]),
452
+ validate: commitV2Validate,
453
+ coexists: ["tpc/clinical-v1", "tpc/clinical-v2", "tpc/clinical-v3", "tpc/clinical-v4", "tpc/differential-commit-1"],
454
+ };
455
+ // ── tpc/rct-input-1 — teachproof src/lib/rct/result-ledger.ts ────────────
456
+ // ledgerFromAnalysis fingerprints the analysis inputs: sha256 of
457
+ // JSON.stringify({studyId, nTreatment, nControl, totalOutcomeEvents,
458
+ // sourcePipes, dimensions: [{dimension, treatmentN, controlN, treatmentMean,
459
+ // controlMean}]}); every ledger entry carries it as inputHash. Unlinked.
460
+ export const TPC_RCT_INPUT_1 = {
461
+ id: "tpc/rct-input-1", family: "tpc/rct-input", since: "2026-09-22",
462
+ hashField: "inputHash", prevField: null, genesis: null, link: "none",
463
+ applies: (e) => Array.isArray(e.dimensions),
464
+ canonical: (e) => JSON.stringify({
465
+ studyId: e.studyId, nTreatment: e.nTreatment, nControl: e.nControl, totalOutcomeEvents: e.totalOutcomeEvents, sourcePipes: e.sourcePipes,
466
+ dimensions: e.dimensions.map((d) => ({ dimension: d.dimension, treatmentN: d.treatmentN, controlN: d.controlN, treatmentMean: d.treatmentMean, controlMean: d.controlMean })),
467
+ }),
468
+ validate: (e) => (typeof e.studyId === "string" && e.studyId ? [] : ["missing studyId"]),
469
+ };
470
+ // ═══════════════════════════════════════════════════════════════════════════
471
+ // 0.6.0 — chains found by the 2026-09-27 audit (docs/PLATFORM.md §12). They
472
+ // were missed by the first inventory because it grepped for function names
473
+ // and the CI gate filtered by file name; both are now digest-based.
474
+ // ═══════════════════════════════════════════════════════════════════════════
475
+ // ── play/world-trace — qlm-games src/app/ecogenesis/worlds/components/*World.tsx
476
+ // Every LabPath world component sealed its own "world-trace/1" trace in the
477
+ // browser with a local djb2: hash = djb2(prevHash + JSON.stringify(array)),
478
+ // prevHash "00000000" on the first event, kept in localStorage and exported
479
+ // as <world>-evidence.json. Fifty files, three canonical arrays:
480
+ // play/world-trace-1 [seq, type, payload] 46 worlds
481
+ // play/world-trace-tsim-1 [seq, type, payload, tSim] simpleforces, soundlab, statesofmatter
482
+ // play/world-trace-identity-1 [seq, type, payload, {missionId, band, seed}] cybersim, schemaVersion "world-trace/2"
483
+ // djb2 is not collision resistant: a trace is order-evident, not tamper-evident.
484
+ const WORLD_TRACE_TSIM_WORLDS = new Set(["simpleforces", "soundlab", "statesofmatter"]);
485
+ function worldTraceValidate(e) {
486
+ const errors = [];
487
+ if (typeof e.seq !== "number")
488
+ errors.push("missing seq");
489
+ if (typeof e.type !== "string" || !e.type)
490
+ errors.push("missing type");
491
+ if (typeof e.prevHash !== "string")
492
+ errors.push("missing prevHash");
493
+ if (!e.payload || typeof e.payload !== "object" || Array.isArray(e.payload))
494
+ errors.push("payload must be an object");
495
+ return errors;
496
+ }
497
+ const worldTraceCommon = { family: "play/world-trace", hashField: "hash", prevField: "prevHash", genesis: "00000000", digest: "djb2-32", validate: worldTraceValidate };
498
+ const isWorldTrace2 = (e) => e.schemaVersion === "world-trace/2";
499
+ const isTsimWorld = (e) => WORLD_TRACE_TSIM_WORLDS.has(String(e.worldId));
500
+ export const PLAY_WORLD_TRACE_IDENTITY_1 = {
501
+ ...worldTraceCommon, id: "play/world-trace-identity-1", since: "2026-09-24",
502
+ applies: (e) => isWorldTrace2(e),
503
+ // The sealer hashed its identity object literal {missionId, band, seed} in that key order.
504
+ canonical: (e) => String(e.prevHash) + JSON.stringify([e.seq, e.type, e.payload, { missionId: e.missionId, band: e.band, seed: e.seed }]),
505
+ };
506
+ export const PLAY_WORLD_TRACE_TSIM_1 = {
507
+ ...worldTraceCommon, id: "play/world-trace-tsim-1", since: "2026-09-24",
508
+ applies: (e) => !isWorldTrace2(e) && isTsimWorld(e),
509
+ canonical: (e) => String(e.prevHash) + JSON.stringify([e.seq, e.type, e.payload, e.tSim]),
510
+ };
511
+ export const PLAY_WORLD_TRACE_1 = {
512
+ ...worldTraceCommon, id: "play/world-trace-1", since: "2026-09-24",
513
+ applies: (e) => !isWorldTrace2(e) && !isTsimWorld(e),
514
+ canonical: (e) => String(e.prevHash) + JSON.stringify([e.seq, e.type, e.payload]),
515
+ };
516
+ // ── tpc/yardstick-record — teachproof src/lib/yardstick/record-types.ts ──
517
+ // computeDigest(content): sha256 of JSON.stringify(content, Object.keys(content).sort())
518
+ // over {sequence, type, studyId, timestamp, payload, prevDigest}; fields
519
+ // digest / prevDigest ("0" × 64 on the first record). The records live in
520
+ // teachproof_studies.record_sequence.
521
+ //
522
+ // An array replacer is a property whitelist that JSON.stringify applies at
523
+ // EVERY depth, so `payload` serializes as `{}` unless a nested key happens
524
+ // to be named like a top-level one: version 1 does not cover the payload.
525
+ // It is kept verbatim so sealed sequences still verify; version 2 hashes
526
+ // the same six fields with keys sorted at every depth and covers the
527
+ // payload. The two coexist in one sequence (old records v1, new v2).
528
+ const YR_FIELDS = ["sequence", "type", "studyId", "timestamp", "payload", "prevDigest"];
529
+ const YR_REPLACER = [...YR_FIELDS].sort();
530
+ function yrContent(e) {
531
+ const c = {};
532
+ for (const f of YR_FIELDS)
533
+ c[f] = e[f];
534
+ return c;
535
+ }
536
+ function yrValidate(e) {
537
+ const errors = [];
538
+ if (typeof e.sequence !== "number")
539
+ errors.push("missing sequence");
540
+ for (const f of ["type", "studyId", "timestamp"])
541
+ if (typeof e[f] !== "string" || !e[f])
542
+ errors.push(`missing ${f}`);
543
+ if (!e.payload || typeof e.payload !== "object" || Array.isArray(e.payload))
544
+ errors.push("payload must be an object");
545
+ return errors;
546
+ }
547
+ const yrCommon = { family: "tpc/yardstick-record", hashField: "digest", prevField: "prevDigest", genesis: "0".repeat(64), applies: () => true, validate: yrValidate };
548
+ export const TPC_YARDSTICK_RECORD_2 = {
549
+ ...yrCommon, id: "tpc/yardstick-record-2", since: "2026-09-28",
550
+ canonical: (e) => JSON.stringify(sortKeysDeep(yrContent(e))),
551
+ coexists: ["tpc/yardstick-record-1"],
552
+ };
553
+ export const TPC_YARDSTICK_RECORD_1 = {
554
+ ...yrCommon, id: "tpc/yardstick-record-1", since: "2026-09-21",
555
+ canonical: (e) => JSON.stringify(yrContent(e), YR_REPLACER),
556
+ coexists: ["tpc/yardstick-record-2"],
557
+ };
558
+ // ── tpc/intervention-1 — teachproof src/lib/clinical/longitudinal/programos/intervention-loop.ts
559
+ // recordAction: sha256 of JSON.stringify([type, provenance, flagId, actor,
560
+ // actionType, constructId, rationale, timestamp, prevHash]); fields hash /
561
+ // prevHash ("genesis" first). id, windowOpportunities and windowDays are not hashed.
562
+ export const TPC_INTERVENTION_1 = {
563
+ id: "tpc/intervention-1", family: "tpc/intervention", since: "2026-09-21",
564
+ hashField: "hash", prevField: "prevHash", genesis: "genesis",
565
+ applies: () => true,
566
+ canonical: (e) => JSON.stringify([e.type, e.provenance, e.flagId, e.actor, e.actionType, e.constructId, e.rationale, e.timestamp, e.prevHash]),
567
+ validate: (e) => {
568
+ const errors = [];
569
+ for (const f of ["flagId", "actor", "actionType", "constructId", "timestamp"])
570
+ if (typeof e[f] !== "string" || !e[f])
571
+ errors.push(`missing ${f}`);
572
+ return errors;
573
+ },
574
+ };
575
+ // ── tpc/rehearsal-stage-1 — teachproof src/lib/clinical/longitudinal/study/rehearsal-runner.ts
576
+ // recordStage: sha256 of JSON.stringify([stage, passed, timestamp, prevHash])
577
+ // where prevHash is the previous stage's hash ("genesis" first) and is not
578
+ // stored on the result: the link is implicit in order.
579
+ export const TPC_REHEARSAL_STAGE_1 = {
580
+ id: "tpc/rehearsal-stage-1", family: "tpc/rehearsal-stage", since: "2026-09-21",
581
+ hashField: "hash", prevField: null, genesis: "genesis", link: "implicit",
582
+ applies: () => true,
583
+ canonical: (e, prev) => JSON.stringify([e.stage, e.passed, e.timestamp, prev ?? "genesis"]),
584
+ validate: (e) => {
585
+ const errors = [];
586
+ if (typeof e.stage !== "string" || !e.stage)
587
+ errors.push("missing stage");
588
+ if (typeof e.passed !== "boolean")
589
+ errors.push("missing passed");
590
+ if (typeof e.timestamp !== "string" || !e.timestamp)
591
+ errors.push("missing timestamp");
592
+ return errors;
593
+ },
594
+ };
595
+ // ── yardstick/activity-1 — yardstick packages/engine/studies/activity.py ──
596
+ // _entry_hash(entry, prev): sha256 of prev + "\n" + json.dumps(body,
597
+ // sort_keys=True, separators=(",", ":"), ensure_ascii=False) where body is
598
+ // the entry minus `hash` (so `prev`, `scheme` and `seq` are hashed); fields
599
+ // hash / prev ("0" × 64 first). One activity.jsonl per study.
600
+ // The Python twin is the reference. This form equals Python's for strings,
601
+ // integers, booleans, null, lists and objects; a float would differ where
602
+ // Python and JavaScript print it differently (5.0 vs 5) and entries carry none.
603
+ export const YARDSTICK_ACTIVITY_1 = {
604
+ id: "yardstick/activity-1", family: "yardstick/activity", since: "2026-09-26",
605
+ hashField: "hash", prevField: "prev", genesis: "0".repeat(64),
606
+ applies: () => true,
607
+ canonical: (e) => {
608
+ const { hash: _h, ...body } = e;
609
+ void _h;
610
+ return String(e.prev) + "\n" + JSON.stringify(sortKeysDeep(body));
611
+ },
612
+ validate: (e) => {
613
+ const errors = [];
614
+ if (typeof e.seq !== "number")
615
+ errors.push("missing seq");
616
+ for (const f of ["at", "who", "action"])
617
+ if (typeof e[f] !== "string" || !e[f])
618
+ errors.push(`missing ${f}`);
619
+ return errors;
620
+ },
621
+ };
622
+ export const PRODUCT_SCHEMES = [
623
+ PLAY_EMIT_1, PLAY_MEASURE_SESSION_1, PLAY_ENCOUNTER_FNV64_1, PLAY_RESEARCH_PROVENANCE_1,
624
+ QCORE_QINVERSE_DJB2_1, STUDIO_LOOP_1, DP_LEDGER_V3, TPC_DSE_JOURNAL_1, YARDSTICK_SPINE_1, LABPATH_LEARNING_EVIDENCE_V1,
625
+ TPC_TRANSCRIPT_1, TPC_DIFFERENTIAL_COMMIT_2, TPC_DIFFERENTIAL_COMMIT_1, TPC_RCT_INPUT_1,
626
+ PLAY_WORLD_TRACE_IDENTITY_1, PLAY_WORLD_TRACE_TSIM_1, PLAY_WORLD_TRACE_1,
627
+ TPC_YARDSTICK_RECORD_2, TPC_YARDSTICK_RECORD_1, TPC_INTERVENTION_1, TPC_REHEARSAL_STAGE_1, YARDSTICK_ACTIVITY_1,
628
+ ];
629
+ //# sourceMappingURL=products.js.map