@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,27 @@
1
+ /**
2
+ * Hash utilities for evidence record integrity.
3
+ *
4
+ * Uses SHA-256 via Node's crypto module (ESM import).
5
+ * Canonical serialization ensures deterministic hashing.
6
+ */
7
+ import type { EvidenceEntry } from "../schema/evidence-record.js";
8
+ /**
9
+ * Canonical JSON serialization — sorted keys, no whitespace.
10
+ * Matches Python's json.dumps(sort_keys=True, separators=(",",":"), default=str)
11
+ * byte-for-byte, including number and string formatting.
12
+ */
13
+ export declare function canonicalize(obj: unknown): string;
14
+ /**
15
+ * Compute SHA-256 hash of a string.
16
+ */
17
+ export declare function sha256(input: string): string;
18
+ /**
19
+ * Compute the canonical hash of an evidence entry.
20
+ * Every field must be explicitly null (not undefined) to match Python.
21
+ */
22
+ export declare function hashEntry(entry: EvidenceEntry): string;
23
+ /**
24
+ * Compute a hash over a canonical string.
25
+ */
26
+ export declare function hashCanonical(input: string): string;
27
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Hash utilities for evidence record integrity.
3
+ *
4
+ * Uses SHA-256 via Node's crypto module (ESM import).
5
+ * Canonical serialization ensures deterministic hashing.
6
+ */
7
+ import { createHash } from "crypto";
8
+ /**
9
+ * Format a number to match Python's json.dumps output.
10
+ *
11
+ * Python's json.dumps uses different formatting for int vs float:
12
+ * int 9200 → "9200" (no decimal point)
13
+ * float 1.0 → "1.0" (always has decimal point)
14
+ * float 3e-05 → "3e-05" (2-digit exponent minimum)
15
+ *
16
+ * JS has no int/float distinction. We use Number.isInteger() as the
17
+ * heuristic: if the JSON source had no decimal point, Python stored
18
+ * it as int and wrote it without .0.
19
+ *
20
+ * For the entry-hash hashable subset, only evidentialWeight,
21
+ * priorPosterior, and updatedPosterior are Python floats. version is
22
+ * an int. Everything inside event follows Python's own int/float typing
23
+ * from the original JSON (which round-trips through JSON.parse identically).
24
+ */
25
+ /**
26
+ * Format a number to match Python's json.dumps output.
27
+ *
28
+ * Python uses the shortest decimal representation that round-trips
29
+ * (David Gay's dtoa algorithm). JS's Number.toString() also uses dtoa
30
+ * but sometimes produces longer representations with floating-point noise.
31
+ *
32
+ * We match Python by finding the shortest representation that round-trips.
33
+ */
34
+ function pyNumberFormat(n) {
35
+ if (Object.is(n, -0))
36
+ return "-0.0";
37
+ if (!Number.isFinite(n))
38
+ return String(n);
39
+ // Python int path
40
+ if (Number.isInteger(n) && Math.abs(n) < Number.MAX_SAFE_INTEGER) {
41
+ return n.toString();
42
+ }
43
+ // Find the shortest decimal representation that round-trips
44
+ // This matches Python's float.__repr__ (shortest via dtoa)
45
+ let best = JSON.stringify(n);
46
+ // Try progressively shorter representations
47
+ for (let prec = 17; prec >= 1; prec--) {
48
+ const candidate = n.toPrecision(prec);
49
+ if (parseFloat(candidate) === n) {
50
+ best = candidate;
51
+ }
52
+ else {
53
+ break; // once it stops round-tripping, shorter won't work either
54
+ }
55
+ }
56
+ // Remove unnecessary trailing zeros and decimal point
57
+ // but keep at least one digit after decimal for floats
58
+ if (best.includes(".") && !best.includes("e") && !best.includes("E")) {
59
+ best = best.replace(/0+$/, "");
60
+ if (best.endsWith("."))
61
+ best += "0"; // keep the .0 for e.g. "1.0"
62
+ }
63
+ // Python uses scientific notation for abs < 1e-4
64
+ const abs = Math.abs(n);
65
+ if (abs > 0 && abs < 1e-4) {
66
+ if (!best.includes("e") && !best.includes("E")) {
67
+ // Use toExponential() for JS's shortest digits (no division noise)
68
+ best = n.toExponential();
69
+ }
70
+ }
71
+ // Fix exponent padding: e-7 → e-07, e+2 → e+02
72
+ best = best.replace(/e([+-])(\d)$/, "e$10$2");
73
+ return best;
74
+ }
75
+ /**
76
+ * Format a string to match Python's json.dumps output.
77
+ * Python escapes all non-ASCII characters to \\uXXXX by default.
78
+ * JS JSON.stringify passes them through as UTF-8.
79
+ */
80
+ function pyStringFormat(s) {
81
+ let result = JSON.stringify(s);
82
+ // Replace non-ASCII characters with \\uXXXX escapes, matching Python
83
+ result = result.replace(/[\u0080-\uffff]/g, (ch) => {
84
+ return "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0");
85
+ });
86
+ return result;
87
+ }
88
+ /**
89
+ * Canonical JSON serialization — sorted keys, no whitespace.
90
+ * Matches Python's json.dumps(sort_keys=True, separators=(",",":"), default=str)
91
+ * byte-for-byte, including number and string formatting.
92
+ */
93
+ export function canonicalize(obj) {
94
+ return _canonicalizeValue(obj);
95
+ }
96
+ function _canonicalizeValue(val) {
97
+ if (val === null)
98
+ return "null";
99
+ if (val === undefined)
100
+ return "null";
101
+ if (typeof val === "boolean")
102
+ return val ? "true" : "false";
103
+ if (typeof val === "number")
104
+ return pyNumberFormat(val);
105
+ if (typeof val === "string")
106
+ return pyStringFormat(val);
107
+ if (Array.isArray(val)) {
108
+ return "[" + val.map(_canonicalizeValue).join(",") + "]";
109
+ }
110
+ if (typeof val === "object") {
111
+ const keys = Object.keys(val).sort();
112
+ const pairs = keys.map(k => {
113
+ const v = val[k];
114
+ return JSON.stringify(k) + ":" + _canonicalizeValue(v);
115
+ });
116
+ return "{" + pairs.join(",") + "}";
117
+ }
118
+ return JSON.stringify(val);
119
+ }
120
+ /**
121
+ * Compute SHA-256 hash of a string.
122
+ */
123
+ export function sha256(input) {
124
+ return createHash("sha256").update(input).digest("hex");
125
+ }
126
+ /**
127
+ * Compute the canonical hash of an evidence entry.
128
+ * Every field must be explicitly null (not undefined) to match Python.
129
+ */
130
+ export function hashEntry(entry) {
131
+ const hashable = {
132
+ version: entry.version,
133
+ timestamp: entry.timestamp,
134
+ event: entry.event,
135
+ scaffoldType: entry.scaffoldType ?? null,
136
+ depthLevel: entry.depthLevel ?? null,
137
+ epistemicMode: entry.epistemicMode ?? null,
138
+ triageResult: entry.triageResult ?? null,
139
+ evidentialWeight: entry.evidentialWeight,
140
+ priorPosterior: entry.priorPosterior,
141
+ updatedPosterior: entry.updatedPosterior,
142
+ nonInterventionDecision: entry.nonInterventionDecision ?? null,
143
+ };
144
+ // Build canonical, then fix the three Python-float fields:
145
+ // evidentialWeight, priorPosterior, updatedPosterior are Python floats.
146
+ // Python writes 1.0 for float(1), but JS Number.isInteger(1) returns true
147
+ // and pyNumberFormat writes "1". Fix by post-processing these three fields.
148
+ let canonical = _canonicalizeValue(hashable);
149
+ for (const field of ["evidentialWeight", "priorPosterior", "updatedPosterior"]) {
150
+ const val = hashable[field];
151
+ if (typeof val === "number" && Number.isInteger(val)) {
152
+ canonical = canonical.replace(new RegExp(`"${field}":(-?\\d+)(?=[,}\\]])`), `"${field}":$1.0`);
153
+ }
154
+ }
155
+ return sha256(canonical);
156
+ }
157
+ /**
158
+ * Compute a hash over a canonical string.
159
+ */
160
+ export function hashCanonical(input) {
161
+ return sha256(input);
162
+ }
163
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Evidence Record Verifier
3
+ *
4
+ * Verifies the bookkeeping integrity of an evidence record WITHOUT
5
+ * containing any estimation mathematics.
6
+ *
7
+ * verify makes no network calls. Records never leave your machine.
8
+ */
9
+ import type { EvidenceRecord } from "../schema/evidence-record.js";
10
+ export interface Violation {
11
+ version: number;
12
+ category: "schema" | "version" | "timestamp" | "hash" | "posterior" | "compaction" | "enum";
13
+ message: string;
14
+ checkId: string;
15
+ }
16
+ export interface VerificationResult {
17
+ valid: boolean;
18
+ violations: Violation[];
19
+ entriesChecked: number;
20
+ }
21
+ export declare function verifyRecord(record: EvidenceRecord, timestampToleranceMs?: number): VerificationResult;
22
+ //# sourceMappingURL=verify-record.d.ts.map
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Evidence Record Verifier
3
+ *
4
+ * Verifies the bookkeeping integrity of an evidence record WITHOUT
5
+ * containing any estimation mathematics.
6
+ *
7
+ * verify makes no network calls. Records never leave your machine.
8
+ */
9
+ import { hashEntry } from "./hash.js";
10
+ const VALID_SCAFFOLD_TYPES = ["none", "socratic", "probing", "metacognitive", "scaffolding", "explain", "hint", "demonstrate"];
11
+ const VALID_DEPTH_LEVELS = ["surface", "conceptual", "transfer", "integration"];
12
+ const VALID_EPISTEMIC_MODES = ["experience", "inference", "analogy", "testimony"];
13
+ const VALID_TRIAGE_RESULTS = ["correct", "slip", "misconception", "disengagement", "ambiguous"];
14
+ function isValidISO8601(ts) {
15
+ const d = new Date(ts);
16
+ return !isNaN(d.getTime()) && ts.length > 0;
17
+ }
18
+ export function verifyRecord(record, timestampToleranceMs = 1000) {
19
+ const violations = [];
20
+ if (!record.studentScopeId || typeof record.studentScopeId !== "string") {
21
+ violations.push({ version: 0, category: "schema", message: "Missing or invalid studentScopeId", checkId: "SCHEMA.REQUIRED" });
22
+ }
23
+ if (!Array.isArray(record.entries)) {
24
+ violations.push({ version: 0, category: "schema", message: "entries must be an array", checkId: "SCHEMA.REQUIRED" });
25
+ return { valid: false, violations, entriesChecked: 0 };
26
+ }
27
+ // Compaction boundary
28
+ if (record.compactedBefore) {
29
+ if (typeof record.compactedBefore.version !== "number") {
30
+ violations.push({ version: 0, category: "compaction", message: "compactedBefore.version must be a number", checkId: "COMPACTION.BOUNDARY" });
31
+ }
32
+ if (typeof record.compactedBefore.summaryHash !== "string" || !record.compactedBefore.summaryHash) {
33
+ violations.push({ version: 0, category: "compaction", message: "compactedBefore.summaryHash must be a non-empty string", checkId: "COMPACTION.BOUNDARY" });
34
+ }
35
+ if (record.entries.length > 0 && record.entries[0].version <= record.compactedBefore.version) {
36
+ violations.push({
37
+ version: record.entries[0].version, category: "compaction",
38
+ message: `First entry version (${record.entries[0].version}) must be > compaction boundary (${record.compactedBefore.version})`,
39
+ checkId: "COMPACTION.BOUNDARY",
40
+ });
41
+ }
42
+ }
43
+ let prevVersion = record.compactedBefore?.version ?? 0;
44
+ let prevTimestamp = "";
45
+ let prevHash = record.compactedBefore?.summaryHash ?? "";
46
+ let lastUpdatedPosterior = null;
47
+ for (const entry of record.entries) {
48
+ const v = entry.version ?? -1;
49
+ // SCHEMA.REQUIRED
50
+ if (typeof v !== "number" || v < 1) {
51
+ violations.push({ version: v, category: "schema", message: "version must be a positive integer", checkId: "SCHEMA.REQUIRED" });
52
+ }
53
+ if (typeof entry.timestamp !== "string" || !entry.timestamp) {
54
+ violations.push({ version: v, category: "schema", message: "timestamp is required", checkId: "SCHEMA.REQUIRED" });
55
+ }
56
+ // SCHEMA.REQUIRED + SCHEMA.TYPES for numeric fields
57
+ for (const f of ["evidentialWeight", "priorPosterior", "updatedPosterior"]) {
58
+ if (!(f in entry)) {
59
+ violations.push({ version: v, category: "schema", message: `${f} must be a number`, checkId: "SCHEMA.REQUIRED" });
60
+ }
61
+ else if (typeof entry[f] !== "number") {
62
+ violations.push({ version: v, category: "schema", message: `${f} must be a number`, checkId: "SCHEMA.TYPES" });
63
+ }
64
+ }
65
+ // SCHEMA.TIMESTAMP_FORMAT
66
+ if (typeof entry.timestamp === "string" && entry.timestamp && !isValidISO8601(entry.timestamp)) {
67
+ violations.push({ version: v, category: "timestamp", message: `Timestamp is not valid ISO 8601: "${entry.timestamp}"`, checkId: "SCHEMA.TIMESTAMP_FORMAT" });
68
+ }
69
+ // VERSION.MONOTONIC
70
+ if (v <= prevVersion) {
71
+ violations.push({ version: v, category: "version", message: `Version ${v} <= previous ${prevVersion}`, checkId: "VERSION.MONOTONIC" });
72
+ }
73
+ // TIMESTAMP.MONOTONIC
74
+ if (prevTimestamp && entry.timestamp) {
75
+ const prevMs = new Date(prevTimestamp).getTime();
76
+ const currMs = new Date(entry.timestamp).getTime();
77
+ if (!isNaN(prevMs) && !isNaN(currMs) && currMs < prevMs - timestampToleranceMs) {
78
+ violations.push({ version: v, category: "timestamp", message: "Timestamp goes backward", checkId: "TIMESTAMP.MONOTONIC" });
79
+ }
80
+ }
81
+ // ENUM.VALID
82
+ if (entry.scaffoldType && !VALID_SCAFFOLD_TYPES.includes(entry.scaffoldType)) {
83
+ violations.push({ version: v, category: "enum", message: `Invalid scaffoldType: ${entry.scaffoldType}`, checkId: "ENUM.VALID" });
84
+ }
85
+ if (entry.depthLevel && !VALID_DEPTH_LEVELS.includes(entry.depthLevel)) {
86
+ violations.push({ version: v, category: "enum", message: `Invalid depthLevel: ${entry.depthLevel}`, checkId: "ENUM.VALID" });
87
+ }
88
+ if (entry.epistemicMode && !VALID_EPISTEMIC_MODES.includes(entry.epistemicMode)) {
89
+ violations.push({ version: v, category: "enum", message: `Invalid epistemicMode: ${entry.epistemicMode}`, checkId: "ENUM.VALID" });
90
+ }
91
+ if (entry.triageResult && !VALID_TRIAGE_RESULTS.includes(entry.triageResult)) {
92
+ violations.push({ version: v, category: "enum", message: `Invalid triageResult: ${entry.triageResult}`, checkId: "ENUM.VALID" });
93
+ }
94
+ // HASH.PREV_LINK
95
+ if (entry.prevHash !== prevHash) {
96
+ violations.push({ version: v, category: "hash", message: "prevHash mismatch", checkId: "HASH.PREV_LINK" });
97
+ }
98
+ // HASH.ENTRY_RECOMPUTE
99
+ const computedHash = hashEntry(entry);
100
+ if (entry.entryHash !== computedHash) {
101
+ violations.push({ version: v, category: "hash", message: "entryHash mismatch", checkId: "HASH.ENTRY_RECOMPUTE" });
102
+ }
103
+ // POSTERIOR.CHAIN
104
+ if (lastUpdatedPosterior !== null && typeof entry.priorPosterior === "number") {
105
+ const diff = Math.abs(entry.priorPosterior - lastUpdatedPosterior);
106
+ if (diff > 1e-9) {
107
+ violations.push({
108
+ version: v, category: "posterior",
109
+ message: `priorPosterior (${entry.priorPosterior}) != previous updatedPosterior (${lastUpdatedPosterior})`,
110
+ checkId: "POSTERIOR.CHAIN",
111
+ });
112
+ }
113
+ }
114
+ prevVersion = v;
115
+ prevTimestamp = entry.timestamp;
116
+ prevHash = entry.entryHash;
117
+ if (typeof entry.updatedPosterior === "number") {
118
+ lastUpdatedPosterior = entry.updatedPosterior;
119
+ }
120
+ }
121
+ return { valid: violations.length === 0, violations, entriesChecked: record.entries.length };
122
+ }
123
+ //# sourceMappingURL=verify-record.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Verifier for schema 0.3 records — two-level verification.
3
+ * Evidence chain + estimate chains + record-level consent/attestation checks.
4
+ */
5
+ export interface Violation03 {
6
+ version: number;
7
+ category: string;
8
+ message: string;
9
+ checkId: string;
10
+ chain: string;
11
+ }
12
+ export interface VerificationResult03 {
13
+ valid: boolean;
14
+ level: number;
15
+ violations: Violation03[];
16
+ entriesChecked: number;
17
+ estimators: Array<{
18
+ name: string;
19
+ version: string;
20
+ opaque: boolean;
21
+ }>;
22
+ }
23
+ export declare function verifyRecordV03(record: Record<string, any>, timestampToleranceMs?: number, estimateTolerance?: number): VerificationResult03;
24
+ //# sourceMappingURL=verifier-v03.d.ts.map
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Verifier for schema 0.3 records — two-level verification.
3
+ * Evidence chain + estimate chains + record-level consent/attestation checks.
4
+ */
5
+ import { createHash } from "crypto";
6
+ import { reproduce } from "./rules.js";
7
+ function canonicalize(obj) {
8
+ return JSON.stringify(obj, (_, v) => {
9
+ if (v && typeof v === "object" && !Array.isArray(v)) {
10
+ return Object.keys(v).sort().reduce((s, k) => {
11
+ s[k] = v[k];
12
+ return s;
13
+ }, {});
14
+ }
15
+ return v;
16
+ });
17
+ }
18
+ function sha256(s) {
19
+ return createHash("sha256").update(s).digest("hex");
20
+ }
21
+ const VALID_ST = new Set(["none", "socratic", "probing", "metacognitive", "scaffolding", "explain", "hint", "demonstrate"]);
22
+ const VALID_EM = new Set(["experience", "inference", "analogy", "testimony"]);
23
+ const VALID_CA = new Set(["granted", "withdrawn"]);
24
+ const VALID_CS = new Set(["instruction", "research", "export"]);
25
+ const VALID_TR = new Set(["correct", "slip", "misconception", "disengagement", "ambiguous"]);
26
+ const VALID_DL = new Set(["surface", "conceptual", "transfer", "integration"]);
27
+ const VALID_DT = new Set(["INSTRUCTIONAL", "REPORTING", "PLACEMENT", "CREDENTIAL"]);
28
+ export function verifyRecordV03(record, timestampToleranceMs = 1000, estimateTolerance = 1e-4) {
29
+ const violations = [];
30
+ const V = (v, cat, msg, id, chain) => violations.push({ version: v, category: cat, message: msg, checkId: id, chain });
31
+ if (!record.studentScopeId)
32
+ V(0, "schema", "Missing studentScopeId", "SCHEMA.REQUIRED", "record");
33
+ const evidence = record.evidence || {};
34
+ const evEntries = evidence.entries || [];
35
+ const estimates = record.estimates || [];
36
+ const attestations = record.attestations || [];
37
+ const exportBlock = record.export;
38
+ const level = estimates.length > 0 ? 2 : 1;
39
+ // Evidence chain
40
+ const evByVersion = {};
41
+ const evByHash = {};
42
+ const compacted = evidence.compactedBefore;
43
+ let prevV = compacted?.version ?? 0;
44
+ let prevH = compacted?.summaryHash ?? "";
45
+ let prevTs = "";
46
+ const consentState = {};
47
+ for (const entry of evEntries) {
48
+ const v = entry.version ?? -1;
49
+ const ch = "evidence";
50
+ for (const req of ["version", "timestamp", "eventHash", "event", "prevHash", "entryHash"]) {
51
+ if (!(req in entry))
52
+ V(v, "schema", `${req} is required`, "SCHEMA.REQUIRED", ch);
53
+ }
54
+ const ts = entry.timestamp || "";
55
+ const tsMs = ts ? new Date(ts).getTime() : NaN;
56
+ if (ts && isNaN(tsMs))
57
+ V(v, "timestamp", `Not valid ISO 8601: ${ts}`, "SCHEMA.TIMESTAMP_FORMAT", ch);
58
+ if (typeof v === "number" && v <= prevV)
59
+ V(v, "version", `Version ${v} <= previous ${prevV}`, "VERSION.MONOTONIC", ch);
60
+ if (prevTs && ts && !isNaN(tsMs)) {
61
+ const pm = new Date(prevTs).getTime();
62
+ if (!isNaN(pm) && tsMs < pm - timestampToleranceMs)
63
+ V(v, "timestamp", "Timestamp goes backward", "TIMESTAMP.MONOTONIC", ch);
64
+ }
65
+ const event = entry.event || {};
66
+ const kind = event.redacted ? "redacted" : (event.action && VALID_CA.has(event.action)) ? "consent" : "correct" in event ? "observation" : "unknown";
67
+ if (kind === "unknown")
68
+ V(v, "schema", "Event is not Observation, Consent, or Redacted", "SCHEMA.EVENT_UNION", ch);
69
+ if (kind === "observation") {
70
+ if (event.scaffoldType && !VALID_ST.has(event.scaffoldType))
71
+ V(v, "enum", `Invalid scaffoldType`, "ENUM.VALID", ch);
72
+ if (event.epistemicMode && !VALID_EM.has(event.epistemicMode))
73
+ V(v, "enum", `Invalid epistemicMode`, "ENUM.VALID", ch);
74
+ }
75
+ if (kind === "consent") {
76
+ if (!VALID_CA.has(event.action))
77
+ V(v, "enum", `Invalid consent action`, "ENUM.VALID", ch);
78
+ for (const s of event.scopes || []) {
79
+ if (!VALID_CS.has(s))
80
+ V(v, "enum", `Invalid consent scope: ${s}`, "ENUM.VALID", ch);
81
+ consentState[s] = event.action;
82
+ }
83
+ }
84
+ if (kind !== "redacted") {
85
+ const computedEvHash = sha256(canonicalize(event));
86
+ if (entry.eventHash !== computedEvHash)
87
+ V(v, "hash", "eventHash mismatch", "EVENT.COMMITMENT", ch);
88
+ }
89
+ if (entry.prevHash !== prevH)
90
+ V(v, "hash", "prevHash mismatch", "HASH.PREV_LINK", ch);
91
+ const entryHashable = { version: entry.version, timestamp: entry.timestamp, eventHash: entry.eventHash, prevHash: entry.prevHash };
92
+ const computedEntryHash = sha256(canonicalize(entryHashable));
93
+ if (entry.entryHash !== computedEntryHash)
94
+ V(v, "hash", "entryHash mismatch", "HASH.ENTRY_RECOMPUTE", ch);
95
+ evByVersion[v] = entry;
96
+ evByHash[entry.entryHash || ""] = entry;
97
+ if (typeof v === "number")
98
+ prevV = v;
99
+ prevTs = ts;
100
+ prevH = entry.entryHash || "";
101
+ }
102
+ // Estimate chains
103
+ const estimatorSummaries = [];
104
+ for (let idx = 0; idx < estimates.length; idx++) {
105
+ const estChain = estimates[idx];
106
+ const chainLabel = `estimate:${idx}`;
107
+ const estimator = estChain.estimator || {};
108
+ if (!estimator.name)
109
+ V(0, "schema", "Estimator name missing", "ESTIMATE.DECLARED", chainLabel);
110
+ const isOpaque = estimator.opaque !== false;
111
+ const ruleId = estimator.ruleId;
112
+ const defaultParams = estimator.params || {};
113
+ estimatorSummaries.push({ name: estimator.name || "unknown", version: estimator.version || "?", opaque: isOpaque });
114
+ const estEntries = estChain.entries || [];
115
+ let estPrevV = estChain.compactedBefore?.version ?? 0;
116
+ let estPrevH = estChain.compactedBefore?.summaryHash ?? "";
117
+ let lastUp = null;
118
+ let lastEvV = 0;
119
+ for (const entry of estEntries) {
120
+ const v = entry.version ?? -1;
121
+ if (typeof v === "number" && v <= estPrevV)
122
+ V(v, "version", `Version ${v} <= previous ${estPrevV}`, "VERSION.MONOTONIC", chainLabel);
123
+ if (entry.prevHash !== estPrevH)
124
+ V(v, "hash", "prevHash mismatch", "HASH.PREV_LINK", chainLabel);
125
+ const hashable = { ...entry };
126
+ delete hashable.entryHash;
127
+ const computed = sha256(canonicalize(hashable));
128
+ if (entry.entryHash !== computed)
129
+ V(v, "hash", "entryHash mismatch", "HASH.ENTRY_RECOMPUTE", chainLabel);
130
+ const evVer = entry.evidenceVersion;
131
+ if (evVer != null && !(evVer in evByVersion))
132
+ V(v, "schema", `evidenceVersion ${evVer} not in evidence`, "ESTIMATE.LINKS_EVIDENCE", chainLabel);
133
+ if (evVer != null && evVer <= lastEvV)
134
+ V(v, "version", `evidenceVersion ${evVer} <= previous ${lastEvV}`, "ESTIMATE.ORDER", chainLabel);
135
+ if (entry.triageResult && !VALID_TR.has(entry.triageResult))
136
+ V(v, "enum", `Invalid triageResult`, "ENUM.VALID", chainLabel);
137
+ if (entry.depthLevel && !VALID_DL.has(entry.depthLevel))
138
+ V(v, "enum", `Invalid depthLevel`, "ENUM.VALID", chainLabel);
139
+ if (entry.decisionTier && !VALID_DT.has(entry.decisionTier))
140
+ V(v, "enum", `Invalid decisionTier`, "ENUM.VALID", chainLabel);
141
+ const w = entry.evidentialWeight;
142
+ if (typeof w === "number" && (w < 0 || w > 1))
143
+ V(v, "schema", `Weight ${w} outside [0,1]`, "WEIGHT.RANGE", chainLabel);
144
+ for (const f of ["priorPosterior", "updatedPosterior"]) {
145
+ const pv = entry[f];
146
+ if (typeof pv === "number" && (pv <= 0 || pv >= 1))
147
+ V(v, "schema", `${f} ${pv} outside (0,1)`, "POSTERIOR.RANGE", chainLabel);
148
+ }
149
+ if (lastUp !== null && typeof entry.priorPosterior === "number" && Math.abs(entry.priorPosterior - lastUp) > 1e-9) {
150
+ V(v, "posterior", `priorPosterior (${entry.priorPosterior}) != previous updatedPosterior (${lastUp})`, "POSTERIOR.CHAIN", chainLabel);
151
+ }
152
+ if (!isOpaque && ruleId) {
153
+ const params = entry.params || defaultParams;
154
+ const evEntry = evByVersion[evVer] || {};
155
+ const evEvent = evEntry.event || {};
156
+ const correct = evEvent.correct;
157
+ if (correct !== undefined && typeof entry.priorPosterior === "number" && typeof w === "number") {
158
+ const reproduced = reproduce(ruleId, params, entry.priorPosterior, correct, w);
159
+ if (reproduced !== null && Math.abs(reproduced - entry.updatedPosterior) > estimateTolerance) {
160
+ V(v, "estimate", `Reproduced ${reproduced.toFixed(6)} != recorded ${entry.updatedPosterior.toFixed(6)}`, "ESTIMATE.REPRODUCE", chainLabel);
161
+ }
162
+ }
163
+ }
164
+ if (typeof v === "number")
165
+ estPrevV = v;
166
+ estPrevH = entry.entryHash || "";
167
+ if (typeof entry.updatedPosterior === "number")
168
+ lastUp = entry.updatedPosterior;
169
+ if (evVer != null)
170
+ lastEvV = evVer;
171
+ }
172
+ }
173
+ // Record-level
174
+ if (exportBlock) {
175
+ const scope = exportBlock.scope;
176
+ if (scope && consentState[scope] === "withdrawn") {
177
+ V(0, "consent", `Scope '${scope}' was withdrawn`, "CONSENT.WITHDRAWAL", "record");
178
+ }
179
+ else if (scope && !(scope in consentState)) {
180
+ V(0, "consent", `No consent for scope '${scope}'`, "CONSENT.SCOPE", "record");
181
+ }
182
+ }
183
+ for (const att of attestations) {
184
+ if (att.payload && att.payloadHash) {
185
+ const computed = sha256(canonicalize(att.payload));
186
+ if (computed !== att.payloadHash)
187
+ V(0, "hash", "Attestation payloadHash mismatch", "ATTEST.INTEGRITY", "record");
188
+ }
189
+ const covers = att.covers || {};
190
+ if (covers.throughEvidenceVersion != null && !(covers.throughEvidenceVersion in evByVersion)) {
191
+ V(0, "schema", `Attestation covers version ${covers.throughEvidenceVersion} not in evidence`, "ATTEST.COVERS", "record");
192
+ }
193
+ if (covers.evidenceEntryHash && !(covers.evidenceEntryHash in evByHash)) {
194
+ V(0, "hash", "Attestation covers hash not in evidence", "ATTEST.COVERS", "record");
195
+ }
196
+ }
197
+ return {
198
+ valid: violations.length === 0,
199
+ level,
200
+ violations,
201
+ entriesChecked: evEntries.length + estimates.reduce((s, e) => s + (e.entries?.length || 0), 0),
202
+ estimators: estimatorSummaries,
203
+ };
204
+ }
205
+ //# sourceMappingURL=verifier-v03.js.map
@@ -0,0 +1,29 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quantum Learning Machines
4
+
5
+ This license applies to the QLM-authored transformer, companion AI Use
6
+ profile, documentation, mapping templates and synthetic examples in this
7
+ project, except for the separately licensed material identified in NOTICE
8
+ and THIRD_PARTY_NOTICES.md. In particular, the bundled Internet-Draft texts
9
+ in spec/draft-srivastava-learnlog-00.txt and
10
+ spec/draft-srivastava-learnlog-01.txt are excluded from this MIT grant.
11
+ Dependencies retain their own upstream licenses.
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ // Type declaration for the vendored cross-source-analysis-data.mjs. Written for this package.
2
+ declare const data: Record<string, unknown>;
3
+ export default data;