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.
- package/README.md +12 -0
- package/dist/cli.js +86 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/schemes/index.d.ts +125 -0
- package/dist/schemes/index.d.ts.map +1 -0
- package/dist/schemes/index.js +309 -0
- package/dist/schemes/index.js.map +1 -0
- package/dist/schemes/node.d.ts +8 -0
- package/dist/schemes/node.d.ts.map +1 -0
- package/dist/schemes/node.js +15 -0
- package/dist/schemes/node.js.map +1 -0
- package/dist/schemes/products.d.ts +36 -0
- package/dist/schemes/products.d.ts.map +1 -0
- package/dist/schemes/products.js +629 -0
- package/dist/schemes/products.js.map +1 -0
- package/package.json +28 -8
|
@@ -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
|