claude-memory-layer 1.0.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.
Files changed (127) hide show
  1. package/.claude-plugin/commands/memory-forget.md +42 -0
  2. package/.claude-plugin/commands/memory-history.md +34 -0
  3. package/.claude-plugin/commands/memory-import.md +56 -0
  4. package/.claude-plugin/commands/memory-list.md +37 -0
  5. package/.claude-plugin/commands/memory-search.md +36 -0
  6. package/.claude-plugin/commands/memory-stats.md +34 -0
  7. package/.claude-plugin/hooks.json +59 -0
  8. package/.claude-plugin/plugin.json +24 -0
  9. package/.history/package_20260201112328.json +45 -0
  10. package/.history/package_20260201113602.json +45 -0
  11. package/.history/package_20260201113713.json +45 -0
  12. package/.history/package_20260201114110.json +45 -0
  13. package/Memo.txt +558 -0
  14. package/README.md +520 -0
  15. package/context.md +636 -0
  16. package/dist/.claude-plugin/commands/memory-forget.md +42 -0
  17. package/dist/.claude-plugin/commands/memory-history.md +34 -0
  18. package/dist/.claude-plugin/commands/memory-import.md +56 -0
  19. package/dist/.claude-plugin/commands/memory-list.md +37 -0
  20. package/dist/.claude-plugin/commands/memory-search.md +36 -0
  21. package/dist/.claude-plugin/commands/memory-stats.md +34 -0
  22. package/dist/.claude-plugin/hooks.json +59 -0
  23. package/dist/.claude-plugin/plugin.json +24 -0
  24. package/dist/cli/index.js +3539 -0
  25. package/dist/cli/index.js.map +7 -0
  26. package/dist/core/index.js +4408 -0
  27. package/dist/core/index.js.map +7 -0
  28. package/dist/hooks/session-end.js +2971 -0
  29. package/dist/hooks/session-end.js.map +7 -0
  30. package/dist/hooks/session-start.js +2969 -0
  31. package/dist/hooks/session-start.js.map +7 -0
  32. package/dist/hooks/stop.js +3123 -0
  33. package/dist/hooks/stop.js.map +7 -0
  34. package/dist/hooks/user-prompt-submit.js +2960 -0
  35. package/dist/hooks/user-prompt-submit.js.map +7 -0
  36. package/dist/services/memory-service.js +2931 -0
  37. package/dist/services/memory-service.js.map +7 -0
  38. package/package.json +45 -0
  39. package/plan.md +1642 -0
  40. package/scripts/build.ts +102 -0
  41. package/spec.md +624 -0
  42. package/specs/citations-system/context.md +243 -0
  43. package/specs/citations-system/plan.md +495 -0
  44. package/specs/citations-system/spec.md +371 -0
  45. package/specs/endless-mode/context.md +305 -0
  46. package/specs/endless-mode/plan.md +620 -0
  47. package/specs/endless-mode/spec.md +455 -0
  48. package/specs/entity-edge-model/context.md +401 -0
  49. package/specs/entity-edge-model/plan.md +459 -0
  50. package/specs/entity-edge-model/spec.md +391 -0
  51. package/specs/evidence-aligner-v2/context.md +401 -0
  52. package/specs/evidence-aligner-v2/plan.md +303 -0
  53. package/specs/evidence-aligner-v2/spec.md +312 -0
  54. package/specs/mcp-desktop-integration/context.md +278 -0
  55. package/specs/mcp-desktop-integration/plan.md +550 -0
  56. package/specs/mcp-desktop-integration/spec.md +494 -0
  57. package/specs/post-tool-use-hook/context.md +319 -0
  58. package/specs/post-tool-use-hook/plan.md +469 -0
  59. package/specs/post-tool-use-hook/spec.md +364 -0
  60. package/specs/private-tags/context.md +288 -0
  61. package/specs/private-tags/plan.md +412 -0
  62. package/specs/private-tags/spec.md +345 -0
  63. package/specs/progressive-disclosure/context.md +346 -0
  64. package/specs/progressive-disclosure/plan.md +663 -0
  65. package/specs/progressive-disclosure/spec.md +415 -0
  66. package/specs/task-entity-system/context.md +297 -0
  67. package/specs/task-entity-system/plan.md +301 -0
  68. package/specs/task-entity-system/spec.md +314 -0
  69. package/specs/vector-outbox-v2/context.md +470 -0
  70. package/specs/vector-outbox-v2/plan.md +562 -0
  71. package/specs/vector-outbox-v2/spec.md +466 -0
  72. package/specs/web-viewer-ui/context.md +384 -0
  73. package/specs/web-viewer-ui/plan.md +797 -0
  74. package/specs/web-viewer-ui/spec.md +516 -0
  75. package/src/cli/index.ts +570 -0
  76. package/src/core/canonical-key.ts +186 -0
  77. package/src/core/citation-generator.ts +63 -0
  78. package/src/core/consolidated-store.ts +279 -0
  79. package/src/core/consolidation-worker.ts +384 -0
  80. package/src/core/context-formatter.ts +276 -0
  81. package/src/core/continuity-manager.ts +336 -0
  82. package/src/core/edge-repo.ts +324 -0
  83. package/src/core/embedder.ts +124 -0
  84. package/src/core/entity-repo.ts +342 -0
  85. package/src/core/event-store.ts +672 -0
  86. package/src/core/evidence-aligner.ts +635 -0
  87. package/src/core/graduation.ts +365 -0
  88. package/src/core/index.ts +32 -0
  89. package/src/core/matcher.ts +210 -0
  90. package/src/core/metadata-extractor.ts +203 -0
  91. package/src/core/privacy/filter.ts +179 -0
  92. package/src/core/privacy/index.ts +20 -0
  93. package/src/core/privacy/tag-parser.ts +145 -0
  94. package/src/core/progressive-retriever.ts +415 -0
  95. package/src/core/retriever.ts +235 -0
  96. package/src/core/task/blocker-resolver.ts +325 -0
  97. package/src/core/task/index.ts +9 -0
  98. package/src/core/task/task-matcher.ts +238 -0
  99. package/src/core/task/task-projector.ts +345 -0
  100. package/src/core/task/task-resolver.ts +414 -0
  101. package/src/core/types.ts +841 -0
  102. package/src/core/vector-outbox.ts +295 -0
  103. package/src/core/vector-store.ts +182 -0
  104. package/src/core/vector-worker.ts +488 -0
  105. package/src/core/working-set-store.ts +244 -0
  106. package/src/hooks/post-tool-use.ts +127 -0
  107. package/src/hooks/session-end.ts +78 -0
  108. package/src/hooks/session-start.ts +57 -0
  109. package/src/hooks/stop.ts +78 -0
  110. package/src/hooks/user-prompt-submit.ts +54 -0
  111. package/src/mcp/handlers.ts +212 -0
  112. package/src/mcp/index.ts +47 -0
  113. package/src/mcp/tools.ts +78 -0
  114. package/src/server/api/citations.ts +101 -0
  115. package/src/server/api/events.ts +101 -0
  116. package/src/server/api/index.ts +18 -0
  117. package/src/server/api/search.ts +98 -0
  118. package/src/server/api/sessions.ts +111 -0
  119. package/src/server/api/stats.ts +97 -0
  120. package/src/server/index.ts +91 -0
  121. package/src/services/memory-service.ts +626 -0
  122. package/src/services/session-history-importer.ts +367 -0
  123. package/tests/canonical-key.test.ts +101 -0
  124. package/tests/evidence-aligner.test.ts +152 -0
  125. package/tests/matcher.test.ts +112 -0
  126. package/tsconfig.json +24 -0
  127. package/vitest.config.ts +15 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Continuity Manager
