opencodekit 0.20.2 → 0.20.4
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/agent/build.md +4 -0
- package/dist/template/.opencode/agent/explore.md +4 -0
- package/dist/template/.opencode/agent/general.md +4 -0
- package/dist/template/.opencode/agent/plan.md +4 -0
- package/dist/template/.opencode/agent/review.md +4 -0
- package/dist/template/.opencode/agent/scout.md +4 -0
- package/dist/template/.opencode/command/create.md +119 -25
- package/dist/template/.opencode/command/design.md +1 -2
- package/dist/template/.opencode/command/health.md +234 -0
- package/dist/template/.opencode/command/init-user.md +15 -0
- package/dist/template/.opencode/command/plan.md +3 -4
- package/dist/template/.opencode/command/pr.md +13 -0
- package/dist/template/.opencode/command/research.md +15 -3
- package/dist/template/.opencode/command/review-codebase.md +11 -1
- package/dist/template/.opencode/command/ship.md +72 -8
- package/dist/template/.opencode/command/status.md +1 -1
- package/dist/template/.opencode/command/ui-review.md +0 -1
- package/dist/template/.opencode/command/ui-slop-check.md +1 -1
- package/dist/template/.opencode/command/verify.md +11 -1
- 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 +1678 -1677
- 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/compile.ts +253 -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/index-generator.ts +170 -0
- package/dist/template/.opencode/plugin/lib/lint.ts +359 -0
- package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +78 -4
- package/dist/template/.opencode/plugin/lib/memory-db.ts +19 -1
- package/dist/template/.opencode/plugin/lib/memory-helpers.ts +30 -0
- package/dist/template/.opencode/plugin/lib/memory-hooks.ts +10 -0
- package/dist/template/.opencode/plugin/lib/memory-tools.ts +167 -2
- package/dist/template/.opencode/plugin/lib/operation-log.ts +109 -0
- package/dist/template/.opencode/plugin/lib/validate.ts +243 -0
- package/dist/template/.opencode/plugin/memory.ts +2 -1
- package/dist/template/.opencode/skill/design-taste-frontend/SKILL.md +13 -1
- package/dist/template/.opencode/skill/figma-go/SKILL.md +1 -1
- package/dist/template/.opencode/skill/full-output-enforcement/SKILL.md +13 -0
- package/dist/template/.opencode/skill/high-end-visual-design/SKILL.md +13 -0
- package/dist/template/.opencode/skill/industrial-brutalist-ui/SKILL.md +13 -0
- package/dist/template/.opencode/skill/memory-system/SKILL.md +65 -1
- package/dist/template/.opencode/skill/minimalist-ui/SKILL.md +13 -0
- package/dist/template/.opencode/skill/redesign-existing-projects/SKILL.md +13 -0
- package/dist/template/.opencode/skill/requesting-code-review/SKILL.md +48 -2
- package/dist/template/.opencode/skill/requesting-code-review/references/specialist-profiles.md +108 -0
- package/dist/template/.opencode/skill/skill-creator/SKILL.md +25 -0
- package/dist/template/.opencode/skill/stitch-design-taste/SKILL.md +13 -0
- package/dist/template/.opencode/skill/verification-before-completion/SKILL.md +46 -0
- package/package.json +1 -1
- package/dist/template/.opencode/agent/runner.md +0 -79
- package/dist/template/.opencode/command/start.md +0 -156
|
@@ -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
|
+
* Memory Compilation — Observations → Structured Articles
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Karpathy's LLM Wiki "compilation" layer:
|
|
5
|
+
* reads clusters of related observations and produces structured
|
|
6
|
+
* summary articles with cross-references. This is the missing 5th stage:
|
|
7
|
+
*
|
|
8
|
+
* capture → distill → curate → **compile** → inject
|
|
9
|
+
*
|
|
10
|
+
* Unlike Karpathy's approach (which uses LLM for compilation), this
|
|
11
|
+
* implementation uses pure heuristics — grouping by concept clusters,
|
|
12
|
+
* ranking by confidence/recency, and templated article generation.
|
|
13
|
+
*
|
|
14
|
+
* For LLM-powered compilation, use the compile admin operation which
|
|
15
|
+
* generates articles the agent can review and edit.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { upsertMemoryFile } from "./db/maintenance.js";
|
|
19
|
+
import type { ObservationRow } from "./db/types.js";
|
|
20
|
+
import { getMemoryDB } from "./memory-db.js";
|
|
21
|
+
import { TYPE_ICONS, parseConcepts } from "./memory-helpers.js";
|
|
22
|
+
import { appendOperationLog } from "./operation-log.js";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export interface ConceptCluster {
|
|
29
|
+
concept: string;
|
|
30
|
+
observations: Pick<
|
|
31
|
+
ObservationRow,
|
|
32
|
+
"id" | "type" | "title" | "narrative" | "confidence" | "created_at"
|
|
33
|
+
>[];
|
|
34
|
+
relatedConcepts: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CompiledArticle {
|
|
38
|
+
concept: string;
|
|
39
|
+
content: string;
|
|
40
|
+
observationCount: number;
|
|
41
|
+
relatedConcepts: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CompileResult {
|
|
45
|
+
articles: CompiledArticle[];
|
|
46
|
+
totalObservations: number;
|
|
47
|
+
skippedClusters: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Compilation Operations
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compile observations into structured articles grouped by concept.
|
|
56
|
+
* Only generates articles for concepts with `minObservations` or more observations.
|
|
57
|
+
* Stores articles in memory_files as "compiled/{concept}".
|
|
58
|
+
*/
|
|
59
|
+
export function compileObservations(
|
|
60
|
+
options: { minObservations?: number; maxArticles?: number } = {},
|
|
61
|
+
): CompileResult {
|
|
62
|
+
const minObs = options.minObservations ?? 3;
|
|
63
|
+
const maxArticles = options.maxArticles ?? 20;
|
|
64
|
+
|
|
65
|
+
const clusters = buildConceptClusters(minObs);
|
|
66
|
+
const articles: CompiledArticle[] = [];
|
|
67
|
+
let skipped = 0;
|
|
68
|
+
|
|
69
|
+
// Sort clusters by observation count (most connected first)
|
|
70
|
+
const sortedClusters = clusters
|
|
71
|
+
.sort((a, b) => b.observations.length - a.observations.length)
|
|
72
|
+
.slice(0, maxArticles);
|
|
73
|
+
|
|
74
|
+
let totalObservations = 0;
|
|
75
|
+
|
|
76
|
+
for (const cluster of sortedClusters) {
|
|
77
|
+
const article = compileCluster(cluster);
|
|
78
|
+
if (article) {
|
|
79
|
+
articles.push(article);
|
|
80
|
+
totalObservations += article.observationCount;
|
|
81
|
+
// Store in memory_files
|
|
82
|
+
const safeName = cluster.concept.replace(/[^a-z0-9-]/g, "-");
|
|
83
|
+
upsertMemoryFile(`compiled/${safeName}`, article.content, "replace");
|
|
84
|
+
} else {
|
|
85
|
+
skipped++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Log the operation
|
|
90
|
+
appendOperationLog({
|
|
91
|
+
operation: "compile-run",
|
|
92
|
+
targets: articles.map((a) => a.concept),
|
|
93
|
+
summary: `Compiled ${articles.length} articles from ${totalObservations} observations (${skipped} clusters skipped)`,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return { articles, totalObservations, skippedClusters: skipped };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Internal
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build concept clusters from active observations.
|
|
105
|
+
*/
|
|
106
|
+
function buildConceptClusters(minObservations: number): ConceptCluster[] {
|
|
107
|
+
const db = getMemoryDB();
|
|
108
|
+
|
|
109
|
+
const observations = db
|
|
110
|
+
.query(
|
|
111
|
+
`SELECT id, type, title, narrative, concepts, confidence, created_at
|
|
112
|
+
FROM observations
|
|
113
|
+
WHERE superseded_by IS NULL AND concepts IS NOT NULL
|
|
114
|
+
ORDER BY created_at_epoch DESC`,
|
|
115
|
+
)
|
|
116
|
+
.all() as Pick<
|
|
117
|
+
ObservationRow,
|
|
118
|
+
| "id"
|
|
119
|
+
| "type"
|
|
120
|
+
| "title"
|
|
121
|
+
| "narrative"
|
|
122
|
+
| "concepts"
|
|
123
|
+
| "confidence"
|
|
124
|
+
| "created_at"
|
|
125
|
+
>[];
|
|
126
|
+
|
|
127
|
+
// Build concept → observations map
|
|
128
|
+
const conceptMap = new Map<string, typeof observations>();
|
|
129
|
+
|
|
130
|
+
for (const obs of observations) {
|
|
131
|
+
const concepts = parseConcepts(obs.concepts);
|
|
132
|
+
for (const concept of concepts) {
|
|
133
|
+
const group = conceptMap.get(concept) ?? [];
|
|
134
|
+
group.push(obs);
|
|
135
|
+
conceptMap.set(concept, group);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Filter to clusters with enough observations
|
|
140
|
+
const clusters: ConceptCluster[] = [];
|
|
141
|
+
for (const [concept, obsGroup] of conceptMap) {
|
|
142
|
+
if (obsGroup.length < minObservations) continue;
|
|
143
|
+
|
|
144
|
+
// Find related concepts (co-occurring)
|
|
145
|
+
const relatedCounts = new Map<string, number>();
|
|
146
|
+
for (const obs of obsGroup) {
|
|
147
|
+
const concepts = parseConcepts(obs.concepts);
|
|
148
|
+
for (const c of concepts) {
|
|
149
|
+
if (c === concept) continue;
|
|
150
|
+
relatedCounts.set(c, (relatedCounts.get(c) ?? 0) + 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const relatedConcepts = [...relatedCounts.entries()]
|
|
154
|
+
.filter(([, count]) => count >= 2)
|
|
155
|
+
.sort((a, b) => b[1] - a[1])
|
|
156
|
+
.map(([c]) => c);
|
|
157
|
+
|
|
158
|
+
clusters.push({
|
|
159
|
+
concept,
|
|
160
|
+
observations: obsGroup,
|
|
161
|
+
relatedConcepts,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return clusters;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Compile a single concept cluster into a markdown article.
|
|
170
|
+
*/
|
|
171
|
+
function compileCluster(cluster: ConceptCluster): CompiledArticle | null {
|
|
172
|
+
if (cluster.observations.length === 0) return null;
|
|
173
|
+
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
|
|
176
|
+
// Header
|
|
177
|
+
lines.push(`# ${cluster.concept}`);
|
|
178
|
+
lines.push("");
|
|
179
|
+
lines.push(`> Compiled from ${cluster.observations.length} observations.`);
|
|
180
|
+
lines.push(`> Last compiled: ${new Date().toISOString().slice(0, 19)}`);
|
|
181
|
+
|
|
182
|
+
// Related concepts
|
|
183
|
+
if (cluster.relatedConcepts.length > 0) {
|
|
184
|
+
lines.push(`> Related: ${cluster.relatedConcepts.join(", ")}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push("");
|
|
187
|
+
|
|
188
|
+
// Group observations by type for structure
|
|
189
|
+
const byType = new Map<string, typeof cluster.observations>();
|
|
190
|
+
for (const obs of cluster.observations) {
|
|
191
|
+
const group = byType.get(obs.type) ?? [];
|
|
192
|
+
group.push(obs);
|
|
193
|
+
byType.set(obs.type, group);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Decisions first (most important)
|
|
197
|
+
const typeOrder = [
|
|
198
|
+
"decision",
|
|
199
|
+
"pattern",
|
|
200
|
+
"warning",
|
|
201
|
+
"bugfix",
|
|
202
|
+
"discovery",
|
|
203
|
+
"feature",
|
|
204
|
+
"learning",
|
|
205
|
+
];
|
|
206
|
+
for (const type of typeOrder) {
|
|
207
|
+
const group = byType.get(type);
|
|
208
|
+
if (!group || group.length === 0) continue;
|
|
209
|
+
|
|
210
|
+
const icon = TYPE_ICONS[type] ?? "📌";
|
|
211
|
+
const heading = type.charAt(0).toUpperCase() + type.slice(1);
|
|
212
|
+
lines.push(`## ${icon} ${heading}s`);
|
|
213
|
+
lines.push("");
|
|
214
|
+
|
|
215
|
+
for (const obs of group) {
|
|
216
|
+
lines.push(`### #${obs.id}: ${obs.title}`);
|
|
217
|
+
if (obs.narrative) {
|
|
218
|
+
// Truncate long narratives for the compiled view
|
|
219
|
+
const narrative =
|
|
220
|
+
obs.narrative.length > 500
|
|
221
|
+
? `${obs.narrative.slice(0, 500)}...`
|
|
222
|
+
: obs.narrative;
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push(narrative);
|
|
225
|
+
}
|
|
226
|
+
lines.push("");
|
|
227
|
+
lines.push(
|
|
228
|
+
`_Confidence: ${obs.confidence} | Created: ${obs.created_at.slice(0, 10)}_`,
|
|
229
|
+
);
|
|
230
|
+
lines.push("");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Cross-reference footer
|
|
235
|
+
lines.push("---");
|
|
236
|
+
lines.push("");
|
|
237
|
+
lines.push(
|
|
238
|
+
`**Source observations:** ${cluster.observations.map((o) => `#${o.id}`).join(", ")}`,
|
|
239
|
+
);
|
|
240
|
+
if (cluster.relatedConcepts.length > 0) {
|
|
241
|
+
lines.push(
|
|
242
|
+
`**See also:** ${cluster.relatedConcepts.map((c) => `[[${c}]]`).join(", ")}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
concept: cluster.concept,
|
|
248
|
+
content: lines.join("\n"),
|
|
249
|
+
observationCount: cluster.observations.length,
|
|
250
|
+
relatedConcepts: cluster.relatedConcepts,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
@@ -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
|
+
}
|