opencodekit 0.20.3 → 0.20.5
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/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +14 -9
- package/dist/template/.opencode/agent/build.md +0 -32
- package/dist/template/.opencode/agent/plan.md +0 -14
- package/dist/template/.opencode/agent/review.md +0 -40
- package/dist/template/.opencode/command/create.md +11 -61
- package/dist/template/.opencode/command/plan.md +11 -12
- package/dist/template/.opencode/command/pr.md +4 -16
- package/dist/template/.opencode/command/research.md +7 -16
- package/dist/template/.opencode/command/resume.md +2 -11
- package/dist/template/.opencode/command/review-codebase.md +9 -15
- package/dist/template/.opencode/command/ship.md +12 -53
- package/dist/template/.opencode/memory/project/user.md +7 -0
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +54 -67
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/README.md +1 -1
- package/dist/template/.opencode/plugin/lib/compact.ts +194 -0
- package/dist/template/.opencode/plugin/lib/db/graph.ts +253 -0
- package/dist/template/.opencode/plugin/lib/db/observations.ts +8 -3
- package/dist/template/.opencode/plugin/lib/db/schema.ts +96 -5
- package/dist/template/.opencode/plugin/lib/db/types.ts +73 -0
- package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +36 -3
- package/dist/template/.opencode/plugin/lib/memory-db.ts +12 -1
- package/dist/template/.opencode/plugin/lib/memory-tools.ts +137 -1
- package/dist/template/.opencode/plugin/memory.ts +2 -1
- package/dist/template/.opencode/skill/memory-grounding/SKILL.md +68 -0
- package/dist/template/.opencode/skill/verification-gates/SKILL.md +63 -0
- package/dist/template/.opencode/skill/workspace-setup/SKILL.md +76 -0
- package/package.json +1 -1
|
@@ -175,23 +175,25 @@
|
|
|
175
175
|
"output": 32000
|
|
176
176
|
},
|
|
177
177
|
"options": {
|
|
178
|
-
"
|
|
179
|
-
"type": "enabled"
|
|
178
|
+
"reasoningEffort": "high"
|
|
180
179
|
},
|
|
181
180
|
"reasoning": true,
|
|
182
181
|
"temperature": true,
|
|
183
182
|
"tool_call": true,
|
|
184
183
|
"variants": {
|
|
185
|
-
"
|
|
184
|
+
"low": {
|
|
186
185
|
"options": {
|
|
187
|
-
"
|
|
188
|
-
"type": "enabled"
|
|
186
|
+
"reasoningEffort": "low"
|
|
189
187
|
}
|
|
190
188
|
},
|
|
191
|
-
"
|
|
189
|
+
"medium": {
|
|
192
190
|
"options": {
|
|
193
|
-
"
|
|
194
|
-
|
|
191
|
+
"reasoningEffort": "medium"
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"high": {
|
|
195
|
+
"options": {
|
|
196
|
+
"reasoningEffort": "high"
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
}
|
|
@@ -203,20 +205,25 @@
|
|
|
203
205
|
"output": 32000
|
|
204
206
|
},
|
|
205
207
|
"options": {
|
|
206
|
-
"
|
|
208
|
+
"reasoningEffort": "medium"
|
|
207
209
|
},
|
|
208
210
|
"reasoning": true,
|
|
209
211
|
"temperature": true,
|
|
210
212
|
"tool_call": true,
|
|
211
213
|
"variants": {
|
|
212
|
-
"
|
|
214
|
+
"low": {
|
|
213
215
|
"options": {
|
|
214
|
-
"
|
|
216
|
+
"reasoningEffort": "low"
|
|
215
217
|
}
|
|
216
218
|
},
|
|
217
|
-
"
|
|
219
|
+
"medium": {
|
|
218
220
|
"options": {
|
|
219
|
-
"
|
|
221
|
+
"reasoningEffort": "medium"
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
"high": {
|
|
225
|
+
"options": {
|
|
226
|
+
"reasoningEffort": "high"
|
|
220
227
|
}
|
|
221
228
|
}
|
|
222
229
|
}
|
|
@@ -228,40 +235,25 @@
|
|
|
228
235
|
"output": 64000
|
|
229
236
|
},
|
|
230
237
|
"options": {
|
|
231
|
-
"
|
|
232
|
-
"budget_tokens": 24000,
|
|
233
|
-
"type": "enabled"
|
|
234
|
-
}
|
|
238
|
+
"reasoningEffort": "high"
|
|
235
239
|
},
|
|
236
240
|
"reasoning": true,
|
|
237
241
|
"temperature": true,
|
|
238
242
|
"tool_call": true,
|
|
239
243
|
"variants": {
|
|
240
|
-
"
|
|
244
|
+
"low": {
|
|
241
245
|
"options": {
|
|
242
|
-
"
|
|
243
|
-
"output_config": {
|
|
244
|
-
"effort": "max"
|
|
245
|
-
},
|
|
246
|
-
"thinking": {
|
|
247
|
-
"type": "adaptive"
|
|
248
|
-
}
|
|
246
|
+
"reasoningEffort": "low"
|
|
249
247
|
}
|
|
250
248
|
},
|
|
251
|
-
"
|
|
249
|
+
"medium": {
|
|
252
250
|
"options": {
|
|
253
|
-
"
|
|
254
|
-
"budget_tokens": 24000,
|
|
255
|
-
"type": "enabled"
|
|
256
|
-
}
|
|
251
|
+
"reasoningEffort": "medium"
|
|
257
252
|
}
|
|
258
253
|
},
|
|
259
|
-
"
|
|
254
|
+
"high": {
|
|
260
255
|
"options": {
|
|
261
|
-
"
|
|
262
|
-
"budget_tokens": 32000,
|
|
263
|
-
"type": "enabled"
|
|
264
|
-
}
|
|
256
|
+
"reasoningEffort": "high"
|
|
265
257
|
}
|
|
266
258
|
}
|
|
267
259
|
}
|
|
@@ -273,20 +265,25 @@
|
|
|
273
265
|
"output": 16000
|
|
274
266
|
},
|
|
275
267
|
"options": {
|
|
276
|
-
"
|
|
268
|
+
"reasoningEffort": "medium"
|
|
277
269
|
},
|
|
278
270
|
"reasoning": true,
|
|
279
271
|
"temperature": true,
|
|
280
272
|
"tool_call": true,
|
|
281
273
|
"variants": {
|
|
282
|
-
"
|
|
274
|
+
"low": {
|
|
283
275
|
"options": {
|
|
284
|
-
"
|
|
276
|
+
"reasoningEffort": "low"
|
|
285
277
|
}
|
|
286
278
|
},
|
|
287
|
-
"
|
|
279
|
+
"medium": {
|
|
280
|
+
"options": {
|
|
281
|
+
"reasoningEffort": "medium"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"high": {
|
|
288
285
|
"options": {
|
|
289
|
-
"
|
|
286
|
+
"reasoningEffort": "high"
|
|
290
287
|
}
|
|
291
288
|
}
|
|
292
289
|
}
|
|
@@ -298,20 +295,25 @@
|
|
|
298
295
|
"output": 32000
|
|
299
296
|
},
|
|
300
297
|
"options": {
|
|
301
|
-
"
|
|
298
|
+
"reasoningEffort": "medium"
|
|
302
299
|
},
|
|
303
300
|
"reasoning": true,
|
|
304
301
|
"temperature": true,
|
|
305
302
|
"tool_call": true,
|
|
306
303
|
"variants": {
|
|
307
|
-
"
|
|
304
|
+
"low": {
|
|
308
305
|
"options": {
|
|
309
|
-
"
|
|
306
|
+
"reasoningEffort": "low"
|
|
310
307
|
}
|
|
311
308
|
},
|
|
312
|
-
"
|
|
309
|
+
"medium": {
|
|
313
310
|
"options": {
|
|
314
|
-
"
|
|
311
|
+
"reasoningEffort": "medium"
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
"high": {
|
|
315
|
+
"options": {
|
|
316
|
+
"reasoningEffort": "high"
|
|
315
317
|
}
|
|
316
318
|
}
|
|
317
319
|
}
|
|
@@ -323,40 +325,25 @@
|
|
|
323
325
|
"output": 32000
|
|
324
326
|
},
|
|
325
327
|
"options": {
|
|
326
|
-
"
|
|
327
|
-
"budget_tokens": 24000,
|
|
328
|
-
"type": "enabled"
|
|
329
|
-
}
|
|
328
|
+
"reasoningEffort": "high"
|
|
330
329
|
},
|
|
331
330
|
"reasoning": true,
|
|
332
331
|
"temperature": true,
|
|
333
332
|
"tool_call": true,
|
|
334
333
|
"variants": {
|
|
335
|
-
"
|
|
334
|
+
"low": {
|
|
336
335
|
"options": {
|
|
337
|
-
"
|
|
338
|
-
"output_config": {
|
|
339
|
-
"effort": "max"
|
|
340
|
-
},
|
|
341
|
-
"thinking": {
|
|
342
|
-
"type": "adaptive"
|
|
343
|
-
}
|
|
336
|
+
"reasoningEffort": "low"
|
|
344
337
|
}
|
|
345
338
|
},
|
|
346
|
-
"
|
|
339
|
+
"medium": {
|
|
347
340
|
"options": {
|
|
348
|
-
"
|
|
349
|
-
"budget_tokens": 16000,
|
|
350
|
-
"type": "enabled"
|
|
351
|
-
}
|
|
341
|
+
"reasoningEffort": "medium"
|
|
352
342
|
}
|
|
353
343
|
},
|
|
354
|
-
"
|
|
344
|
+
"high": {
|
|
355
345
|
"options": {
|
|
356
|
-
"
|
|
357
|
-
"budget_tokens": 32000,
|
|
358
|
-
"type": "enabled"
|
|
359
|
-
}
|
|
346
|
+
"reasoningEffort": "high"
|
|
360
347
|
}
|
|
361
348
|
}
|
|
362
349
|
}
|
|
@@ -41,7 +41,7 @@ plugin/
|
|
|
41
41
|
- Injects relevant knowledge into system prompt (BM25 _ recency _ confidence scoring)
|
|
42
42
|
- Manages context window via messages.transform (token budget enforcement)
|
|
43
43
|
- Merges compaction logic (beads, handoffs, project memory, knowledge)
|
|
44
|
-
- Provides
|
|
44
|
+
- Provides 11 tools: observation, memory-search, memory-get, memory-read, memory-update, memory-timeline, memory-graph-add, memory-graph-query, memory-graph-invalidate, memory-compact, memory-admin
|
|
45
45
|
|
|
46
46
|
- `sessions.ts`
|
|
47
47
|
- Provides tools: `find_sessions`, `read_session`
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact Format — AAAK-Inspired Memory Compression (v3)
|
|
3
|
+
*
|
|
4
|
+
* Inspired by MemPalace's AAAK dialect: a symbolic, pipe-separated format
|
|
5
|
+
* achieving ~3-5x compression while remaining readable by any LLM.
|
|
6
|
+
*
|
|
7
|
+
* Used for L1 wake-up context: compress top observations into a dense
|
|
8
|
+
* format that fits in ~200-300 tokens instead of 800+.
|
|
9
|
+
*
|
|
10
|
+
* Format rules:
|
|
11
|
+
* - Entities: 3-letter uppercase codes (ALC=Alice, KAI=Kai)
|
|
12
|
+
* - Categories: UPPERCASE labels (DECISION, PATTERN, WARNING)
|
|
13
|
+
* - Relationships: arrow notation (→, ←, ↔)
|
|
14
|
+
* - Importance: 1-5 stars (★ to ★★★★★)
|
|
15
|
+
* - Dates: ISO short (2026-03-31)
|
|
16
|
+
* - Pipe-separated fields within lines
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { CompactResult } from "./db/types.js";
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Entity Code Generation
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a 3-letter uppercase code from a name.
|
|
27
|
+
* Prioritizes: first 3 consonants, then first 3 chars.
|
|
28
|
+
*/
|
|
29
|
+
function generateCode(name: string): string {
|
|
30
|
+
const cleaned = name.replace(/[^a-zA-Z]/g, "").toUpperCase();
|
|
31
|
+
const consonants = cleaned.replace(/[AEIOU]/g, "");
|
|
32
|
+
if (consonants.length >= 3) return consonants.slice(0, 3);
|
|
33
|
+
return cleaned.slice(0, 3).padEnd(3, "X");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build entity code map from observation data.
|
|
38
|
+
* Deduplicates codes by appending numbers.
|
|
39
|
+
*/
|
|
40
|
+
function buildCodeMap(names: string[]): Map<string, string> {
|
|
41
|
+
const codeMap = new Map<string, string>();
|
|
42
|
+
const usedCodes = new Set<string>();
|
|
43
|
+
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
let code = generateCode(name);
|
|
46
|
+
if (usedCodes.has(code)) {
|
|
47
|
+
// Append incrementing suffix
|
|
48
|
+
let i = 2;
|
|
49
|
+
while (usedCodes.has(`${code.slice(0, 2)}${i}`)) i++;
|
|
50
|
+
code = `${code.slice(0, 2)}${i}`;
|
|
51
|
+
}
|
|
52
|
+
usedCodes.add(code);
|
|
53
|
+
codeMap.set(name.toLowerCase(), code);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return codeMap;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Observation Type Mapping
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
64
|
+
decision: "DECISION",
|
|
65
|
+
bugfix: "FIX",
|
|
66
|
+
feature: "FEAT",
|
|
67
|
+
pattern: "PATTERN",
|
|
68
|
+
discovery: "DISC",
|
|
69
|
+
learning: "LEARN",
|
|
70
|
+
warning: "WARN",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const CONFIDENCE_STARS: Record<string, string> = {
|
|
74
|
+
high: "★★★",
|
|
75
|
+
medium: "★★",
|
|
76
|
+
low: "★",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Compact Compression
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
interface ObservationSummary {
|
|
84
|
+
id: number;
|
|
85
|
+
type: string;
|
|
86
|
+
title: string;
|
|
87
|
+
narrative?: string | null;
|
|
88
|
+
concepts?: string | null;
|
|
89
|
+
wing?: string | null;
|
|
90
|
+
hall?: string | null;
|
|
91
|
+
room?: string | null;
|
|
92
|
+
confidence?: string | null;
|
|
93
|
+
created_at?: string | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Compress an array of observations into compact AAAK-inspired format.
|
|
98
|
+
* Returns compressed text + compression metrics.
|
|
99
|
+
*/
|
|
100
|
+
export function compactObservations(
|
|
101
|
+
observations: ObservationSummary[],
|
|
102
|
+
): CompactResult {
|
|
103
|
+
if (observations.length === 0) {
|
|
104
|
+
return {
|
|
105
|
+
compressed: "",
|
|
106
|
+
token_estimate: 0,
|
|
107
|
+
original_tokens: 0,
|
|
108
|
+
compression_ratio: 0,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Estimate original size
|
|
113
|
+
const originalText = observations
|
|
114
|
+
.map(
|
|
115
|
+
(o) =>
|
|
116
|
+
`[${o.type}] ${o.title}: ${o.narrative ?? ""} (${o.concepts ?? ""})`,
|
|
117
|
+
)
|
|
118
|
+
.join("\n");
|
|
119
|
+
const originalTokens = Math.ceil(originalText.length / 4);
|
|
120
|
+
|
|
121
|
+
// Collect entities for code generation
|
|
122
|
+
const entities: string[] = [];
|
|
123
|
+
for (const obs of observations) {
|
|
124
|
+
if (obs.wing) entities.push(obs.wing);
|
|
125
|
+
if (obs.room) entities.push(obs.room);
|
|
126
|
+
}
|
|
127
|
+
const codeMap = buildCodeMap([...new Set(entities)]);
|
|
128
|
+
|
|
129
|
+
// Group by type
|
|
130
|
+
const grouped = new Map<string, ObservationSummary[]>();
|
|
131
|
+
for (const obs of observations) {
|
|
132
|
+
const key = obs.type;
|
|
133
|
+
const existing = grouped.get(key) ?? [];
|
|
134
|
+
existing.push(obs);
|
|
135
|
+
grouped.set(key, existing);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Build compact lines
|
|
139
|
+
const lines: string[] = [];
|
|
140
|
+
|
|
141
|
+
// Header: entity legend (if entities exist)
|
|
142
|
+
if (codeMap.size > 0) {
|
|
143
|
+
const legend = [...codeMap.entries()]
|
|
144
|
+
.map(([name, code]) => `${code}=${name}`)
|
|
145
|
+
.join(" ");
|
|
146
|
+
lines.push(`ENTITIES: ${legend}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Observations grouped by type
|
|
150
|
+
for (const [type, obs] of grouped) {
|
|
151
|
+
const label = TYPE_LABELS[type] ?? type.toUpperCase();
|
|
152
|
+
const entries = obs.map((o) => {
|
|
153
|
+
const parts: string[] = [];
|
|
154
|
+
|
|
155
|
+
// Title (abbreviated)
|
|
156
|
+
const shortTitle =
|
|
157
|
+
o.title.length > 60 ? `${o.title.slice(0, 57)}...` : o.title;
|
|
158
|
+
parts.push(shortTitle);
|
|
159
|
+
|
|
160
|
+
// Navigation context
|
|
161
|
+
if (o.wing || o.room) {
|
|
162
|
+
const nav: string[] = [];
|
|
163
|
+
if (o.wing) nav.push(codeMap.get(o.wing.toLowerCase()) ?? o.wing);
|
|
164
|
+
if (o.room) nav.push(codeMap.get(o.room.toLowerCase()) ?? o.room);
|
|
165
|
+
parts.push(`@${nav.join("/")}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Confidence
|
|
169
|
+
if (o.confidence) {
|
|
170
|
+
parts.push(CONFIDENCE_STARS[o.confidence] ?? "★★");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Date
|
|
174
|
+
if (o.created_at) {
|
|
175
|
+
parts.push(o.created_at.slice(0, 10));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return parts.join(" | ");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
lines.push(`${label}: ${entries.join(" // ")}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const compressed = lines.join("\n");
|
|
185
|
+
const compressedTokens = Math.ceil(compressed.length / 4);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
compressed,
|
|
189
|
+
token_estimate: compressedTokens,
|
|
190
|
+
original_tokens: originalTokens,
|
|
191
|
+
compression_ratio:
|
|
192
|
+
originalTokens > 0 ? compressedTokens / originalTokens : 0,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity Graph Operations (v3)
|
|
3
|
+
*
|
|
4
|
+
* Temporal knowledge graph backed by SQLite entity_triples table.
|
|
5
|
+
* Supports time-aware queries (as_of), invalidation, and entity timeline.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by MemPalace's temporal KG pattern: facts have valid_from/valid_to
|
|
8
|
+
* dates, enabling "what was true on date X?" queries.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getMemoryDB } from "./schema.js";
|
|
12
|
+
import type {
|
|
13
|
+
EntityQueryResult,
|
|
14
|
+
EntityTripleInput,
|
|
15
|
+
EntityTripleRow,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// CRUD
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add a new entity triple to the knowledge graph.
|
|
24
|
+
*/
|
|
25
|
+
export function addEntityTriple(input: EntityTripleInput): number {
|
|
26
|
+
const db = getMemoryDB();
|
|
27
|
+
const now = new Date();
|
|
28
|
+
|
|
29
|
+
const validFrom = input.valid_from ?? now.toISOString().slice(0, 10);
|
|
30
|
+
|
|
31
|
+
const result = db
|
|
32
|
+
.query(
|
|
33
|
+
`INSERT INTO entity_triples
|
|
34
|
+
(subject, predicate, object, valid_from, valid_to, confidence, source_observation_id, created_at, created_at_epoch)
|
|
35
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
36
|
+
)
|
|
37
|
+
.run(
|
|
38
|
+
input.subject.toLowerCase().trim(),
|
|
39
|
+
input.predicate.toLowerCase().trim(),
|
|
40
|
+
input.object.toLowerCase().trim(),
|
|
41
|
+
validFrom,
|
|
42
|
+
input.valid_to ?? null,
|
|
43
|
+
input.confidence ?? 1.0,
|
|
44
|
+
input.source_observation_id ?? null,
|
|
45
|
+
now.toISOString(),
|
|
46
|
+
now.getTime(),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return Number(result.lastInsertRowid);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Invalidate a triple by setting its valid_to date.
|
|
54
|
+
* Finds active triples matching subject+predicate+object and closes them.
|
|
55
|
+
*/
|
|
56
|
+
export function invalidateTriple(
|
|
57
|
+
subject: string,
|
|
58
|
+
predicate: string,
|
|
59
|
+
object: string,
|
|
60
|
+
endDate?: string,
|
|
61
|
+
): number {
|
|
62
|
+
const db = getMemoryDB();
|
|
63
|
+
const validTo = endDate ?? new Date().toISOString().slice(0, 10);
|
|
64
|
+
|
|
65
|
+
const result = db.run(
|
|
66
|
+
`UPDATE entity_triples
|
|
67
|
+
SET valid_to = ?
|
|
68
|
+
WHERE LOWER(subject) = LOWER(?) AND LOWER(predicate) = LOWER(?) AND LOWER(object) = LOWER(?)
|
|
69
|
+
AND valid_to IS NULL`,
|
|
70
|
+
[validTo, subject.trim(), predicate.trim(), object.trim()],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return result.changes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get a triple by ID.
|
|
78
|
+
*/
|
|
79
|
+
export function getTripleById(id: number): EntityTripleRow | null {
|
|
80
|
+
const db = getMemoryDB();
|
|
81
|
+
return db
|
|
82
|
+
.query("SELECT * FROM entity_triples WHERE id = ?")
|
|
83
|
+
.get(id) as EntityTripleRow | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Queries
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Query entity relationships with optional time filtering.
|
|
92
|
+
* Returns triples where entity appears as subject or object.
|
|
93
|
+
*/
|
|
94
|
+
export function queryEntity(
|
|
95
|
+
entity: string,
|
|
96
|
+
options: {
|
|
97
|
+
as_of?: string; // ISO date — filter to triples valid at this time
|
|
98
|
+
direction?: "out" | "in" | "both"; // out = subject, in = object, both = either
|
|
99
|
+
predicate?: string; // filter by predicate
|
|
100
|
+
activeOnly?: boolean; // only return currently active triples
|
|
101
|
+
limit?: number;
|
|
102
|
+
} = {},
|
|
103
|
+
): EntityQueryResult[] {
|
|
104
|
+
const db = getMemoryDB();
|
|
105
|
+
const direction = options.direction ?? "both";
|
|
106
|
+
const limit = options.limit ?? 50;
|
|
107
|
+
const entityLower = entity.toLowerCase().trim();
|
|
108
|
+
|
|
109
|
+
const conditions: string[] = [];
|
|
110
|
+
const params: (string | number)[] = [];
|
|
111
|
+
|
|
112
|
+
// Direction filter
|
|
113
|
+
if (direction === "out") {
|
|
114
|
+
conditions.push("LOWER(subject) = ?");
|
|
115
|
+
params.push(entityLower);
|
|
116
|
+
} else if (direction === "in") {
|
|
117
|
+
conditions.push("LOWER(object) = ?");
|
|
118
|
+
params.push(entityLower);
|
|
119
|
+
} else {
|
|
120
|
+
conditions.push("(LOWER(subject) = ? OR LOWER(object) = ?)");
|
|
121
|
+
params.push(entityLower, entityLower);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Predicate filter
|
|
125
|
+
if (options.predicate) {
|
|
126
|
+
conditions.push("LOWER(predicate) = ?");
|
|
127
|
+
params.push(options.predicate.toLowerCase().trim());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Time filter
|
|
131
|
+
if (options.as_of) {
|
|
132
|
+
conditions.push("valid_from <= ?");
|
|
133
|
+
params.push(options.as_of);
|
|
134
|
+
conditions.push("(valid_to IS NULL OR valid_to >= ?)");
|
|
135
|
+
params.push(options.as_of);
|
|
136
|
+
} else if (options.activeOnly) {
|
|
137
|
+
conditions.push("valid_to IS NULL");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
params.push(limit);
|
|
141
|
+
|
|
142
|
+
const sql = `
|
|
143
|
+
SELECT id, subject, predicate, object, valid_from, valid_to, confidence,
|
|
144
|
+
CASE WHEN valid_to IS NULL THEN 1 ELSE 0 END as is_active
|
|
145
|
+
FROM entity_triples
|
|
146
|
+
WHERE ${conditions.join(" AND ")}
|
|
147
|
+
ORDER BY valid_from DESC, created_at_epoch DESC
|
|
148
|
+
LIMIT ?
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
return db.query(sql).all(...params) as EntityQueryResult[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get entity timeline — all triples involving an entity, sorted chronologically.
|
|
156
|
+
*/
|
|
157
|
+
export function getEntityTimeline(
|
|
158
|
+
entity: string,
|
|
159
|
+
options: { limit?: number } = {},
|
|
160
|
+
): EntityTripleRow[] {
|
|
161
|
+
const db = getMemoryDB();
|
|
162
|
+
const limit = options.limit ?? 100;
|
|
163
|
+
const entityLower = entity.toLowerCase().trim();
|
|
164
|
+
|
|
165
|
+
return db
|
|
166
|
+
.query(
|
|
167
|
+
`SELECT * FROM entity_triples
|
|
168
|
+
WHERE LOWER(subject) = ? OR LOWER(object) = ?
|
|
169
|
+
ORDER BY valid_from ASC, created_at_epoch ASC
|
|
170
|
+
LIMIT ?`,
|
|
171
|
+
)
|
|
172
|
+
.all(entityLower, entityLower, limit) as EntityTripleRow[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Find contradictions: active triples with opposing predicates for same subject-object pair.
|
|
177
|
+
*/
|
|
178
|
+
export function findContradictions(
|
|
179
|
+
subject: string,
|
|
180
|
+
predicate: string,
|
|
181
|
+
object: string,
|
|
182
|
+
): EntityTripleRow[] {
|
|
183
|
+
const db = getMemoryDB();
|
|
184
|
+
|
|
185
|
+
return db
|
|
186
|
+
.query(
|
|
187
|
+
`SELECT * FROM entity_triples
|
|
188
|
+
WHERE LOWER(subject) = LOWER(?) AND LOWER(object) = LOWER(?)
|
|
189
|
+
AND LOWER(predicate) != LOWER(?)
|
|
190
|
+
AND valid_to IS NULL
|
|
191
|
+
ORDER BY created_at_epoch DESC`,
|
|
192
|
+
)
|
|
193
|
+
.all(subject.trim(), object.trim(), predicate.trim()) as EntityTripleRow[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Stats
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get entity graph statistics.
|
|
202
|
+
*/
|
|
203
|
+
export function getEntityGraphStats(): {
|
|
204
|
+
total_triples: number;
|
|
205
|
+
active_triples: number;
|
|
206
|
+
unique_entities: number;
|
|
207
|
+
unique_predicates: number;
|
|
208
|
+
} {
|
|
209
|
+
const db = getMemoryDB();
|
|
210
|
+
|
|
211
|
+
const total = (
|
|
212
|
+
db.query("SELECT COUNT(*) as count FROM entity_triples").get() as {
|
|
213
|
+
count: number;
|
|
214
|
+
}
|
|
215
|
+
).count;
|
|
216
|
+
|
|
217
|
+
const active = (
|
|
218
|
+
db
|
|
219
|
+
.query(
|
|
220
|
+
"SELECT COUNT(*) as count FROM entity_triples WHERE valid_to IS NULL",
|
|
221
|
+
)
|
|
222
|
+
.get() as {
|
|
223
|
+
count: number;
|
|
224
|
+
}
|
|
225
|
+
).count;
|
|
226
|
+
|
|
227
|
+
const entities = (
|
|
228
|
+
db
|
|
229
|
+
.query(
|
|
230
|
+
`SELECT COUNT(DISTINCT entity) as count FROM (
|
|
231
|
+
SELECT subject as entity FROM entity_triples
|
|
232
|
+
UNION ALL
|
|
233
|
+
SELECT object as entity FROM entity_triples
|
|
234
|
+
)`,
|
|
235
|
+
)
|
|
236
|
+
.get() as { count: number }
|
|
237
|
+
).count;
|
|
238
|
+
|
|
239
|
+
const predicates = (
|
|
240
|
+
db
|
|
241
|
+
.query("SELECT COUNT(DISTINCT predicate) as count FROM entity_triples")
|
|
242
|
+
.get() as {
|
|
243
|
+
count: number;
|
|
244
|
+
}
|
|
245
|
+
).count;
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
total_triples: total,
|
|
249
|
+
active_triples: active,
|
|
250
|
+
unique_entities: entities,
|
|
251
|
+
unique_predicates: predicates,
|
|
252
|
+
};
|
|
253
|
+
}
|