pi-mega-compact 0.9.2 → 0.10.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.
- package/README.md +3 -2
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +95 -47
- package/dist/extensions/dashboard-server/api-contracts/index.js +1 -1
- package/dist/extensions/dashboard-server/api-contracts/turns.js +10 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
- package/dist/extensions/dashboard-server/routes-topics.js +87 -0
- package/dist/extensions/dashboard-server/routes-turns.js +251 -0
- package/dist/extensions/dashboard-server/routes.js +3 -1
- package/dist/extensions/dashboard-server/server.js +6 -2
- package/dist/extensions/mega-compact.js +10 -6
- package/dist/extensions/mega-config.js +4 -0
- package/dist/extensions/mega-events/agent-handlers.js +62 -49
- package/dist/extensions/mega-events/context-handler.js +48 -2
- package/dist/extensions/mega-events/session-handlers.js +24 -0
- package/dist/extensions/mega-metrics-cmds.js +90 -0
- package/dist/extensions/mega-pipeline/recall.js +47 -17
- package/dist/extensions/mega-topics-cmds.js +111 -0
- package/dist/extensions/mega-turn-store.js +110 -0
- package/dist/extensions/mega-turn-store.test.js +68 -0
- package/dist/src/config/turns.js +48 -0
- package/dist/src/fork.js +33 -0
- package/dist/src/fork.test.js +81 -0
- package/dist/src/intent.js +51 -0
- package/dist/src/intent.test.js +78 -0
- package/dist/src/metrics/index.js +4 -0
- package/dist/src/metrics/turns.js +118 -0
- package/dist/src/metrics/turns.test.js +152 -0
- package/dist/src/store/sqlite/raw-transcript.js +5 -3
- package/dist/src/store/sqlite.dbmirror.test.js +9 -0
- package/dist/src/store/turns/connection.js +123 -0
- package/dist/src/store/turns/contract-compliance.test.js +428 -0
- package/dist/src/store/turns/index.js +31 -0
- package/dist/src/store/turns/memory-store.js +357 -0
- package/dist/src/store/turns/memory-store.test.js +9 -0
- package/dist/src/store/turns/migrations.js +178 -0
- package/dist/src/store/turns/migrations.test.js +138 -0
- package/dist/src/store/turns/schema.js +115 -0
- package/dist/src/store/turns/sqlite-store.js +455 -0
- package/dist/src/store/turns/sqlite-store.test.js +144 -0
- package/dist/src/store/turns/types.js +16 -0
- package/dist/src/topics/cluster.js +141 -0
- package/dist/src/topics/cluster.test.js +150 -0
- package/dist/src/topics/index.js +6 -0
- package/dist/src/topics/kselection.js +142 -0
- package/dist/src/topics/labels.js +56 -0
- package/dist/src/topics/store.js +119 -0
- package/dist/src/topics/store.test.js +117 -0
- package/dist/src/topics/types.js +8 -0
- package/dist/src/wiki.js +133 -0
- package/dist/src/wiki.test.js +171 -0
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js → AchievementTiles-DUulvXUY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js.map → AchievementTiles-DUulvXUY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js → AchievementsTab-Dx0YtCNf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js.map → AchievementsTab-Dx0YtCNf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js → CacheTab-DtkaUo1v.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js.map → CacheTab-DtkaUo1v.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js → ConfigTab-CLa4KfiZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js.map → ConfigTab-CLa4KfiZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js → EventsTab-Q6tEIBBi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js.map → EventsTab-Q6tEIBBi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js → GameTab-Hq7GNWSY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js.map → GameTab-Hq7GNWSY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js → MetricsTab-BtmBV6lU.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js.map → MetricsTab-BtmBV6lU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js → OverviewTab-CL10PwVU.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js.map → OverviewTab-CL10PwVU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js → ReposTab-DAwANwbI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js.map → ReposTab-DAwANwbI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js → SessionsTab-Ec08TZRQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js.map → SessionsTab-Ec08TZRQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js → TimeSavedCard-COb8dpTy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js.map → TimeSavedCard-COb8dpTy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-kvX3F3dY.js +2 -0
- package/extensions/dashboard-client/dist/assets/TopicsTab-kvX3F3dY.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-CCUE73Aa.js +2 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-CCUE73Aa.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{index-D_WtU2TV.js → index-UMrussFX.js} +10 -10
- package/extensions/dashboard-client/dist/assets/index-UMrussFX.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{index-og3ghxyY.css → index-gn66lYD2.css} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js → useSSE-BDHZ7CeS.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js.map → useSSE-BDHZ7CeS.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/App.tsx +10 -2
- package/extensions/dashboard-client/src/api/client.ts +84 -1
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/turns.css +142 -0
- package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +185 -0
- package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +326 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +389 -291
- package/extensions/dashboard-server/api-contracts/game-types.ts +141 -88
- package/extensions/dashboard-server/api-contracts/index.ts +153 -133
- package/extensions/dashboard-server/api-contracts/turns.ts +114 -0
- package/extensions/dashboard-server/api-contracts.test.ts +920 -919
- package/extensions/dashboard-server/routes-topics.ts +119 -0
- package/extensions/dashboard-server/routes-turns.ts +282 -0
- package/extensions/dashboard-server/routes.ts +8 -1
- package/extensions/dashboard-server/server.ts +6 -2
- package/extensions/mega-compact.ts +75 -59
- package/extensions/mega-config.ts +232 -220
- package/extensions/mega-events/agent-handlers.ts +194 -151
- package/extensions/mega-events/context-handler.ts +50 -1
- package/extensions/mega-events/session-handlers.ts +23 -0
- package/extensions/mega-metrics-cmds.ts +114 -0
- package/extensions/mega-pipeline/recall.ts +215 -152
- package/extensions/mega-topics-cmds.ts +151 -0
- package/extensions/mega-turn-store.test.ts +108 -0
- package/extensions/mega-turn-store.ts +169 -0
- package/package.json +3 -1
- package/src/config/turns.ts +69 -0
- package/src/fork.test.ts +94 -0
- package/src/fork.ts +78 -0
- package/src/intent.test.ts +87 -0
- package/src/intent.ts +115 -0
- package/src/metrics/index.ts +9 -0
- package/src/metrics/turns.test.ts +165 -0
- package/src/metrics/turns.ts +201 -0
- package/src/store/sqlite/raw-transcript.ts +9 -3
- package/src/store/sqlite.dbmirror.test.ts +10 -0
- package/src/store/turns/connection.ts +137 -0
- package/src/store/turns/contract-compliance.test.ts +562 -0
- package/src/store/turns/index.ts +65 -0
- package/src/store/turns/memory-store.test.ts +15 -0
- package/src/store/turns/memory-store.ts +469 -0
- package/src/store/turns/migrations.test.ts +161 -0
- package/src/store/turns/migrations.ts +203 -0
- package/src/store/turns/schema.ts +158 -0
- package/src/store/turns/sqlite-store.test.ts +161 -0
- package/src/store/turns/sqlite-store.ts +656 -0
- package/src/store/turns/types.ts +181 -0
- package/src/topics/cluster.test.ts +176 -0
- package/src/topics/cluster.ts +191 -0
- package/src/topics/index.ts +18 -0
- package/src/topics/kselection.ts +182 -0
- package/src/topics/labels.ts +67 -0
- package/src/topics/store.test.ts +132 -0
- package/src/topics/store.ts +208 -0
- package/src/topics/types.ts +64 -0
- package/src/wiki.test.ts +217 -0
- package/src/wiki.ts +198 -0
- package/dist/extensions/openclaw-mega-compact.js +0 -291
- package/dist/src/minilm.js +0 -92
- package/dist/src/wordpiece.js +0 -129
- package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +0 -1
package/src/wiki.test.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wiki.test.ts — S51C extractive wiki page tests. No network; temp dirs.
|
|
3
|
+
* Includes the honest-boundary grep-assert (no ollama/llm/fetch) required by s47/s51.
|
|
4
|
+
*/
|
|
5
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { mkdtempSync, rmSync, readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { extractiveSummary, generateWikiPage, buildWikiIndex } from "./wiki.js";
|
|
12
|
+
import type { Topic, TopicAssignment } from "./topics/types.js";
|
|
13
|
+
|
|
14
|
+
let tmpDir: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-wiki-"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function mkTopic(
|
|
25
|
+
id: string,
|
|
26
|
+
label: string,
|
|
27
|
+
termScores: Array<{ term: string; score: number }>,
|
|
28
|
+
): Topic {
|
|
29
|
+
return { id, label, termScores, memoryCount: 0, lastUpdated: 0 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mkAssignment(
|
|
33
|
+
memId: string,
|
|
34
|
+
topicId: string,
|
|
35
|
+
confidence = 0.8,
|
|
36
|
+
): TopicAssignment {
|
|
37
|
+
return {
|
|
38
|
+
memoryId: memId,
|
|
39
|
+
sessionId: "s",
|
|
40
|
+
topicId,
|
|
41
|
+
confidence,
|
|
42
|
+
assignedAt: 0,
|
|
43
|
+
method: "kmeans+tfidf",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test("extractiveSummary returns top sentences joined", () => {
|
|
48
|
+
const topic = mkTopic("t1", "sqlite", [{ term: "sqlite", score: 1 }]);
|
|
49
|
+
const assignments = [mkAssignment("a", "t1"), mkAssignment("b", "t1")];
|
|
50
|
+
const getContent = (id: string): string => {
|
|
51
|
+
if (id === "a")
|
|
52
|
+
return "SQLite uses WAL mode. SQLite uses WAL mode. SQLite uses WAL mode.";
|
|
53
|
+
if (id === "b") return "Postgres uses WAL mode. Postgres uses WAL mode.";
|
|
54
|
+
return "";
|
|
55
|
+
};
|
|
56
|
+
const summary = extractiveSummary(topic, assignments, getContent);
|
|
57
|
+
// Should be non-empty and contain at least one sentence
|
|
58
|
+
assert.ok(summary.length > 0);
|
|
59
|
+
assert.ok(summary.includes("WAL"));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("extractiveSummary returns empty string when topic has no members", () => {
|
|
63
|
+
const topic = mkTopic("t1", "empty", []);
|
|
64
|
+
const summary = extractiveSummary(topic, [], () => "");
|
|
65
|
+
assert.equal(summary, "");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("generateWikiPage produces page with summary + key/recent memories", () => {
|
|
69
|
+
const topic = mkTopic("t1", "sqlite", [{ term: "sqlite", score: 1 }]);
|
|
70
|
+
const assignments = [
|
|
71
|
+
mkAssignment("a", "t1", 0.9),
|
|
72
|
+
mkAssignment("b", "t1", 0.5),
|
|
73
|
+
];
|
|
74
|
+
const getMemoryInfo = (
|
|
75
|
+
id: string,
|
|
76
|
+
): { content: string; timestamp: number; importance: number } | null => {
|
|
77
|
+
if (id === "a")
|
|
78
|
+
return {
|
|
79
|
+
content: "First memory content",
|
|
80
|
+
timestamp: 100,
|
|
81
|
+
importance: 0.9,
|
|
82
|
+
};
|
|
83
|
+
if (id === "b")
|
|
84
|
+
return {
|
|
85
|
+
content: "Second memory content",
|
|
86
|
+
timestamp: 200,
|
|
87
|
+
importance: 0.5,
|
|
88
|
+
};
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
const allTopics = [topic];
|
|
92
|
+
|
|
93
|
+
const page = generateWikiPage(topic, assignments, getMemoryInfo, allTopics);
|
|
94
|
+
assert.equal(page.topic.id, "t1");
|
|
95
|
+
assert.equal(page.keyMemories.length, 2);
|
|
96
|
+
// Key memories sorted by importance DESC
|
|
97
|
+
assert.equal(page.keyMemories[0].importance, 0.9);
|
|
98
|
+
// Recent memories sorted by timestamp DESC
|
|
99
|
+
assert.equal(page.recentMemories[0].timestamp, 200);
|
|
100
|
+
assert.equal(page.relatedTopics.length, 0);
|
|
101
|
+
assert.ok(page.generatedAt > 0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("generateWikiPage finds related topics via co-occurrence", () => {
|
|
105
|
+
const topicA = mkTopic("A", "topic-a", []);
|
|
106
|
+
const topicB = mkTopic("B", "topic-b", []);
|
|
107
|
+
const topicC = mkTopic("C", "topic-c", []);
|
|
108
|
+
|
|
109
|
+
// Memory "shared" appears in both topicA and topicB → A and B are related.
|
|
110
|
+
// Memory "a-only" only in topicA.
|
|
111
|
+
// Memory "c-only" only in topicC (no overlap with A).
|
|
112
|
+
const assignments = [
|
|
113
|
+
mkAssignment("shared", "A"),
|
|
114
|
+
mkAssignment("shared", "B"),
|
|
115
|
+
mkAssignment("a-only", "A"),
|
|
116
|
+
mkAssignment("c-only", "C"),
|
|
117
|
+
];
|
|
118
|
+
const getMemoryInfo = (
|
|
119
|
+
id: string,
|
|
120
|
+
): { content: string; timestamp: number; importance: number } | null => ({
|
|
121
|
+
content: `content for ${id}`,
|
|
122
|
+
timestamp: 100,
|
|
123
|
+
importance: 0.5,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const page = generateWikiPage(topicA, assignments, getMemoryInfo, [
|
|
127
|
+
topicA,
|
|
128
|
+
topicB,
|
|
129
|
+
topicC,
|
|
130
|
+
]);
|
|
131
|
+
// topicB should be related (shares "shared" with topicA); topicC should not.
|
|
132
|
+
const relatedIds = page.relatedTopics.map((t) => t.id);
|
|
133
|
+
assert.ok(
|
|
134
|
+
relatedIds.includes("B"),
|
|
135
|
+
`expected B in related topics, got ${relatedIds.join(",")}`,
|
|
136
|
+
);
|
|
137
|
+
assert.ok(
|
|
138
|
+
!relatedIds.includes("C"),
|
|
139
|
+
`C has no co-occurrence with A, should not be related`,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("buildWikiIndex aggregates topics + memory counts", () => {
|
|
144
|
+
const topics = [mkTopic("t1", "sqlite", []), mkTopic("t2", "wal", [])];
|
|
145
|
+
topics[0].memoryCount = 5;
|
|
146
|
+
topics[1].memoryCount = 3;
|
|
147
|
+
topics[0].lastUpdated = 1000;
|
|
148
|
+
topics[1].lastUpdated = 2000;
|
|
149
|
+
|
|
150
|
+
const idx = buildWikiIndex(topics);
|
|
151
|
+
assert.equal(idx.totalTopics, 2);
|
|
152
|
+
assert.equal(idx.totalMemories, 8);
|
|
153
|
+
assert.equal(idx.topics.length, 2);
|
|
154
|
+
assert.ok(idx.lastRebuildAt !== null);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("buildWikiIndex handles empty topics", () => {
|
|
158
|
+
const idx = buildWikiIndex([]);
|
|
159
|
+
assert.equal(idx.totalTopics, 0);
|
|
160
|
+
assert.equal(idx.totalMemories, 0);
|
|
161
|
+
assert.equal(idx.lastRebuildAt, null);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("honest boundary: no LLM/network/keyword-literals in src/wiki.ts", () => {
|
|
165
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
166
|
+
const wikiPath = join(here, "..", "..", "src", "wiki.ts");
|
|
167
|
+
const text = readFileSync(wikiPath, "utf8");
|
|
168
|
+
const banned = [/ollama/i, /\bllm\b/i, /\bfetch\s*\(/, /\bhybrid\b/i];
|
|
169
|
+
const fabricated = [
|
|
170
|
+
"graphql",
|
|
171
|
+
"docker",
|
|
172
|
+
"stacktrace",
|
|
173
|
+
"bottleneck",
|
|
174
|
+
"trade-off",
|
|
175
|
+
];
|
|
176
|
+
for (const re of banned) {
|
|
177
|
+
assert.ok(
|
|
178
|
+
!re.test(text),
|
|
179
|
+
`wiki.ts must not contain ${re} (PREVENT-PI-004 / no-model boundary)`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
for (const word of fabricated) {
|
|
183
|
+
assert.ok(
|
|
184
|
+
!text.toLowerCase().includes(`"${word}"`),
|
|
185
|
+
`wiki.ts must not hardcode fabricated taxonomy term "${word}"`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("honest boundary: no LLM/Ollama/network in src/topics/* (non-test)", () => {
|
|
191
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
192
|
+
// dist/src/wiki.test.js → ../../src/topics
|
|
193
|
+
const topicsDir = join(here, "..", "..", "src", "topics");
|
|
194
|
+
const files = readdirSync(topicsDir).filter(
|
|
195
|
+
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts"),
|
|
196
|
+
);
|
|
197
|
+
const banned = [/ollama/i, /\bllm\b/i, /\bfetch\s*\(/, /\bhybrid\b/i];
|
|
198
|
+
const fabricated = [
|
|
199
|
+
"graphql",
|
|
200
|
+
"docker",
|
|
201
|
+
"stacktrace",
|
|
202
|
+
"bottleneck",
|
|
203
|
+
"trade-off",
|
|
204
|
+
];
|
|
205
|
+
for (const f of files) {
|
|
206
|
+
const text = readFileSync(join(topicsDir, f), "utf8");
|
|
207
|
+
for (const re of banned) {
|
|
208
|
+
assert.ok(!re.test(text), `${f} must not contain ${re}`);
|
|
209
|
+
}
|
|
210
|
+
for (const word of fabricated) {
|
|
211
|
+
assert.ok(
|
|
212
|
+
!text.toLowerCase().includes(`"${word}"`),
|
|
213
|
+
`${f} must not hardcode fabricated taxonomy term "${word}"`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
package/src/wiki.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wiki.ts — S51C extractive wiki page generation from topic models.
|
|
3
|
+
*
|
|
4
|
+
* Host-agnostic (no pi imports). Pure local math + string processing
|
|
5
|
+
* (PREVENT-PI-004); read-only over the topic store + memory data. No
|
|
6
|
+
* model/network calls.
|
|
7
|
+
*
|
|
8
|
+
* Extractive summary uses a local TF-IDF sentence ranker (advances the dedup
|
|
9
|
+
* `src/dedup/raptor` patterns).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Topic, TopicAssignment, EmbeddedChunk } from "./topics/types.js";
|
|
13
|
+
import { tfidfScores } from "./topics/labels.js";
|
|
14
|
+
|
|
15
|
+
/** A wiki page for a single topic. */
|
|
16
|
+
export interface WikiPage {
|
|
17
|
+
topic: Topic;
|
|
18
|
+
/** Extractive summary: top-scoring sentences from topic members, in original order. */
|
|
19
|
+
summary: string;
|
|
20
|
+
/** Memories with importance for the key-memories panel. */
|
|
21
|
+
keyMemories: Array<{
|
|
22
|
+
content: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
importance: number;
|
|
25
|
+
}>;
|
|
26
|
+
/** Recently added or used memories (ordered by timestamp DESC). */
|
|
27
|
+
recentMemories: Array<{
|
|
28
|
+
content: string;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
importance: number;
|
|
31
|
+
}>;
|
|
32
|
+
/** Topics that share members with this one (co-occurrence), limited to top few. */
|
|
33
|
+
relatedTopics: Topic[];
|
|
34
|
+
/** Generated at epoch ms. */
|
|
35
|
+
generatedAt: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Index shown on the wiki landing page. */
|
|
39
|
+
export interface WikiIndex {
|
|
40
|
+
topics: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
memoryCount: number;
|
|
44
|
+
lastUpdated: number;
|
|
45
|
+
}>;
|
|
46
|
+
totalTopics: number;
|
|
47
|
+
totalMemories: number;
|
|
48
|
+
lastRebuildAt: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Split text into sentences (very simple: period + newline + question mark + exclamation). */
|
|
52
|
+
function splitSentences(text: string): string[] {
|
|
53
|
+
return text
|
|
54
|
+
.split(/[.\n!?]/)
|
|
55
|
+
.map((s) => s.trim())
|
|
56
|
+
.filter((s) => s.length > 10);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Wrap a text string as a pseudo EmbeddedChunk for tfidfScores reuse. */
|
|
60
|
+
function pseudoChunk(text: string): EmbeddedChunk {
|
|
61
|
+
return { chunkId: "", sessionId: "", vec: [], text };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface MemoryContent {
|
|
65
|
+
content: string;
|
|
66
|
+
timestamp: number;
|
|
67
|
+
importance: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate an extractive summary for a topic. Takes the topic's member texts,
|
|
72
|
+
* ranks sentences by TF-IDF, and returns the top-scoring ones joined.
|
|
73
|
+
* Pure local math; no model/network calls.
|
|
74
|
+
*/
|
|
75
|
+
export function extractiveSummary(
|
|
76
|
+
topic: Topic,
|
|
77
|
+
assignments: TopicAssignment[],
|
|
78
|
+
getContent: (memId: string) => string,
|
|
79
|
+
): string {
|
|
80
|
+
// Join all member texts for the corpus.
|
|
81
|
+
const members: string[] = [];
|
|
82
|
+
for (const a of assignments) {
|
|
83
|
+
if (a.topicId === topic.id) {
|
|
84
|
+
const txt = getContent(a.memoryId);
|
|
85
|
+
if (txt) members.push(txt);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const corpusText = members.join(" ");
|
|
89
|
+
if (corpusText.length === 0) return "";
|
|
90
|
+
// Extractively summarize: rank sentences by TF-IDF, pick top 3.
|
|
91
|
+
const sentences = splitSentences(corpusText);
|
|
92
|
+
if (sentences.length === 0) return "";
|
|
93
|
+
|
|
94
|
+
// Reuse tfidfScores from labels.ts: each sentence is a pseudo-chunk scored
|
|
95
|
+
// against the full corpus. Sum the top-N term scores for each sentence.
|
|
96
|
+
const corpusChunks = [pseudoChunk(corpusText)];
|
|
97
|
+
const scored = sentences.map((s, i) => {
|
|
98
|
+
const sentScores = tfidfScores([pseudoChunk(s)], corpusChunks);
|
|
99
|
+
const totalScore = sentScores.reduce((sum, ts) => sum + ts.score, 0);
|
|
100
|
+
return { text: s, score: totalScore, idx: i };
|
|
101
|
+
});
|
|
102
|
+
scored.sort((a, b) => b.score - a.score || a.idx - b.idx);
|
|
103
|
+
const top = Math.min(3, sentences.length);
|
|
104
|
+
const chosen = scored.slice(0, top).sort((a, b) => a.idx - b.idx);
|
|
105
|
+
return chosen.map((s) => s.text).join(". ");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Find other topics (not `selfId`) that a memory also belongs to. Returns
|
|
110
|
+
* distinct topic ids where the memory has assignments for both `selfId` and
|
|
111
|
+
* that other topic.
|
|
112
|
+
*/
|
|
113
|
+
function coTopicsForMemory(
|
|
114
|
+
memId: string,
|
|
115
|
+
selfId: string,
|
|
116
|
+
allAssignments: TopicAssignment[],
|
|
117
|
+
): string[] {
|
|
118
|
+
const memAssignments = allAssignments.filter((a) => a.memoryId === memId);
|
|
119
|
+
const topicSet = new Set(memAssignments.map((a) => a.topicId));
|
|
120
|
+
// Memory must belong to selfId + at least one other for co-occurrence.
|
|
121
|
+
if (!topicSet.has(selfId)) return [];
|
|
122
|
+
topicSet.delete(selfId);
|
|
123
|
+
return [...topicSet];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate a full wiki page for a topic.
|
|
128
|
+
*
|
|
129
|
+
* @param topic The topic to render.
|
|
130
|
+
* @param assignments All topic assignments to pull member memories.
|
|
131
|
+
* @param getMemoryInfo Function returning content/timestamp/importance for a memoryId.
|
|
132
|
+
* @param allTopics All topics (for co-occurrence computation).
|
|
133
|
+
*/
|
|
134
|
+
export function generateWikiPage(
|
|
135
|
+
topic: Topic,
|
|
136
|
+
assignments: TopicAssignment[],
|
|
137
|
+
getMemoryInfo: (memId: string) => MemoryContent | null,
|
|
138
|
+
allTopics: Topic[],
|
|
139
|
+
): WikiPage {
|
|
140
|
+
const topicAssignments = assignments.filter((a) => a.topicId === topic.id);
|
|
141
|
+
|
|
142
|
+
const keyMemories = topicAssignments
|
|
143
|
+
.map((a) => getMemoryInfo(a.memoryId))
|
|
144
|
+
.filter((m): m is MemoryContent => m !== null)
|
|
145
|
+
.sort((a, b) => b.importance - a.importance)
|
|
146
|
+
.slice(0, 10);
|
|
147
|
+
|
|
148
|
+
const recentMemories = topicAssignments
|
|
149
|
+
.map((a) => getMemoryInfo(a.memoryId))
|
|
150
|
+
.filter((m): m is MemoryContent => m !== null)
|
|
151
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
152
|
+
.slice(0, 5);
|
|
153
|
+
|
|
154
|
+
// Co-occurrence: scan ALL assignments (not just this topic's) and count how
|
|
155
|
+
// many memories also belong to each other topic. Topics sharing member ids
|
|
156
|
+
// with this one are "related". A memory that belongs to topicA + topicB is
|
|
157
|
+
// evidence that A and B are related.
|
|
158
|
+
const topicIdCount = new Map<string, number>();
|
|
159
|
+
for (const a of assignments) {
|
|
160
|
+
for (const other of coTopicsForMemory(a.memoryId, topic.id, assignments)) {
|
|
161
|
+
topicIdCount.set(other, (topicIdCount.get(other) ?? 0) + 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const relatedTopics: Topic[] = [];
|
|
165
|
+
const ordered = [...topicIdCount.entries()].sort((a, b) => b[1] - a[1]);
|
|
166
|
+
for (const [tid] of ordered.slice(0, 3)) {
|
|
167
|
+
const t = allTopics.find((x) => x.id === tid);
|
|
168
|
+
if (t) relatedTopics.push(t);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const getContent = (memId: string): string =>
|
|
172
|
+
getMemoryInfo(memId)?.content ?? "";
|
|
173
|
+
const summary = extractiveSummary(topic, assignments, getContent);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
topic,
|
|
177
|
+
summary,
|
|
178
|
+
keyMemories,
|
|
179
|
+
recentMemories,
|
|
180
|
+
relatedTopics,
|
|
181
|
+
generatedAt: Date.now(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Build the wiki index for the landing page. */
|
|
186
|
+
export function buildWikiIndex(topics: Topic[]): WikiIndex {
|
|
187
|
+
return {
|
|
188
|
+
topics: topics.map((t) => ({
|
|
189
|
+
id: t.id,
|
|
190
|
+
label: t.label,
|
|
191
|
+
memoryCount: t.memoryCount,
|
|
192
|
+
lastUpdated: t.lastUpdated,
|
|
193
|
+
})),
|
|
194
|
+
totalTopics: topics.length,
|
|
195
|
+
totalMemories: topics.reduce((s, t) => s + t.memoryCount, 0),
|
|
196
|
+
lastRebuildAt: topics.length > 0 ? topics[0].lastUpdated : null,
|
|
197
|
+
};
|
|
198
|
+
}
|