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,325 @@
1
+ /**
2
+ * Blocker Resolver - Resolve blocker texts to entity references
3
+ * AXIOMMIND: No stub task creation, fallback to condition
4
+ */
5
+
6
+ import { Database } from 'duckdb';
7
+ import { randomUUID } from 'crypto';
8
+ import type { BlockerRef, BlockerKind, Entity } from '../types.js';
9
+ import { makeEntityCanonicalKey, makeArtifactKey } from '../canonical-key.js';
10
+ import { TaskMatcher } from './task-matcher.js';
11
+
12
+ export interface BlockerResolverConfig {
13
+ project?: string;
14
+ }
15
+
16
+ // Patterns for artifact detection
17
+ const URL_PATTERN = /^https?:\/\/.+/;
18
+ const JIRA_PATTERN = /^[A-Z]+-\d+$/;
19
+ const GITHUB_ISSUE_PATTERN = /^[^\/]+\/[^#]+#\d+$/;
20
+ const TASK_ID_PATTERN = /^task:[^:]+:[^:]+$/;
21
+
22
+ export class BlockerResolver {
23
+ private taskMatcher: TaskMatcher;
24
+
25
+ constructor(
26
+ private db: Database,
27
+ private config: BlockerResolverConfig = {}
28
+ ) {
29
+ this.taskMatcher = new TaskMatcher(db);
30
+ }
31
+
32
+ /**
33
+ * Resolve a single blocker text to entity reference
34
+ * Rules:
35
+ * 1. Strong ID/URL/key pattern → artifact
36
+ * 2. Explicit task_id → task
37
+ * 3. Task title match (strict only) → task
38
+ * 4. Fallback → condition (no stub task creation)
39
+ */
40
+ async resolveBlocker(
41
+ text: string,
42
+ sourceEntryId?: string
43
+ ): Promise<BlockerRef> {
44
+ const trimmedText = text.trim();
45
+
46
+ // Rule 1: Check for artifact patterns
47
+ const artifactRef = await this.tryResolveAsArtifact(trimmedText);
48
+ if (artifactRef) {
49
+ return artifactRef;
50
+ }
51
+
52
+ // Rule 2: Check for explicit task_id pattern
53
+ if (TASK_ID_PATTERN.test(trimmedText)) {
54
+ const taskRef = await this.tryResolveAsTaskId(trimmedText);
55
+ if (taskRef) {
56
+ return taskRef;
57
+ }
58
+ // Task ID not found, fall through to condition
59
+ }
60
+
61
+ // Rule 3: Try task title matching (strict only)
62
+ const taskMatch = await this.taskMatcher.match(trimmedText, this.config.project);
63
+
64
+ if (taskMatch.confidence === 'high' && taskMatch.match) {
65
+ // Strict match found
66
+ return {
67
+ kind: 'task',
68
+ entityId: taskMatch.match.entityId,
69
+ rawText: trimmedText,
70
+ confidence: taskMatch.score
71
+ };
72
+ }
73
+
74
+ // Rule 4: Fallback to condition (get-or-create)
75
+ // Also store candidates if any
76
+ const conditionRef = await this.createConditionBlocker(
77
+ trimmedText,
78
+ taskMatch.candidates
79
+ );
80
+
81
+ return conditionRef;
82
+ }
83
+
84
+ /**
85
+ * Resolve multiple blocker texts
86
+ */
87
+ async resolveBlockers(
88
+ texts: string[],
89
+ sourceEntryId?: string
90
+ ): Promise<BlockerRef[]> {
91
+ const results: BlockerRef[] = [];
92
+
93
+ for (const text of texts) {
94
+ const ref = await this.resolveBlocker(text, sourceEntryId);
95
+ results.push(ref);
96
+ }
97
+
98
+ return results;
99
+ }
100
+
101
+ /**
102
+ * Try to resolve as artifact (URL, JIRA, GitHub)
103
+ */
104
+ private async tryResolveAsArtifact(text: string): Promise<BlockerRef | null> {
105
+ // Check patterns
106
+ if (!URL_PATTERN.test(text) && !JIRA_PATTERN.test(text) && !GITHUB_ISSUE_PATTERN.test(text)) {
107
+ return null;
108
+ }
109
+
110
+ const canonicalKey = makeArtifactKey(text);
111
+
112
+ // Find or create artifact
113
+ const existing = await this.db.all<Array<Record<string, unknown>>>(
114
+ `SELECT entity_id FROM entities
115
+ WHERE entity_type = 'artifact' AND canonical_key = ?`,
116
+ [canonicalKey]
117
+ );
118
+
119
+ let entityId: string;
120
+
121
+ if (existing.length > 0) {
122
+ entityId = existing[0].entity_id as string;
123
+ } else {
124
+ // Create artifact entity via event
125
+ entityId = await this.declareArtifact(text, canonicalKey);
126
+ }
127
+
128
+ return {
129
+ kind: 'artifact',
130
+ entityId,
131
+ rawText: text,
132
+ confidence: 1.0
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Try to resolve as explicit task ID
138
+ */
139
+ private async tryResolveAsTaskId(taskId: string): Promise<BlockerRef | null> {
140
+ // taskId format: task:project:identifier
141
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
142
+ `SELECT entity_id FROM entities
143
+ WHERE entity_type = 'task' AND canonical_key = ?
144
+ AND status = 'active'`,
145
+ [taskId]
146
+ );
147
+
148
+ if (rows.length === 0) {
149
+ return null;
150
+ }
151
+
152
+ return {
153
+ kind: 'task',
154
+ entityId: rows[0].entity_id as string,
155
+ rawText: taskId,
156
+ confidence: 1.0
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Create condition blocker (get-or-create)
162
+ */
163
+ private async createConditionBlocker(
164
+ text: string,
165
+ candidates?: Entity[]
166
+ ): Promise<BlockerRef> {
167
+ const canonicalKey = makeEntityCanonicalKey('condition', text, {
168
+ project: this.config.project
169
+ });
170
+
171
+ // Find existing condition
172
+ const existing = await this.db.all<Array<Record<string, unknown>>>(
173
+ `SELECT entity_id FROM entities
174
+ WHERE entity_type = 'condition' AND canonical_key = ?`,
175
+ [canonicalKey]
176
+ );
177
+
178
+ let entityId: string;
179
+
180
+ if (existing.length > 0) {
181
+ entityId = existing[0].entity_id as string;
182
+ } else {
183
+ // Create condition entity via event
184
+ entityId = await this.declareCondition(text, canonicalKey, candidates);
185
+ }
186
+
187
+ return {
188
+ kind: 'condition',
189
+ entityId,
190
+ rawText: text,
191
+ confidence: 0.5,
192
+ candidates: candidates?.map(c => c.entityId)
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Declare a new condition entity
198
+ */
199
+ private async declareCondition(
200
+ text: string,
201
+ canonicalKey: string,
202
+ candidates?: Entity[]
203
+ ): Promise<string> {
204
+ const entityId = randomUUID();
205
+ const now = new Date().toISOString();
206
+
207
+ const currentJson = {
208
+ text,
209
+ resolved: false,
210
+ candidates: candidates?.map(c => ({
211
+ entityId: c.entityId,
212
+ title: c.title
213
+ }))
214
+ };
215
+
216
+ await this.db.run(
217
+ `INSERT INTO entities (
218
+ entity_id, entity_type, canonical_key, title, stage, status,
219
+ current_json, title_norm, search_text, created_at, updated_at
220
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
221
+ [
222
+ entityId,
223
+ 'condition',
224
+ canonicalKey,
225
+ text,
226
+ 'raw',
227
+ 'active',
228
+ JSON.stringify(currentJson),
229
+ text.toLowerCase().trim(),
230
+ text,
231
+ now,
232
+ now
233
+ ]
234
+ );
235
+
236
+ // Create alias
237
+ await this.db.run(
238
+ `INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
239
+ VALUES (?, ?, ?, TRUE)
240
+ ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
241
+ ['condition', canonicalKey, entityId]
242
+ );
243
+
244
+ return entityId;
245
+ }
246
+
247
+ /**
248
+ * Declare a new artifact entity
249
+ */
250
+ private async declareArtifact(
251
+ identifier: string,
252
+ canonicalKey: string
253
+ ): Promise<string> {
254
+ const entityId = randomUUID();
255
+ const now = new Date().toISOString();
256
+
257
+ // Determine artifact type
258
+ let artifactType = 'generic';
259
+ if (URL_PATTERN.test(identifier)) {
260
+ artifactType = 'url';
261
+ } else if (JIRA_PATTERN.test(identifier)) {
262
+ artifactType = 'jira';
263
+ } else if (GITHUB_ISSUE_PATTERN.test(identifier)) {
264
+ artifactType = 'github_issue';
265
+ }
266
+
267
+ const currentJson = {
268
+ identifier,
269
+ artifactType
270
+ };
271
+
272
+ await this.db.run(
273
+ `INSERT INTO entities (
274
+ entity_id, entity_type, canonical_key, title, stage, status,
275
+ current_json, title_norm, search_text, created_at, updated_at
276
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
277
+ [
278
+ entityId,
279
+ 'artifact',
280
+ canonicalKey,
281
+ identifier,
282
+ 'raw',
283
+ 'active',
284
+ JSON.stringify(currentJson),
285
+ identifier.toLowerCase(),
286
+ identifier,
287
+ now,
288
+ now
289
+ ]
290
+ );
291
+
292
+ // Create alias
293
+ await this.db.run(
294
+ `INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
295
+ VALUES (?, ?, ?, TRUE)
296
+ ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
297
+ ['artifact', canonicalKey, entityId]
298
+ );
299
+
300
+ return entityId;
301
+ }
302
+
303
+ /**
304
+ * Create unknown placeholder condition
305
+ * Used when task is blocked but no blocker text provided
306
+ */
307
+ async createUnknownPlaceholder(taskTitle: string): Promise<BlockerRef> {
308
+ const text = `Unknown blocker for: ${taskTitle}`;
309
+
310
+ const ref = await this.createConditionBlocker(text);
311
+
312
+ // Mark as auto placeholder
313
+ await this.db.run(
314
+ `UPDATE entities
315
+ SET current_json = json_set(current_json, '$.auto_placeholder', true)
316
+ WHERE entity_id = ?`,
317
+ [ref.entityId]
318
+ );
319
+
320
+ return {
321
+ ...ref,
322
+ confidence: 0.0
323
+ };
324
+ }
325
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Task Entity System - AXIOMMIND Principle 5
3
+ * Task is Entity, state via event fold
4
+ */
5
+
6
+ export * from './task-matcher.js';
7
+ export * from './blocker-resolver.js';
8
+ export * from './task-resolver.js';
9
+ export * from './task-projector.js';
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Task Matcher - Find existing tasks by title similarity
3
+ * AXIOMMIND: strict matching (≥0.92, gap≥0.03)
4
+ */
5
+
6
+ import { Database } from 'duckdb';
7
+ import type { Entity, MatchConfidence } from '../types.js';
8
+ import { makeEntityCanonicalKey } from '../canonical-key.js';
9
+ import { MATCH_THRESHOLDS } from '../types.js';
10
+
11
+ export interface TaskMatchResult {
12
+ match: Entity | null;
13
+ confidence: MatchConfidence;
14
+ score: number;
15
+ gap?: number;
16
+ candidates?: Entity[];
17
+ }
18
+
19
+ export interface TaskMatcherConfig {
20
+ minCombinedScore: number;
21
+ minGap: number;
22
+ suggestionThreshold: number;
23
+ maxCandidates: number;
24
+ }
25
+
26
+ const DEFAULT_CONFIG: TaskMatcherConfig = {
27
+ minCombinedScore: MATCH_THRESHOLDS.minCombinedScore,
28
+ minGap: MATCH_THRESHOLDS.minGap,
29
+ suggestionThreshold: MATCH_THRESHOLDS.suggestionThreshold,
30
+ maxCandidates: 5
31
+ };
32
+
33
+ export class TaskMatcher {
34
+ private readonly config: TaskMatcherConfig;
35
+
36
+ constructor(
37
+ private db: Database,
38
+ config?: Partial<TaskMatcherConfig>
39
+ ) {
40
+ this.config = { ...DEFAULT_CONFIG, ...config };
41
+ }
42
+
43
+ /**
44
+ * Find task by exact canonical key match
45
+ */
46
+ async findExact(title: string, project?: string): Promise<Entity | null> {
47
+ const canonicalKey = makeEntityCanonicalKey('task', title, { project });
48
+
49
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
50
+ `SELECT * FROM entities
51
+ WHERE entity_type = 'task' AND canonical_key = ?
52
+ AND status = 'active'`,
53
+ [canonicalKey]
54
+ );
55
+
56
+ if (rows.length === 0) return null;
57
+ return this.rowToEntity(rows[0]);
58
+ }
59
+
60
+ /**
61
+ * Find task by alias
62
+ */
63
+ async findByAlias(title: string, project?: string): Promise<Entity | null> {
64
+ const canonicalKey = makeEntityCanonicalKey('task', title, { project });
65
+
66
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
67
+ `SELECT e.* FROM entities e
68
+ JOIN entity_aliases a ON e.entity_id = a.entity_id
69
+ WHERE a.entity_type = 'task' AND a.canonical_key = ?
70
+ AND e.status = 'active'`,
71
+ [canonicalKey]
72
+ );
73
+
74
+ if (rows.length === 0) return null;
75
+ return this.rowToEntity(rows[0]);
76
+ }
77
+
78
+ /**
79
+ * Search tasks by text (FTS-like)
80
+ */
81
+ async searchByText(query: string, project?: string): Promise<Array<{ entity: Entity; score: number }>> {
82
+ const searchPattern = `%${query.toLowerCase()}%`;
83
+
84
+ let sql = `
85
+ SELECT *,
86
+ CASE
87
+ WHEN title_norm = ? THEN 1.0
88
+ WHEN title_norm LIKE ? THEN 0.9
89
+ ELSE 0.7
90
+ END as match_score
91
+ FROM entities
92
+ WHERE entity_type = 'task'
93
+ AND status = 'active'
94
+ AND (title_norm LIKE ? OR search_text LIKE ?)
95
+ `;
96
+
97
+ const normalizedQuery = query.toLowerCase().trim();
98
+ const params: unknown[] = [normalizedQuery, `%${normalizedQuery}%`, searchPattern, searchPattern];
99
+
100
+ if (project) {
101
+ sql += ` AND json_extract(current_json, '$.project') = ?`;
102
+ params.push(project);
103
+ }
104
+
105
+ sql += ` ORDER BY match_score DESC, updated_at DESC LIMIT ?`;
106
+ params.push(this.config.maxCandidates);
107
+
108
+ const rows = await this.db.all<Array<Record<string, unknown>>>(sql, params);
109
+
110
+ return rows.map(row => ({
111
+ entity: this.rowToEntity(row),
112
+ score: row.match_score as number
113
+ }));
114
+ }
115
+
116
+ /**
117
+ * Match task with confidence classification
118
+ * Returns high confidence only if score ≥ 0.92 AND gap ≥ 0.03
119
+ */
120
+ async match(title: string, project?: string): Promise<TaskMatchResult> {
121
+ // Step 1: Try exact match
122
+ const exactMatch = await this.findExact(title, project);
123
+ if (exactMatch) {
124
+ return {
125
+ match: exactMatch,
126
+ confidence: 'high',
127
+ score: 1.0
128
+ };
129
+ }
130
+
131
+ // Step 2: Try alias match
132
+ const aliasMatch = await this.findByAlias(title, project);
133
+ if (aliasMatch) {
134
+ return {
135
+ match: aliasMatch,
136
+ confidence: 'high',
137
+ score: 0.98
138
+ };
139
+ }
140
+
141
+ // Step 3: Try text search
142
+ const searchResults = await this.searchByText(title, project);
143
+ if (searchResults.length === 0) {
144
+ return {
145
+ match: null,
146
+ confidence: 'none',
147
+ score: 0
148
+ };
149
+ }
150
+
151
+ const topResult = searchResults[0];
152
+ const secondScore = searchResults.length > 1 ? searchResults[1].score : null;
153
+
154
+ // Calculate gap
155
+ const gap = secondScore !== null ? topResult.score - secondScore : Infinity;
156
+
157
+ // Classify confidence
158
+ const confidence = this.classifyConfidence(topResult.score, gap);
159
+
160
+ // For strict matching, only return high confidence if criteria met
161
+ if (confidence === 'high') {
162
+ return {
163
+ match: topResult.entity,
164
+ confidence: 'high',
165
+ score: topResult.score,
166
+ gap
167
+ };
168
+ }
169
+
170
+ // For suggested, return candidates
171
+ if (confidence === 'suggested') {
172
+ return {
173
+ match: null,
174
+ confidence: 'suggested',
175
+ score: topResult.score,
176
+ gap,
177
+ candidates: searchResults.slice(0, this.config.maxCandidates).map(r => r.entity)
178
+ };
179
+ }
180
+
181
+ return {
182
+ match: null,
183
+ confidence: 'none',
184
+ score: topResult.score
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Classify confidence based on AXIOMMIND thresholds
190
+ */
191
+ private classifyConfidence(score: number, gap: number): MatchConfidence {
192
+ const { minCombinedScore, minGap, suggestionThreshold } = this.config;
193
+
194
+ // High confidence: score ≥ 0.92 AND gap ≥ 0.03
195
+ if (score >= minCombinedScore && gap >= minGap) {
196
+ return 'high';
197
+ }
198
+
199
+ // Suggested: score ≥ 0.75
200
+ if (score >= suggestionThreshold) {
201
+ return 'suggested';
202
+ }
203
+
204
+ return 'none';
205
+ }
206
+
207
+ /**
208
+ * Get suggestion candidates (for condition fallback)
209
+ */
210
+ async getSuggestionCandidates(title: string, project?: string): Promise<Entity[]> {
211
+ const searchResults = await this.searchByText(title, project);
212
+ return searchResults
213
+ .filter(r => r.score >= this.config.suggestionThreshold)
214
+ .slice(0, this.config.maxCandidates)
215
+ .map(r => r.entity);
216
+ }
217
+
218
+ /**
219
+ * Convert database row to Entity
220
+ */
221
+ private rowToEntity(row: Record<string, unknown>): Entity {
222
+ return {
223
+ entityId: row.entity_id as string,
224
+ entityType: row.entity_type as 'task' | 'condition' | 'artifact',
225
+ canonicalKey: row.canonical_key as string,
226
+ title: row.title as string,
227
+ stage: row.stage as 'raw' | 'working' | 'candidate' | 'verified' | 'certified',
228
+ status: row.status as 'active' | 'contested' | 'deprecated' | 'superseded',
229
+ currentJson: typeof row.current_json === 'string'
230
+ ? JSON.parse(row.current_json)
231
+ : row.current_json as Record<string, unknown>,
232
+ titleNorm: row.title_norm as string | undefined,
233
+ searchText: row.search_text as string | undefined,
234
+ createdAt: new Date(row.created_at as string),
235
+ updatedAt: new Date(row.updated_at as string)
236
+ };
237
+ }
238
+ }