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