@jungjaehoon/mama-core 1.0.1
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/README.md +171 -0
- package/package.json +68 -0
- package/src/config-loader.js +218 -0
- package/src/db-adapter/README.md +110 -0
- package/src/db-adapter/base-adapter.js +91 -0
- package/src/db-adapter/index.js +31 -0
- package/src/db-adapter/sqlite-adapter.js +364 -0
- package/src/db-adapter/statement.js +127 -0
- package/src/db-manager.js +671 -0
- package/src/debug-logger.js +86 -0
- package/src/decision-formatter.js +1276 -0
- package/src/decision-tracker.js +621 -0
- package/src/embedding-cache.js +222 -0
- package/src/embedding-client.js +141 -0
- package/src/embedding-server/index.js +424 -0
- package/src/embedding-server/mobile/auth.js +160 -0
- package/src/embedding-server/mobile/daemon.js +313 -0
- package/src/embedding-server/mobile/output-parser.js +281 -0
- package/src/embedding-server/mobile/session-api.js +279 -0
- package/src/embedding-server/mobile/session-manager.js +377 -0
- package/src/embedding-server/mobile/websocket-handler.js +389 -0
- package/src/embeddings.js +305 -0
- package/src/errors.js +326 -0
- package/src/index.js +41 -0
- package/src/mama-api.js +2614 -0
- package/src/memory-inject.js +174 -0
- package/src/memory-store.js +89 -0
- package/src/notification-manager.js +3 -0
- package/src/ollama-client.js +391 -0
- package/src/outcome-tracker.js +351 -0
- package/src/progress-indicator.js +110 -0
- package/src/query-intent.js +237 -0
- package/src/relevance-scorer.js +286 -0
- package/src/tier-validator.js +269 -0
- package/src/time-formatter.js +98 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAMA (Memory-Augmented MCP Architecture) - Decision Tracker
|
|
3
|
+
*
|
|
4
|
+
* Learn and store decisions with graph relationships
|
|
5
|
+
* Tasks: 3.1-3.9 (Learn decision, ID generation, supersedes edges, refinement, embeddings)
|
|
6
|
+
* AC #1: Decision stored with outcome=NULL, confidence from LLM
|
|
7
|
+
* AC #2: Supersedes relationship creation
|
|
8
|
+
* AC #5: Multi-parent refinement with confidence calculation
|
|
9
|
+
*
|
|
10
|
+
* Updated for PostgreSQL compatibility via db-manager
|
|
11
|
+
*
|
|
12
|
+
* @module decision-tracker
|
|
13
|
+
* @version 2.0
|
|
14
|
+
* @date 2025-11-17
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line no-unused-vars
|
|
18
|
+
const { info, error: logError } = require('./debug-logger');
|
|
19
|
+
const {
|
|
20
|
+
initDB,
|
|
21
|
+
insertDecisionWithEmbedding,
|
|
22
|
+
// eslint-disable-next-line no-unused-vars
|
|
23
|
+
queryDecisionGraph,
|
|
24
|
+
getAdapter,
|
|
25
|
+
} = require('./memory-store');
|
|
26
|
+
|
|
27
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
// Story 2.1: Extended Edge Types
|
|
29
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
// Valid relationship types for decision_edges
|
|
31
|
+
// Original: supersedes, refines, contradicts
|
|
32
|
+
// v1.3 Extension: builds_on, debates, synthesizes
|
|
33
|
+
const VALID_EDGE_TYPES = [
|
|
34
|
+
'supersedes', // Original: New decision replaces old one
|
|
35
|
+
'refines', // Original: Decision refines another
|
|
36
|
+
'contradicts', // Original: Decision contradicts another
|
|
37
|
+
'builds_on', // v1.3: Extends existing decision with new insights
|
|
38
|
+
'debates', // v1.3: Presents counter-argument with evidence
|
|
39
|
+
'synthesizes', // v1.3: Merges multiple decisions into unified approach
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate decision ID
|
|
44
|
+
*
|
|
45
|
+
* Task 3.2: Generate decision ID: `decision_${topic}_${timestamp}`
|
|
46
|
+
*
|
|
47
|
+
* @param {string} topic - Decision topic
|
|
48
|
+
* @returns {string} Decision ID
|
|
49
|
+
*/
|
|
50
|
+
function generateDecisionId(topic) {
|
|
51
|
+
// Sanitize topic: remove spaces, lowercase, max 50 chars
|
|
52
|
+
const sanitized = topic
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/\s+/g, '_')
|
|
55
|
+
.replace(/[^a-z0-9_]/g, '')
|
|
56
|
+
.substring(0, 50);
|
|
57
|
+
|
|
58
|
+
const timestamp = Date.now();
|
|
59
|
+
const random = Math.random().toString(36).substr(2, 4);
|
|
60
|
+
|
|
61
|
+
return `decision_${sanitized}_${timestamp}_${random}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check for previous decision on same topic
|
|
66
|
+
*
|
|
67
|
+
* Task 3.3: Query decisions table WHERE topic=? AND superseded_by IS NULL
|
|
68
|
+
* AC #2: Find previous decision to create supersedes relationship
|
|
69
|
+
*
|
|
70
|
+
* @param {string} topic - Decision topic
|
|
71
|
+
* @returns {Promise<Object|null>} Previous decision or null
|
|
72
|
+
*/
|
|
73
|
+
async function getPreviousDecision(topic) {
|
|
74
|
+
const adapter = getAdapter();
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const stmt = adapter.prepare(`
|
|
78
|
+
SELECT * FROM decisions
|
|
79
|
+
WHERE topic = ? AND superseded_by IS NULL
|
|
80
|
+
ORDER BY created_at DESC
|
|
81
|
+
LIMIT 1
|
|
82
|
+
`);
|
|
83
|
+
|
|
84
|
+
const previous = await stmt.get(topic);
|
|
85
|
+
return previous || null;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Failed to query previous decision: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a decision edge with specified relationship type
|
|
93
|
+
*
|
|
94
|
+
* Story 2.1: Generic edge creation supporting all relationship types
|
|
95
|
+
*
|
|
96
|
+
* @param {string} fromId - Source decision ID
|
|
97
|
+
* @param {string} toId - Target decision ID
|
|
98
|
+
* @param {string} relationship - Edge type (supersedes, builds_on, debates, synthesizes, etc.)
|
|
99
|
+
* @param {string} reason - Reason for the relationship
|
|
100
|
+
* @returns {Promise<boolean>} Success status
|
|
101
|
+
*/
|
|
102
|
+
async function createEdge(fromId, toId, relationship, reason) {
|
|
103
|
+
const adapter = getAdapter();
|
|
104
|
+
|
|
105
|
+
// Story 2.1: Runtime validation of edge types
|
|
106
|
+
if (!VALID_EDGE_TYPES.includes(relationship)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Invalid edge type: "${relationship}". Valid types: ${VALID_EDGE_TYPES.join(', ')}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Note: SQLite CHECK constraint only allows supersedes/refines/contradicts
|
|
114
|
+
// New types (builds_on, debates, synthesizes) bypass CHECK via runtime validation
|
|
115
|
+
// The INSERT will fail for new types due to CHECK constraint
|
|
116
|
+
// WORKAROUND: Use PRAGMA ignore_check_constraints or recreate table
|
|
117
|
+
// For now, we'll catch the error and handle gracefully
|
|
118
|
+
|
|
119
|
+
// Story 2.1: LLM auto-detected edges are approved by default (approved_by_user=1)
|
|
120
|
+
// This allows them to appear in search results via querySemanticEdges
|
|
121
|
+
const stmt = adapter.prepare(`
|
|
122
|
+
INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, created_at, created_by, approved_by_user)
|
|
123
|
+
VALUES (?, ?, ?, ?, ?, 'llm', 1)
|
|
124
|
+
`);
|
|
125
|
+
|
|
126
|
+
await stmt.run(fromId, toId, relationship, reason, Date.now());
|
|
127
|
+
return true;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Handle CHECK constraint failure for new edge types
|
|
130
|
+
if (error.message.includes('CHECK constraint failed')) {
|
|
131
|
+
info(
|
|
132
|
+
`[decision-tracker] Edge type "${relationship}" not yet supported in schema, skipping edge creation`
|
|
133
|
+
);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Failed to create ${relationship} edge: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create supersedes edge
|
|
142
|
+
*
|
|
143
|
+
* Task 3.5: Create supersedes edge (INSERT INTO decision_edges)
|
|
144
|
+
* AC #2: Supersedes relationship creation
|
|
145
|
+
*
|
|
146
|
+
* @param {string} fromId - New decision ID
|
|
147
|
+
* @param {string} toId - Previous decision ID
|
|
148
|
+
* @param {string} reason - Reason for superseding
|
|
149
|
+
*/
|
|
150
|
+
async function createSupersedesEdge(fromId, toId, reason) {
|
|
151
|
+
return createEdge(fromId, toId, 'supersedes', reason);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Update previous decision's superseded_by field
|
|
156
|
+
*
|
|
157
|
+
* Task 3.5: Update previous decision's superseded_by field
|
|
158
|
+
* AC #2: Previous decision's superseded_by field updated
|
|
159
|
+
*
|
|
160
|
+
* @param {string} previousId - Previous decision ID
|
|
161
|
+
* @param {string} newId - New decision ID
|
|
162
|
+
*/
|
|
163
|
+
async function markSuperseded(previousId, newId) {
|
|
164
|
+
const adapter = getAdapter();
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const stmt = adapter.prepare(`
|
|
168
|
+
UPDATE decisions
|
|
169
|
+
SET superseded_by = ?, updated_at = ?
|
|
170
|
+
WHERE id = ?
|
|
171
|
+
`);
|
|
172
|
+
|
|
173
|
+
await stmt.run(newId, Date.now(), previousId);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
throw new Error(`Failed to mark decision as superseded: ${error.message}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Calculate combined confidence (Bayesian update)
|
|
181
|
+
*
|
|
182
|
+
* Task 3.6: Calculate combined confidence for multi-parent refinement
|
|
183
|
+
* AC #5: Confidence score calculated based on history
|
|
184
|
+
*
|
|
185
|
+
* @param {number} prior - Prior confidence
|
|
186
|
+
* @param {Array<Object>} parents - Parent decisions
|
|
187
|
+
* @returns {number} Updated confidence (0.0-1.0)
|
|
188
|
+
*/
|
|
189
|
+
function calculateCombinedConfidence(prior, parents) {
|
|
190
|
+
if (!parents || parents.length === 0) {
|
|
191
|
+
return prior;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Bayesian update: Average parent confidences + prior
|
|
195
|
+
const parentConfidences = parents.map((p) => p.confidence || 0.5);
|
|
196
|
+
const avgParentConfidence =
|
|
197
|
+
parentConfidences.reduce((a, b) => a + b, 0) / parentConfidences.length;
|
|
198
|
+
|
|
199
|
+
// Weighted average: 60% prior, 40% parent history
|
|
200
|
+
const combined = prior * 0.6 + avgParentConfidence * 0.4;
|
|
201
|
+
|
|
202
|
+
// Clamp to [0.0, 1.0]
|
|
203
|
+
return Math.max(0, Math.min(1, combined));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Detect multi-parent refinement
|
|
208
|
+
*
|
|
209
|
+
* Task 3.6: Detect if new decision refines multiple previous decisions
|
|
210
|
+
* AC #5: Multi-parent refinement
|
|
211
|
+
*
|
|
212
|
+
* @param {Object} detection - Decision detection result
|
|
213
|
+
* @param {Object} sessionContext - Session context
|
|
214
|
+
* @returns {Array<string>|null} Array of parent decision IDs or null
|
|
215
|
+
*/
|
|
216
|
+
function detectRefinement(_detection, _sessionContext) {
|
|
217
|
+
// TODO: Implement refinement detection heuristics
|
|
218
|
+
// For now, return null (single-parent only)
|
|
219
|
+
// Future: Analyze session context for references to multiple decisions
|
|
220
|
+
|
|
221
|
+
// Example heuristics:
|
|
222
|
+
// 1. User message mentions "combine", "merge", "refine"
|
|
223
|
+
// 2. Recent exchange references multiple topics
|
|
224
|
+
// 3. Decision reasoning mentions multiple approaches
|
|
225
|
+
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
230
|
+
// Story 2.2: Reasoning Field Parsing
|
|
231
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parse reasoning field for relationship references
|
|
235
|
+
*
|
|
236
|
+
* Story 2.2: Detect patterns like:
|
|
237
|
+
* - builds_on: <decision_id>
|
|
238
|
+
* - debates: <decision_id>
|
|
239
|
+
* - synthesizes: [id1, id2]
|
|
240
|
+
*
|
|
241
|
+
* @param {string} reasoning - Decision reasoning text
|
|
242
|
+
* @returns {Array<{type: string, targetIds: string[]}>} Detected relationships
|
|
243
|
+
*/
|
|
244
|
+
function parseReasoningForRelationships(reasoning) {
|
|
245
|
+
if (!reasoning || typeof reasoning !== 'string') {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const relationships = [];
|
|
250
|
+
|
|
251
|
+
// Pattern 1: builds_on: <id> (allows optional markdown **bold**)
|
|
252
|
+
const buildsOnMatch = reasoning.match(
|
|
253
|
+
/\*{0,2}builds_on\*{0,2}:\*{0,2}\s*(decision_[a-z0-9_]+)/gi
|
|
254
|
+
);
|
|
255
|
+
if (buildsOnMatch) {
|
|
256
|
+
buildsOnMatch.forEach((match) => {
|
|
257
|
+
const id = match.replace(/\*{0,2}builds_on\*{0,2}:\*{0,2}\s*/i, '').trim();
|
|
258
|
+
if (id) {
|
|
259
|
+
relationships.push({ type: 'builds_on', targetIds: [id] });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Pattern 2: debates: <id> (allows optional markdown **bold**)
|
|
265
|
+
const debatesMatch = reasoning.match(/\*{0,2}debates\*{0,2}:\*{0,2}\s*(decision_[a-z0-9_]+)/gi);
|
|
266
|
+
if (debatesMatch) {
|
|
267
|
+
debatesMatch.forEach((match) => {
|
|
268
|
+
const id = match.replace(/\*{0,2}debates\*{0,2}:\*{0,2}\s*/i, '').trim();
|
|
269
|
+
if (id) {
|
|
270
|
+
relationships.push({ type: 'debates', targetIds: [id] });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Pattern 3: synthesizes: [id1, id2] (allows optional markdown **bold**)
|
|
276
|
+
const synthesizesMatch = reasoning.match(
|
|
277
|
+
/\*{0,2}synthesizes\*{0,2}:\*{0,2}\s*\[?\s*(decision_[a-z0-9_]+(?:\s*,\s*decision_[a-z0-9_]+)*)\s*\]?/gi
|
|
278
|
+
);
|
|
279
|
+
if (synthesizesMatch) {
|
|
280
|
+
synthesizesMatch.forEach((match) => {
|
|
281
|
+
const idsStr = match
|
|
282
|
+
.replace(/\*{0,2}synthesizes\*{0,2}:\*{0,2}\s*\[?\s*/i, '')
|
|
283
|
+
.replace(/\s*\]?\s*$/, '');
|
|
284
|
+
const ids = idsStr.split(/\s*,\s*/).filter((id) => id.startsWith('decision_'));
|
|
285
|
+
if (ids.length > 0) {
|
|
286
|
+
relationships.push({ type: 'synthesizes', targetIds: ids });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return relationships;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Create edges from parsed reasoning relationships
|
|
296
|
+
*
|
|
297
|
+
* Story 2.2: Auto-create edges when reasoning references other decisions
|
|
298
|
+
*
|
|
299
|
+
* @param {string} fromId - Source decision ID
|
|
300
|
+
* @param {string} reasoning - Decision reasoning text
|
|
301
|
+
* @returns {Promise<{created: number, failed: number}>} Edge creation stats
|
|
302
|
+
*/
|
|
303
|
+
async function createEdgesFromReasoning(fromId, reasoning) {
|
|
304
|
+
const relationships = parseReasoningForRelationships(reasoning);
|
|
305
|
+
let created = 0;
|
|
306
|
+
let failed = 0;
|
|
307
|
+
|
|
308
|
+
for (const rel of relationships) {
|
|
309
|
+
for (const targetId of rel.targetIds) {
|
|
310
|
+
try {
|
|
311
|
+
// Verify target decision exists
|
|
312
|
+
const adapter = getAdapter();
|
|
313
|
+
const stmt = adapter.prepare('SELECT id FROM decisions WHERE id = ?');
|
|
314
|
+
const target = await stmt.get(targetId);
|
|
315
|
+
|
|
316
|
+
if (!target) {
|
|
317
|
+
info(`[decision-tracker] Referenced decision not found: ${targetId}, skipping edge`);
|
|
318
|
+
failed++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Create the edge
|
|
323
|
+
const reason = `Auto-detected from reasoning: ${rel.type} reference`;
|
|
324
|
+
const success = await createEdge(fromId, targetId, rel.type, reason);
|
|
325
|
+
|
|
326
|
+
if (success) {
|
|
327
|
+
created++;
|
|
328
|
+
info(`[decision-tracker] Created ${rel.type} edge: ${fromId} -> ${targetId}`);
|
|
329
|
+
} else {
|
|
330
|
+
failed++;
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
info(`[decision-tracker] Failed to create edge to ${targetId}: ${error.message}`);
|
|
334
|
+
failed++;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return { created, failed };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get supersedes chain depth for a topic
|
|
344
|
+
*
|
|
345
|
+
* Story 2.2: Calculate how many times a topic has been superseded
|
|
346
|
+
*
|
|
347
|
+
* @param {string} topic - Decision topic
|
|
348
|
+
* @returns {Promise<{depth: number, chain: string[]}>} Chain depth and decision IDs
|
|
349
|
+
*/
|
|
350
|
+
async function getSupersededChainDepth(topic) {
|
|
351
|
+
const adapter = getAdapter();
|
|
352
|
+
const chain = [];
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Start from the latest decision (superseded_by IS NULL)
|
|
356
|
+
let stmt = adapter.prepare(`
|
|
357
|
+
SELECT id, supersedes FROM decisions
|
|
358
|
+
WHERE topic = ? AND superseded_by IS NULL
|
|
359
|
+
ORDER BY created_at DESC
|
|
360
|
+
LIMIT 1
|
|
361
|
+
`);
|
|
362
|
+
|
|
363
|
+
let current = await stmt.get(topic);
|
|
364
|
+
|
|
365
|
+
if (!current) {
|
|
366
|
+
return { depth: 0, chain: [] };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
chain.push(current.id);
|
|
370
|
+
|
|
371
|
+
// Walk back through supersedes chain
|
|
372
|
+
while (current && current.supersedes) {
|
|
373
|
+
stmt = adapter.prepare('SELECT id, supersedes FROM decisions WHERE id = ?');
|
|
374
|
+
current = await stmt.get(current.supersedes);
|
|
375
|
+
|
|
376
|
+
if (current) {
|
|
377
|
+
chain.push(current.id);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
depth: chain.length - 1, // depth = number of supersedes edges
|
|
383
|
+
chain: chain.reverse(), // oldest to newest
|
|
384
|
+
};
|
|
385
|
+
} catch (error) {
|
|
386
|
+
throw new Error(`Failed to get supersedes chain: ${error.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
391
|
+
// NOTE: Auto-link functions REMOVED in v1.2.0
|
|
392
|
+
//
|
|
393
|
+
// Removed functions:
|
|
394
|
+
// - createRefinesEdge
|
|
395
|
+
// - detectConflicts
|
|
396
|
+
// - createContradictsEdge
|
|
397
|
+
// - findRelatedDecisions
|
|
398
|
+
// - isConflicting
|
|
399
|
+
//
|
|
400
|
+
// Reason: LLM can infer decision evolution from time-ordered search results.
|
|
401
|
+
// Auto-links created 366 noise edges (100% cross-topic).
|
|
402
|
+
// Only supersedes (same topic) is reliable.
|
|
403
|
+
//
|
|
404
|
+
// See: CHANGELOG.md v1.2.0 - 2025-11-25
|
|
405
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Learn Decision Function (Main API)
|
|
409
|
+
*
|
|
410
|
+
* Task 3.1: Create Learn Decision Function
|
|
411
|
+
* Task 3.2: Generate decision ID
|
|
412
|
+
* Task 3.3: Check for previous decision on same topic
|
|
413
|
+
* Task 3.4: Insert new decision with outcome=NULL, confidence from LLM
|
|
414
|
+
* Task 3.5: If previous exists: Create supersedes edge, Update previous superseded_by
|
|
415
|
+
* Task 3.6: If multi-parent refinement: Store refined_from, Calculate combined confidence
|
|
416
|
+
* Task 3.7: Generate enhanced embedding
|
|
417
|
+
* Task 3.8: Store in vss_memories (link via rowid)
|
|
418
|
+
*
|
|
419
|
+
* AC #1: Decision stored with outcome=NULL, confidence from LLM
|
|
420
|
+
* AC #2: Supersedes relationship creation
|
|
421
|
+
* AC #5: Multi-parent refinement with confidence calculation
|
|
422
|
+
*
|
|
423
|
+
* @param {Object} detection - Decision detection result
|
|
424
|
+
* @param {string} detection.topic - Decision topic
|
|
425
|
+
* @param {string} detection.decision - Decision value
|
|
426
|
+
* @param {string} detection.reasoning - Decision reasoning
|
|
427
|
+
* @param {number} detection.confidence - Confidence score (0.0-1.0)
|
|
428
|
+
* @param {Object} toolExecution - Tool execution data
|
|
429
|
+
* @param {Object} sessionContext - Session context
|
|
430
|
+
* @returns {Promise<Object>} { decisionId, notification }
|
|
431
|
+
*/
|
|
432
|
+
async function learnDecision(detection, toolExecution, sessionContext) {
|
|
433
|
+
try {
|
|
434
|
+
// Ensure database is initialized
|
|
435
|
+
await initDB();
|
|
436
|
+
|
|
437
|
+
// ════════════════════════════════════════════════════════
|
|
438
|
+
// Task 3.2: Generate Decision ID
|
|
439
|
+
// ════════════════════════════════════════════════════════
|
|
440
|
+
const decisionId = generateDecisionId(detection.topic);
|
|
441
|
+
|
|
442
|
+
// ════════════════════════════════════════════════════════
|
|
443
|
+
// Task 3.3: Check for Previous Decision on Same Topic
|
|
444
|
+
// ════════════════════════════════════════════════════════
|
|
445
|
+
const previous = await getPreviousDecision(detection.topic);
|
|
446
|
+
|
|
447
|
+
// ════════════════════════════════════════════════════════
|
|
448
|
+
// Task 3.6: Detect Multi-Parent Refinement
|
|
449
|
+
// ════════════════════════════════════════════════════════
|
|
450
|
+
const refinedFrom = detectRefinement(detection, sessionContext);
|
|
451
|
+
let finalConfidence = detection.confidence;
|
|
452
|
+
|
|
453
|
+
if (refinedFrom && refinedFrom.length > 0) {
|
|
454
|
+
// AC #5: Multi-parent refinement
|
|
455
|
+
// Get parent decisions
|
|
456
|
+
const adapter = getAdapter();
|
|
457
|
+
const stmt = adapter.prepare('SELECT * FROM decisions WHERE id = ?');
|
|
458
|
+
|
|
459
|
+
const parents = await Promise.all(
|
|
460
|
+
refinedFrom.map(async (parentId) => await stmt.get(parentId))
|
|
461
|
+
);
|
|
462
|
+
const validParents = parents.filter(Boolean);
|
|
463
|
+
|
|
464
|
+
// Calculate combined confidence
|
|
465
|
+
finalConfidence = calculateCombinedConfidence(detection.confidence, validParents);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ════════════════════════════════════════════════════════
|
|
469
|
+
// Task 3.4: Insert New Decision
|
|
470
|
+
// ════════════════════════════════════════════════════════
|
|
471
|
+
// ════════════════════════════════════════════════════════
|
|
472
|
+
// Story 014.7.6: Set needs_validation for assistant insights
|
|
473
|
+
// ════════════════════════════════════════════════════════
|
|
474
|
+
const isAssistantInsight = detection.type === 'assistant_insight';
|
|
475
|
+
const needsValidation = isAssistantInsight ? 1 : 0;
|
|
476
|
+
|
|
477
|
+
// AC #1: Decision stored with outcome=NULL, confidence from LLM
|
|
478
|
+
const decision = {
|
|
479
|
+
id: decisionId,
|
|
480
|
+
topic: detection.topic,
|
|
481
|
+
decision: detection.decision,
|
|
482
|
+
reasoning: detection.reasoning,
|
|
483
|
+
outcome: null, // AC #1: outcome=NULL (not yet tracked)
|
|
484
|
+
failure_reason: null,
|
|
485
|
+
limitation: null,
|
|
486
|
+
user_involvement: 'requested', // Inferred from tool execution
|
|
487
|
+
session_id: sessionContext.session_id,
|
|
488
|
+
supersedes: previous ? previous.id : null,
|
|
489
|
+
superseded_by: null,
|
|
490
|
+
refined_from: refinedFrom, // AC #5: Multi-parent refinement
|
|
491
|
+
confidence: finalConfidence, // AC #1, AC #5: Confidence from LLM
|
|
492
|
+
needs_validation: needsValidation, // Story 014.7.6: AC #1 - Validation for assistant insights
|
|
493
|
+
validation_attempts: 0, // Story 014.7.6: Track skip count
|
|
494
|
+
usage_count: 0, // Story 014.7.6: Track usage for periodic review
|
|
495
|
+
created_at: toolExecution.timestamp || Date.now(),
|
|
496
|
+
updated_at: Date.now(),
|
|
497
|
+
// Story 014.7.10: Add trust_context for Claude-Friendly Context Formatting
|
|
498
|
+
trust_context: detection.trust_context ? JSON.stringify(detection.trust_context) : null,
|
|
499
|
+
// Story 2.1: 5-layer narrative fields
|
|
500
|
+
evidence: detection.evidence
|
|
501
|
+
? Array.isArray(detection.evidence)
|
|
502
|
+
? JSON.stringify(detection.evidence)
|
|
503
|
+
: detection.evidence
|
|
504
|
+
: null,
|
|
505
|
+
alternatives: detection.alternatives
|
|
506
|
+
? Array.isArray(detection.alternatives)
|
|
507
|
+
? JSON.stringify(detection.alternatives)
|
|
508
|
+
: detection.alternatives
|
|
509
|
+
: null,
|
|
510
|
+
risks: detection.risks || null,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Task 3.7, 3.8: Generate enhanced embedding and store in vss_memories
|
|
514
|
+
// (Handled by insertDecisionWithEmbedding function)
|
|
515
|
+
await insertDecisionWithEmbedding(decision);
|
|
516
|
+
|
|
517
|
+
// ════════════════════════════════════════════════════════
|
|
518
|
+
// Task 3.5: Create Supersedes Relationship (if previous exists)
|
|
519
|
+
// ════════════════════════════════════════════════════════
|
|
520
|
+
if (previous) {
|
|
521
|
+
// AC #2: Supersedes relationship creation
|
|
522
|
+
const reason = `User changed from "${previous.decision}" to "${detection.decision}"`;
|
|
523
|
+
|
|
524
|
+
// Create edge: new decision → previous decision
|
|
525
|
+
await createSupersedesEdge(decisionId, previous.id, reason);
|
|
526
|
+
|
|
527
|
+
// Update previous decision's superseded_by field
|
|
528
|
+
await markSuperseded(previous.id, decisionId);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ════════════════════════════════════════════════════════
|
|
532
|
+
// NOTE: Auto-link generation (refines, contradicts) REMOVED
|
|
533
|
+
//
|
|
534
|
+
// Reason: LLM can infer decision evolution from time-ordered
|
|
535
|
+
// search results. Auto-links created 366 noise edges (100%
|
|
536
|
+
// cross-topic). Only supersedes (same topic) is reliable.
|
|
537
|
+
//
|
|
538
|
+
// See: 2025-11-25 discussion on decision tracking algorithm
|
|
539
|
+
// ════════════════════════════════════════════════════════
|
|
540
|
+
|
|
541
|
+
// ════════════════════════════════════════════════════════
|
|
542
|
+
// Story 014.7.6: Generate notification if needs validation
|
|
543
|
+
// ════════════════════════════════════════════════════════
|
|
544
|
+
let notification = null;
|
|
545
|
+
if (needsValidation) {
|
|
546
|
+
const { notifyInsight } = require('./notification-manager');
|
|
547
|
+
notification = notifyInsight({
|
|
548
|
+
id: decisionId,
|
|
549
|
+
topic: decision.topic,
|
|
550
|
+
decision: decision.decision,
|
|
551
|
+
reasoning: decision.reasoning,
|
|
552
|
+
confidence: decision.confidence,
|
|
553
|
+
needs_validation: true,
|
|
554
|
+
validation_attempts: 0,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ════════════════════════════════════════════════════════
|
|
559
|
+
// Task 3.9: Return decision ID (+ notification for Story 014.7.6)
|
|
560
|
+
// ════════════════════════════════════════════════════════
|
|
561
|
+
return {
|
|
562
|
+
decisionId,
|
|
563
|
+
notification, // null if no validation needed, notification object otherwise
|
|
564
|
+
};
|
|
565
|
+
} catch (error) {
|
|
566
|
+
// CLAUDE.md Rule #1: No silent failures
|
|
567
|
+
throw new Error(`Failed to learn decision: ${error.message}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Update confidence score
|
|
573
|
+
*
|
|
574
|
+
* Task 6: Confidence evolution (used in outcome tracking)
|
|
575
|
+
* AC #5: Confidence score calculated based on history
|
|
576
|
+
*
|
|
577
|
+
* @param {number} prior - Prior confidence
|
|
578
|
+
* @param {Array<Object>} evidence - Evidence items
|
|
579
|
+
* @param {string} evidence[].type - Evidence type (success, failure, partial)
|
|
580
|
+
* @param {number} evidence[].impact - Impact on confidence
|
|
581
|
+
* @returns {number} Updated confidence (0.0-1.0)
|
|
582
|
+
*/
|
|
583
|
+
function updateConfidence(prior, evidence) {
|
|
584
|
+
if (!evidence || evidence.length === 0) {
|
|
585
|
+
return prior;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Calculate total impact
|
|
589
|
+
const totalImpact = evidence.reduce((acc, e) => acc + e.impact, 0);
|
|
590
|
+
|
|
591
|
+
// Update confidence
|
|
592
|
+
const updated = prior + totalImpact;
|
|
593
|
+
|
|
594
|
+
// Clamp to [0.0, 1.0]
|
|
595
|
+
return Math.max(0, Math.min(1, updated));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Export API
|
|
599
|
+
// NOTE: Auto-link functions (createRefinesEdge, createContradictsEdge,
|
|
600
|
+
// findRelatedDecisions, isConflicting, detectConflicts) removed from exports.
|
|
601
|
+
// LLM infers relationships from search results instead.
|
|
602
|
+
//
|
|
603
|
+
// Story 2.1/2.2: Added new edge type support and reasoning parsing
|
|
604
|
+
module.exports = {
|
|
605
|
+
// Core functions
|
|
606
|
+
learnDecision,
|
|
607
|
+
generateDecisionId,
|
|
608
|
+
getPreviousDecision,
|
|
609
|
+
createSupersedesEdge,
|
|
610
|
+
markSuperseded,
|
|
611
|
+
calculateCombinedConfidence,
|
|
612
|
+
detectRefinement,
|
|
613
|
+
updateConfidence,
|
|
614
|
+
// Story 2.1: Edge type extension
|
|
615
|
+
VALID_EDGE_TYPES,
|
|
616
|
+
createEdge,
|
|
617
|
+
// Story 2.2: Reasoning field parsing
|
|
618
|
+
parseReasoningForRelationships,
|
|
619
|
+
createEdgesFromReasoning,
|
|
620
|
+
getSupersededChainDepth,
|
|
621
|
+
};
|