pi-mega-compact 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +264 -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 +25 -1
- 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-DZp_xQOW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js.map → AchievementTiles-DZp_xQOW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js → AchievementsTab-BzFHpAHM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js.map → AchievementsTab-BzFHpAHM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js → CacheTab-DEQbTq2T.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js.map → CacheTab-DEQbTq2T.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js → ConfigTab-DQ4FG33J.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js.map → ConfigTab-DQ4FG33J.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js → EventsTab-D5HoEVYo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js.map → EventsTab-D5HoEVYo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js → GameTab-Jzsc4yeY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js.map → GameTab-Jzsc4yeY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js → MetricsTab-D_XnsEYX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js.map → MetricsTab-D_XnsEYX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js → OverviewTab-BMREvAth.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js.map → OverviewTab-BMREvAth.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js → ReposTab-DuyhOktG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js.map → ReposTab-DuyhOktG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js → SessionsTab-mz_vz8i2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js.map → SessionsTab-mz_vz8i2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js → TimeSavedCard-B8AIjNTY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js.map → TimeSavedCard-B8AIjNTY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-DKuct4oD.js +2 -0
- package/extensions/dashboard-client/dist/assets/TopicsTab-DKuct4oD.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-11MGLxY8.js +2 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-11MGLxY8.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{index-D_WtU2TV.js → index-CqLN7STl.js} +10 -10
- package/extensions/dashboard-client/dist/assets/index-CqLN7STl.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-CXrQSbVY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js.map → useSSE-CXrQSbVY.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 +90 -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 +186 -0
- package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +336 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +394 -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 +305 -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 +26 -5
- 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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* store.ts — S51B topic persistence over the turns.db `topics`/`memory_topics`
|
|
3
|
+
* shells (reserved in S49's schema, so NO migration / no schema drift).
|
|
4
|
+
*
|
|
5
|
+
* Host-agnostic (no pi imports). Pure local node:sqlite (PREVENT-PI-004), all
|
|
6
|
+
* queries parameterized (PREVENT-002). This is a derived, read-mostly view:
|
|
7
|
+
* `replaceTopicModel` rewrites ONLY the two topic tables in one transaction and
|
|
8
|
+
* never touches turns/memories/checkpoints (PREVENT-PI-001/002).
|
|
9
|
+
*/
|
|
10
|
+
import { openTurnStore, withTx } from "../store/turns/connection.js";
|
|
11
|
+
import { getStateDir } from "../store.js";
|
|
12
|
+
function parseTermScores(json) {
|
|
13
|
+
if (!json)
|
|
14
|
+
return [];
|
|
15
|
+
try {
|
|
16
|
+
const v = JSON.parse(json);
|
|
17
|
+
return Array.isArray(v)
|
|
18
|
+
? v
|
|
19
|
+
: [];
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function rowToTopic(r) {
|
|
26
|
+
return {
|
|
27
|
+
id: r.id,
|
|
28
|
+
label: r.label,
|
|
29
|
+
termScores: parseTermScores(r.term_scores),
|
|
30
|
+
memoryCount: r.memory_count,
|
|
31
|
+
lastUpdated: r.last_updated ?? 0,
|
|
32
|
+
clusterModelBuiltAt: r.cluster_model_built_at,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function rowToAssignment(r) {
|
|
36
|
+
return {
|
|
37
|
+
memoryId: r.memory_id,
|
|
38
|
+
// memory_topics has no session_id column; the chunk id is globally unique
|
|
39
|
+
// enough for the wiki view (context_chunks PK is (session_id, id)).
|
|
40
|
+
// sessionId omitted — memory_topics has no session_id column;
|
|
41
|
+
// the TopicAssignment type marks it optional for this reason.
|
|
42
|
+
topicId: r.topic_id,
|
|
43
|
+
confidence: r.confidence ?? 0,
|
|
44
|
+
assignedAt: r.assigned_at ?? 0,
|
|
45
|
+
method: "kmeans+tfidf",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Open a TopicStore over the same turns.db the S49 TurnStore uses (shared connection cache). */
|
|
49
|
+
export function createTopicStore(stateDir) {
|
|
50
|
+
const db = openTurnStore(stateDir ?? getStateDir());
|
|
51
|
+
function replaceTopicModel(model) {
|
|
52
|
+
withTx(db, () => {
|
|
53
|
+
db.prepare("DELETE FROM memory_topics").run();
|
|
54
|
+
db.prepare("DELETE FROM topics").run();
|
|
55
|
+
const insTopic = db.prepare(`INSERT INTO topics (id, label, term_scores, memory_count, last_updated, cluster_model_built_at)
|
|
56
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
57
|
+
for (const t of model.topics) {
|
|
58
|
+
insTopic.run(t.id, t.label, JSON.stringify(t.termScores), t.memoryCount, t.lastUpdated, model.builtAt);
|
|
59
|
+
}
|
|
60
|
+
const insAssign = db.prepare(`INSERT OR REPLACE INTO memory_topics (memory_id, topic_id, confidence, assigned_at, method)
|
|
61
|
+
VALUES (?, ?, ?, ?, 'kmeans+tfidf')`);
|
|
62
|
+
for (const a of model.assignments) {
|
|
63
|
+
insAssign.run(a.memoryId, a.topicId, a.confidence, a.assignedAt);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
replaceTopicModel,
|
|
69
|
+
getTopics() {
|
|
70
|
+
const rows = db
|
|
71
|
+
.prepare("SELECT * FROM topics ORDER BY memory_count DESC, id ASC")
|
|
72
|
+
.all();
|
|
73
|
+
return rows.map(rowToTopic);
|
|
74
|
+
},
|
|
75
|
+
getMemoriesForTopic(topicId, limit = 50, offset = 0) {
|
|
76
|
+
const rows = db
|
|
77
|
+
.prepare(`SELECT memory_id, topic_id, confidence, assigned_at, method
|
|
78
|
+
FROM memory_topics WHERE topic_id = ?
|
|
79
|
+
ORDER BY confidence DESC, memory_id ASC LIMIT ? OFFSET ?`)
|
|
80
|
+
.all(topicId, limit, offset);
|
|
81
|
+
return rows.map(rowToAssignment);
|
|
82
|
+
},
|
|
83
|
+
getTopicForMemory(memoryId) {
|
|
84
|
+
const r = db
|
|
85
|
+
.prepare(`SELECT memory_id, topic_id, confidence, assigned_at, method
|
|
86
|
+
FROM memory_topics WHERE memory_id = ? LIMIT 1`)
|
|
87
|
+
.get(memoryId);
|
|
88
|
+
return r ? rowToAssignment(r) : null;
|
|
89
|
+
},
|
|
90
|
+
getTopicStats() {
|
|
91
|
+
const t = db.prepare("SELECT COUNT(*) AS c FROM topics").get();
|
|
92
|
+
const a = db.prepare("SELECT COUNT(*) AS c FROM memory_topics").get();
|
|
93
|
+
const r = db
|
|
94
|
+
.prepare("SELECT MAX(cluster_model_built_at) AS m FROM topics")
|
|
95
|
+
.get();
|
|
96
|
+
return {
|
|
97
|
+
totalTopics: t.c,
|
|
98
|
+
totalAssigned: a.c,
|
|
99
|
+
lastRebuildAt: r.m ?? null,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Turns-meta counter key for the every-Nth-compaction rebuild trigger. */
|
|
105
|
+
const WIKI_COUNTER_KEY = "wiki_compact_counter";
|
|
106
|
+
/** Read the rebuild counter (0 when unset). */
|
|
107
|
+
export function getWikiCompactCounter(db) {
|
|
108
|
+
const r = db
|
|
109
|
+
.prepare("SELECT value FROM turns_meta WHERE key = ?")
|
|
110
|
+
.get(WIKI_COUNTER_KEY);
|
|
111
|
+
const n = r ? Number(r.value) : 0;
|
|
112
|
+
return Number.isFinite(n) ? n : 0;
|
|
113
|
+
}
|
|
114
|
+
/** Increment + persist the rebuild counter; returns the new value. */
|
|
115
|
+
export function bumpWikiCompactCounter(db) {
|
|
116
|
+
const next = getWikiCompactCounter(db) + 1;
|
|
117
|
+
db.prepare("INSERT OR REPLACE INTO turns_meta (key, value) VALUES (?, ?)").run(WIKI_COUNTER_KEY, String(next));
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* store.test.ts — S51B topic persistence tests. Temp dirs; node:sqlite only.
|
|
3
|
+
* Verifies the topics/memory_topics shells (S49) are written atomically, CRUD
|
|
4
|
+
* round-trips, and the every-Nth-compaction counter behaves.
|
|
5
|
+
*/
|
|
6
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { createTopicStore, getWikiCompactCounter, bumpWikiCompactCounter, } from "./store.js";
|
|
12
|
+
import { openTurnStore, closeTurnStore } from "../store/turns/connection.js";
|
|
13
|
+
let tmpDir;
|
|
14
|
+
let counter = 0;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-topicstore-"));
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
function stateDir() {
|
|
22
|
+
return join(tmpDir, `run-${counter++}`);
|
|
23
|
+
}
|
|
24
|
+
function makeModel(builtAt, topics) {
|
|
25
|
+
const tops = [];
|
|
26
|
+
const assigns = [];
|
|
27
|
+
for (let i = 0; i < topics; i++) {
|
|
28
|
+
tops.push({
|
|
29
|
+
id: `topic_${i}`,
|
|
30
|
+
label: `label ${i}`,
|
|
31
|
+
termScores: [{ term: `t${i}`, score: i + 1 }],
|
|
32
|
+
memoryCount: 2,
|
|
33
|
+
lastUpdated: builtAt,
|
|
34
|
+
});
|
|
35
|
+
for (let m = 0; m < 2; m++) {
|
|
36
|
+
assigns.push({
|
|
37
|
+
memoryId: `mem_${i}_${m}`,
|
|
38
|
+
sessionId: "s",
|
|
39
|
+
topicId: `topic_${i}`,
|
|
40
|
+
confidence: 0.9 - m * 0.1,
|
|
41
|
+
assignedAt: builtAt,
|
|
42
|
+
method: "kmeans+tfidf",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
topics: tops,
|
|
48
|
+
assignments: assigns,
|
|
49
|
+
k: topics,
|
|
50
|
+
criterion: "silhouette",
|
|
51
|
+
silhouetteScore: 0.5,
|
|
52
|
+
totalChunks: topics * 2,
|
|
53
|
+
builtAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
test("replaceTopicModel inserts topics + assignments; CRUD round-trips", () => {
|
|
57
|
+
const dir = stateDir();
|
|
58
|
+
const store = createTopicStore(dir);
|
|
59
|
+
store.replaceTopicModel(makeModel(1000, 2));
|
|
60
|
+
const topics = store.getTopics();
|
|
61
|
+
assert.equal(topics.length, 2);
|
|
62
|
+
assert.equal(topics[0].label, "label 0");
|
|
63
|
+
assert.equal(topics[0].clusterModelBuiltAt, 1000);
|
|
64
|
+
assert.deepEqual(topics[0].termScores, [{ term: "t0", score: 1 }]);
|
|
65
|
+
const members = store.getMemoriesForTopic("topic_0");
|
|
66
|
+
assert.equal(members.length, 2);
|
|
67
|
+
assert.equal(members[0].memoryId, "mem_0_0"); // confidence DESC
|
|
68
|
+
const a = store.getTopicForMemory("mem_1_1");
|
|
69
|
+
assert.equal(a?.topicId, "topic_1");
|
|
70
|
+
assert.ok(Math.abs((a?.confidence ?? 0) - 0.8) < 1e-9);
|
|
71
|
+
closeTurnStore(dir);
|
|
72
|
+
});
|
|
73
|
+
test("replaceTopicModel is atomic — old model fully replaced", () => {
|
|
74
|
+
const dir = stateDir();
|
|
75
|
+
const store = createTopicStore(dir);
|
|
76
|
+
store.replaceTopicModel(makeModel(1000, 3)); // 3 topics, 6 assignments
|
|
77
|
+
store.replaceTopicModel(makeModel(2000, 1)); // 1 topic, 2 assignments
|
|
78
|
+
const stats = store.getTopicStats();
|
|
79
|
+
assert.equal(stats.totalTopics, 1);
|
|
80
|
+
assert.equal(stats.totalAssigned, 2);
|
|
81
|
+
assert.equal(stats.lastRebuildAt, 2000);
|
|
82
|
+
// Old topic ids gone.
|
|
83
|
+
assert.equal(store.getTopicForMemory("mem_2_0"), null);
|
|
84
|
+
closeTurnStore(dir);
|
|
85
|
+
});
|
|
86
|
+
test("getTopics sorted by memory_count DESC; stats roll up", () => {
|
|
87
|
+
const dir = stateDir();
|
|
88
|
+
const store = createTopicStore(dir);
|
|
89
|
+
const model = makeModel(500, 2);
|
|
90
|
+
model.topics[1].memoryCount = 99; // make topic_1 biggest
|
|
91
|
+
// give topic_1 more assignments to match memoryCount semantics loosely
|
|
92
|
+
store.replaceTopicModel(model);
|
|
93
|
+
const topics = store.getTopics();
|
|
94
|
+
assert.equal(topics[0].id, "topic_1"); // highest count first
|
|
95
|
+
const stats = store.getTopicStats();
|
|
96
|
+
assert.equal(stats.totalTopics, 2);
|
|
97
|
+
closeTurnStore(dir);
|
|
98
|
+
});
|
|
99
|
+
test("wiki compact counter increments + persists; default 0", () => {
|
|
100
|
+
const dir = stateDir();
|
|
101
|
+
const db = openTurnStore(dir);
|
|
102
|
+
assert.equal(getWikiCompactCounter(db), 0);
|
|
103
|
+
assert.equal(bumpWikiCompactCounter(db), 1);
|
|
104
|
+
assert.equal(bumpWikiCompactCounter(db), 2);
|
|
105
|
+
// Reopen handle (same cached conn) — value persists.
|
|
106
|
+
assert.equal(getWikiCompactCounter(openTurnStore(dir)), 2);
|
|
107
|
+
closeTurnStore(dir);
|
|
108
|
+
});
|
|
109
|
+
test("topics + memory_topics shells exist with no seed data (S49 schema)", () => {
|
|
110
|
+
const dir = stateDir();
|
|
111
|
+
const db = openTurnStore(dir);
|
|
112
|
+
const topics = db.prepare("SELECT COUNT(*) AS c FROM topics").get();
|
|
113
|
+
const assigns = db.prepare("SELECT COUNT(*) AS c FROM memory_topics").get();
|
|
114
|
+
assert.equal(topics.c, 0);
|
|
115
|
+
assert.equal(assigns.c, 0);
|
|
116
|
+
closeTurnStore(dir);
|
|
117
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — S51A auto-categorizing wiki: shared types.
|
|
3
|
+
*
|
|
4
|
+
* Host-agnostic (no pi imports). These describe a derived, read-only topic view
|
|
5
|
+
* over stored memory chunks — see docs/specs/s51-auto-categorizing-wiki.md and
|
|
6
|
+
* the extended s47 spec. No model calls, no network (PREVENT-PI-004); pure local math.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/dist/src/wiki.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
import { tfidfScores } from "./topics/labels.js";
|
|
12
|
+
/** Split text into sentences (very simple: period + newline + question mark + exclamation). */
|
|
13
|
+
function splitSentences(text) {
|
|
14
|
+
return text
|
|
15
|
+
.split(/[.\n!?]/)
|
|
16
|
+
.map((s) => s.trim())
|
|
17
|
+
.filter((s) => s.length > 10);
|
|
18
|
+
}
|
|
19
|
+
/** Wrap a text string as a pseudo EmbeddedChunk for tfidfScores reuse. */
|
|
20
|
+
function pseudoChunk(text) {
|
|
21
|
+
return { chunkId: "", sessionId: "", vec: [], text };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate an extractive summary for a topic. Takes the topic's member texts,
|
|
25
|
+
* ranks sentences by TF-IDF, and returns the top-scoring ones joined.
|
|
26
|
+
* Pure local math; no model/network calls.
|
|
27
|
+
*/
|
|
28
|
+
export function extractiveSummary(topic, assignments, getContent) {
|
|
29
|
+
// Join all member texts for the corpus.
|
|
30
|
+
const members = [];
|
|
31
|
+
for (const a of assignments) {
|
|
32
|
+
if (a.topicId === topic.id) {
|
|
33
|
+
const txt = getContent(a.memoryId);
|
|
34
|
+
if (txt)
|
|
35
|
+
members.push(txt);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const corpusText = members.join(" ");
|
|
39
|
+
if (corpusText.length === 0)
|
|
40
|
+
return "";
|
|
41
|
+
// Extractively summarize: rank sentences by TF-IDF, pick top 3.
|
|
42
|
+
const sentences = splitSentences(corpusText);
|
|
43
|
+
if (sentences.length === 0)
|
|
44
|
+
return "";
|
|
45
|
+
// Reuse tfidfScores from labels.ts: each sentence is a pseudo-chunk scored
|
|
46
|
+
// against the full corpus. Sum the top-N term scores for each sentence.
|
|
47
|
+
const corpusChunks = [pseudoChunk(corpusText)];
|
|
48
|
+
const scored = sentences.map((s, i) => {
|
|
49
|
+
const sentScores = tfidfScores([pseudoChunk(s)], corpusChunks);
|
|
50
|
+
const totalScore = sentScores.reduce((sum, ts) => sum + ts.score, 0);
|
|
51
|
+
return { text: s, score: totalScore, idx: i };
|
|
52
|
+
});
|
|
53
|
+
scored.sort((a, b) => b.score - a.score || a.idx - b.idx);
|
|
54
|
+
const top = Math.min(3, sentences.length);
|
|
55
|
+
const chosen = scored.slice(0, top).sort((a, b) => a.idx - b.idx);
|
|
56
|
+
return chosen.map((s) => s.text).join(". ");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find other topics (not `selfId`) that a memory also belongs to. Returns
|
|
60
|
+
* distinct topic ids where the memory has assignments for both `selfId` and
|
|
61
|
+
* that other topic.
|
|
62
|
+
*/
|
|
63
|
+
function coTopicsForMemory(memId, selfId, allAssignments) {
|
|
64
|
+
const memAssignments = allAssignments.filter((a) => a.memoryId === memId);
|
|
65
|
+
const topicSet = new Set(memAssignments.map((a) => a.topicId));
|
|
66
|
+
// Memory must belong to selfId + at least one other for co-occurrence.
|
|
67
|
+
if (!topicSet.has(selfId))
|
|
68
|
+
return [];
|
|
69
|
+
topicSet.delete(selfId);
|
|
70
|
+
return [...topicSet];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate a full wiki page for a topic.
|
|
74
|
+
*
|
|
75
|
+
* @param topic The topic to render.
|
|
76
|
+
* @param assignments All topic assignments to pull member memories.
|
|
77
|
+
* @param getMemoryInfo Function returning content/timestamp/importance for a memoryId.
|
|
78
|
+
* @param allTopics All topics (for co-occurrence computation).
|
|
79
|
+
*/
|
|
80
|
+
export function generateWikiPage(topic, assignments, getMemoryInfo, allTopics) {
|
|
81
|
+
const topicAssignments = assignments.filter((a) => a.topicId === topic.id);
|
|
82
|
+
const keyMemories = topicAssignments
|
|
83
|
+
.map((a) => getMemoryInfo(a.memoryId))
|
|
84
|
+
.filter((m) => m !== null)
|
|
85
|
+
.sort((a, b) => b.importance - a.importance)
|
|
86
|
+
.slice(0, 10);
|
|
87
|
+
const recentMemories = topicAssignments
|
|
88
|
+
.map((a) => getMemoryInfo(a.memoryId))
|
|
89
|
+
.filter((m) => m !== null)
|
|
90
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
91
|
+
.slice(0, 5);
|
|
92
|
+
// Co-occurrence: scan ALL assignments (not just this topic's) and count how
|
|
93
|
+
// many memories also belong to each other topic. Topics sharing member ids
|
|
94
|
+
// with this one are "related". A memory that belongs to topicA + topicB is
|
|
95
|
+
// evidence that A and B are related.
|
|
96
|
+
const topicIdCount = new Map();
|
|
97
|
+
for (const a of assignments) {
|
|
98
|
+
for (const other of coTopicsForMemory(a.memoryId, topic.id, assignments)) {
|
|
99
|
+
topicIdCount.set(other, (topicIdCount.get(other) ?? 0) + 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const relatedTopics = [];
|
|
103
|
+
const ordered = [...topicIdCount.entries()].sort((a, b) => b[1] - a[1]);
|
|
104
|
+
for (const [tid] of ordered.slice(0, 3)) {
|
|
105
|
+
const t = allTopics.find((x) => x.id === tid);
|
|
106
|
+
if (t)
|
|
107
|
+
relatedTopics.push(t);
|
|
108
|
+
}
|
|
109
|
+
const getContent = (memId) => getMemoryInfo(memId)?.content ?? "";
|
|
110
|
+
const summary = extractiveSummary(topic, assignments, getContent);
|
|
111
|
+
return {
|
|
112
|
+
topic,
|
|
113
|
+
summary,
|
|
114
|
+
keyMemories,
|
|
115
|
+
recentMemories,
|
|
116
|
+
relatedTopics,
|
|
117
|
+
generatedAt: Date.now(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Build the wiki index for the landing page. */
|
|
121
|
+
export function buildWikiIndex(topics) {
|
|
122
|
+
return {
|
|
123
|
+
topics: topics.map((t) => ({
|
|
124
|
+
id: t.id,
|
|
125
|
+
label: t.label,
|
|
126
|
+
memoryCount: t.memoryCount,
|
|
127
|
+
lastUpdated: t.lastUpdated,
|
|
128
|
+
})),
|
|
129
|
+
totalTopics: topics.length,
|
|
130
|
+
totalMemories: topics.reduce((s, t) => s + t.memoryCount, 0),
|
|
131
|
+
lastRebuildAt: topics.length > 0 ? topics[0].lastUpdated : null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
let tmpDir;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-wiki-"));
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
function mkTopic(id, label, termScores) {
|
|
20
|
+
return { id, label, termScores, memoryCount: 0, lastUpdated: 0 };
|
|
21
|
+
}
|
|
22
|
+
function mkAssignment(memId, topicId, confidence = 0.8) {
|
|
23
|
+
return {
|
|
24
|
+
memoryId: memId,
|
|
25
|
+
sessionId: "s",
|
|
26
|
+
topicId,
|
|
27
|
+
confidence,
|
|
28
|
+
assignedAt: 0,
|
|
29
|
+
method: "kmeans+tfidf",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
test("extractiveSummary returns top sentences joined", () => {
|
|
33
|
+
const topic = mkTopic("t1", "sqlite", [{ term: "sqlite", score: 1 }]);
|
|
34
|
+
const assignments = [mkAssignment("a", "t1"), mkAssignment("b", "t1")];
|
|
35
|
+
const getContent = (id) => {
|
|
36
|
+
if (id === "a")
|
|
37
|
+
return "SQLite uses WAL mode. SQLite uses WAL mode. SQLite uses WAL mode.";
|
|
38
|
+
if (id === "b")
|
|
39
|
+
return "Postgres uses WAL mode. Postgres uses WAL mode.";
|
|
40
|
+
return "";
|
|
41
|
+
};
|
|
42
|
+
const summary = extractiveSummary(topic, assignments, getContent);
|
|
43
|
+
// Should be non-empty and contain at least one sentence
|
|
44
|
+
assert.ok(summary.length > 0);
|
|
45
|
+
assert.ok(summary.includes("WAL"));
|
|
46
|
+
});
|
|
47
|
+
test("extractiveSummary returns empty string when topic has no members", () => {
|
|
48
|
+
const topic = mkTopic("t1", "empty", []);
|
|
49
|
+
const summary = extractiveSummary(topic, [], () => "");
|
|
50
|
+
assert.equal(summary, "");
|
|
51
|
+
});
|
|
52
|
+
test("generateWikiPage produces page with summary + key/recent memories", () => {
|
|
53
|
+
const topic = mkTopic("t1", "sqlite", [{ term: "sqlite", score: 1 }]);
|
|
54
|
+
const assignments = [
|
|
55
|
+
mkAssignment("a", "t1", 0.9),
|
|
56
|
+
mkAssignment("b", "t1", 0.5),
|
|
57
|
+
];
|
|
58
|
+
const getMemoryInfo = (id) => {
|
|
59
|
+
if (id === "a")
|
|
60
|
+
return {
|
|
61
|
+
content: "First memory content",
|
|
62
|
+
timestamp: 100,
|
|
63
|
+
importance: 0.9,
|
|
64
|
+
};
|
|
65
|
+
if (id === "b")
|
|
66
|
+
return {
|
|
67
|
+
content: "Second memory content",
|
|
68
|
+
timestamp: 200,
|
|
69
|
+
importance: 0.5,
|
|
70
|
+
};
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
const allTopics = [topic];
|
|
74
|
+
const page = generateWikiPage(topic, assignments, getMemoryInfo, allTopics);
|
|
75
|
+
assert.equal(page.topic.id, "t1");
|
|
76
|
+
assert.equal(page.keyMemories.length, 2);
|
|
77
|
+
// Key memories sorted by importance DESC
|
|
78
|
+
assert.equal(page.keyMemories[0].importance, 0.9);
|
|
79
|
+
// Recent memories sorted by timestamp DESC
|
|
80
|
+
assert.equal(page.recentMemories[0].timestamp, 200);
|
|
81
|
+
assert.equal(page.relatedTopics.length, 0);
|
|
82
|
+
assert.ok(page.generatedAt > 0);
|
|
83
|
+
});
|
|
84
|
+
test("generateWikiPage finds related topics via co-occurrence", () => {
|
|
85
|
+
const topicA = mkTopic("A", "topic-a", []);
|
|
86
|
+
const topicB = mkTopic("B", "topic-b", []);
|
|
87
|
+
const topicC = mkTopic("C", "topic-c", []);
|
|
88
|
+
// Memory "shared" appears in both topicA and topicB → A and B are related.
|
|
89
|
+
// Memory "a-only" only in topicA.
|
|
90
|
+
// Memory "c-only" only in topicC (no overlap with A).
|
|
91
|
+
const assignments = [
|
|
92
|
+
mkAssignment("shared", "A"),
|
|
93
|
+
mkAssignment("shared", "B"),
|
|
94
|
+
mkAssignment("a-only", "A"),
|
|
95
|
+
mkAssignment("c-only", "C"),
|
|
96
|
+
];
|
|
97
|
+
const getMemoryInfo = (id) => ({
|
|
98
|
+
content: `content for ${id}`,
|
|
99
|
+
timestamp: 100,
|
|
100
|
+
importance: 0.5,
|
|
101
|
+
});
|
|
102
|
+
const page = generateWikiPage(topicA, assignments, getMemoryInfo, [
|
|
103
|
+
topicA,
|
|
104
|
+
topicB,
|
|
105
|
+
topicC,
|
|
106
|
+
]);
|
|
107
|
+
// topicB should be related (shares "shared" with topicA); topicC should not.
|
|
108
|
+
const relatedIds = page.relatedTopics.map((t) => t.id);
|
|
109
|
+
assert.ok(relatedIds.includes("B"), `expected B in related topics, got ${relatedIds.join(",")}`);
|
|
110
|
+
assert.ok(!relatedIds.includes("C"), `C has no co-occurrence with A, should not be related`);
|
|
111
|
+
});
|
|
112
|
+
test("buildWikiIndex aggregates topics + memory counts", () => {
|
|
113
|
+
const topics = [mkTopic("t1", "sqlite", []), mkTopic("t2", "wal", [])];
|
|
114
|
+
topics[0].memoryCount = 5;
|
|
115
|
+
topics[1].memoryCount = 3;
|
|
116
|
+
topics[0].lastUpdated = 1000;
|
|
117
|
+
topics[1].lastUpdated = 2000;
|
|
118
|
+
const idx = buildWikiIndex(topics);
|
|
119
|
+
assert.equal(idx.totalTopics, 2);
|
|
120
|
+
assert.equal(idx.totalMemories, 8);
|
|
121
|
+
assert.equal(idx.topics.length, 2);
|
|
122
|
+
assert.ok(idx.lastRebuildAt !== null);
|
|
123
|
+
});
|
|
124
|
+
test("buildWikiIndex handles empty topics", () => {
|
|
125
|
+
const idx = buildWikiIndex([]);
|
|
126
|
+
assert.equal(idx.totalTopics, 0);
|
|
127
|
+
assert.equal(idx.totalMemories, 0);
|
|
128
|
+
assert.equal(idx.lastRebuildAt, null);
|
|
129
|
+
});
|
|
130
|
+
test("honest boundary: no LLM/network/keyword-literals in src/wiki.ts", () => {
|
|
131
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
132
|
+
const wikiPath = join(here, "..", "..", "src", "wiki.ts");
|
|
133
|
+
const text = readFileSync(wikiPath, "utf8");
|
|
134
|
+
const banned = [/ollama/i, /\bllm\b/i, /\bfetch\s*\(/, /\bhybrid\b/i];
|
|
135
|
+
const fabricated = [
|
|
136
|
+
"graphql",
|
|
137
|
+
"docker",
|
|
138
|
+
"stacktrace",
|
|
139
|
+
"bottleneck",
|
|
140
|
+
"trade-off",
|
|
141
|
+
];
|
|
142
|
+
for (const re of banned) {
|
|
143
|
+
assert.ok(!re.test(text), `wiki.ts must not contain ${re} (PREVENT-PI-004 / no-model boundary)`);
|
|
144
|
+
}
|
|
145
|
+
for (const word of fabricated) {
|
|
146
|
+
assert.ok(!text.toLowerCase().includes(`"${word}"`), `wiki.ts must not hardcode fabricated taxonomy term "${word}"`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
test("honest boundary: no LLM/Ollama/network in src/topics/* (non-test)", () => {
|
|
150
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
151
|
+
// dist/src/wiki.test.js → ../../src/topics
|
|
152
|
+
const topicsDir = join(here, "..", "..", "src", "topics");
|
|
153
|
+
const files = readdirSync(topicsDir).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"));
|
|
154
|
+
const banned = [/ollama/i, /\bllm\b/i, /\bfetch\s*\(/, /\bhybrid\b/i];
|
|
155
|
+
const fabricated = [
|
|
156
|
+
"graphql",
|
|
157
|
+
"docker",
|
|
158
|
+
"stacktrace",
|
|
159
|
+
"bottleneck",
|
|
160
|
+
"trade-off",
|
|
161
|
+
];
|
|
162
|
+
for (const f of files) {
|
|
163
|
+
const text = readFileSync(join(topicsDir, f), "utf8");
|
|
164
|
+
for (const re of banned) {
|
|
165
|
+
assert.ok(!re.test(text), `${f} must not contain ${re}`);
|
|
166
|
+
}
|
|
167
|
+
for (const word of fabricated) {
|
|
168
|
+
assert.ok(!text.toLowerCase().includes(`"${word}"`), `${f} must not hardcode fabricated taxonomy term "${word}"`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as o,j as n}from"./index-
|
|
2
|
-
//# sourceMappingURL=AchievementTiles-
|
|
1
|
+
import{r as o,j as n}from"./index-CqLN7STl.js";function x(s){try{return new Date(s*1e3).toLocaleString()}catch{return"—"}}function f({achievements:s}){const i=o.useRef(0),[r,a]=o.useState(null),l=s.reduce((e,t)=>Math.max(e,t.unlocked_at??0),0),c=i.current;o.useEffect(()=>{l>i.current&&(i.current=l)},[l]),o.useEffect(()=>{if(c>0&&l>c){const e=s.filter(t=>t.unlocked_at!=null&&(t.unlocked_at??0)>c);if(e.length>0){const t=e.map(d=>`${d.icon??""} ${d.title}`.trim()).join(", ")+" unlocked!";a(t);const h=setTimeout(()=>a(null),4e3);return()=>clearTimeout(h)}}},[s,l,c]);const u=s.filter(e=>!(e.hidden===1&&e.unlocked_at==null));return n.jsxs("div",{className:"ach-section",children:[r!=null&&n.jsx("div",{className:"ach-toast show",children:r}),n.jsxs("div",{className:"ach-tiles",children:[u.length===0&&n.jsx("span",{className:"repo-none",children:"no achievements yet"}),u.map(e=>{if(e.unlocked_at!=null){const t=c>0&&(e.unlocked_at??0)>c;return n.jsxs("div",{className:`ach-tile unlocked${t?" just-unlocked":""}`,children:[e.icon??""," ",e.title,n.jsxs("span",{className:"ach-detail",children:["unlocked ",x(e.unlocked_at)]})]},e.id)}return n.jsxs("div",{className:"ach-tile locked",children:["??? ",e.title]},e.id)})]})]})}export{f as A};
|
|
2
|
+
//# sourceMappingURL=AchievementTiles-DZp_xQOW.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AchievementTiles-
|
|
1
|
+
{"version":3,"file":"AchievementTiles-DZp_xQOW.js","sources":["../../src/components/AchievementTiles.tsx"],"sourcesContent":["/**\n * dashboard-client/src/components/AchievementTiles.tsx — shared achievement grid.\n *\n * Renders a grid of achievement tiles from fetchAchievements() data.\n * Tile states: unlocked (icon+title+date), visible-locked (??? teaser),\n * hidden+locked (not rendered). Just-unlocked tiles get a pulse animation.\n * Toast notification fires for newly-unlocked achievements.\n *\n * Used by both GameTab (sub-section) and AchievementsTab (standalone).\n */\n\nimport type React from \"react\";\nimport { useRef, useState, useEffect } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\n\nexport interface AchievementTilesProps {\n\t/** Achievement rows from /api/achievements. */\n\tachievements: AchievementRow[];\n}\n\n/** Format unlock timestamp (seconds → locale string). */\nfunction fmtUnlockDate(ts: number): string {\n\ttry {\n\t\treturn new Date(ts * 1000).toLocaleString();\n\t} catch {\n\t\treturn \"\\u2014\";\n\t}\n}\n\nexport function AchievementTiles({\n\tachievements,\n}: AchievementTilesProps): React.ReactElement {\n\tconst lastMaxTsRef = useRef(0);\n\tconst [toastText, setToastText] = useState<string | null>(null);\n\n\tconst maxTs = achievements.reduce(\n\t\t(mx, a) => Math.max(mx, a.unlocked_at ?? 0),\n\t\t0,\n\t);\n\tconst prevMaxTs = lastMaxTsRef.current;\n\n\t/* Update ref after render so next poll sees previous baseline. */\n\tuseEffect(() => {\n\t\tif (maxTs > lastMaxTsRef.current) {\n\t\t\tlastMaxTsRef.current = maxTs;\n\t\t}\n\t}, [maxTs]);\n\n\t/* Toast for newly-unlocked achievements. */\n\tuseEffect(() => {\n\t\tif (prevMaxTs > 0 && maxTs > prevMaxTs) {\n\t\t\tconst newly = achievements.filter(\n\t\t\t\t(a) => a.unlocked_at != null && (a.unlocked_at ?? 0) > prevMaxTs,\n\t\t\t);\n\t\t\tif (newly.length > 0) {\n\t\t\t\tconst text =\n\t\t\t\t\tnewly\n\t\t\t\t\t\t.map((a) => `${a.icon ?? \"\"} ${a.title}`.trim())\n\t\t\t\t\t\t.join(\", \") + \" unlocked!\";\n\t\t\t\tsetToastText(text);\n\t\t\t\tconst t = setTimeout(() => setToastText(null), 4000);\n\t\t\t\treturn (): void => clearTimeout(t);\n\t\t\t}\n\t\t}\n\t}, [achievements, maxTs, prevMaxTs]);\n\n\t/* Visible tiles: skip hidden+locked (hidden === 1 && unlocked_at == null). */\n\tconst tiles = achievements.filter(\n\t\t(a) => !(a.hidden === 1 && a.unlocked_at == null),\n\t);\n\n\treturn (\n\t\t<div className=\"ach-section\">\n\t\t\t{toastText != null && (\n\t\t\t\t<div className=\"ach-toast show\">{toastText}</div>\n\t\t\t)}\n\t\t\t<div className=\"ach-tiles\">\n\t\t\t\t{tiles.length === 0 && (\n\t\t\t\t\t<span className=\"repo-none\">no achievements yet</span>\n\t\t\t\t)}\n\t\t\t\t{tiles.map((a) => {\n\t\t\t\t\tif (a.unlocked_at != null) {\n\t\t\t\t\t\tconst isNew = prevMaxTs > 0 && (a.unlocked_at ?? 0) > prevMaxTs;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={a.id}\n\t\t\t\t\t\t\t\tclassName={`ach-tile unlocked${isNew ? \" just-unlocked\" : \"\"}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{a.icon ?? \"\"} {a.title}\n\t\t\t\t\t\t\t\t<span className=\"ach-detail\">\n\t\t\t\t\t\t\t\t\tunlocked {fmtUnlockDate(a.unlocked_at)}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={a.id} className=\"ach-tile locked\">\n\t\t\t\t\t\t\t??? {a.title}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"],"names":["fmtUnlockDate","ts","AchievementTiles","achievements","lastMaxTsRef","useRef","toastText","setToastText","useState","maxTs","mx","a","prevMaxTs","useEffect","newly","text","t","tiles","jsxs","jsx","isNew"],"mappings":"+CAqBA,SAASA,EAAcC,EAAoB,CAC1C,GAAI,CACH,OAAO,IAAI,KAAKA,EAAK,GAAI,EAAE,eAAA,CAC5B,MAAQ,CACP,MAAO,GACR,CACD,CAEO,SAASC,EAAiB,CAChC,aAAAC,CACD,EAA8C,CAC7C,MAAMC,EAAeC,EAAAA,OAAO,CAAC,EACvB,CAACC,EAAWC,CAAY,EAAIC,EAAAA,SAAwB,IAAI,EAExDC,EAAQN,EAAa,OAC1B,CAACO,EAAIC,IAAM,KAAK,IAAID,EAAIC,EAAE,aAAe,CAAC,EAC1C,CAAA,EAEKC,EAAYR,EAAa,QAG/BS,EAAAA,UAAU,IAAM,CACXJ,EAAQL,EAAa,UACxBA,EAAa,QAAUK,EAEzB,EAAG,CAACA,CAAK,CAAC,EAGVI,EAAAA,UAAU,IAAM,CACf,GAAID,EAAY,GAAKH,EAAQG,EAAW,CACvC,MAAME,EAAQX,EAAa,OACzBQ,GAAMA,EAAE,aAAe,OAASA,EAAE,aAAe,GAAKC,CAAA,EAExD,GAAIE,EAAM,OAAS,EAAG,CACrB,MAAMC,EACLD,EACE,IAAKH,GAAM,GAAGA,EAAE,MAAQ,EAAE,IAAIA,EAAE,KAAK,GAAG,KAAA,CAAM,EAC9C,KAAK,IAAI,EAAI,aAChBJ,EAAaQ,CAAI,EACjB,MAAMC,EAAI,WAAW,IAAMT,EAAa,IAAI,EAAG,GAAI,EACnD,MAAO,IAAY,aAAaS,CAAC,CAClC,CACD,CACD,EAAG,CAACb,EAAcM,EAAOG,CAAS,CAAC,EAGnC,MAAMK,EAAQd,EAAa,OACzBQ,GAAM,EAAEA,EAAE,SAAW,GAAKA,EAAE,aAAe,KAAA,EAG7C,OACCO,EAAAA,KAAC,MAAA,CAAI,UAAU,cACb,SAAA,CAAAZ,GAAa,MACba,EAAAA,IAAC,MAAA,CAAI,UAAU,iBAAkB,SAAAb,EAAU,EAE5CY,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAD,EAAM,SAAW,GACjBE,MAAC,OAAA,CAAK,UAAU,YAAY,SAAA,sBAAmB,EAE/CF,EAAM,IAAKN,GAAM,CACjB,GAAIA,EAAE,aAAe,KAAM,CAC1B,MAAMS,EAAQR,EAAY,IAAMD,EAAE,aAAe,GAAKC,EACtD,OACCM,EAAAA,KAAC,MAAA,CAEA,UAAW,oBAAoBE,EAAQ,iBAAmB,EAAE,GAE3D,SAAA,CAAAT,EAAE,MAAQ,GAAG,IAAEA,EAAE,MAClBO,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAa,SAAA,CAAA,YAClBlB,EAAcW,EAAE,WAAW,CAAA,CAAA,CACtC,CAAA,CAAA,EANKA,EAAE,EAAA,CASV,CACA,OACCO,EAAAA,KAAC,MAAA,CAAe,UAAU,kBAAkB,SAAA,CAAA,OACtCP,EAAE,KAAA,CAAA,EADEA,EAAE,EAEZ,CAEF,CAAC,CAAA,CAAA,CACF,CAAA,EACD,CAEF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as t,h as m,j as s}from"./index-
|
|
2
|
-
//# sourceMappingURL=AchievementsTab-
|
|
1
|
+
import{r as t,h as m,j as s}from"./index-CqLN7STl.js";import{A as v}from"./AchievementTiles-DZp_xQOW.js";function x(){const[r,o]=t.useState([]),[l,h]=t.useState(!0),[a,n]=t.useState(null),c=t.useCallback(async()=>{try{const e=await m();o(e),n(null)}catch(e){n(e instanceof Error?e:new Error(String(e)))}finally{h(!1)}},[]);return t.useEffect(()=>{let e=!0;const i=async()=>{e&&await c()};i();const u=setInterval(()=>void i(),15e3);return()=>{e=!1,clearInterval(u)}},[c]),l&&r.length===0?s.jsx("div",{className:"tab-stub",children:"Loading achievements…"}):a&&r.length===0?s.jsxs("div",{className:"tab-stub",children:["Error: ",a.message]}):s.jsxs("div",{className:"achievements-tab",children:[s.jsx("h2",{children:"Achievements"}),s.jsx(v,{achievements:r})]})}export{x as default};
|
|
2
|
+
//# sourceMappingURL=AchievementsTab-BzFHpAHM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AchievementsTab-
|
|
1
|
+
{"version":3,"file":"AchievementsTab-BzFHpAHM.js","sources":["../../src/tabs/AchievementsTab.tsx"],"sourcesContent":["/**\n * dashboard-client/src/tabs/AchievementsTab.tsx — Achievements tab (NEW).\n *\n * Renders the shared AchievementTiles component standalone, plus a toast area.\n * Polls /api/achievements every 15s.\n */\n\nimport type React from \"react\";\nimport { useState, useEffect, useCallback } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\nimport { fetchAchievements } from \"../api/client\";\nimport { AchievementTiles } from \"../components/AchievementTiles\";\n\nexport default function AchievementsTab(): React.ReactElement {\n\tconst [achievements, setAchievements] = useState<AchievementRow[]>([]);\n\tconst [loading, setLoading] = useState(true);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst doFetch = useCallback(async (): Promise<void> => {\n\t\ttry {\n\t\t\tconst rows = await fetchAchievements();\n\t\t\tsetAchievements(rows);\n\t\t\tsetError(null);\n\t\t} catch (e) {\n\t\t\tsetError(e instanceof Error ? e : new Error(String(e)));\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t}, []);\n\n\tuseEffect(() => {\n\t\tlet active = true;\n\t\tconst run = async (): Promise<void> => {\n\t\t\tif (active) await doFetch();\n\t\t};\n\t\tvoid run();\n\t\tconst timer = setInterval(() => void run(), 15000);\n\t\treturn () => {\n\t\t\tactive = false;\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, [doFetch]);\n\n\tif (loading && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Loading achievements…</div>;\n\tif (error && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Error: {error.message}</div>;\n\n\treturn (\n\t\t<div className=\"achievements-tab\">\n\t\t\t<h2>Achievements</h2>\n\t\t\t<AchievementTiles achievements={achievements} />\n\t\t</div>\n\t);\n}\n"],"names":["AchievementsTab","achievements","setAchievements","useState","loading","setLoading","error","setError","doFetch","useCallback","rows","fetchAchievements","useEffect","active","run","timer","jsx","jsxs","AchievementTiles"],"mappings":"yGAaA,SAAwBA,GAAsC,CAC7D,KAAM,CAACC,EAAcC,CAAe,EAAIC,EAAAA,SAA2B,CAAA,CAAE,EAC/D,CAACC,EAASC,CAAU,EAAIF,EAAAA,SAAS,EAAI,EACrC,CAACG,EAAOC,CAAQ,EAAIJ,EAAAA,SAAuB,IAAI,EAE/CK,EAAUC,EAAAA,YAAY,SAA2B,CACtD,GAAI,CACH,MAAMC,EAAO,MAAMC,EAAA,EACnBT,EAAgBQ,CAAI,EACpBH,EAAS,IAAI,CACd,OAAS,EAAG,CACXA,EAAS,aAAa,MAAQ,EAAI,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,CACvD,QAAA,CACCF,EAAW,EAAK,CACjB,CACD,EAAG,CAAA,CAAE,EAeL,OAbAO,EAAAA,UAAU,IAAM,CACf,IAAIC,EAAS,GACb,MAAMC,EAAM,SAA2B,CAClCD,SAAcL,EAAA,CACnB,EACKM,EAAA,EACL,MAAMC,EAAQ,YAAY,IAAM,KAAKD,EAAA,EAAO,IAAK,EACjD,MAAO,IAAM,CACZD,EAAS,GACT,cAAcE,CAAK,CACpB,CACD,EAAG,CAACP,CAAO,CAAC,EAERJ,GAAWH,EAAa,SAAW,EAC/Be,EAAAA,IAAC,MAAA,CAAI,UAAU,WAAW,SAAA,wBAAqB,EACnDV,GAASL,EAAa,SAAW,EAC7BgB,EAAAA,KAAC,MAAA,CAAI,UAAU,WAAW,SAAA,CAAA,UAAQX,EAAM,OAAA,EAAQ,EAGvDW,EAAAA,KAAC,MAAA,CAAI,UAAU,mBACd,SAAA,CAAAD,EAAAA,IAAC,MAAG,SAAA,cAAA,CAAY,EAChBA,MAACE,GAAiB,aAAAjB,CAAA,CAA4B,CAAA,EAC/C,CAEF"}
|
package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js → CacheTab-DEQbTq2T.js}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{u as n,r,c as d,j as s}from"./index-
|
|
2
|
-
//# sourceMappingURL=CacheTab-
|
|
1
|
+
import{u as n,r,c as d,j as s}from"./index-CqLN7STl.js";import{C as l,T as h}from"./TimeSavedCard-B8AIjNTY.js";function p(){const{data:a,loading:i,error:c}=n(r.useCallback(()=>d(),[]),{pollInterval:5e3});if(i&&!a)return s.jsx("div",{className:"tab-stub",children:"Loading snapshot…"});if(c&&!a)return s.jsxs("div",{className:"tab-stub",children:["Error: ",c.message]});if(!a)return s.jsx("div",{className:"tab-stub",children:"No snapshot data."});const{cacheHits:e,compacts:o,timeSaved:t}=a;return s.jsx("div",{className:"cache-tab",children:s.jsxs("div",{className:"card-grid overview-card-grid",children:[s.jsx(l,{cacheHitsSession:e.session,cacheHitsTotal:e.total,tokensSavedSession:e.sessionTokensSaved,tokensSavedTotal:e.totalTokensSaved,compactionsSession:o.session,compactionsTotal:o.total}),s.jsx(h,{compactSessionSec:t.compact.sessionSec,compactTotalSec:t.compact.totalSec,cacheHitSessionSec:t.cacheHit.sessionSec,cacheHitTotalSec:t.cacheHit.totalSec})]})})}export{p as default};
|
|
2
|
+
//# sourceMappingURL=CacheTab-DEQbTq2T.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CacheTab-
|
|
1
|
+
{"version":3,"file":"CacheTab-DEQbTq2T.js","sources":["../../src/tabs/CacheTab.tsx"],"sourcesContent":["/**\n * dashboard-client/src/tabs/CacheTab.tsx — Cache tab (NEW).\n *\n * Renders CacheHitsCard + TimeSavedCard from /api/snapshot\n * via the useApi hook with 5s polling.\n */\n\nimport type React from \"react\";\nimport { useCallback } from \"react\";\nimport type { SnapshotResponse } from \"@contracts\";\nimport { useApi } from \"../hooks/useApi\";\nimport { fetchSnapshot } from \"../api/client\";\nimport { CacheHitsCard } from \"../components/CacheHitsCard\";\nimport { TimeSavedCard } from \"../components/TimeSavedCard\";\n\nexport default function CacheTab(): React.ReactElement {\n\tconst { data: snapshot, loading, error } = useApi<SnapshotResponse>(\n\t\tuseCallback(() => fetchSnapshot(), []),\n\t\t{ pollInterval: 5000 },\n\t);\n\n\tif (loading && !snapshot)\n\t\treturn <div className=\"tab-stub\">Loading snapshot…</div>;\n\tif (error && !snapshot)\n\t\treturn <div className=\"tab-stub\">Error: {error.message}</div>;\n\tif (!snapshot) return <div className=\"tab-stub\">No snapshot data.</div>;\n\n\tconst { cacheHits, compacts, timeSaved } = snapshot;\n\n\treturn (\n\t\t<div className=\"cache-tab\">\n\t\t\t<div className=\"card-grid overview-card-grid\">\n\t\t\t\t<CacheHitsCard\n\t\t\t\t\tcacheHitsSession={cacheHits.session}\n\t\t\t\t\tcacheHitsTotal={cacheHits.total}\n\t\t\t\t\ttokensSavedSession={cacheHits.sessionTokensSaved}\n\t\t\t\t\ttokensSavedTotal={cacheHits.totalTokensSaved}\n\t\t\t\t\tcompactionsSession={compacts.session}\n\t\t\t\t\tcompactionsTotal={compacts.total}\n\t\t\t\t/>\n\t\t\t\t<TimeSavedCard\n\t\t\t\t\tcompactSessionSec={timeSaved.compact.sessionSec}\n\t\t\t\t\tcompactTotalSec={timeSaved.compact.totalSec}\n\t\t\t\t\tcacheHitSessionSec={timeSaved.cacheHit.sessionSec}\n\t\t\t\t\tcacheHitTotalSec={timeSaved.cacheHit.totalSec}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"],"names":["CacheTab","snapshot","loading","error","useApi","useCallback","fetchSnapshot","jsx","jsxs","cacheHits","compacts","timeSaved","CacheHitsCard","TimeSavedCard"],"mappings":"+GAeA,SAAwBA,GAA+B,CACtD,KAAM,CAAE,KAAMC,EAAU,QAAAC,EAAS,MAAAC,GAAUC,EAC1CC,cAAY,IAAMC,EAAA,EAAiB,EAAE,EACrC,CAAE,aAAc,GAAA,CAAK,EAGtB,GAAIJ,GAAW,CAACD,EACf,OAAOM,EAAAA,IAAC,MAAA,CAAI,UAAU,WAAW,SAAA,oBAAiB,EACnD,GAAIJ,GAAS,CAACF,EACb,OAAOO,EAAAA,KAAC,MAAA,CAAI,UAAU,WAAW,SAAA,CAAA,UAAQL,EAAM,OAAA,EAAQ,EACxD,GAAI,CAACF,EAAU,aAAQ,MAAA,CAAI,UAAU,WAAW,SAAA,oBAAiB,EAEjE,KAAM,CAAE,UAAAQ,EAAW,SAAAC,EAAU,UAAAC,CAAA,EAAcV,EAE3C,aACE,MAAA,CAAI,UAAU,YACd,SAAAO,EAAAA,KAAC,MAAA,CAAI,UAAU,+BACd,SAAA,CAAAD,EAAAA,IAACK,EAAA,CACA,iBAAkBH,EAAU,QAC5B,eAAgBA,EAAU,MAC1B,mBAAoBA,EAAU,mBAC9B,iBAAkBA,EAAU,iBAC5B,mBAAoBC,EAAS,QAC7B,iBAAkBA,EAAS,KAAA,CAAA,EAE5BH,EAAAA,IAACM,EAAA,CACA,kBAAmBF,EAAU,QAAQ,WACrC,gBAAiBA,EAAU,QAAQ,SACnC,mBAAoBA,EAAU,SAAS,WACvC,iBAAkBA,EAAU,SAAS,QAAA,CAAA,CACtC,CAAA,CACD,CAAA,CACD,CAEF"}
|