3
+ * Tracks and calculates context continuity between interactions
4
+ * Biomimetic: Simulates context-dependent memory retrieval
5
+ */
6
+
7
+ import { randomUUID } from 'crypto';
8
+ import { Database } from 'duckdb';
9
+ import type {
10
+ EndlessModeConfig,
11
+ ContextSnapshot,
12
+ ContinuityScore,
13
+ TransitionType,
14
+ ContinuityLog
15
+ } from './types.js';
16
+ import { EventStore } from './event-store.js';
17
+
18
+ export class ContinuityManager {
19
+ private lastContext: ContextSnapshot | null = null;
20
+
21
+ constructor(
22
+ private eventStore: EventStore,
23
+ private config: EndlessModeConfig
24
+ ) {}
25
+
26
+ private get db(): Database {
27
+ return this.eventStore.getDatabase();
28
+ }
29
+
30
+ /**
31
+ * Calculate continuity score between current and previous context
32
+ */
33
+ async calculateScore(
34
+ currentContext: ContextSnapshot,
35
+ previousContext?: ContextSnapshot
36
+ ): Promise<ContinuityScore> {
37
+ const prev = previousContext || this.lastContext;
38
+
39
+ if (!prev) {
40
+ // No previous context - this is a fresh start
41
+ this.lastContext = currentContext;
42
+ return { score: 0.5, transitionType: 'break' };
43
+ }
44
+
45
+ let score = 0;
46
+
47
+ // Topic continuity (30%)
48
+ const topicOverlap = this.calculateOverlap(
49
+ currentContext.topics,
50
+ prev.topics
51
+ );
52
+ score += topicOverlap * 0.3;
53
+
54
+ // File continuity (20%)
55
+ const fileOverlap = this.calculateOverlap(
56
+ currentContext.files,
57
+ prev.files
58
+ );
59
+ score += fileOverlap * 0.2;
60
+
61
+ // Time proximity (30%)
62
+ const timeDiff = currentContext.timestamp - prev.timestamp;
63
+ const decayHours = this.config.continuity.topicDecayHours;
64
+ const timeScore = Math.exp(-timeDiff / (decayHours * 3600000));
65
+ score += timeScore * 0.3;
66
+
67
+ // Entity continuity (20%)
68
+ const entityOverlap = this.calculateOverlap(
69
+ currentContext.entities,
70
+ prev.entities
71
+ );
72
+ score += entityOverlap * 0.2;
73
+
74
+ // Determine transition type
75
+ const transitionType = this.determineTransitionType(score);
76
+
77
+ // Log the transition
78
+ await this.logTransition(currentContext, prev, score, transitionType);
79
+
80
+ // Update last context
81
+ this.lastContext = currentContext;
82
+
83
+ return { score, transitionType };
84
+ }
85
+
86
+ /**
87
+ * Create a context snapshot from current state
88
+ */
89
+ createSnapshot(
90
+ id: string,
91
+ content: string,
92
+ metadata?: {
93
+ files?: string[];
94
+ entities?: string[];
95
+ }
96
+ ): ContextSnapshot {
97
+ return {
98
+ id,
99
+ timestamp: Date.now(),
100
+ topics: this.extractTopics(content),
101
+ files: metadata?.files || this.extractFiles(content),
102
+ entities: metadata?.entities || this.extractEntities(content)
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Get recent continuity logs
108
+ */
109
+ async getRecentLogs(limit: number = 10): Promise<ContinuityLog[]> {
110
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
111
+ `SELECT * FROM continuity_log
112
+ ORDER BY created_at DESC
113
+ LIMIT ?`,
114
+ [limit]
115
+ );
116
+
117
+ return rows.map(row => ({
118
+ logId: row.log_id as string,
119
+ fromContextId: row.from_context_id as string | undefined,
120
+ toContextId: row.to_context_id as string | undefined,
121
+ continuityScore: row.continuity_score as number,
122
+ transitionType: row.transition_type as TransitionType,
123
+ createdAt: new Date(row.created_at as string)
124
+ }));
125
+ }
126
+
127
+ /**
128
+ * Get average continuity score over time period
129
+ */
130
+ async getAverageScore(hours: number = 1): Promise<number> {
131
+ const result = await this.db.all<Array<{ avg_score: number | null }>>(
132
+ `SELECT AVG(continuity_score) as avg_score
133
+ FROM continuity_log
134
+ WHERE created_at > datetime('now', '-${hours} hours')`
135
+ );
136
+
137
+ return result[0]?.avg_score ?? 0.5;
138
+ }
139
+
140
+ /**
141
+ * Get transition type distribution
142
+ */
143
+ async getTransitionStats(hours: number = 24): Promise<Record<TransitionType, number>> {
144
+ const rows = await this.db.all<Array<{ transition_type: string; count: number }>>(
145
+ `SELECT transition_type, COUNT(*) as count
146
+ FROM continuity_log
147
+ WHERE created_at > datetime('now', '-${hours} hours')
148
+ GROUP BY transition_type`
149
+ );
150
+
151
+ const stats: Record<TransitionType, number> = {
152
+ seamless: 0,
153
+ topic_shift: 0,
154
+ break: 0
155
+ };
156
+
157
+ for (const row of rows) {
158
+ stats[row.transition_type as TransitionType] = row.count;
159
+ }
160
+
161
+ return stats;
162
+ }
163
+
164
+ /**
165
+ * Clear old continuity logs
166
+ */
167
+ async cleanup(olderThanDays: number = 7): Promise<number> {
168
+ const result = await this.db.all<Array<{ changes: number }>>(
169
+ `DELETE FROM continuity_log
170
+ WHERE created_at < datetime('now', '-${olderThanDays} days')
171
+ RETURNING COUNT(*) as changes`
172
+ );
173
+
174
+ return result[0]?.changes || 0;
175
+ }
176
+
177
+ /**
178
+ * Calculate overlap between two arrays
179
+ */
180
+ private calculateOverlap(a: string[], b: string[]): number {
181
+ if (a.length === 0 || b.length === 0) return 0;
182
+
183
+ const setA = new Set(a.map(s => s.toLowerCase()));
184
+ const setB = new Set(b.map(s => s.toLowerCase()));
185
+
186
+ const intersection = [...setA].filter(x => setB.has(x));
187
+ const union = new Set([...setA, ...setB]);
188
+
189
+ return intersection.length / union.size; // Jaccard similarity
190
+ }
191
+
192
+ /**
193
+ * Determine transition type based on score
194
+ */
195
+ private determineTransitionType(score: number): TransitionType {
196
+ if (score >= this.config.continuity.minScoreForSeamless) {
197
+ return 'seamless';
198
+ } else if (score >= 0.4) {
199
+ return 'topic_shift';
200
+ } else {
201
+ return 'break';
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Log a context transition
207
+ */
208
+ private async logTransition(
209
+ current: ContextSnapshot,
210
+ previous: ContextSnapshot,
211
+ score: number,
212
+ type: TransitionType
213
+ ): Promise<void> {
214
+ await this.db.run(
215
+ `INSERT INTO continuity_log
216
+ (log_id, from_context_id, to_context_id, continuity_score, transition_type, created_at)
217
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
218
+ [randomUUID(), previous.id, current.id, score, type]
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Extract topics from content
224
+ */
225
+ private extractTopics(content: string): string[] {
226
+ const topics: string[] = [];
227
+ const contentLower = content.toLowerCase();
228
+
229
+ // Programming language keywords
230
+ const langPatterns = [
231
+ { pattern: /typescript|\.ts\b/i, topic: 'typescript' },
232
+ { pattern: /javascript|\.js\b/i, topic: 'javascript' },
233
+ { pattern: /python|\.py\b/i, topic: 'python' },
234
+ { pattern: /rust|\.rs\b/i, topic: 'rust' },
235
+ { pattern: /go\b|golang/i, topic: 'go' }
236
+ ];
237
+
238
+ for (const { pattern, topic } of langPatterns) {
239
+ if (pattern.test(content)) {
240
+ topics.push(topic);
241
+ }
242
+ }
243
+
244
+ // Common development topics
245
+ const devTopics = [
246
+ 'api', 'database', 'test', 'bug', 'feature', 'refactor',
247
+ 'component', 'function', 'class', 'module', 'hook',
248
+ 'deploy', 'build', 'config', 'docker', 'git'
249
+ ];
250
+
251
+ for (const topic of devTopics) {
252
+ if (contentLower.includes(topic)) {
253
+ topics.push(topic);
254
+ }
255
+ }
256
+
257
+ return [...new Set(topics)].slice(0, 10);
258
+ }
259
+
260
+ /**
261
+ * Extract file paths from content
262
+ */
263
+ private extractFiles(content: string): string[] {
264
+ const filePatterns = [
265
+ /(?:^|\s)([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)(?:\s|$|:)/gm,
266
+ /['"](\.?\/[^'"]+\.[a-zA-Z0-9]+)['"]/g,
267
+ /file[:\s]+([^\s,]+)/gi
268
+ ];
269
+
270
+ const files = new Set<string>();
271
+
272
+ for (const pattern of filePatterns) {
273
+ let match;
274
+ while ((match = pattern.exec(content)) !== null) {
275
+ const file = match[1];
276
+ if (file && file.length > 3 && file.length < 100) {
277
+ // Filter out common non-file patterns
278
+ if (!file.match(/^(https?:|mailto:|ftp:)/i)) {
279
+ files.add(file);
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ return Array.from(files).slice(0, 10);
286
+ }
287
+
288
+ /**
289
+ * Extract entity names from content (functions, classes, variables)
290
+ */
291
+ private extractEntities(content: string): string[] {
292
+ const entities = new Set<string>();
293
+
294
+ const entityPatterns = [
295
+ /\b(function|const|let|var|class|interface|type)\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
296
+ /\b([A-Z][a-zA-Z0-9_]*(?:Component|Service|Store|Manager|Handler|Factory|Provider))\b/g,
297
+ /\b(use[A-Z][a-zA-Z0-9_]*)\b/g // React hooks
298
+ ];
299
+
300
+ for (const pattern of entityPatterns) {
301
+ let match;
302
+ while ((match = pattern.exec(content)) !== null) {
303
+ const entity = match[2] || match[1];
304
+ if (entity && entity.length > 2) {
305
+ entities.add(entity);
306
+ }
307
+ }
308
+ }
309
+
310
+ return Array.from(entities).slice(0, 20);
311
+ }
312
+
313
+ /**
314
+ * Reset the last context (for testing or manual reset)
315
+ */
316
+ resetLastContext(): void {
317
+ this.lastContext = null;
318
+ }
319
+
320
+ /**
321
+ * Get the last context snapshot
322
+ */
323
+ getLastContext(): ContextSnapshot | null {
324
+ return this.lastContext;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Create a Continuity Manager instance
330
+ */
331
+ export function createContinuityManager(
332
+ eventStore: EventStore,
333
+ config: EndlessModeConfig
334
+ ): ContinuityManager {
335
+ return new ContinuityManager(eventStore, config);
336
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Edge Repository - CRUD operations for entity/entry relationships
3
+ * AXIOMMIND Entity-Edge Model
4
+ */
5
+
6
+ import { Database } from 'duckdb';
7
+ import { randomUUID } from 'crypto';
8
+ import type { Edge, NodeType, RelationType } from './types.js';
9
+
10
+ export interface CreateEdgeInput {
11
+ srcType: NodeType;
12
+ srcId: string;
13
+ relType: RelationType;
14
+ dstType: NodeType;
15
+ dstId: string;
16
+ metaJson?: Record<string, unknown>;
17
+ }
18
+
19
+ export class EdgeRepo {
20
+ constructor(private db: Database) {}
21
+
22
+ /**
23
+ * Create a new edge (idempotent - ignores duplicates)
24
+ */
25
+ async create(input: CreateEdgeInput): Promise<Edge> {
26
+ const edgeId = randomUUID();
27
+ const now = new Date();
28
+
29
+ await this.db.run(
30
+ `INSERT INTO edges (edge_id, src_type, src_id, rel_type, dst_type, dst_id, meta_json, created_at)
31
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
32
+ ON CONFLICT DO NOTHING`,
33
+ [
34
+ edgeId,
35
+ input.srcType,
36
+ input.srcId,
37
+ input.relType,
38
+ input.dstType,
39
+ input.dstId,
40
+ JSON.stringify(input.metaJson ?? {}),
41
+ now.toISOString()
42
+ ]
43
+ );
44
+
45
+ return {
46
+ edgeId,
47
+ srcType: input.srcType,
48
+ srcId: input.srcId,
49
+ relType: input.relType,
50
+ dstType: input.dstType,
51
+ dstId: input.dstId,
52
+ metaJson: input.metaJson,
53
+ createdAt: now
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Create or update edge
59
+ */
60
+ async upsert(input: CreateEdgeInput): Promise<Edge> {
61
+ // Check for existing edge
62
+ const existing = await this.findByEndpoints(
63
+ input.srcType,
64
+ input.srcId,
65
+ input.relType,
66
+ input.dstType,
67
+ input.dstId
68
+ );
69
+
70
+ if (existing) {
71
+ // Update meta_json
72
+ await this.db.run(
73
+ `UPDATE edges SET meta_json = ? WHERE edge_id = ?`,
74
+ [JSON.stringify(input.metaJson ?? {}), existing.edgeId]
75
+ );
76
+ return { ...existing, metaJson: input.metaJson };
77
+ }
78
+
79
+ return this.create(input);
80
+ }
81
+
82
+ /**
83
+ * Find edge by endpoints
84
+ */
85
+ async findByEndpoints(
86
+ srcType: NodeType,
87
+ srcId: string,
88
+ relType: RelationType,
89
+ dstType: NodeType,
90
+ dstId: string
91
+ ): Promise<Edge | null> {
92
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
93
+ `SELECT * FROM edges
94
+ WHERE src_type = ? AND src_id = ? AND rel_type = ?
95
+ AND dst_type = ? AND dst_id = ?`,
96
+ [srcType, srcId, relType, dstType, dstId]
97
+ );
98
+
99
+ if (rows.length === 0) return null;
100
+ return this.rowToEdge(rows[0]);
101
+ }
102
+
103
+ /**
104
+ * Find edges by source
105
+ */
106
+ async findBySrc(
107
+ srcId: string,
108
+ relType?: RelationType
109
+ ): Promise<Edge[]> {
110
+ let query = `SELECT * FROM edges WHERE src_id = ?`;
111
+ const params: unknown[] = [srcId];
112
+
113
+ if (relType) {
114
+ query += ` AND rel_type = ?`;
115
+ params.push(relType);
116
+ }
117
+
118
+ query += ` ORDER BY created_at DESC`;
119
+
120
+ const rows = await this.db.all<Array<Record<string, unknown>>>(query, params);
121
+ return rows.map(row => this.rowToEdge(row));
122
+ }
123
+
124
+ /**
125
+ * Find edges by destination
126
+ */
127
+ async findByDst(
128
+ dstId: string,
129
+ relType?: RelationType
130
+ ): Promise<Edge[]> {
131
+ let query = `SELECT * FROM edges WHERE dst_id = ?`;
132
+ const params: unknown[] = [dstId];
133
+
134
+ if (relType) {
135
+ query += ` AND rel_type = ?`;
136
+ params.push(relType);
137
+ }
138
+
139
+ query += ` ORDER BY created_at DESC`;
140
+
141
+ const rows = await this.db.all<Array<Record<string, unknown>>>(query, params);
142
+ return rows.map(row => this.rowToEdge(row));
143
+ }
144
+
145
+ /**
146
+ * Find all edges for a node (both directions)
147
+ */
148
+ async findByNode(nodeId: string): Promise<{ outgoing: Edge[]; incoming: Edge[] }> {
149
+ const outgoing = await this.findBySrc(nodeId);
150
+ const incoming = await this.findByDst(nodeId);
151
+ return { outgoing, incoming };
152
+ }
153
+
154
+ /**
155
+ * Delete edge by ID
156
+ */
157
+ async delete(edgeId: string): Promise<boolean> {
158
+ const result = await this.db.run(
159
+ `DELETE FROM edges WHERE edge_id = ?`,
160
+ [edgeId]
161
+ );
162
+ return true; // DuckDB doesn't return affected rows easily
163
+ }
164
+
165
+ /**
166
+ * Delete edges by source and relation type
167
+ */
168
+ async deleteBySrcAndRel(srcId: string, relType: RelationType): Promise<number> {
169
+ await this.db.run(
170
+ `DELETE FROM edges WHERE src_id = ? AND rel_type = ?`,
171
+ [srcId, relType]
172
+ );
173
+ return 0; // DuckDB doesn't return affected rows easily
174
+ }
175
+
176
+ /**
177
+ * Delete edges by destination and relation type
178
+ */
179
+ async deleteByDstAndRel(dstId: string, relType: RelationType): Promise<number> {
180
+ await this.db.run(
181
+ `DELETE FROM edges WHERE dst_id = ? AND rel_type = ?`,
182
+ [dstId, relType]
183
+ );
184
+ return 0;
185
+ }
186
+
187
+ /**
188
+ * Replace edges for a source and relation type
189
+ * Used for mode=replace in task_blockers_set
190
+ */
191
+ async replaceEdges(
192
+ srcId: string,
193
+ relType: RelationType,
194
+ newEdges: Omit<CreateEdgeInput, 'srcId' | 'relType'>[]
195
+ ): Promise<Edge[]> {
196
+ // Delete existing edges
197
+ await this.deleteBySrcAndRel(srcId, relType);
198
+
199
+ // Create new edges
200
+ const created: Edge[] = [];
201
+ for (const edge of newEdges) {
202
+ const newEdge = await this.create({
203
+ srcType: edge.srcType,
204
+ srcId,
205
+ relType,
206
+ dstType: edge.dstType,
207
+ dstId: edge.dstId,
208
+ metaJson: edge.metaJson
209
+ });
210
+ created.push(newEdge);
211
+ }
212
+
213
+ return created;
214
+ }
215
+
216
+ /**
217
+ * Get effective blockers (resolving condition → task)
218
+ * Returns resolved blocker if condition has resolves_to edge
219
+ */
220
+ async getEffectiveBlockers(taskId: string): Promise<Array<{
221
+ originalId: string;
222
+ effectiveId: string;
223
+ isResolved: boolean;
224
+ }>> {
225
+ const blockerEdges = await this.findBySrc(taskId, 'blocked_by');
226
+ const results: Array<{
227
+ originalId: string;
228
+ effectiveId: string;
229
+ isResolved: boolean;
230
+ }> = [];
231
+
232
+ for (const edge of blockerEdges) {
233
+ // Check if blocker has resolves_to edge
234
+ const resolvesTo = await this.db.all<Array<Record<string, unknown>>>(
235
+ `SELECT dst_id FROM edges
236
+ WHERE src_id = ? AND rel_type = 'resolves_to'
237
+ LIMIT 1`,
238
+ [edge.dstId]
239
+ );
240
+
241
+ if (resolvesTo.length > 0) {
242
+ results.push({
243
+ originalId: edge.dstId,
244
+ effectiveId: resolvesTo[0].dst_id as string,
245
+ isResolved: true
246
+ });
247
+ } else {
248
+ results.push({
249
+ originalId: edge.dstId,
250
+ effectiveId: edge.dstId,
251
+ isResolved: false
252
+ });
253
+ }
254
+ }
255
+
256
+ return results;
257
+ }
258
+
259
+ /**
260
+ * Find 2-hop related entries (Entry → Entity → Entry)
261
+ */
262
+ async findRelatedEntries(entryId: string): Promise<Array<{
263
+ entryId: string;
264
+ viaEntityId: string;
265
+ relationPath: string;
266
+ }>> {
267
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
268
+ `WITH first_hop AS (
269
+ SELECT e1.dst_id AS entity_id
270
+ FROM edges e1
271
+ WHERE e1.src_type = 'entry'
272
+ AND e1.rel_type = 'evidence_of'
273
+ AND e1.src_id = ?
274
+ )
275
+ SELECT
276
+ e2.src_id AS entry_id,
277
+ f.entity_id AS via_entity_id,
278
+ 'evidence_of→evidence_of' AS relation_path
279
+ FROM first_hop f
280
+ JOIN edges e2 ON e2.dst_id = f.entity_id
281
+ AND e2.rel_type = 'evidence_of'
282
+ AND e2.src_type = 'entry'
283
+ WHERE e2.src_id != ?`,
284
+ [entryId, entryId]
285
+ );
286
+
287
+ return rows.map(row => ({
288
+ entryId: row.entry_id as string,
289
+ viaEntityId: row.via_entity_id as string,
290
+ relationPath: row.relation_path as string
291
+ }));
292
+ }
293
+
294
+ /**
295
+ * Count edges by relation type
296
+ */
297
+ async countByRelType(): Promise<Array<{ relType: string; count: number }>> {
298
+ const rows = await this.db.all<Array<{ rel_type: string; count: number }>>(
299
+ `SELECT rel_type, COUNT(*) as count FROM edges GROUP BY rel_type`
300
+ );
301
+ return rows.map(row => ({
302
+ relType: row.rel_type,
303
+ count: Number(row.count)
304
+ }));
305
+ }
306
+
307
+ /**
308
+ * Convert database row to Edge
309
+ */
310
+ private rowToEdge(row: Record<string, unknown>): Edge {
311
+ return {
312
+ edgeId: row.edge_id as string,
313
+ srcType: row.src_type as NodeType,
314
+ srcId: row.src_id as string,
315
+ relType: row.rel_type as RelationType,
316
+ dstType: row.dst_type as NodeType,
317
+ dstId: row.dst_id as string,
318
+ metaJson: typeof row.meta_json === 'string'
319
+ ? JSON.parse(row.meta_json)
320
+ : row.meta_json as Record<string, unknown> | undefined,
321
+ createdAt: new Date(row.created_at as string)
322
+ };
323
+ }
324
+ }