chapterhouse 0.7.0 → 0.8.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/agents/korg.agent.md +65 -0
- package/dist/api/korg.js +34 -0
- package/dist/api/korg.test.js +42 -0
- package/dist/api/server.js +238 -2
- package/dist/api/server.test.js +199 -0
- package/dist/config.js +28 -0
- package/dist/config.test.js +20 -0
- package/dist/copilot/agents.js +3 -4
- package/dist/copilot/agents.test.js +12 -1
- package/dist/copilot/orchestrator.js +12 -1
- package/dist/copilot/orchestrator.test.js +3 -7
- package/dist/copilot/system-message.js +11 -10
- package/dist/copilot/system-message.test.js +6 -1
- package/dist/copilot/tools.js +184 -376
- package/dist/copilot/tools.memory.test.js +32 -0
- package/dist/copilot/tools.wiki.test.js +53 -59
- package/dist/daemon.js +9 -0
- package/dist/memory/decisions.js +6 -5
- package/dist/memory/entities.js +20 -9
- package/dist/memory/hooks.js +151 -0
- package/dist/memory/hooks.test.js +325 -0
- package/dist/memory/hot-tier.js +37 -0
- package/dist/memory/hot-tier.test.js +30 -0
- package/dist/memory/housekeeping-scheduler.js +35 -0
- package/dist/memory/housekeeping-scheduler.test.js +50 -0
- package/dist/memory/inbox.js +10 -0
- package/dist/memory/index.js +3 -1
- package/dist/memory/migration.js +244 -0
- package/dist/memory/migration.test.js +100 -0
- package/dist/memory/reflect.js +273 -0
- package/dist/memory/reflect.test.js +254 -0
- package/dist/store/db.js +119 -4
- package/dist/store/db.test.js +19 -1
- package/dist/test/setup-env.js +1 -0
- package/dist/wiki/consolidation.js +641 -0
- package/dist/wiki/consolidation.test.js +140 -0
- package/dist/wiki/frontmatter.js +48 -0
- package/dist/wiki/frontmatter.test.js +42 -0
- package/dist/wiki/index-manager.js +246 -330
- package/dist/wiki/index-manager.test.js +138 -145
- package/dist/wiki/ingest.js +347 -0
- package/dist/wiki/ingest.test.js +111 -0
- package/dist/wiki/links.js +151 -0
- package/dist/wiki/links.test.js +176 -0
- package/dist/wiki/migrate-topics.test.js +16 -6
- package/dist/wiki/scheduler.js +118 -0
- package/dist/wiki/scheduler.test.js +64 -0
- package/dist/wiki/timeline.js +51 -0
- package/dist/wiki/timeline.test.js +65 -0
- package/dist/wiki/topic-structure.js +1 -1
- package/package.json +1 -1
- package/skills/pkb-ideas/SKILL.md +78 -0
- package/skills/pkb-ideas/_meta.json +4 -0
- package/skills/pkb-org/SKILL.md +82 -0
- package/skills/pkb-org/_meta.json +4 -0
- package/skills/pkb-people/SKILL.md +74 -0
- package/skills/pkb-people/_meta.json +4 -0
- package/skills/pkb-research/SKILL.md +83 -0
- package/skills/pkb-research/_meta.json +4 -0
- package/skills/pkb-source/SKILL.md +38 -0
- package/skills/pkb-source/_meta.json +4 -0
- package/skills/wiki-conventions/SKILL.md +5 -5
- package/web/dist/assets/{index-DuKYxMIR.css → index-5kz9aRU9.css} +1 -1
- package/web/dist/assets/{index-DytB69KC.js → index-BbX9RKf3.js} +91 -89
- package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/dist/wiki/context.js +0 -138
- package/dist/wiki/fix.js +0 -335
- package/dist/wiki/fix.test.js +0 -350
- package/dist/wiki/lint.js +0 -451
- package/dist/wiki/lint.test.js +0 -329
- package/web/dist/assets/index-DytB69KC.js.map +0 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
const repoRoot = process.cwd();
|
|
6
|
+
const testWorkRoot = join(repoRoot, ".test-work");
|
|
7
|
+
let sandboxRoot = "";
|
|
8
|
+
function resetSandbox() {
|
|
9
|
+
mkdirSync(testWorkRoot, { recursive: true });
|
|
10
|
+
sandboxRoot = mkdtempSync(join(testWorkRoot, "memory-reflect-"));
|
|
11
|
+
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
12
|
+
}
|
|
13
|
+
async function loadBaseModules() {
|
|
14
|
+
const nonce = `${Date.now()}-${Math.random()}`;
|
|
15
|
+
const dbModule = await import(new URL(`../store/db.js?case=${nonce}`, import.meta.url).href);
|
|
16
|
+
const memoryModule = await import(new URL(`./index.js?case=${nonce}`, import.meta.url).href);
|
|
17
|
+
return { dbModule, memoryModule };
|
|
18
|
+
}
|
|
19
|
+
async function loadReflectModule(t, llmResponse) {
|
|
20
|
+
t.mock.module("../copilot/oneshot.js", {
|
|
21
|
+
namedExports: {
|
|
22
|
+
runOneShotPrompt: async () => ({ content: llmResponse, model: "mock-model", attempts: 1 }),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
t.mock.module("../copilot/client.js", {
|
|
26
|
+
namedExports: {
|
|
27
|
+
getClient: async () => ({}),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
t.mock.module("../util/logger.js", {
|
|
31
|
+
namedExports: {
|
|
32
|
+
childLogger: () => ({ info: () => { }, warn: () => { }, error: () => { } }),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return await import(new URL(`./reflect.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
36
|
+
}
|
|
37
|
+
async function loadToolsModule(t) {
|
|
38
|
+
t.mock.module("../copilot/orchestrator.js", {
|
|
39
|
+
namedExports: {
|
|
40
|
+
getCurrentSourceChannel: () => "web",
|
|
41
|
+
getCurrentActivityCallback: () => undefined,
|
|
42
|
+
getCurrentActiveProjectRules: () => null,
|
|
43
|
+
getCurrentAuthenticatedUser: () => undefined,
|
|
44
|
+
getLastAuthenticatedUser: () => undefined,
|
|
45
|
+
getCurrentAuthorizationHeader: () => undefined,
|
|
46
|
+
getCurrentSessionKey: () => "session-reflect-test",
|
|
47
|
+
sendToAgentSession: async () => "",
|
|
48
|
+
invalidateOrchestratorSession: () => { },
|
|
49
|
+
maybeScheduleScopeChangeCheckpoint: () => { },
|
|
50
|
+
resetCheckpointSessionState: () => { },
|
|
51
|
+
switchSessionModel: async () => { },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
t.mock.module("../memory/reflect.js", {
|
|
55
|
+
namedExports: {
|
|
56
|
+
reflectOnScope: async () => ({ patternsCreated: 1, patternsUpdated: 0, contradictionsFound: 0 }),
|
|
57
|
+
reflectAllScopes: async () => ({
|
|
58
|
+
chapterhouse: { patternsCreated: 1, patternsUpdated: 0, contradictionsFound: 0 },
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
t.mock.module("../util/logger.js", {
|
|
63
|
+
namedExports: {
|
|
64
|
+
childLogger: () => ({ info: () => { }, warn: () => { }, error: () => { } }),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const nonce = `${Date.now()}-${Math.random()}`;
|
|
68
|
+
const toolsModule = await import(new URL(`../copilot/tools.js?case=${nonce}`, import.meta.url).href);
|
|
69
|
+
const agentsModule = await import(new URL(`../copilot/agents.js?case=${nonce}`, import.meta.url).href);
|
|
70
|
+
const dbModule = await import(new URL(`../store/db.js?case=${nonce}`, import.meta.url).href);
|
|
71
|
+
return { toolsModule, agentsModule, dbModule };
|
|
72
|
+
}
|
|
73
|
+
function getFunction(module, name) {
|
|
74
|
+
const value = module[name];
|
|
75
|
+
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function findTool(tools, name) {
|
|
79
|
+
const tool = tools.find((entry) => entry.name === name);
|
|
80
|
+
assert.ok(tool, `${name} tool should be registered`);
|
|
81
|
+
return tool;
|
|
82
|
+
}
|
|
83
|
+
test.beforeEach(async () => {
|
|
84
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
85
|
+
dbModule.closeDb();
|
|
86
|
+
resetSandbox();
|
|
87
|
+
});
|
|
88
|
+
test.afterEach(async () => {
|
|
89
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
90
|
+
dbModule.closeDb();
|
|
91
|
+
if (sandboxRoot) {
|
|
92
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
test("reflectOnScope creates a pattern when three similar observations accumulate for one entity", async (t) => {
|
|
96
|
+
const { dbModule, memoryModule } = await loadBaseModules();
|
|
97
|
+
const db = dbModule.getDb();
|
|
98
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
99
|
+
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
100
|
+
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
101
|
+
const chapterhouse = getScope("chapterhouse");
|
|
102
|
+
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
103
|
+
const workerQueue = upsertEntity({ scope_id: chapterhouse.id, kind: "subsystem", name: "worker-queue" });
|
|
104
|
+
const first = recordObservation({
|
|
105
|
+
scope_id: chapterhouse.id,
|
|
106
|
+
entity_id: workerQueue.id,
|
|
107
|
+
content: "The worker queue serializes task execution through SQLite state.",
|
|
108
|
+
source: "test",
|
|
109
|
+
tier: "hot",
|
|
110
|
+
});
|
|
111
|
+
const second = recordObservation({
|
|
112
|
+
scope_id: chapterhouse.id,
|
|
113
|
+
entity_id: workerQueue.id,
|
|
114
|
+
content: "Worker queue execution is serialized using SQLite-backed state.",
|
|
115
|
+
source: "test",
|
|
116
|
+
tier: "warm",
|
|
117
|
+
});
|
|
118
|
+
const third = recordObservation({
|
|
119
|
+
scope_id: chapterhouse.id,
|
|
120
|
+
entity_id: workerQueue.id,
|
|
121
|
+
content: "SQLite keeps worker queue execution serialized across turns.",
|
|
122
|
+
source: "test",
|
|
123
|
+
tier: "warm",
|
|
124
|
+
});
|
|
125
|
+
const reflectModule = await loadReflectModule(t, JSON.stringify({
|
|
126
|
+
title: "Queue execution pattern",
|
|
127
|
+
summary: "Worker queue execution stays serialized through SQLite-backed coordination.",
|
|
128
|
+
confidence: 0.88,
|
|
129
|
+
}));
|
|
130
|
+
const result = await reflectModule.reflectOnScope("chapterhouse", db);
|
|
131
|
+
assert.equal(result.patternsCreated >= 1, true);
|
|
132
|
+
assert.equal(result.patternsCreated + result.patternsUpdated >= 1, true);
|
|
133
|
+
assert.equal(result.contradictionsFound, 0);
|
|
134
|
+
const pattern = db.prepare(`
|
|
135
|
+
SELECT title, summary, source_observation_ids, confidence, tier
|
|
136
|
+
FROM mem_patterns
|
|
137
|
+
ORDER BY id DESC
|
|
138
|
+
LIMIT 1
|
|
139
|
+
`).get();
|
|
140
|
+
assert.ok(pattern, "reflectOnScope should persist a pattern");
|
|
141
|
+
assert.equal(pattern.title, "Queue execution pattern");
|
|
142
|
+
assert.match(pattern.summary, /serialized/i);
|
|
143
|
+
assert.deepEqual(JSON.parse(pattern.source_observation_ids), [first.id, second.id, third.id]);
|
|
144
|
+
assert.equal(pattern.confidence, 0.88);
|
|
145
|
+
assert.equal(pattern.tier, "warm");
|
|
146
|
+
});
|
|
147
|
+
test("reflectOnScope counts contradictions inside the same entity group", async (t) => {
|
|
148
|
+
const { dbModule, memoryModule } = await loadBaseModules();
|
|
149
|
+
const db = dbModule.getDb();
|
|
150
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
151
|
+
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
152
|
+
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
153
|
+
const chapterhouse = getScope("chapterhouse");
|
|
154
|
+
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
155
|
+
const auth = upsertEntity({ scope_id: chapterhouse.id, kind: "subsystem", name: "auth" });
|
|
156
|
+
recordObservation({ scope_id: chapterhouse.id, entity_id: auth.id, content: "Auth used GitHub login for sign-in.", source: "test" });
|
|
157
|
+
recordObservation({ scope_id: chapterhouse.id, entity_id: auth.id, content: "Auth changed to Entra ID for sign-in.", source: "test" });
|
|
158
|
+
recordObservation({ scope_id: chapterhouse.id, entity_id: auth.id, content: "Auth no longer uses GitHub login.", source: "test" });
|
|
159
|
+
const reflectModule = await loadReflectModule(t, JSON.stringify({
|
|
160
|
+
title: "Auth provider transition",
|
|
161
|
+
summary: "Authentication moved away from GitHub login toward Entra ID.",
|
|
162
|
+
confidence: 0.82,
|
|
163
|
+
}));
|
|
164
|
+
const result = await reflectModule.reflectOnScope("chapterhouse", db);
|
|
165
|
+
assert.equal(result.contradictionsFound, 1);
|
|
166
|
+
});
|
|
167
|
+
test("reflectOnScope folds global observations into project-scope reflection", async (t) => {
|
|
168
|
+
const { dbModule, memoryModule } = await loadBaseModules();
|
|
169
|
+
const db = dbModule.getDb();
|
|
170
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
171
|
+
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
172
|
+
const chapterhouse = getScope("chapterhouse");
|
|
173
|
+
const global = getScope("global");
|
|
174
|
+
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
175
|
+
assert.ok(global, "global scope should be seeded");
|
|
176
|
+
const globalObservation = recordObservation({
|
|
177
|
+
scope_id: global.id,
|
|
178
|
+
content: "SQLite WAL keeps Chapterhouse memory writes fast.",
|
|
179
|
+
source: "test",
|
|
180
|
+
tier: "warm",
|
|
181
|
+
});
|
|
182
|
+
const scopedOne = recordObservation({
|
|
183
|
+
scope_id: chapterhouse.id,
|
|
184
|
+
content: "Chapterhouse memory writes stay fast because SQLite uses WAL mode.",
|
|
185
|
+
source: "test",
|
|
186
|
+
tier: "hot",
|
|
187
|
+
});
|
|
188
|
+
const scopedTwo = recordObservation({
|
|
189
|
+
scope_id: chapterhouse.id,
|
|
190
|
+
content: "WAL mode keeps Chapterhouse memory writes quick under concurrency.",
|
|
191
|
+
source: "test",
|
|
192
|
+
tier: "warm",
|
|
193
|
+
});
|
|
194
|
+
const reflectModule = await loadReflectModule(t, JSON.stringify({
|
|
195
|
+
title: "SQLite WAL performance pattern",
|
|
196
|
+
summary: "Across scopes, Chapterhouse relies on SQLite WAL mode for fast memory writes.",
|
|
197
|
+
confidence: 0.91,
|
|
198
|
+
}));
|
|
199
|
+
const result = await reflectModule.reflectOnScope("chapterhouse", db);
|
|
200
|
+
assert.equal(result.patternsCreated, 1);
|
|
201
|
+
const pattern = db.prepare(`
|
|
202
|
+
SELECT source_observation_ids
|
|
203
|
+
FROM mem_patterns
|
|
204
|
+
ORDER BY id DESC
|
|
205
|
+
LIMIT 1
|
|
206
|
+
`).get();
|
|
207
|
+
assert.ok(pattern, "cross-scope reflection should persist a pattern");
|
|
208
|
+
assert.deepEqual(JSON.parse(pattern.source_observation_ids), [globalObservation.id, scopedOne.id, scopedTwo.id]);
|
|
209
|
+
});
|
|
210
|
+
test("memory_reflect runs end-to-end for chapterhouse and is only bound to orchestrator tools", async (t) => {
|
|
211
|
+
const { toolsModule, agentsModule, dbModule } = await loadToolsModule(t);
|
|
212
|
+
const db = dbModule.getDb();
|
|
213
|
+
const tools = toolsModule.createTools({
|
|
214
|
+
client: { async listModels() { return []; } },
|
|
215
|
+
onAgentTaskComplete: () => { },
|
|
216
|
+
});
|
|
217
|
+
const bindToolsToAgent = agentsModule.bindToolsToAgent;
|
|
218
|
+
const filterToolsForAgent = agentsModule.filterToolsForAgent;
|
|
219
|
+
assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
|
|
220
|
+
assert.equal(typeof filterToolsForAgent, "function", "filterToolsForAgent should be exported");
|
|
221
|
+
const chapterhouseVisibleTools = filterToolsForAgent({
|
|
222
|
+
slug: "chapterhouse",
|
|
223
|
+
name: "Chapterhouse",
|
|
224
|
+
description: "Orchestrator",
|
|
225
|
+
model: "auto",
|
|
226
|
+
systemMessage: "test",
|
|
227
|
+
}, tools);
|
|
228
|
+
const coderVisibleTools = filterToolsForAgent({
|
|
229
|
+
slug: "coder",
|
|
230
|
+
name: "Coder",
|
|
231
|
+
description: "Software engineer",
|
|
232
|
+
model: "gpt-5.4",
|
|
233
|
+
systemMessage: "test",
|
|
234
|
+
}, tools);
|
|
235
|
+
const chapterhouseTools = bindToolsToAgent("chapterhouse", chapterhouseVisibleTools);
|
|
236
|
+
const coderTools = bindToolsToAgent("coder", coderVisibleTools);
|
|
237
|
+
assert.equal(chapterhouseTools.some((tool) => tool.name === "memory_reflect"), true);
|
|
238
|
+
assert.equal(coderTools.some((tool) => tool.name === "memory_reflect"), false);
|
|
239
|
+
const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = 'chapterhouse'`).get();
|
|
240
|
+
db.prepare(`
|
|
241
|
+
INSERT INTO mem_observations (scope_id, content, source, tier)
|
|
242
|
+
VALUES (?, ?, 'test', 'hot'), (?, ?, 'test', 'warm'), (?, ?, 'test', 'warm')
|
|
243
|
+
`).run(scope.id, "The worker queue serializes task execution through SQLite state.", scope.id, "Worker queue execution is serialized using SQLite-backed state.", scope.id, "SQLite keeps worker queue execution serialized across turns.");
|
|
244
|
+
const memoryReflect = findTool(chapterhouseTools, "memory_reflect");
|
|
245
|
+
const result = await memoryReflect.handler({ scope: "chapterhouse" }, {});
|
|
246
|
+
assert.deepEqual(result, {
|
|
247
|
+
ok: true,
|
|
248
|
+
scope: "chapterhouse",
|
|
249
|
+
patterns_created: 1,
|
|
250
|
+
patterns_updated: 0,
|
|
251
|
+
contradictions_found: 0,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
//# sourceMappingURL=reflect.test.js.map
|
package/dist/store/db.js
CHANGED
|
@@ -63,6 +63,7 @@ function rebuildMemoryTierTables(database) {
|
|
|
63
63
|
CREATE TABLE mem_entities (
|
|
64
64
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
65
|
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
66
|
+
slug TEXT,
|
|
66
67
|
kind TEXT NOT NULL,
|
|
67
68
|
name TEXT NOT NULL,
|
|
68
69
|
summary TEXT,
|
|
@@ -76,8 +77,8 @@ function rebuildMemoryTierTables(database) {
|
|
|
76
77
|
)
|
|
77
78
|
`);
|
|
78
79
|
database.exec(`
|
|
79
|
-
INSERT INTO mem_entities (id, scope_id, kind, name, summary, tier, confidence, created_at, updated_at)
|
|
80
|
-
SELECT id, scope_id, kind, name, summary, ${entityTierCase()}, confidence, created_at, updated_at
|
|
80
|
+
INSERT INTO mem_entities (id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at)
|
|
81
|
+
SELECT id, scope_id, NULL, kind, name, summary, ${entityTierCase()}, confidence, created_at, updated_at
|
|
81
82
|
FROM mem_entities_legacy_tier
|
|
82
83
|
`);
|
|
83
84
|
database.exec(`DROP TABLE mem_entities_legacy_tier`);
|
|
@@ -117,6 +118,7 @@ function rebuildMemoryTierTables(database) {
|
|
|
117
118
|
title TEXT NOT NULL,
|
|
118
119
|
rationale TEXT NOT NULL,
|
|
119
120
|
decided_at TEXT NOT NULL,
|
|
121
|
+
source TEXT,
|
|
120
122
|
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
121
123
|
superseded_by INTEGER REFERENCES mem_decisions(id) ON DELETE SET NULL,
|
|
122
124
|
archived_at DATETIME,
|
|
@@ -128,9 +130,9 @@ function rebuildMemoryTierTables(database) {
|
|
|
128
130
|
`);
|
|
129
131
|
database.exec(`
|
|
130
132
|
INSERT INTO mem_decisions (
|
|
131
|
-
id, scope_id, entity_id, title, rationale, decided_at, tier, superseded_by, archived_at, created_at
|
|
133
|
+
id, scope_id, entity_id, title, rationale, decided_at, source, tier, superseded_by, archived_at, created_at
|
|
132
134
|
)
|
|
133
|
-
SELECT id, scope_id, entity_id, title, rationale, decided_at, ${memoryTierCase()}, superseded_by, archived_at, created_at
|
|
135
|
+
SELECT id, scope_id, entity_id, title, rationale, decided_at, NULL, ${memoryTierCase()}, superseded_by, archived_at, created_at
|
|
134
136
|
FROM mem_decisions_legacy_tier
|
|
135
137
|
`);
|
|
136
138
|
database.exec(`DROP TABLE mem_decisions_legacy_tier`);
|
|
@@ -198,6 +200,7 @@ function ensureMemoryTierColumns(database) {
|
|
|
198
200
|
function ensureMemoryIndexes(database) {
|
|
199
201
|
database.exec(`CREATE INDEX IF NOT EXISTS mem_entities_scope_kind_idx ON mem_entities(scope_id, kind)`);
|
|
200
202
|
database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_kind_name_idx ON mem_entities(scope_id, kind, name)`);
|
|
203
|
+
database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_slug_idx ON mem_entities(scope_id, slug) WHERE slug IS NOT NULL`);
|
|
201
204
|
database.exec(`CREATE INDEX IF NOT EXISTS mem_observations_scope_idx ON mem_observations(scope_id)`);
|
|
202
205
|
database.exec(`CREATE INDEX IF NOT EXISTS mem_decisions_scope_idx ON mem_decisions(scope_id)`);
|
|
203
206
|
database.exec(`CREATE INDEX IF NOT EXISTS idx_mem_action_items_scope_status ON mem_action_items(scope_id, status)`);
|
|
@@ -752,9 +755,24 @@ export function getDb() {
|
|
|
752
755
|
`);
|
|
753
756
|
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_scopes_slug_idx ON mem_scopes(slug)`);
|
|
754
757
|
db.exec(`
|
|
758
|
+
CREATE TABLE IF NOT EXISTS mem_patterns (
|
|
759
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
760
|
+
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
761
|
+
title TEXT NOT NULL,
|
|
762
|
+
summary TEXT NOT NULL,
|
|
763
|
+
source_observation_ids TEXT NOT NULL DEFAULT '[]',
|
|
764
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
765
|
+
tier TEXT NOT NULL DEFAULT 'warm',
|
|
766
|
+
created_at TEXT NOT NULL,
|
|
767
|
+
last_updated TEXT NOT NULL
|
|
768
|
+
)
|
|
769
|
+
`);
|
|
770
|
+
db.exec(`CREATE INDEX IF NOT EXISTS mem_patterns_scope_tier_idx ON mem_patterns(scope_id, tier)`);
|
|
771
|
+
db.exec(`
|
|
755
772
|
CREATE TABLE IF NOT EXISTS mem_entities (
|
|
756
773
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
757
774
|
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
775
|
+
slug TEXT,
|
|
758
776
|
kind TEXT NOT NULL,
|
|
759
777
|
name TEXT NOT NULL,
|
|
760
778
|
summary TEXT,
|
|
@@ -767,6 +785,10 @@ export function getDb() {
|
|
|
767
785
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
768
786
|
)
|
|
769
787
|
`);
|
|
788
|
+
const entityCols = db.prepare(`PRAGMA table_info(mem_entities)`).all();
|
|
789
|
+
if (!entityCols.some((column) => column.name === "slug")) {
|
|
790
|
+
db.exec(`ALTER TABLE mem_entities ADD COLUMN slug TEXT`);
|
|
791
|
+
}
|
|
770
792
|
db.exec(`
|
|
771
793
|
DELETE FROM mem_entities
|
|
772
794
|
WHERE id NOT IN (
|
|
@@ -776,6 +798,7 @@ export function getDb() {
|
|
|
776
798
|
)
|
|
777
799
|
`);
|
|
778
800
|
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_kind_name_idx ON mem_entities(scope_id, kind, name)`);
|
|
801
|
+
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_slug_idx ON mem_entities(scope_id, slug) WHERE slug IS NOT NULL`);
|
|
779
802
|
db.exec(`
|
|
780
803
|
CREATE TABLE IF NOT EXISTS mem_observations (
|
|
781
804
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -809,6 +832,7 @@ export function getDb() {
|
|
|
809
832
|
title TEXT NOT NULL,
|
|
810
833
|
rationale TEXT NOT NULL,
|
|
811
834
|
decided_at TEXT NOT NULL,
|
|
835
|
+
source TEXT,
|
|
812
836
|
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
813
837
|
superseded_by INTEGER REFERENCES mem_decisions(id) ON DELETE SET NULL,
|
|
814
838
|
archived_at DATETIME,
|
|
@@ -839,6 +863,55 @@ export function getDb() {
|
|
|
839
863
|
last_recalled_at TEXT
|
|
840
864
|
)
|
|
841
865
|
`);
|
|
866
|
+
db.exec(`
|
|
867
|
+
CREATE TABLE IF NOT EXISTS wiki_pages (
|
|
868
|
+
path TEXT PRIMARY KEY,
|
|
869
|
+
title TEXT NOT NULL,
|
|
870
|
+
entity_type TEXT,
|
|
871
|
+
tags TEXT DEFAULT '[]',
|
|
872
|
+
summary TEXT,
|
|
873
|
+
last_updated TEXT,
|
|
874
|
+
visibility TEXT DEFAULT 'private',
|
|
875
|
+
version INTEGER DEFAULT 1,
|
|
876
|
+
compiled_truth_hash TEXT,
|
|
877
|
+
pinned INTEGER DEFAULT 0
|
|
878
|
+
)
|
|
879
|
+
`);
|
|
880
|
+
db.exec(`
|
|
881
|
+
CREATE TABLE IF NOT EXISTS wiki_sources (
|
|
882
|
+
id TEXT PRIMARY KEY,
|
|
883
|
+
source_type TEXT NOT NULL,
|
|
884
|
+
origin TEXT NOT NULL,
|
|
885
|
+
title TEXT,
|
|
886
|
+
ingested_at TEXT NOT NULL,
|
|
887
|
+
raw_path TEXT,
|
|
888
|
+
parsed_content TEXT,
|
|
889
|
+
pages_updated TEXT DEFAULT '[]',
|
|
890
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
891
|
+
session_id TEXT,
|
|
892
|
+
session_name TEXT
|
|
893
|
+
)
|
|
894
|
+
`);
|
|
895
|
+
db.exec(`
|
|
896
|
+
CREATE TABLE IF NOT EXISTS wiki_links (
|
|
897
|
+
from_page TEXT NOT NULL,
|
|
898
|
+
to_page TEXT NOT NULL,
|
|
899
|
+
link_type TEXT NOT NULL,
|
|
900
|
+
extracted_at TEXT NOT NULL,
|
|
901
|
+
PRIMARY KEY (from_page, to_page, link_type)
|
|
902
|
+
)
|
|
903
|
+
`);
|
|
904
|
+
db.exec(`CREATE INDEX IF NOT EXISTS wiki_links_to ON wiki_links(to_page)`);
|
|
905
|
+
const wikiSourceCols = db.prepare(`PRAGMA table_info(wiki_sources)`).all();
|
|
906
|
+
if (!wikiSourceCols.some((column) => column.name === "status")) {
|
|
907
|
+
db.exec(`ALTER TABLE wiki_sources ADD COLUMN status TEXT NOT NULL DEFAULT 'active'`);
|
|
908
|
+
}
|
|
909
|
+
if (!wikiSourceCols.some((column) => column.name === "session_id")) {
|
|
910
|
+
db.exec(`ALTER TABLE wiki_sources ADD COLUMN session_id TEXT`);
|
|
911
|
+
}
|
|
912
|
+
if (!wikiSourceCols.some((column) => column.name === "session_name")) {
|
|
913
|
+
db.exec(`ALTER TABLE wiki_sources ADD COLUMN session_name TEXT`);
|
|
914
|
+
}
|
|
842
915
|
const decisionCols = db.prepare(`PRAGMA table_info(mem_decisions)`).all();
|
|
843
916
|
if (!decisionCols.some((column) => column.name === "superseded_by")) {
|
|
844
917
|
db.exec(`ALTER TABLE mem_decisions ADD COLUMN superseded_by INTEGER REFERENCES mem_decisions(id) ON DELETE SET NULL`);
|
|
@@ -846,6 +919,9 @@ export function getDb() {
|
|
|
846
919
|
if (!decisionCols.some((column) => column.name === "archived_at")) {
|
|
847
920
|
db.exec(`ALTER TABLE mem_decisions ADD COLUMN archived_at DATETIME`);
|
|
848
921
|
}
|
|
922
|
+
if (!decisionCols.some((column) => column.name === "source")) {
|
|
923
|
+
db.exec(`ALTER TABLE mem_decisions ADD COLUMN source TEXT`);
|
|
924
|
+
}
|
|
849
925
|
rebuildMemoryTierTables(db);
|
|
850
926
|
ensureMemoryTierColumns(db);
|
|
851
927
|
ensureMemoryIndexes(db);
|
|
@@ -997,6 +1073,40 @@ export function getDb() {
|
|
|
997
1073
|
INSERT INTO mem_action_items_fts(rowid, title, detail)
|
|
998
1074
|
VALUES (new.id, new.title, new.detail);
|
|
999
1075
|
END
|
|
1076
|
+
`);
|
|
1077
|
+
db.exec(`
|
|
1078
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS wiki_pages_fts USING fts5(
|
|
1079
|
+
path UNINDEXED,
|
|
1080
|
+
title,
|
|
1081
|
+
entity_type,
|
|
1082
|
+
tags,
|
|
1083
|
+
summary,
|
|
1084
|
+
content='wiki_pages',
|
|
1085
|
+
content_rowid='rowid'
|
|
1086
|
+
)
|
|
1087
|
+
`);
|
|
1088
|
+
db.exec(`DROP TRIGGER IF EXISTS wiki_pages_ai`);
|
|
1089
|
+
db.exec(`DROP TRIGGER IF EXISTS wiki_pages_ad`);
|
|
1090
|
+
db.exec(`DROP TRIGGER IF EXISTS wiki_pages_au`);
|
|
1091
|
+
db.exec(`
|
|
1092
|
+
CREATE TRIGGER wiki_pages_ai AFTER INSERT ON wiki_pages BEGIN
|
|
1093
|
+
INSERT INTO wiki_pages_fts(rowid, path, title, entity_type, tags, summary)
|
|
1094
|
+
VALUES (new.rowid, new.path, new.title, new.entity_type, new.tags, new.summary);
|
|
1095
|
+
END
|
|
1096
|
+
`);
|
|
1097
|
+
db.exec(`
|
|
1098
|
+
CREATE TRIGGER wiki_pages_ad AFTER DELETE ON wiki_pages BEGIN
|
|
1099
|
+
INSERT INTO wiki_pages_fts(wiki_pages_fts, rowid, path, title, entity_type, tags, summary)
|
|
1100
|
+
VALUES('delete', old.rowid, old.path, old.title, old.entity_type, old.tags, old.summary);
|
|
1101
|
+
END
|
|
1102
|
+
`);
|
|
1103
|
+
db.exec(`
|
|
1104
|
+
CREATE TRIGGER wiki_pages_au AFTER UPDATE ON wiki_pages BEGIN
|
|
1105
|
+
INSERT INTO wiki_pages_fts(wiki_pages_fts, rowid, path, title, entity_type, tags, summary)
|
|
1106
|
+
VALUES('delete', old.rowid, old.path, old.title, old.entity_type, old.tags, old.summary);
|
|
1107
|
+
INSERT INTO wiki_pages_fts(rowid, path, title, entity_type, tags, summary)
|
|
1108
|
+
VALUES(new.rowid, new.path, new.title, new.entity_type, new.tags, new.summary);
|
|
1109
|
+
END
|
|
1000
1110
|
`);
|
|
1001
1111
|
// Backfill: check if FTS is in sync by comparing row counts
|
|
1002
1112
|
const memCount = db.prepare(`SELECT COUNT(*) as c FROM memories`).get().c;
|
|
@@ -1019,6 +1129,11 @@ export function getDb() {
|
|
|
1019
1129
|
if (actionItemCount > 0 && actionItemFtsCount < actionItemCount) {
|
|
1020
1130
|
db.exec(`INSERT INTO mem_action_items_fts(mem_action_items_fts) VALUES ('rebuild')`);
|
|
1021
1131
|
}
|
|
1132
|
+
const wikiPageCount = db.prepare(`SELECT COUNT(*) as c FROM wiki_pages`).get().c;
|
|
1133
|
+
const wikiPageFtsCount = db.prepare(`SELECT COUNT(*) as c FROM wiki_pages_fts`).get().c;
|
|
1134
|
+
if (wikiPageCount > 0 && wikiPageFtsCount < wikiPageCount) {
|
|
1135
|
+
db.exec(`INSERT INTO wiki_pages_fts(wiki_pages_fts) VALUES ('rebuild')`);
|
|
1136
|
+
}
|
|
1022
1137
|
fts5Available = true;
|
|
1023
1138
|
}
|
|
1024
1139
|
catch {
|
package/dist/store/db.test.js
CHANGED
|
@@ -58,7 +58,7 @@ test("getDb initializes schema, state helpers, and conversation formatting", asy
|
|
|
58
58
|
dbModule.closeDb();
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
|
-
test("getDb initializes action-item memory schema and FTS shadow", async () => {
|
|
61
|
+
test("getDb initializes action-item memory schema, reflect patterns schema, and FTS shadow", async () => {
|
|
62
62
|
const dbModule = await loadDbModule();
|
|
63
63
|
try {
|
|
64
64
|
const db = dbModule.getDb();
|
|
@@ -66,6 +66,7 @@ test("getDb initializes action-item memory schema and FTS shadow", async () => {
|
|
|
66
66
|
const tableNames = new Set(tables.map((row) => row.name));
|
|
67
67
|
assert.equal(tableNames.has("mem_action_items"), true, "expected mem_action_items table");
|
|
68
68
|
assert.equal(tableNames.has("mem_action_items_fts"), true, "expected mem_action_items_fts virtual table");
|
|
69
|
+
assert.equal(tableNames.has("mem_patterns"), true, "expected mem_patterns table");
|
|
69
70
|
const columns = db.prepare(`PRAGMA table_info(mem_action_items)`).all();
|
|
70
71
|
const columnNames = new Set(columns.map((column) => column.name));
|
|
71
72
|
for (const name of [
|
|
@@ -89,6 +90,23 @@ test("getDb initializes action-item memory schema and FTS shadow", async () => {
|
|
|
89
90
|
]) {
|
|
90
91
|
assert.equal(columnNames.has(name), true, `expected mem_action_items.${name}`);
|
|
91
92
|
}
|
|
93
|
+
const patternColumns = db.prepare(`PRAGMA table_info(mem_patterns)`).all();
|
|
94
|
+
const patternColumnNames = new Set(patternColumns.map((column) => column.name));
|
|
95
|
+
for (const name of [
|
|
96
|
+
"id",
|
|
97
|
+
"scope_id",
|
|
98
|
+
"title",
|
|
99
|
+
"summary",
|
|
100
|
+
"source_observation_ids",
|
|
101
|
+
"confidence",
|
|
102
|
+
"tier",
|
|
103
|
+
"created_at",
|
|
104
|
+
"last_updated",
|
|
105
|
+
]) {
|
|
106
|
+
assert.equal(patternColumnNames.has(name), true, `expected mem_patterns.${name}`);
|
|
107
|
+
}
|
|
108
|
+
const patternIndexes = db.prepare(`PRAGMA index_list(mem_patterns)`).all();
|
|
109
|
+
assert.equal(patternIndexes.some((index) => index.name === "mem_patterns_scope_tier_idx"), true, "expected mem_patterns scope/tier index");
|
|
92
110
|
const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = 'chapterhouse'`).get();
|
|
93
111
|
const inserted = db.prepare(`
|
|
94
112
|
INSERT INTO mem_action_items (scope_id, title, detail, source)
|
package/dist/test/setup-env.js
CHANGED
|
@@ -12,6 +12,7 @@ const RUNTIME_OVERRIDE_ENV_VARS = [
|
|
|
12
12
|
"CHAPTERHOUSE_MEMORY_INJECT",
|
|
13
13
|
"CHAPTERHOUSE_MEMORY_AUTO_ACCEPT",
|
|
14
14
|
"CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED",
|
|
15
|
+
"CHAPTERHOUSE_MEMORY_HOOKS_ENABLED",
|
|
15
16
|
"CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED",
|
|
16
17
|
"CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS",
|
|
17
18
|
"CHAPTERHOUSE_MEMORY_DECAY_DAYS",
|