ai-dev-analytics 2.0.1 → 2.0.3

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.
@@ -0,0 +1,624 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { green, red, yellow } from '../../utils/display.js';
4
+ import { fileExists, readText, extractConflictSections, writeJson, } from '../../utils/fs.js';
5
+ import { aidaDir, configPath, memoryIndexPath, moduleMemoriesDir, runsDir, } from '../../utils/paths.js';
6
+ import { buildMemoryViews, loadMemoryIndex, loadModuleMemory, normalizeModuleKey, saveModuleMemory, } from '../../utils/memory.js';
7
+ import { normalizeRunData, recalcMetrics, resolveCurrentTaskId } from '../../internal/runtime/state.js';
8
+ function uniqueStrings(values) {
9
+ const result = [];
10
+ const seen = new Set();
11
+ for (const value of values) {
12
+ const normalized = `${value || ''}`.trim();
13
+ if (!normalized || seen.has(normalized))
14
+ continue;
15
+ seen.add(normalized);
16
+ result.push(normalized);
17
+ }
18
+ return result;
19
+ }
20
+ function latestIso(a, b) {
21
+ if (!a)
22
+ return b || '';
23
+ if (!b)
24
+ return a;
25
+ return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
26
+ }
27
+ function pickLatestString(current, incoming, currentUpdatedAt, incomingUpdatedAt) {
28
+ const currentValue = (current || '').trim();
29
+ const incomingValue = (incoming || '').trim();
30
+ if (!currentValue)
31
+ return incomingValue;
32
+ if (!incomingValue)
33
+ return currentValue;
34
+ return latestIso(currentUpdatedAt, incomingUpdatedAt) === incomingUpdatedAt ? incomingValue : currentValue;
35
+ }
36
+ function parseConflictJsonObject(raw) {
37
+ const trimmed = raw.trim();
38
+ if (!trimmed || trimmed === 'null')
39
+ return null;
40
+ try {
41
+ return JSON.parse(trimmed);
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ function hasConflict(filePath) {
48
+ if (!fileExists(filePath))
49
+ return false;
50
+ const raw = readText(filePath);
51
+ return raw.includes('<<<<<<<') || raw.includes('>>>>>>>');
52
+ }
53
+ function mergeByKey(items, keyOf, mergeItem) {
54
+ const map = new Map();
55
+ for (const item of items) {
56
+ const key = keyOf(item);
57
+ const existing = map.get(key);
58
+ map.set(key, existing ? mergeItem(existing, item) : item);
59
+ }
60
+ return [...map.values()];
61
+ }
62
+ function mergeIndexEntry(current, incoming) {
63
+ const useIncoming = latestIso(current.updatedAt, incoming.updatedAt) === incoming.updatedAt;
64
+ return {
65
+ key: current.key || incoming.key,
66
+ title: pickLatestString(current.title, incoming.title, current.updatedAt, incoming.updatedAt),
67
+ summary: pickLatestString(current.summary, incoming.summary, current.updatedAt, incoming.updatedAt),
68
+ keywords: uniqueStrings([...current.keywords, ...incoming.keywords]),
69
+ paths: uniqueStrings([...current.paths, ...incoming.paths]),
70
+ tickets: uniqueStrings([...(current.tickets || []), ...(incoming.tickets || [])]),
71
+ updatedAt: useIncoming ? incoming.updatedAt : current.updatedAt,
72
+ };
73
+ }
74
+ function mergeMemoryIndex(ours, theirs) {
75
+ const mergedModules = mergeByKey([...((ours?.items || ours?.modules || [])), ...((theirs?.items || theirs?.modules || []))], (item) => normalizeModuleKey(item.key), (current, incoming) => mergeIndexEntry({ ...current, key: normalizeModuleKey(current.key) }, { ...incoming, key: normalizeModuleKey(incoming.key) })).sort((a, b) => a.key.localeCompare(b.key));
76
+ return {
77
+ schemaVersion: '2.0',
78
+ updatedAt: latestIso(ours?.updatedAt, theirs?.updatedAt),
79
+ items: mergedModules,
80
+ };
81
+ }
82
+ function hydrateModuleMemoryFromIndex(projectRoot, entry) {
83
+ const moduleKey = normalizeModuleKey(entry.key);
84
+ if (!moduleKey)
85
+ return;
86
+ const existing = loadModuleMemory(projectRoot, moduleKey);
87
+ if (existing) {
88
+ saveModuleMemory(projectRoot, {
89
+ ...existing,
90
+ moduleKey,
91
+ title: existing.title || entry.title || moduleKey,
92
+ summary: existing.summary || entry.summary || '',
93
+ keywords: uniqueStrings([...(existing.keywords || []), moduleKey, entry.title, ...(entry.keywords || [])]),
94
+ entryFiles: uniqueStrings([...(existing.entryFiles || []), ...(entry.paths || [])]),
95
+ relatedPaths: uniqueStrings([...(existing.relatedPaths || []), ...(entry.paths || [])]),
96
+ updatedAt: latestIso(existing.updatedAt, entry.updatedAt) || existing.updatedAt || entry.updatedAt || new Date().toISOString(),
97
+ });
98
+ return;
99
+ }
100
+ saveModuleMemory(projectRoot, {
101
+ schemaVersion: '2.0',
102
+ moduleKey,
103
+ title: entry.title || moduleKey,
104
+ summary: entry.summary || '',
105
+ keywords: uniqueStrings([moduleKey, entry.title, ...(entry.keywords || [])]),
106
+ entryFiles: uniqueStrings(entry.paths || []),
107
+ relatedPaths: uniqueStrings(entry.paths || []),
108
+ dataFlow: [],
109
+ decisions: [],
110
+ constraints: [],
111
+ pitfalls: [],
112
+ relatedRules: [],
113
+ tickets: [],
114
+ changes: [],
115
+ updatedAt: entry.updatedAt || new Date().toISOString(),
116
+ });
117
+ }
118
+ function hydrateMissingModuleMemoriesFromIndex(projectRoot) {
119
+ const index = loadMemoryIndex(projectRoot);
120
+ for (const entry of index.items) {
121
+ hydrateModuleMemoryFromIndex(projectRoot, entry);
122
+ }
123
+ }
124
+ function mergeModuleMemoryRecord(current, incoming) {
125
+ const useIncoming = latestIso(current.updatedAt, incoming.updatedAt) === incoming.updatedAt;
126
+ const mergedTickets = mergeByKey([...current.tickets, ...incoming.tickets], (item) => `${item.ticket || ''}|${item.branch || ''}`, (left, right) => ({
127
+ ticket: left.ticket || right.ticket,
128
+ branch: left.branch || right.branch,
129
+ summary: pickLatestString(left.summary, right.summary, left.updatedAt, right.updatedAt),
130
+ updatedAt: latestIso(left.updatedAt, right.updatedAt),
131
+ }));
132
+ const mergedChanges = mergeByKey([...(current.changes || []), ...(incoming.changes || [])], (item) => item.ticket || item.branch
133
+ ? `${item.ticket || ''}|${item.branch || ''}`
134
+ : `${item.title || ''}|${item.summary}`, (left, right) => ({
135
+ ticket: left.ticket || right.ticket,
136
+ branch: left.branch || right.branch,
137
+ title: pickLatestString(left.title, right.title, left.updatedAt, right.updatedAt) || undefined,
138
+ summary: pickLatestString(left.summary, right.summary, left.updatedAt, right.updatedAt),
139
+ updatedAt: latestIso(left.updatedAt, right.updatedAt),
140
+ }));
141
+ return {
142
+ schemaVersion: '2.0',
143
+ moduleKey: current.moduleKey || incoming.moduleKey,
144
+ title: pickLatestString(current.title, incoming.title, current.updatedAt, incoming.updatedAt),
145
+ summary: pickLatestString(current.summary, incoming.summary, current.updatedAt, incoming.updatedAt),
146
+ keywords: uniqueStrings([...current.keywords, ...incoming.keywords]),
147
+ entryFiles: uniqueStrings([...current.entryFiles, ...incoming.entryFiles]),
148
+ relatedPaths: uniqueStrings([...current.relatedPaths, ...incoming.relatedPaths]),
149
+ dataFlow: uniqueStrings([...current.dataFlow, ...incoming.dataFlow]),
150
+ decisions: uniqueStrings([...current.decisions, ...incoming.decisions]),
151
+ constraints: uniqueStrings([...current.constraints, ...incoming.constraints]),
152
+ pitfalls: uniqueStrings([...current.pitfalls, ...incoming.pitfalls]),
153
+ relatedRules: uniqueStrings([...current.relatedRules, ...incoming.relatedRules]),
154
+ tickets: mergedTickets,
155
+ changes: mergedChanges,
156
+ updatedAt: useIncoming ? incoming.updatedAt : current.updatedAt,
157
+ };
158
+ }
159
+ function mergeRunContextRecord(current, incoming) {
160
+ const useIncoming = latestIso(current.updatedAt, incoming.updatedAt) === incoming.updatedAt;
161
+ return {
162
+ branch: current.branch || incoming.branch,
163
+ ticket: pickLatestString(current.ticket, incoming.ticket, current.updatedAt, incoming.updatedAt) || undefined,
164
+ title: pickLatestString(current.title, incoming.title, current.updatedAt, incoming.updatedAt),
165
+ summary: pickLatestString(current.summary, incoming.summary, current.updatedAt, incoming.updatedAt),
166
+ currentPhase: pickLatestString(current.currentPhase, incoming.currentPhase, current.updatedAt, incoming.updatedAt),
167
+ modules: uniqueStrings([...current.modules, ...incoming.modules]),
168
+ completed: uniqueStrings([...current.completed, ...incoming.completed]),
169
+ inProgress: uniqueStrings([...current.inProgress, ...incoming.inProgress]),
170
+ next: uniqueStrings([...current.next, ...incoming.next]),
171
+ decisions: uniqueStrings([...current.decisions, ...incoming.decisions]),
172
+ constraints: uniqueStrings([...current.constraints, ...incoming.constraints]),
173
+ keyFiles: uniqueStrings([...current.keyFiles, ...incoming.keyFiles]),
174
+ risks: uniqueStrings([...current.risks, ...incoming.risks]),
175
+ updatedAt: useIncoming ? incoming.updatedAt : current.updatedAt,
176
+ };
177
+ }
178
+ function mergeRequirementModule(current, incoming) {
179
+ return {
180
+ id: current.id || incoming.id,
181
+ name: pickLatestString(current.name, incoming.name),
182
+ description: pickLatestString(current.description, incoming.description),
183
+ assignee: incoming.assignee || current.assignee,
184
+ };
185
+ }
186
+ function mergeRequirementPhase(current, incoming) {
187
+ return {
188
+ phase: current.phase || incoming.phase,
189
+ file: current.file || incoming.file,
190
+ title: pickLatestString(current.title, incoming.title),
191
+ confirmedAt: latestIso(current.confirmedAt || undefined, incoming.confirmedAt || undefined) || null,
192
+ };
193
+ }
194
+ function mergeDeveloper(current, incoming) {
195
+ return {
196
+ name: current.name || incoming.name,
197
+ modules: uniqueStrings([...current.modules, ...incoming.modules]),
198
+ tasks: Math.max(current.tasks, incoming.tasks),
199
+ completedTasks: Math.max(current.completedTasks, incoming.completedTasks),
200
+ bugs: Math.max(current.bugs, incoming.bugs),
201
+ deviations: Math.max(current.deviations, incoming.deviations),
202
+ linesAdded: Math.max(current.linesAdded, incoming.linesAdded),
203
+ linesRemoved: Math.max(current.linesRemoved, incoming.linesRemoved),
204
+ firstPassRate: Math.max(current.firstPassRate, incoming.firstPassRate),
205
+ actualWorkSeconds: Math.max(current.actualWorkSeconds, incoming.actualWorkSeconds),
206
+ totalTokens: Math.max(current.totalTokens, incoming.totalTokens),
207
+ };
208
+ }
209
+ function mergeRequirementRecord(current, incoming) {
210
+ const developers = mergeByKey([...current.developers, ...incoming.developers], (item) => item.name, mergeDeveloper).sort((a, b) => a.name.localeCompare(b.name));
211
+ const modules = mergeByKey([...current.modules, ...incoming.modules], (item) => item.id || item.name, mergeRequirementModule);
212
+ const prdPhases = mergeByKey([...current.prdPhases, ...incoming.prdPhases], (item) => `${item.phase}|${item.file}`, mergeRequirementPhase);
213
+ const highlights = mergeByKey([...current.highlights, ...incoming.highlights], (item) => `${item.content}|${item.createdAt}`, (left) => left);
214
+ return {
215
+ branch: current.branch || incoming.branch,
216
+ title: pickLatestString(current.title, incoming.title, current.updatedAt, incoming.updatedAt),
217
+ summary: pickLatestString(current.summary, incoming.summary, current.updatedAt, incoming.updatedAt),
218
+ prdPhases,
219
+ modules,
220
+ highlights,
221
+ developers,
222
+ totals: {
223
+ tasks: developers.reduce((sum, item) => sum + item.tasks, 0),
224
+ completedTasks: developers.reduce((sum, item) => sum + item.completedTasks, 0),
225
+ bugs: developers.reduce((sum, item) => sum + item.bugs, 0),
226
+ deviations: developers.reduce((sum, item) => sum + item.deviations, 0),
227
+ linesAdded: developers.reduce((sum, item) => sum + item.linesAdded, 0),
228
+ linesRemoved: developers.reduce((sum, item) => sum + item.linesRemoved, 0),
229
+ totalTokens: developers.reduce((sum, item) => sum + item.totalTokens, 0),
230
+ },
231
+ createdAt: current.createdAt || incoming.createdAt,
232
+ updatedAt: latestIso(current.updatedAt, incoming.updatedAt),
233
+ };
234
+ }
235
+ const taskStatusRank = {
236
+ pending: 0,
237
+ 'in-progress': 1,
238
+ done: 2,
239
+ };
240
+ function mergeTask(current, incoming) {
241
+ const currentRank = taskStatusRank[current.status];
242
+ const incomingRank = taskStatusRank[incoming.status];
243
+ const preferred = incomingRank >= currentRank ? incoming : current;
244
+ const other = preferred === incoming ? current : incoming;
245
+ return {
246
+ ...preferred,
247
+ title: pickLatestString(current.title, incoming.title),
248
+ stageName: pickLatestString(current.stageName, incoming.stageName),
249
+ prdPhase: pickLatestString(current.prdPhase, incoming.prdPhase),
250
+ acceptance: pickLatestString(current.acceptance, incoming.acceptance) || undefined,
251
+ createdAt: current.createdAt || incoming.createdAt,
252
+ startedAt: latestIso(current.startedAt, incoming.startedAt) || current.startedAt || incoming.startedAt,
253
+ completedAt: latestIso(current.completedAt || undefined, incoming.completedAt || undefined) || preferred.completedAt || other.completedAt || null,
254
+ status: currentRank === incomingRank ? preferred.status : (incomingRank > currentRank ? incoming.status : current.status),
255
+ };
256
+ }
257
+ function mergeBug(current, incoming) {
258
+ const status = current.status === 'fixed' || incoming.status === 'fixed' ? 'fixed' : 'open';
259
+ const severityOrder = { low: 0, medium: 1, high: 2, critical: 3 };
260
+ return {
261
+ ...current,
262
+ ...incoming,
263
+ title: pickLatestString(current.title, incoming.title),
264
+ severity: severityOrder[current.severity] >= severityOrder[incoming.severity] ? current.severity : incoming.severity,
265
+ source: current.source || incoming.source,
266
+ status,
267
+ files: uniqueStrings([...current.files, ...incoming.files]),
268
+ fix: pickLatestString(current.fix || undefined, incoming.fix || undefined) || null,
269
+ taskId: incoming.taskId || current.taskId || null,
270
+ reportedAt: current.reportedAt || incoming.reportedAt,
271
+ fixedAt: latestIso(current.fixedAt || undefined, incoming.fixedAt || undefined) || null,
272
+ };
273
+ }
274
+ function mergeDeviation(current, incoming) {
275
+ return {
276
+ ...current,
277
+ ...incoming,
278
+ title: pickLatestString(current.title, incoming.title),
279
+ aiOutput: pickLatestString(current.aiOutput, incoming.aiOutput) || undefined,
280
+ expectedOutput: pickLatestString(current.expectedOutput, incoming.expectedOutput) || undefined,
281
+ files: uniqueStrings([...current.files, ...incoming.files]),
282
+ ruleSedimented: incoming.ruleSedimented ?? current.ruleSedimented,
283
+ detectedAt: current.detectedAt || incoming.detectedAt,
284
+ fixedAt: latestIso(current.fixedAt || undefined, incoming.fixedAt || undefined) || null,
285
+ };
286
+ }
287
+ function mergeReview(current, incoming) {
288
+ const preferred = latestIso(current.reviewedAt, incoming.reviewedAt) === incoming.reviewedAt ? incoming : current;
289
+ return {
290
+ ...preferred,
291
+ taskId: incoming.taskId || current.taskId || null,
292
+ scope: pickLatestString(current.scope, incoming.scope),
293
+ result: preferred.result,
294
+ issueCount: Math.max(current.issueCount, incoming.issueCount),
295
+ issues: uniqueStrings([...(current.issues || []), ...(incoming.issues || [])]),
296
+ reviewedAt: preferred.reviewedAt,
297
+ };
298
+ }
299
+ function mergeRuleItem(current, incoming) {
300
+ const preferred = latestIso(current.sedimentedAt || undefined, incoming.sedimentedAt || undefined) === incoming.sedimentedAt ? incoming : current;
301
+ return {
302
+ ...preferred,
303
+ content: pickLatestString(current.content, incoming.content),
304
+ category: pickLatestString(current.category, incoming.category) || undefined,
305
+ sourceDeviation: incoming.sourceDeviation || current.sourceDeviation || null,
306
+ sedimentedAt: latestIso(current.sedimentedAt || undefined, incoming.sedimentedAt || undefined) || null,
307
+ file: pickLatestString(current.file, incoming.file),
308
+ status: incoming.status || current.status,
309
+ };
310
+ }
311
+ function mergeFileItem(current, incoming) {
312
+ const changeType = current.changeType === incoming.changeType
313
+ ? current.changeType
314
+ : (current.changeType === 'deleted' || incoming.changeType === 'deleted' ? 'deleted' : 'modified');
315
+ return {
316
+ path: current.path || incoming.path,
317
+ changeType,
318
+ linesAdded: Math.max(current.linesAdded, incoming.linesAdded),
319
+ linesRemoved: Math.max(current.linesRemoved, incoming.linesRemoved),
320
+ changeCount: Math.max(current.changeCount, incoming.changeCount),
321
+ lastModified: latestIso(current.lastModified, incoming.lastModified) || undefined,
322
+ };
323
+ }
324
+ function mergeWorkflow(current, incoming) {
325
+ const statusRank = {
326
+ pending: 0,
327
+ in_progress: 1,
328
+ completed: 2,
329
+ failed: 3,
330
+ };
331
+ const preferred = statusRank[incoming.status] >= statusRank[current.status] ? incoming : current;
332
+ return {
333
+ ...preferred,
334
+ stage: pickLatestString(current.stage, incoming.stage),
335
+ prdPhase: pickLatestString(current.prdPhase, incoming.prdPhase) || undefined,
336
+ startTime: current.startTime || incoming.startTime,
337
+ endTime: latestIso(current.endTime, incoming.endTime) || undefined,
338
+ };
339
+ }
340
+ function recalcRunSummary(data) {
341
+ data.summary.totalTasks = data.tasks.length;
342
+ data.summary.completedTasks = data.tasks.filter((item) => item.status === 'done').length;
343
+ data.summary.bugCount = data.bugs.length;
344
+ data.summary.deviationCount = data.deviations.length;
345
+ data.summary.reviewCount = data.reviews.length;
346
+ data.summary.reviewPassCount = data.reviews.filter((item) => item.result === 'pass').length;
347
+ data.summary.reviewFailCount = data.reviews.filter((item) => item.result === 'fail').length;
348
+ data.summary.rulesSedimented = data.rules.filter((item) => item.status !== 'pending').length;
349
+ data.summary.prdPhaseCount = uniqueStrings(data.tasks.map((item) => item.prdPhase)).length;
350
+ data.summary.filesChanged = data.files.length;
351
+ data.summary.linesAdded = data.files.reduce((sum, item) => sum + (item.linesAdded || 0), 0);
352
+ data.summary.linesRemoved = data.files.reduce((sum, item) => sum + (item.linesRemoved || 0), 0);
353
+ }
354
+ function mergeRunDataRecord(currentRaw, incomingRaw) {
355
+ const current = normalizeRunData(currentRaw);
356
+ const incoming = normalizeRunData(incomingRaw);
357
+ const merged = normalizeRunData({
358
+ ...current,
359
+ ...incoming,
360
+ meta: {
361
+ ...current.meta,
362
+ ...incoming.meta,
363
+ schemaVersion: incoming.meta.schemaVersion || current.meta.schemaVersion,
364
+ branch: current.meta.branch || incoming.meta.branch,
365
+ developer: current.meta.developer || incoming.meta.developer,
366
+ project: pickLatestString(current.meta.project, incoming.meta.project),
367
+ aiModel: pickLatestString(current.meta.aiModel, incoming.meta.aiModel),
368
+ aiTool: pickLatestString(current.meta.aiTool, incoming.meta.aiTool),
369
+ startTime: current.meta.startTime || incoming.meta.startTime,
370
+ endTime: latestIso(current.meta.endTime, incoming.meta.endTime) || undefined,
371
+ status: pickLatestString(current.meta.status, incoming.meta.status),
372
+ prdPhases: uniqueStrings([...(current.meta.prdPhases || []), ...(incoming.meta.prdPhases || [])]),
373
+ },
374
+ summary: { ...current.summary, ...incoming.summary },
375
+ metrics: { ...current.metrics, ...incoming.metrics },
376
+ context: {
377
+ ...current.context,
378
+ ...incoming.context,
379
+ currentStage: pickLatestString(current.context.currentStage, incoming.context.currentStage) || undefined,
380
+ currentPrdPhase: pickLatestString(current.context.currentPrdPhase, incoming.context.currentPrdPhase) || undefined,
381
+ currentTaskId: undefined,
382
+ lastUpdated: latestIso(current.context.lastUpdated, incoming.context.lastUpdated) || undefined,
383
+ },
384
+ tasks: mergeByKey([...current.tasks, ...incoming.tasks], (item) => item.taskId, mergeTask),
385
+ bugs: mergeByKey([...current.bugs, ...incoming.bugs], (item) => item.bugId, mergeBug),
386
+ deviations: mergeByKey([...current.deviations, ...incoming.deviations], (item) => item.deviationId, mergeDeviation),
387
+ reviews: mergeByKey([...current.reviews, ...incoming.reviews], (item) => item.reviewId, mergeReview),
388
+ rules: mergeByKey([...current.rules, ...incoming.rules], (item) => item.ruleId, mergeRuleItem),
389
+ files: mergeByKey([...current.files, ...incoming.files], (item) => item.path, mergeFileItem),
390
+ timeline: mergeByKey([...current.timeline, ...incoming.timeline], (item) => `${item.type}|${item.title}|${item.timestamp}`, (left) => left)
391
+ .sort((a, b) => a.timestamp.localeCompare(b.timestamp)),
392
+ workflow: mergeByKey([...current.workflow, ...incoming.workflow], (item) => `${item.stage}|${item.prdPhase || ''}`, mergeWorkflow),
393
+ events: mergeByKey([...current.events, ...incoming.events], (item) => `${item.type}|${item.time}|${JSON.stringify(item.data)}`, (left) => left)
394
+ .sort((a, b) => a.time.localeCompare(b.time)),
395
+ cost: {
396
+ ...current.cost,
397
+ ...incoming.cost,
398
+ totalTokens: Math.max(current.cost.totalTokens || 0, incoming.cost.totalTokens || 0),
399
+ estimatedManualHours: Math.max(current.cost.estimatedManualHours || 0, incoming.cost.estimatedManualHours || 0),
400
+ actualHours: Math.max(current.cost.actualHours || 0, incoming.cost.actualHours || 0),
401
+ tokenBreakdown: mergeByKey([...(current.cost.tokenBreakdown || []), ...(incoming.cost.tokenBreakdown || [])], (item) => item.stage, (left, right) => ({ stage: left.stage || right.stage, tokens: Math.max(left.tokens, right.tokens) })),
402
+ tokenDetail: current.cost.tokenDetail || incoming.cost.tokenDetail
403
+ ? {
404
+ inputTokens: Math.max(current.cost.tokenDetail?.inputTokens || 0, incoming.cost.tokenDetail?.inputTokens || 0),
405
+ outputTokens: Math.max(current.cost.tokenDetail?.outputTokens || 0, incoming.cost.tokenDetail?.outputTokens || 0),
406
+ cacheCreationTokens: Math.max(current.cost.tokenDetail?.cacheCreationTokens || 0, incoming.cost.tokenDetail?.cacheCreationTokens || 0),
407
+ cacheReadTokens: Math.max(current.cost.tokenDetail?.cacheReadTokens || 0, incoming.cost.tokenDetail?.cacheReadTokens || 0),
408
+ }
409
+ : undefined,
410
+ },
411
+ highlights: mergeByKey([...current.highlights, ...incoming.highlights], (item) => `${item.content}|${item.createdAt}`, (left) => left),
412
+ });
413
+ recalcRunSummary(merged);
414
+ merged.context.currentTaskId = resolveCurrentTaskId(merged.tasks);
415
+ const activeTask = merged.tasks.find((item) => item.taskId === merged.context.currentTaskId);
416
+ merged.context.currentStage = activeTask?.stageName;
417
+ merged.context.currentPrdPhase = activeTask?.prdPhase;
418
+ recalcMetrics(merged);
419
+ return merged;
420
+ }
421
+ function mergeConflictFile(filePath, mergeObjects) {
422
+ if (!fileExists(filePath))
423
+ return 'missing';
424
+ const raw = readText(filePath);
425
+ if (!raw.includes('<<<<<<<') && !raw.includes('>>>>>>>'))
426
+ return 'no-conflict';
427
+ const sections = extractConflictSections(raw);
428
+ if (!sections)
429
+ return 'error';
430
+ const ours = parseConflictJsonObject(sections.ours);
431
+ const theirs = parseConflictJsonObject(sections.theirs);
432
+ const merged = mergeObjects(ours, theirs);
433
+ writeJson(filePath, merged);
434
+ return 'merged';
435
+ }
436
+ function walkJsonTargets(rootDir, targetName) {
437
+ if (!fileExists(rootDir))
438
+ return [];
439
+ const results = [];
440
+ const stack = [rootDir];
441
+ while (stack.length > 0) {
442
+ const current = stack.pop();
443
+ for (const name of readdirSync(current)) {
444
+ const fullPath = resolve(current, name);
445
+ const stat = statSync(fullPath);
446
+ if (stat.isDirectory())
447
+ stack.push(fullPath);
448
+ else if (name === targetName)
449
+ results.push(fullPath);
450
+ }
451
+ }
452
+ return results.sort();
453
+ }
454
+ function walkMemoryModuleFiles(rootDir) {
455
+ if (!fileExists(rootDir))
456
+ return [];
457
+ const results = [];
458
+ const stack = [rootDir];
459
+ while (stack.length > 0) {
460
+ const current = stack.pop();
461
+ for (const name of readdirSync(current)) {
462
+ const fullPath = resolve(current, name);
463
+ const stat = statSync(fullPath);
464
+ if (stat.isDirectory())
465
+ stack.push(fullPath);
466
+ else if (name.endsWith('.json'))
467
+ results.push(fullPath);
468
+ }
469
+ }
470
+ return results.sort();
471
+ }
472
+ function mergeFixedFile(filePath, merger, counters) {
473
+ const status = mergeConflictFile(filePath, merger);
474
+ if (status === 'merged')
475
+ counters.merged++;
476
+ else if (status === 'missing')
477
+ counters.missing++;
478
+ else if (status === 'error')
479
+ counters.errors++;
480
+ }
481
+ function mergeModuleFiles(files, merger) {
482
+ const counters = { status: 'no-conflict', merged: 0, missing: 0, errors: 0 };
483
+ for (const filePath of files) {
484
+ if (!hasConflict(filePath))
485
+ continue;
486
+ const status = mergeConflictFile(filePath, (ours, theirs) => merger(ours, theirs));
487
+ if (status === 'merged')
488
+ counters.merged++;
489
+ else if (status === 'error')
490
+ counters.errors++;
491
+ }
492
+ counters.status = counters.errors > 0 ? 'error' : counters.merged > 0 ? 'merged' : 'no-conflict';
493
+ return counters;
494
+ }
495
+ function mergeRunFiles(files) {
496
+ const counters = { status: 'no-conflict', merged: 0, missing: 0, errors: 0 };
497
+ for (const filePath of files) {
498
+ if (!hasConflict(filePath))
499
+ continue;
500
+ const status = mergeConflictFile(filePath, (ours, theirs) => mergeRunDataRecord(ours, theirs));
501
+ if (status === 'merged')
502
+ counters.merged++;
503
+ else if (status === 'error')
504
+ counters.errors++;
505
+ }
506
+ counters.status = counters.errors > 0 ? 'error' : counters.merged > 0 ? 'merged' : 'no-conflict';
507
+ return counters;
508
+ }
509
+ export function mergeAidaJsonData(projectRoot) {
510
+ const summary = {
511
+ memoryIndex: { status: 'missing', merged: 0, missing: 1, errors: 0 },
512
+ moduleMemories: { status: 'missing', merged: 0, missing: 0, errors: 0 },
513
+ contexts: { status: 'missing', merged: 0, missing: 0, errors: 0 },
514
+ requirements: { status: 'missing', merged: 0, missing: 0, errors: 0 },
515
+ runs: { status: 'missing', merged: 0, missing: 0, errors: 0 },
516
+ rebuiltMemoryViews: false,
517
+ };
518
+ if (!fileExists(configPath(projectRoot)) || !fileExists(aidaDir(projectRoot))) {
519
+ return summary;
520
+ }
521
+ const memoryIndex = memoryIndexPath(projectRoot);
522
+ if (fileExists(memoryIndex)) {
523
+ summary.memoryIndex = { status: 'no-conflict', merged: 0, missing: 0, errors: 0 };
524
+ mergeFixedFile(memoryIndex, mergeMemoryIndex, summary.memoryIndex);
525
+ summary.memoryIndex.status = summary.memoryIndex.errors > 0 ? 'error' : summary.memoryIndex.merged > 0 ? 'merged' : 'no-conflict';
526
+ }
527
+ const moduleFiles = walkMemoryModuleFiles(moduleMemoriesDir(projectRoot));
528
+ summary.moduleMemories = moduleFiles.length === 0
529
+ ? { status: 'missing', merged: 0, missing: 0, errors: 0 }
530
+ : mergeModuleFiles(moduleFiles, mergeModuleMemoryRecord);
531
+ const contextFiles = walkJsonTargets(runsDir(projectRoot), 'context.json');
532
+ summary.contexts = { status: contextFiles.length === 0 ? 'missing' : 'no-conflict', merged: 0, missing: 0, errors: 0 };
533
+ for (const filePath of contextFiles) {
534
+ const status = mergeConflictFile(filePath, (ours, theirs) => mergeRunContextRecord(ours, theirs));
535
+ if (status === 'merged')
536
+ summary.contexts.merged++;
537
+ else if (status === 'error')
538
+ summary.contexts.errors++;
539
+ }
540
+ if (summary.contexts.status !== 'missing') {
541
+ summary.contexts.status = summary.contexts.errors > 0 ? 'error' : summary.contexts.merged > 0 ? 'merged' : 'no-conflict';
542
+ }
543
+ const requirementFiles = walkJsonTargets(runsDir(projectRoot), 'requirement.json');
544
+ summary.requirements = { status: requirementFiles.length === 0 ? 'missing' : 'no-conflict', merged: 0, missing: 0, errors: 0 };
545
+ for (const filePath of requirementFiles) {
546
+ const status = mergeConflictFile(filePath, (ours, theirs) => mergeRequirementRecord(ours, theirs));
547
+ if (status === 'merged')
548
+ summary.requirements.merged++;
549
+ else if (status === 'error')
550
+ summary.requirements.errors++;
551
+ }
552
+ if (summary.requirements.status !== 'missing') {
553
+ summary.requirements.status = summary.requirements.errors > 0 ? 'error' : summary.requirements.merged > 0 ? 'merged' : 'no-conflict';
554
+ }
555
+ const runFiles = walkJsonTargets(runsDir(projectRoot), 'run.json');
556
+ summary.runs = runFiles.length === 0 ? { status: 'missing', merged: 0, missing: 0, errors: 0 } : mergeRunFiles(runFiles);
557
+ const hasAnyMerge = [
558
+ summary.memoryIndex,
559
+ summary.moduleMemories,
560
+ summary.contexts,
561
+ summary.requirements,
562
+ summary.runs,
563
+ ].some((item) => item.status === 'merged');
564
+ if (hasAnyMerge) {
565
+ hydrateMissingModuleMemoriesFromIndex(projectRoot);
566
+ buildMemoryViews(projectRoot);
567
+ summary.rebuiltMemoryViews = true;
568
+ }
569
+ return summary;
570
+ }
571
+ function printLine(label, result) {
572
+ if (result.status === 'merged') {
573
+ console.log(` ${label}: merged, ${result.merged} file(s)`);
574
+ }
575
+ else if (result.status === 'no-conflict') {
576
+ console.log(` ${label}: no conflict`);
577
+ }
578
+ else if (result.status === 'missing') {
579
+ console.log(` ${label}: missing`);
580
+ }
581
+ else {
582
+ console.log(` ${label}: parse error`);
583
+ }
584
+ }
585
+ export async function mergeData() {
586
+ const projectRoot = process.cwd();
587
+ if (!fileExists(configPath(projectRoot))) {
588
+ console.log(red('\n AIDA not initialized. Run `npx aida init` first.\n'));
589
+ return;
590
+ }
591
+ const summary = mergeAidaJsonData(projectRoot);
592
+ const hasError = [
593
+ summary.memoryIndex,
594
+ summary.moduleMemories,
595
+ summary.contexts,
596
+ summary.requirements,
597
+ summary.runs,
598
+ ].some((item) => item.status === 'error');
599
+ if (hasError) {
600
+ console.log(red('\n AIDA data merge finished with parse errors. Resolve the remaining conflicted JSON manually.\n'));
601
+ return;
602
+ }
603
+ const changed = [
604
+ summary.memoryIndex,
605
+ summary.moduleMemories,
606
+ summary.contexts,
607
+ summary.requirements,
608
+ summary.runs,
609
+ ].some((item) => item.status === 'merged');
610
+ if (!changed) {
611
+ console.log(yellow('\n No AIDA JSON conflicts detected.\n'));
612
+ return;
613
+ }
614
+ console.log(green('\n ✓ AIDA data merge completed\n'));
615
+ printLine('memory index', summary.memoryIndex);
616
+ printLine('module memories', summary.moduleMemories);
617
+ printLine('branch contexts', summary.contexts);
618
+ printLine('requirements', summary.requirements);
619
+ printLine('run.json', summary.runs);
620
+ if (summary.rebuiltMemoryViews)
621
+ console.log(' memory views: rebuilt');
622
+ console.log('');
623
+ }
624
+ //# sourceMappingURL=merge-data.js.map