pi-mega-compact 0.17.0 → 0.17.1

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 (99) hide show
  1. package/dist/config/vector-cortex.js +10 -0
  2. package/dist/dedup/digest.js +30 -0
  3. package/dist/dedup/l1-lsh-v2.js +60 -0
  4. package/dist/dedup/l1-lsh.js +52 -0
  5. package/dist/dedup/l1-minhash-v2.js +193 -0
  6. package/dist/dedup/l1-minhash.js +91 -0
  7. package/dist/dedup/l1-verify.js +54 -0
  8. package/dist/dedup/mmr.js +45 -0
  9. package/dist/dedup/normalize.js +39 -0
  10. package/dist/dedup/raptor/buildHistory.js +164 -0
  11. package/dist/dedup/raptor/guardrails.js +83 -0
  12. package/dist/dedup/raptor/incremental.js +304 -0
  13. package/dist/dedup/raptor/index.js +190 -0
  14. package/dist/dedup/raptor/kmeans.js +152 -0
  15. package/dist/dedup/raptor/multilevel.js +184 -0
  16. package/dist/dedup/raptor/retrieval.js +93 -0
  17. package/dist/dedup/raptor/summarizer.js +86 -0
  18. package/dist/dedup/raptor/tree.js +202 -0
  19. package/dist/dedup/topk.js +60 -0
  20. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
  21. package/dist/src/config/vector-cortex.js +10 -0
  22. package/dist/src/config.js +1 -1
  23. package/dist/src/dedup/l1-lsh-v2.js +60 -0
  24. package/dist/src/dedup/l1-minhash-v2.js +193 -0
  25. package/dist/src/dedup/normalize.js +1 -1
  26. package/dist/src/vector-cortex/conformance/emit.js +46 -0
  27. package/dist/src/vector-cortex/conformance/manifest.js +235 -0
  28. package/dist/src/vector-cortex/conformance/runner.js +111 -0
  29. package/dist/src/vector-cortex/conformance/triadB-reader.js +95 -0
  30. package/dist/src/vector-cortex/migrations/minhash-v2.js +209 -0
  31. package/dist/vector-cortex/conformance/emit.js +46 -0
  32. package/dist/vector-cortex/conformance/manifest.js +235 -0
  33. package/dist/vector-cortex/conformance/runner.js +111 -0
  34. package/dist/vector-cortex/conformance/triadB-reader.js +95 -0
  35. package/dist/vector-cortex/migrations/minhash-v2.js +209 -0
  36. package/extensions/dashboard-client/dist/assets/{AreaChart-CAnWomhw.js → AreaChart-BQXFF7UI.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{AreaChart-CAnWomhw.js.map → AreaChart-BQXFF7UI.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{BarChart-lZ2-s71k.js → BarChart-BVtoz9Eg.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{BarChart-lZ2-s71k.js.map → BarChart-BVtoz9Eg.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{CacheTab-BL2Thh66.js → CacheTab-trI2TIGy.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{CacheTab-BL2Thh66.js.map → CacheTab-trI2TIGy.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{EventsTab-DEh2Z9fp.js → EventsTab-CNUbk9__.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{EventsTab-DEh2Z9fp.js.map → EventsTab-CNUbk9__.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{HealthTab-DTwKclxk.js → HealthTab-K-bd_CM-.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{HealthTab-DTwKclxk.js.map → HealthTab-K-bd_CM-.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DDfBEdbX.js → MaintenanceTab-CoEioKzx.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DDfBEdbX.js.map → MaintenanceTab-CoEioKzx.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dgfmg-0Q.js → MemoryMapTab-CJo24ulv.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dgfmg-0Q.js.map → MemoryMapTab-CJo24ulv.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{MetricsTab-DOcMm0Tx.js → MetricsTab-DCNlQWWP.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{MetricsTab-DOcMm0Tx.js.map → MetricsTab-DCNlQWWP.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{OverviewTab-DHT5aoHg.js → OverviewTab-CQQlyIQ6.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{OverviewTab-DHT5aoHg.js.map → OverviewTab-CQQlyIQ6.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{ReposTab-CGAmtR_c.js → ReposTab-Cqm82_yn.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{ReposTab-CGAmtR_c.js.map → ReposTab-Cqm82_yn.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{SessionsTab-BIgx6iKT.js → SessionsTab-A8xTAdET.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{SessionsTab-BIgx6iKT.js.map → SessionsTab-A8xTAdET.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{SetupTab-ADghKuEI.js → SetupTab-CqaSgaZl.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{SetupTab-ADghKuEI.js.map → SetupTab-CqaSgaZl.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C33UiiOM.js → TimeSavedCard-C_f3q8GR.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C33UiiOM.js.map → TimeSavedCard-C_f3q8GR.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{TurnsTab-DdbL9wPN.js → TurnsTab-BEsafACo.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{TurnsTab-DdbL9wPN.js.map → TurnsTab-BEsafACo.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js +2 -0
  65. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js.map +1 -0
  66. package/extensions/dashboard-client/dist/assets/{WikiTab-Ch00OFKs.js → WikiTab-Bf01M2_Z.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{WikiTab-Ch00OFKs.js.map → WikiTab-Bf01M2_Z.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/{button-ByeEdo2g.js → button-BEJPTk_E.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{button-ByeEdo2g.js.map → button-BEJPTk_E.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{card-DesoLXvc.js → card-DK1V382s.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{card-DesoLXvc.js.map → card-DK1V382s.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CCAB7Iz4.js → generateCategoricalChart-CRtaPphL.js} +2 -2
  73. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CCAB7Iz4.js.map → generateCategoricalChart-CRtaPphL.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/{index-B6cgyAPr.js → index-CB6BslE4.js} +3 -3
  75. package/extensions/dashboard-client/dist/assets/{index-B6cgyAPr.js.map → index-CB6BslE4.js.map} +1 -1
  76. package/extensions/dashboard-client/dist/assets/{switch-DIZC-oe-.js → switch-C4FV6B9k.js} +2 -2
  77. package/extensions/dashboard-client/dist/assets/{switch-DIZC-oe-.js.map → switch-C4FV6B9k.js.map} +1 -1
  78. package/extensions/dashboard-client/dist/assets/{toggle-CIoWaH2e.js → toggle-GBmeENMd.js} +2 -2
  79. package/extensions/dashboard-client/dist/assets/{toggle-CIoWaH2e.js.map → toggle-GBmeENMd.js.map} +1 -1
  80. package/extensions/dashboard-client/dist/assets/{useSSE-Bv4943l1.js → useSSE-BOKZGSn1.js} +2 -2
  81. package/extensions/dashboard-client/dist/assets/{useSSE-Bv4943l1.js.map → useSSE-BOKZGSn1.js.map} +1 -1
  82. package/extensions/dashboard-client/dist/index.html +1 -1
  83. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +1 -1
  84. package/extensions/dashboard-client/tsconfig.json +1 -1
  85. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
  86. package/package.json +1 -1
  87. package/src/config/vector-cortex.ts +11 -0
  88. package/src/config.ts +1 -0
  89. package/src/dedup/l1-lsh-v2.ts +78 -0
  90. package/src/dedup/l1-minhash-v2.ts +221 -0
  91. package/src/dedup/normalize.ts +1 -1
  92. package/src/store/sqlite/game-scores.ts +2 -1
  93. package/src/vector-cortex/conformance/emit.ts +56 -0
  94. package/src/vector-cortex/conformance/manifest.ts +298 -0
  95. package/src/vector-cortex/conformance/runner.ts +165 -0
  96. package/src/vector-cortex/conformance/triadB-reader.ts +110 -0
  97. package/src/vector-cortex/migrations/minhash-v2.ts +263 -0
  98. package/extensions/dashboard-client/dist/assets/VectorCortexTab-Cl1pbEdR.js +0 -2
  99. package/extensions/dashboard-client/dist/assets/VectorCortexTab-Cl1pbEdR.js.map +0 -1
@@ -0,0 +1,86 @@
1
+ /**
2
+ * summarizer.ts — per-cluster summary for the RAPTOR tree (Sprint 13, Phase 6).
3
+ *
4
+ * Default: pure extractive (deterministic, zero network, zero model) reusing
5
+ * extractive.ts. Optional: a LOCAL Ollama model (llama3.2:3b by default) when
6
+ * MEGACOMPACT_RAPTOR_MODEL is set — localhost only, same PREVENT-PI-004
7
+ * exception class as HttpEmbedder/the dashboard. No remote API is ever called.
8
+ *
9
+ * The summarizer returns structured text + a token estimate. Faithfulness is
10
+ * enforced downstream by guardrails.ts — this module only produces candidates.
11
+ */
12
+ import { extractiveSummarize } from "../../extractive.js";
13
+ import { estimateBlockTokens } from "../../tokens.js";
14
+ import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: localhost-only user-spawned Ollama server (BYO local model, never remote)
15
+ /** The local Ollama endpoint (loopback). Read lazily so tests can avoid it. */
16
+ function ollamaEndpoint() {
17
+ const model = process.env.MEGACOMPACT_RAPTOR_MODEL;
18
+ if (!model)
19
+ return null;
20
+ const base = process.env.MEGACOMPACT_RAPTOR_URL ?? "http://127.0.0.1:11434"; // guardrails-allow PREVENT-PI-004: loopback-only Ollama endpoint for local RAPTOR summarization
21
+ // Guard: only loopback is permitted (remote Ollama would violate PREVENT-PI-004).
22
+ if (!/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/.test(base)) {
23
+ throw new Error(`MEGACOMPACT_RAPTOR_URL must be localhost/127.0.0.1 (got ${base}). ` +
24
+ `Remote Ollama is not allowed (PREVENT-PI-004).`);
25
+ }
26
+ return { url: `${base}/api/generate`, model };
27
+ }
28
+ /**
29
+ * Extractive summarization of a cluster's source messages. Deterministic and
30
+ * fully local — the on-by-default path.
31
+ */
32
+ export function extractiveClusterSummary(messages) {
33
+ const s = extractiveSummarize(messages);
34
+ return { summary: s.topicSummary, tokenEstimate: s.tokenEstimate };
35
+ }
36
+ /**
37
+ * Build a summary for one cluster of source messages.
38
+ *
39
+ * Uses local Ollama when MEGACOMPACT_RAPTOR_MODEL is set (localhost-only);
40
+ * otherwise falls back to deterministic extractive. The `fetch` is a localhost
41
+ * call inside the PREVENT-PI-004 exception — annotated accordingly.
42
+ */
43
+ export function summarizeCluster(messages) {
44
+ const ollama = ollamaEndpoint();
45
+ if (!ollama)
46
+ return extractiveClusterSummary(messages);
47
+ return ollamaSummarize(messages, ollama);
48
+ }
49
+ function ollamaSummarize(messages, ollama) {
50
+ // The fetch below is localhost-only (loopback Ollama) — the PREVENT-PI-004
51
+ // sanctioned local-model exceptions (same class as /dashboard, HttpEmbedder).
52
+ const prompt = messages.map((m) => `${m.role}: ${m.text}`).join("\n");
53
+ // Synchronous bridge: spawnSync an inline worker so the call blocks without
54
+ // deadlocking fetch (mirrors HttpEmbedder — Atomics.wait on main thread would
55
+ // hang). A blocked main thread cannot pump the socket.
56
+ const WORKER = String.raw `
57
+ const url = process.env.R_URL, model = process.env.R_MODEL, prompt = process.env.R_PROMPT;
58
+ try {
59
+ const r = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model, prompt, stream: false }) }); // guardrails-allow PREVENT-PI-004: localhost-only user-spawned Ollama server (BYO local model, never remote)
60
+ const j = await r.json();
61
+ process.stdout.write(JSON.stringify({ ok: r.ok, text: j.response || "" }));
62
+ } catch (e) {
63
+ process.stdout.write(JSON.stringify({ ok: false, error: String(e && e.message ? e.message : e) }));
64
+ }
65
+ `;
66
+ const res = spawnSync(process.execPath, ["-e", WORKER], {
67
+ encoding: "utf8",
68
+ timeout: 30_000,
69
+ env: { ...process.env, R_URL: ollama.url, R_MODEL: ollama.model, R_PROMPT: prompt },
70
+ });
71
+ let parsed = { ok: false, error: "no response" };
72
+ if (typeof res.stdout === "string" && res.stdout.length > 0) {
73
+ try {
74
+ parsed = JSON.parse(res.stdout);
75
+ }
76
+ catch {
77
+ parsed = { ok: false, error: "bad json" };
78
+ }
79
+ }
80
+ if (!parsed.ok || !parsed.text) {
81
+ // Ollama unavailable → deterministic extractive fallback (never fail the build).
82
+ return extractiveClusterSummary(messages);
83
+ }
84
+ const summary = parsed.text.trim();
85
+ return { summary, tokenEstimate: estimateBlockTokens(summary) };
86
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * tree.ts — RAPTOR hierarchical summary-tree builder (Sprint 13, Phase 6).
3
+ *
4
+ * Builds a multi-level tree of summary nodes over leaf chunks: leaves are the
5
+ * original regions; each higher level summarizes clusters of the level below
6
+ * until a single root remains. QA ops: a wall-clock budget guard — on exhaustion
7
+ * we build an extractive fallback root. <10 leaves → a single summary node.
8
+ *
9
+ * Node model (kept simple + flat): every RaptorNode stores the LIST OF LEAF IDS
10
+ * it ultimately covers in `children` (not a mix of node/leaf ids). So the node
11
+ * map holds ONLY internal summary nodes — never per-leaf wrappers — which is
12
+ * what makes RAPTOR consolidate (nodes.size << leaves) and makes retrieval's
13
+ * leaf walk trivial.
14
+ *
15
+ * PREVENT-PI-004: no network here. summarizeCluster() may call a localhost
16
+ * Ollama (annotated in summarizer.ts); extractive is the default.
17
+ */
18
+ import { kmeanspp, meanVector } from "./kmeans.js";
19
+ import { summarizeCluster } from "./summarizer.js";
20
+ import { applyHallucinationGuardrails, sourceTokenSet } from "./guardrails.js";
21
+ const DEFAULT_BUDGET_MS = 5000;
22
+ const DEFAULT_CLUSTERS = 5;
23
+ function defaultNextId(level, index) {
24
+ return `r${level}_${index}`;
25
+ }
26
+ function summarizeInto(item, centroid, embedder, consistencyThreshold) {
27
+ let summary = summarizeCluster(item.messages);
28
+ const guard = applyHallucinationGuardrails({
29
+ summary: summary.summary,
30
+ sources: item.sources,
31
+ centroid,
32
+ embedder,
33
+ sourceTokens: sourceTokenSet(item.sources),
34
+ consistencyThreshold,
35
+ });
36
+ if (guard.marker === "extractive_fallback") {
37
+ summary = summarizeCluster(item.messages); // deterministic extractive text
38
+ }
39
+ return {
40
+ summary: summary.summary,
41
+ tokenEstimate: summary.tokenEstimate,
42
+ qualityMarker: guard.marker === "extractive_fallback" ? "low" : guard.marker,
43
+ };
44
+ }
45
+ /**
46
+ * Build a RAPTOR tree from leaf chunks. Synchronous; guarded by an elapsed-time
47
+ * budget. Returns a tree whose `nodes` map holds ONLY internal summary nodes.
48
+ */
49
+ export function buildRaptorTree(leaves, opts) {
50
+ const embedder = opts.embedder;
51
+ const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
52
+ const clustersPerLevel = opts.clustersPerLevel ?? DEFAULT_CLUSTERS;
53
+ const nextId = opts.nextId ?? defaultNextId;
54
+ const now = opts.now ?? (() => Date.now());
55
+ const start = now();
56
+ const within = () => now() - start <= budgetMs;
57
+ const nodes = new Map();
58
+ // <10 leaves → single summary root (no hierarchy needed).
59
+ if (leaves.length < 10) {
60
+ const item = {
61
+ id: "root",
62
+ parentId: null,
63
+ embedding: meanVector(leaves.map((l) => l.embedding)),
64
+ leafIds: leaves.map((l) => l.id),
65
+ messages: leaves.flatMap((l) => l.messages),
66
+ sources: leaves.map((l) => l.sourceText),
67
+ };
68
+ const centroid = item.embedding;
69
+ const { summary, tokenEstimate, qualityMarker } = summarizeInto(item, centroid, embedder, opts.consistencyThreshold);
70
+ const rootId = nextId(0, 0);
71
+ nodes.set(rootId, {
72
+ id: rootId,
73
+ level: 0,
74
+ parentId: null,
75
+ children: item.leafIds,
76
+ summary,
77
+ embedding: centroid,
78
+ qualityMarker,
79
+ tokenEstimate,
80
+ });
81
+ return { nodes, rootId, levels: 1, timedOut: false };
82
+ }
83
+ let currentLevel = leaves.map((l) => ({
84
+ id: l.id,
85
+ parentId: null,
86
+ embedding: l.embedding,
87
+ leafIds: [l.id],
88
+ messages: l.messages,
89
+ sources: [l.sourceText],
90
+ }));
91
+ let level = 0;
92
+ while (currentLevel.length > 1) {
93
+ if (!within())
94
+ return extractiveFallbackRoot(leaves, nodes, nextId);
95
+ // Once we're down to a handful of items, collapse them all into one root.
96
+ // (k === currentLevel.length would make every item its own singleton
97
+ // cluster and never shrink — an infinite loop until the budget blows.)
98
+ if (currentLevel.length <= clustersPerLevel) {
99
+ const merged = {
100
+ id: "merge",
101
+ parentId: null,
102
+ embedding: meanVector(currentLevel.map((c) => c.embedding)),
103
+ leafIds: currentLevel.flatMap((c) => c.leafIds),
104
+ messages: currentLevel.flatMap((c) => c.messages),
105
+ sources: currentLevel.flatMap((c) => c.sources),
106
+ };
107
+ const centroid = merged.embedding;
108
+ const { summary, tokenEstimate, qualityMarker } = summarizeInto(merged, centroid, embedder, opts.consistencyThreshold);
109
+ const rootId = nextId(level + 1, 0);
110
+ nodes.set(rootId, {
111
+ id: rootId,
112
+ level: level + 1,
113
+ parentId: null,
114
+ children: merged.leafIds,
115
+ summary,
116
+ embedding: centroid,
117
+ qualityMarker,
118
+ tokenEstimate,
119
+ });
120
+ // Populate parentId for the internal nodes being merged into this root.
121
+ // currentLevel items with ids in `nodes` are internal summary nodes (level
122
+ // >= 1); raw leaf ids are not in `nodes` and are correctly skipped.
123
+ for (const c of currentLevel) {
124
+ const child = nodes.get(c.id);
125
+ if (child && child.id !== rootId && child.parentId === null) {
126
+ child.parentId = rootId;
127
+ }
128
+ }
129
+ return { nodes, rootId, levels: level + 2, timedOut: false };
130
+ }
131
+ const k = Math.max(1, Math.min(clustersPerLevel, currentLevel.length));
132
+ const clustered = kmeanspp(currentLevel.map((c) => c.embedding), k, { seed: 0x1234 + level });
133
+ const groups = Array.from({ length: clustered.k }, () => []);
134
+ clustered.assignments.forEach((c, i) => groups[c].push(currentLevel[i]));
135
+ const nextLevel = [];
136
+ for (let g = 0; g < groups.length; g++) {
137
+ const group = groups[g];
138
+ if (group.length === 0)
139
+ continue;
140
+ const merged = {
141
+ id: nextId(level + 1, g),
142
+ parentId: null,
143
+ embedding: clustered.centroids[g],
144
+ leafIds: group.flatMap((c) => c.leafIds),
145
+ messages: group.flatMap((c) => c.messages),
146
+ sources: group.flatMap((c) => c.sources),
147
+ };
148
+ const { summary, tokenEstimate, qualityMarker } = summarizeInto(merged, merged.embedding, embedder, opts.consistencyThreshold);
149
+ nodes.set(merged.id, {
150
+ id: merged.id,
151
+ level: level + 1,
152
+ parentId: null,
153
+ children: merged.leafIds,
154
+ summary,
155
+ embedding: merged.embedding,
156
+ qualityMarker,
157
+ tokenEstimate,
158
+ });
159
+ // Populate parentId for the internal nodes being absorbed into this
160
+ // parent summary. Group members with ids in `nodes` are internal summary
161
+ // nodes (level >= 1); raw leaf ids are not in `nodes` (per-leaf wrappers
162
+ // are intentionally absent) and are correctly skipped — leaf→summary
163
+ // walks go through the parent's `children` list instead.
164
+ for (const c of group) {
165
+ const child = nodes.get(c.id);
166
+ if (child && child.id !== merged.id) {
167
+ child.parentId = merged.id;
168
+ }
169
+ }
170
+ nextLevel.push(merged);
171
+ }
172
+ currentLevel = nextLevel;
173
+ level++;
174
+ }
175
+ const root = currentLevel[0];
176
+ const rootId = root ? root.id : null;
177
+ return {
178
+ nodes,
179
+ rootId,
180
+ levels: level + 1,
181
+ timedOut: false,
182
+ };
183
+ }
184
+ /**
185
+ * Budget-exceeded fallback: build a single deterministic extractive root over
186
+ * all leaves and mark it low quality. Keeps a valid (if shallow) tree.
187
+ */
188
+ function extractiveFallbackRoot(leaves, nodes, nextId) {
189
+ const summary = summarizeCluster(leaves.flatMap((l) => l.messages));
190
+ const rootId = nextId(99, 0);
191
+ nodes.set(rootId, {
192
+ id: rootId,
193
+ level: 99,
194
+ parentId: null,
195
+ children: leaves.map((l) => l.id),
196
+ summary: summary.summary,
197
+ embedding: meanVector(leaves.map((l) => l.embedding)),
198
+ qualityMarker: "low",
199
+ tokenEstimate: summary.tokenEstimate,
200
+ });
201
+ return { nodes, rootId, levels: 100, timedOut: true };
202
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * topk.ts — min-heap based top-K selection (Sprint 12, QA #4).
3
+ *
4
+ * Replaces the O(N log N) full `.sort()` in search() with an O(N log k) heap,
5
+ * which matters once a session holds thousands of checkpoints. Generic over any
6
+ * scored item with a numeric `score`.
7
+ *
8
+ * Pure, no deps, no network (PREVENT-PI-004).
9
+ */
10
+ /**
11
+ * Return the `k` highest-scoring items (stable insertion order on ties).
12
+ * O(N log k) — a bounded min-heap of size k.
13
+ */
14
+ export function topK(items, k) {
15
+ if (k <= 0)
16
+ return [];
17
+ if (items.length <= k)
18
+ return [...items].sort((a, b) => b.score - a.score);
19
+ // Min-heap of the current top-k, stored as a flat array of Scored<T>.
20
+ const heap = [];
21
+ const push = (e) => {
22
+ heap.push(e);
23
+ let i = heap.length - 1;
24
+ while (i > 0) {
25
+ const parent = (i - 1) >> 1;
26
+ if (heap[parent].score <= heap[i].score)
27
+ break;
28
+ [heap[parent], heap[i]] = [heap[i], heap[parent]];
29
+ i = parent;
30
+ }
31
+ };
32
+ const siftDown = (start) => {
33
+ let i = start;
34
+ for (;;) {
35
+ const l = 2 * i + 1;
36
+ const r = 2 * i + 2;
37
+ let smallest = i;
38
+ if (l < heap.length && heap[l].score < heap[smallest].score)
39
+ smallest = l;
40
+ if (r < heap.length && heap[r].score < heap[smallest].score)
41
+ smallest = r;
42
+ if (smallest === i)
43
+ break;
44
+ [heap[smallest], heap[i]] = [heap[i], heap[smallest]];
45
+ i = smallest;
46
+ }
47
+ };
48
+ for (const it of items) {
49
+ if (heap.length < k) {
50
+ push(it);
51
+ }
52
+ else if (it.score > heap[0].score) {
53
+ // Replace the current minimum with this better-scoring item, then sift
54
+ // it down to restore the min-heap invariant.
55
+ heap[0] = it;
56
+ siftDown(0);
57
+ }
58
+ }
59
+ return heap.sort((a, b) => b.score - a.score);
60
+ }
@@ -137,6 +137,7 @@ export const SETTINGS = [
137
137
  boolDirect("MEGACOMPACT_VC1A", "VC1A Canonical Byte Events", "EventV2 byte-authority ledger codec (original bytes + SHA-256, strict UTF-8, derived NFC) and canonical validator (EVT_DIGEST_MISMATCH / EVT_UTF8_TAG_INVALID / EVT_DUPLICATE_ID). OFF = mode C, transcript codec unchanged, byte-identical.", true),
138
138
  boolDirect("MEGACOMPACT_VC1B", "VC1B Occurrence Ledger + Tool Identity", "Neutral occurrence ledger (LedgerReader/Writer/Admin + CompatJournalV1): per-session monotonic seq, tool result references one earlier call, uniqueness by (eventId,digest) only, and the M2 copy-validate-switch downgrade journal. OFF = mode C, ledger unwritten, byte-identical.", true),
139
139
  boolDirect("MEGACOMPACT_VC0C", "VC0C Live Safety Envelope", "TriadResult/Breaker live circuit breaker (60s window, 20 attempts, 30s cooldown, 3 probes, 5min healthy residence) + durable spool before provider invocation; manual reset clears cooldown but never evidence. OFF = mode C, unchanged transcript, byte-identical.", true),
140
+ boolDirect("MEGACOMPACT_VC1C", "VC1C Cross-Language Conformance v2", "FixtureManifestV2 canonical manifest validator + DowngradeReport deterministic downgrade export + MinHashV2 exact big-integer signatures and the M4 copy/validate/switch minhash-v2 migration (seed table frozen, cross-language byte-exact). OFF = mode C, v1 sync dedup scan unchanged, byte-identical.", true),
140
141
  ],
141
142
  },
142
143
  {
@@ -53,6 +53,16 @@ export const VC1B_ENABLED = () => sprintFlag("MEGACOMPACT_VC1B");
53
53
  * consumer is the resilience emit seam + the safety adapter's triad selection.
54
54
  */
55
55
  export const VC0C_ENABLED = () => sprintFlag("MEGACOMPACT_VC0C");
56
+ /**
57
+ * VC1C — cross-language conformance v2 (FixtureManifestV2 / DowngradeReport /
58
+ * MinHashV2 + M4 minhash-v2 migration).
59
+ * Default ON. `MEGACOMPACT_VC1C=0` disables and is byte-identical to the
60
+ * predecessor (mode C: a v2 conformance runner that accepts authority fixtures
61
+ * and the manifest validator idle; the sync dedup scan stays on the v1 path;
62
+ * zero `vector_cortex_*` VC1C emissions). The real consumers are the conformance
63
+ * emit seam, the minhash-v2 backfill seam and the downgrade-export seam.
64
+ */
65
+ export const VC1C_ENABLED = () => sprintFlag("MEGACOMPACT_VC1C");
56
66
  // ---------------------------------------------------------------------------
57
67
  // Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
58
68
  // Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
114
114
  // default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
115
115
  // so root consumers share one source of truth.
116
116
  // ---------------------------------------------------------------------------
117
- export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
117
+ export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
@@ -0,0 +1,60 @@
1
+ /**
2
+ * l1-lsh-v2.ts — Locality-Sensitive Hashing banding over MinHashV2 signatures
3
+ * (VC1C, M4 minhash-v2).
4
+ *
5
+ * Splits the frozen 2048-byte v2 signature (256 x u64 LE slots) into 64 bands
6
+ * of FOUR u64 values each. Each band yields a deterministic bucket key derived
7
+ * from its 4 u64 little-endian bytes (32 bytes) hashed with a 64-bit FNV-1a,
8
+ * scoped by session id and the frozen v2 version tag so buckets never collide
9
+ * across sessions or the v1 path. Deterministic: same (sessionId,
10
+ * signatureBytes) -> same 64 bucket keys, every run, every language.
11
+ *
12
+ * Mixed v1/v2 comparison and bucket mixing is REJECTED upstream with
13
+ * `MINHASH_VERSION_MISMATCH` (see minhash-v2 migration); this module only ever
14
+ * bands a v2 signature.
15
+ *
16
+ * Pure compute, no deps, no storage, no network (PREVENT-PI-004 / PREVENT-011).
17
+ */
18
+ import { MINHASH_VERSION, NUM_HASHES_V2, U64_BYTES, } from "./l1-minhash-v2.js";
19
+ /** Number of bands (frozen). */
20
+ export const BANDS_V2 = 64;
21
+ /** u64 slots per band (frozen): 64 * 4 = 256 total slots. */
22
+ export const VALUES_PER_BAND_V2 = 4;
23
+ /** Band byte length: 4 u64 LE = 32 bytes. */
24
+ export const BAND_BYTES_V2 = VALUES_PER_BAND_V2 * U64_BYTES;
25
+ /** Total signature byte length this scheme expects (256 * 8 = 2048). */
26
+ export const SIGNATURE_BYTES_EXPECTED_V2 = NUM_HASHES_V2 * U64_BYTES;
27
+ const MASK64 = 0xffffffffffffffffn;
28
+ /** 64-bit FNV-1a over a byte buffer. */
29
+ function fnv1a64Bytes(buf) {
30
+ let h = 0xcbf29ce484222325n;
31
+ for (let i = 0; i < buf.length; i++) {
32
+ h ^= BigInt(buf[i] ?? 0);
33
+ h = (h * 0x100000001b3n) & MASK64;
34
+ }
35
+ return h;
36
+ }
37
+ /**
38
+ * Compute the 64 LSH bucket keys for a v2 signature within a session. `bytes`
39
+ * is the 2048-byte little-endian signature (see encodeSignatureV2). Each bucket
40
+ * key is the hex of an 8-byte little-endian FNV-1a-64 over the band's 32 bytes
41
+ * prefixed by `<sessionId>|v2|band-` — so the "bucket bytes" are deterministic
42
+ * and reproducible across languages.
43
+ */
44
+ export function lshBandsV2(bytes, sessionId, version = MINHASH_VERSION) {
45
+ if (bytes.length !== SIGNATURE_BYTES_EXPECTED_V2) {
46
+ throw new Error(`lshBandsV2: expected ${SIGNATURE_BYTES_EXPECTED_V2} signature bytes, got ${bytes.length}`);
47
+ }
48
+ const keys = [];
49
+ const prefix = Buffer.from(`${sessionId}|v${version}|`, "utf8");
50
+ for (let band = 0; band < BANDS_V2; band++) {
51
+ const start = band * BAND_BYTES_V2;
52
+ const bandBytes = bytes.slice(start, start + BAND_BYTES_V2);
53
+ const combined = Buffer.concat([prefix, Buffer.from(bandBytes)]);
54
+ const h = fnv1a64Bytes(combined);
55
+ const out = Buffer.allocUnsafe(U64_BYTES);
56
+ out.writeBigUInt64LE(h, 0);
57
+ keys.push(`b${band}:${out.toString("hex")}`);
58
+ }
59
+ return keys;
60
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * l1-minhash-v2.ts — MinHash V2 signatures (VC1C, M4 minhash-v2).
3
+ *
4
+ * The v1 path (l1-minhash.ts) uses 32-bit universal hashing with `p=2^31-1`
5
+ * and Number math, which is exact because a*x < 2^62 can overflow double
6
+ * precision (>= 2^53) and is reduced too eagerly. MinHashV2 freezes a v2
7
+ * scheme that is CROSS-LANGUAGE deterministic and byte-exact:
8
+ *
9
+ * - normalization: the stable `normalize()` from normalize.ts (NFC, NFKC
10
+ * case-fold, CRLF->LF, whitespace collapse, 32K cap);
11
+ * - shingles: windows of FIVE CODE POINTS over the normalized text
12
+ * (SHINGLE_CP=5), each shingle hashed to a u64 with 64-bit FNV-1a over its
13
+ * UTF-8 bytes;
14
+ * - 256 PUBLISHED unsigned (a_i, b_i) seed pairs, derived deterministically
15
+ * via splitmix64 and listed in
16
+ * conformance/vector-cortex/v2/minhash/seeds-v2.json;
17
+ * - p = 2^61 - 1 (2305843009213693951), EXACT BigInt multiply/modulo — a_i*x
18
+ * can reach ~2^124 so Number math would silently corrupt high-bit products;
19
+ * - signature = 256 slots, slot i = min over shingles of (a_i*x + b_i) mod p;
20
+ * the empty signature is all-`p` sentinel slots;
21
+ * - encoding: 256 x u64 LITTLE-ENDIAN bytes (2048 bytes total);
22
+ * - bands: 64 bands of FOUR u64 slots each (256 total), see l1-lsh-v2.ts.
23
+ *
24
+ * NEVER compares a v2 signature against a v1 signature — cross-version compare
25
+ * is rejected with `MINHASH_VERSION_MISMATCH` (see minhash-v2 migration).
26
+ *
27
+ * Pure compute, no deps, no storage, no network (PREVENT-PI-004 / PREVENT-011).
28
+ */
29
+ import { normalize } from "./normalize.js";
30
+ /** MinHashV2 scheme version tag (frozen). */
31
+ export const MINHASH_VERSION = 2;
32
+ /** The published count of slots / seed pairs (frozen). */
33
+ export const NUM_HASHES_V2 = 256;
34
+ /** Five-code-point shingle length (frozen). */
35
+ export const SHINGLE_CP = 5;
36
+ /** p = 2^61 - 1 (Mersenne prime), as exact BigInt. */
37
+ export const P_V2 = (1n << 61n) - 1n;
38
+ /** Little-endian bytes per u64 signature slot. */
39
+ export const U64_BYTES = 8;
40
+ /** Signature byte length: 256 slots x 8 LE bytes. */
41
+ export const SIGNATURE_BYTES_V2 = NUM_HASHES_V2 * U64_BYTES;
42
+ /** Fixed splitmix64 initial state so the seed table is reproducible everywhere. */
43
+ export const SEED_STATE_INIT = 0x5eedc0def001ba11n & 0xffffffffffffffffn;
44
+ /** splitmix64 mixing constants (frozen). */
45
+ const SM_GOLDEN = 0x9e3779b97f4a7c15n;
46
+ const SM_M1 = 0xbf58476d1ce4e5b9n;
47
+ const SM_M2 = 0x94d049bb133111ebn;
48
+ const MASK64 = 0xffffffffffffffffn;
49
+ function mix(z0) {
50
+ let z = z0;
51
+ z = (z ^ (z >> 30n)) * SM_M1;
52
+ z &= MASK64;
53
+ z = (z ^ (z >> 27n)) * SM_M2;
54
+ z &= MASK64;
55
+ return z ^ (z >> 31n);
56
+ }
57
+ /** Create a splitmix64 PRNG from a fixed unsigned 64-bit state. */
58
+ export function splitmix64(state = SEED_STATE_INIT) {
59
+ let s = state & MASK64;
60
+ return () => {
61
+ s = (s + SM_GOLDEN) & MASK64;
62
+ return mix(s) & MASK64;
63
+ };
64
+ }
65
+ /**
66
+ * Derive the frozen 256 published (a_i, b_i) seed pairs. Each pair maps two
67
+ * splitmix64 draws into [1, p-1]. Deterministic and cross-language reproducible
68
+ * from SEED_STATE_INIT — mirrors `seeds-v2.json`.
69
+ *
70
+ * The table is frozen: computed ONCE at module load and reused for every call.
71
+ * Recomputing the 256 splitmix64 pairs (and re-walking all shingles) per
72
+ * signature/backfill row is wasteful for a large v1 index backfill, and the
73
+ * table is immutable by construction, so a single shared instance is safe and
74
+ * byte-identical. The returned sub-arrays share the frozen pair objects; they
75
+ * are themselves frozen so no caller can mutate the shared cache.
76
+ */
77
+ export function minhashV2Seeds(count = NUM_HASHES_V2) {
78
+ return FROZEN_SEEDS.slice(0, count);
79
+ }
80
+ /** Singleton seed table, computed exactly once and frozen (see minhashV2Seeds). */
81
+ const FROZEN_SEEDS = (() => {
82
+ const prng = splitmix64();
83
+ const pairs = [];
84
+ for (let i = 0; i < NUM_HASHES_V2; i++) {
85
+ const a = (prng() % (P_V2 - 1n)) + 1n;
86
+ const b = (prng() % (P_V2 - 1n)) + 1n;
87
+ pairs.push(Object.freeze({ a, b }));
88
+ }
89
+ return Object.freeze(pairs);
90
+ })();
91
+ /** 64-bit FNV-1a over a UTF-8 string (for shingle hashing). */
92
+ function fnv1a64String(s) {
93
+ let h = 0xcbf29ce484222325n;
94
+ const bytes = Buffer.from(s, "utf8");
95
+ for (let i = 0; i < bytes.length; i++) {
96
+ h ^= BigInt(bytes[i] ?? 0);
97
+ h = (h * 0x100000001b3n) & MASK64;
98
+ }
99
+ return h;
100
+ }
101
+ /**
102
+ * Five-code-point shingle set (deduped, capped) of normalized text as u64
103
+ * hashes. Iterates over code points (not UTF-16 code units) so astral-plane
104
+ * characters do not split a shingle.
105
+ */
106
+ export function shinglesV2(text, shingleCp = SHINGLE_CP, maxShingles = 50_000) {
107
+ const norm = normalize(text);
108
+ if (norm.length === 0)
109
+ return [];
110
+ const cps = Array.from(norm); // code points
111
+ const set = new Set();
112
+ if (cps.length < shingleCp) {
113
+ set.add(fnv1a64String(norm));
114
+ }
115
+ else {
116
+ for (let i = 0; i + shingleCp <= cps.length; i++) {
117
+ const sh = cps.slice(i, i + shingleCp).join("");
118
+ set.add(fnv1a64String(sh));
119
+ if (set.size >= maxShingles)
120
+ break;
121
+ }
122
+ }
123
+ return [...set];
124
+ }
125
+ /**
126
+ * Compute the 256-slot MinHashV2 signature of a text as an array of exact u64
127
+ * BigInt values (each in [0, p], p = sentinel when empty). Uses EXACT BigInt
128
+ * multiply/modulo so high-bit products (a*x ~ up to 2^124) are never corrupted.
129
+ */
130
+ export function minhashV2Signature(text) {
131
+ const grams = shinglesV2(text);
132
+ const seeds = minhashV2Seeds();
133
+ const sig = new Array(NUM_HASHES_V2).fill(P_V2);
134
+ if (grams.length === 0)
135
+ return sig;
136
+ for (let i = 0; i < NUM_HASHES_V2; i++) {
137
+ const { a, b } = seeds[i];
138
+ let min = P_V2;
139
+ for (const x of grams) {
140
+ const h = (a * x + b) % P_V2;
141
+ if (h < min)
142
+ min = h;
143
+ }
144
+ sig[i] = min;
145
+ }
146
+ return sig;
147
+ }
148
+ /** Encode a 256-slot signature to 2048 bytes as 256 x u64 little-endian. */
149
+ export function encodeSignatureV2(sig) {
150
+ if (sig.length !== NUM_HASHES_V2) {
151
+ throw new Error(`encodeSignatureV2: expected ${NUM_HASHES_V2} slots, got ${sig.length}`);
152
+ }
153
+ const buf = Buffer.allocUnsafe(SIGNATURE_BYTES_V2);
154
+ for (let i = 0; i < NUM_HASHES_V2; i++) {
155
+ buf.writeBigUInt64LE(sig[i], i * U64_BYTES);
156
+ }
157
+ return new Uint8Array(buf);
158
+ }
159
+ /**
160
+ * Estimated Jaccard similarity of two v2 signatures (fraction of equal slots).
161
+ *
162
+ * THIS IS v2-ONLY. Cross-version compare is rejected INTERNALLY with
163
+ * `MINHASH_VERSION_MISMATCH`: both inputs must be exact 256-slot v2 signatures,
164
+ * i.e. every slot a `bigint` in [0, P_V2] (the empty sentinel is exactly P_V2).
165
+ * A v1 signature is a `number[]` of u31 values, so passing a v1 array fails the
166
+ * bigint type/range check and is rejected — a mixed v1/v2 compare can never
167
+ * produce a similarity number. Never call this with v1 arrays.
168
+ */
169
+ export function signatureSimilarityV2(a, b) {
170
+ assertV2Signature(a);
171
+ assertV2Signature(b);
172
+ const n = Math.min(a.length, b.length);
173
+ if (n === 0)
174
+ return 0;
175
+ let equal = 0;
176
+ for (let i = 0; i < n; i++)
177
+ if (a[i] === b[i])
178
+ equal++;
179
+ return equal / n;
180
+ }
181
+ /** Reject any array that is not an exact 256-slot v2 signature (bigint in [0, P_V2]). */
182
+ function assertV2Signature(sig) {
183
+ if (sig.length !== NUM_HASHES_V2) {
184
+ throw new Error(MINHASH_VERSION_MISMATCH_MSG);
185
+ }
186
+ for (const s of sig) {
187
+ if (typeof s !== "bigint" || s < 0n || s > P_V2) {
188
+ throw new Error(MINHASH_VERSION_MISMATCH_MSG);
189
+ }
190
+ }
191
+ }
192
+ /** Frozen cross-version-reject message shared by the v2-only function guards. */
193
+ const MINHASH_VERSION_MISMATCH_MSG = "MINHASH_VERSION_MISMATCH";
@@ -30,7 +30,7 @@ export function normalize(text) {
30
30
  return "";
31
31
  let out = stripAnsi(text);
32
32
  out = out.normalize("NFC");
33
- out = out.toLocaleLowerCase(); // case-fold so "Foo"/"FOO" collapse to one key
33
+ out = out.toLowerCase(); // case-fold so "Foo"/"FOO" collapse to one key (locale-independent: byte-exact across languages)
34
34
  out = out.replace(/\r\n?/g, "\n"); // CRLF / CR → LF
35
35
  out = out.replace(/\s+/g, " ").trim();
36
36
  if (out.length > MAX_CHARS)