ai-dev-analytics 1.1.11 → 2.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 (87) hide show
  1. package/README.en.md +63 -55
  2. package/README.md +49 -60
  3. package/dist/cli/commands/build.d.ts.map +1 -1
  4. package/dist/cli/commands/build.js +0 -1
  5. package/dist/cli/commands/build.js.map +1 -1
  6. package/dist/cli/commands/doctor.d.ts +2 -0
  7. package/dist/cli/commands/doctor.d.ts.map +1 -0
  8. package/dist/cli/commands/doctor.js +70 -0
  9. package/dist/cli/commands/doctor.js.map +1 -0
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +17 -176
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/merge-data.d.ts.map +1 -1
  14. package/dist/cli/commands/merge-data.js +65 -5
  15. package/dist/cli/commands/merge-data.js.map +1 -1
  16. package/dist/cli/commands/merge.js +1 -1
  17. package/dist/cli/commands/merge.js.map +1 -1
  18. package/dist/cli/commands/migrate-legacy.d.ts.map +1 -1
  19. package/dist/cli/commands/migrate-legacy.js +7 -9
  20. package/dist/cli/commands/migrate-legacy.js.map +1 -1
  21. package/dist/cli/commands/migrate.d.ts.map +1 -1
  22. package/dist/cli/commands/migrate.js +4 -1
  23. package/dist/cli/commands/migrate.js.map +1 -1
  24. package/dist/cli/commands/reindex.d.ts.map +1 -1
  25. package/dist/cli/commands/reindex.js +4 -74
  26. package/dist/cli/commands/reindex.js.map +1 -1
  27. package/dist/cli/commands/report.d.ts.map +1 -1
  28. package/dist/cli/commands/report.js +7 -2
  29. package/dist/cli/commands/report.js.map +1 -1
  30. package/dist/cli/commands/rules.d.ts +1 -1
  31. package/dist/cli/commands/rules.d.ts.map +1 -1
  32. package/dist/cli/commands/rules.js +9 -8
  33. package/dist/cli/commands/rules.js.map +1 -1
  34. package/dist/cli/commands/skills.d.ts.map +1 -1
  35. package/dist/cli/commands/skills.js +5 -4
  36. package/dist/cli/commands/skills.js.map +1 -1
  37. package/dist/cli/commands/sync.d.ts +2 -0
  38. package/dist/cli/commands/sync.d.ts.map +1 -0
  39. package/dist/cli/commands/sync.js +37 -0
  40. package/dist/cli/commands/sync.js.map +1 -0
  41. package/dist/cli/commands/update.d.ts.map +1 -1
  42. package/dist/cli/commands/update.js +2 -1
  43. package/dist/cli/commands/update.js.map +1 -1
  44. package/dist/cli/index.js +24 -12
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/schemas/aida-project.d.ts +23 -1
  47. package/dist/schemas/aida-project.d.ts.map +1 -1
  48. package/dist/server/api.d.ts.map +1 -1
  49. package/dist/server/api.js +2 -9
  50. package/dist/server/api.js.map +1 -1
  51. package/dist/server/index.js +2 -2
  52. package/dist/server/index.js.map +1 -1
  53. package/dist/utils/ai-build.d.ts +3 -3
  54. package/dist/utils/ai-build.d.ts.map +1 -1
  55. package/dist/utils/ai-build.js +33 -86
  56. package/dist/utils/ai-build.js.map +1 -1
  57. package/dist/utils/guide.d.ts.map +1 -1
  58. package/dist/utils/guide.js +90 -55
  59. package/dist/utils/guide.js.map +1 -1
  60. package/dist/utils/memory.d.ts +2 -0
  61. package/dist/utils/memory.d.ts.map +1 -1
  62. package/dist/utils/memory.js +491 -98
  63. package/dist/utils/memory.js.map +1 -1
  64. package/dist/utils/paths.d.ts +8 -0
  65. package/dist/utils/paths.d.ts.map +1 -1
  66. package/dist/utils/paths.js +19 -1
  67. package/dist/utils/paths.js.map +1 -1
  68. package/dist/utils/project-health.d.ts +38 -0
  69. package/dist/utils/project-health.d.ts.map +1 -0
  70. package/dist/utils/project-health.js +263 -0
  71. package/dist/utils/project-health.js.map +1 -0
  72. package/dist/utils/registry.d.ts +11 -0
  73. package/dist/utils/registry.d.ts.map +1 -0
  74. package/dist/utils/registry.js +65 -0
  75. package/dist/utils/registry.js.map +1 -0
  76. package/dist/utils/rules.d.ts.map +1 -1
  77. package/dist/utils/rules.js +50 -8
  78. package/dist/utils/rules.js.map +1 -1
  79. package/dist/utils/skills.d.ts +1 -0
  80. package/dist/utils/skills.d.ts.map +1 -1
  81. package/dist/utils/skills.js +21 -5
  82. package/dist/utils/skills.js.map +1 -1
  83. package/dist/utils/summary.d.ts +8 -0
  84. package/dist/utils/summary.d.ts.map +1 -0
  85. package/dist/utils/summary.js +260 -0
  86. package/dist/utils/summary.js.map +1 -0
  87. package/package.json +2 -2
@@ -1,7 +1,8 @@
1
- import { readdirSync, statSync } from 'node:fs';
1
+ import { readdirSync, renameSync, rmSync, statSync } from 'node:fs';
2
2
  import { basename, dirname, resolve } from 'node:path';
3
3
  import { ensureDir, fileExists, readJson, readText, writeJson, writeText } from './fs.js';
4
- import { branchDir, memoriesDir, memoryIndexPath, moduleMemoriesDir, moduleMemoryPath, moduleMemoryViewPath, requirementPath, runContextPath, runMemoryPackViewPath, runContextViewPath, runsDir, } from './paths.js';
4
+ import { branchDir, legacyModuleMemoryPath, legacyModuleMemoryViewPath, memoriesDir, memoryIndexPath, moduleMemoriesDir, moduleMemoryPath, moduleMemoryViewPath, requirementPath, runContextPath, runsDir, } from './paths.js';
5
+ const MEMORY_SCHEMA_VERSION = '2.0';
5
6
  function uniqueStrings(values) {
6
7
  const result = [];
7
8
  const seen = new Set();
@@ -14,18 +15,71 @@ function uniqueStrings(values) {
14
15
  }
15
16
  return result;
16
17
  }
18
+ function latestIso(a, b) {
19
+ if (!a)
20
+ return b || '';
21
+ if (!b)
22
+ return a;
23
+ return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
24
+ }
17
25
  function normalizeRepoPath(value) {
18
26
  return value.trim().replace(/\\/g, '/');
19
27
  }
28
+ function stripKnownSourcePrefix(path) {
29
+ const normalized = normalizeRepoPath(path);
30
+ const prefixes = [
31
+ 'src/modules/',
32
+ 'src/module/',
33
+ 'src/features/',
34
+ 'src/feature/',
35
+ 'src/pages/',
36
+ 'src/views/',
37
+ 'src/components/',
38
+ 'src/',
39
+ ];
40
+ for (const prefix of prefixes) {
41
+ if (normalized.startsWith(prefix)) {
42
+ return normalized.slice(prefix.length);
43
+ }
44
+ }
45
+ return normalized;
46
+ }
47
+ function isNoisePath(value) {
48
+ const path = normalizeRepoPath(value);
49
+ const fileName = basename(path);
50
+ return path.startsWith('.../')
51
+ || fileName.startsWith('.')
52
+ || fileName === 'yarn.lock'
53
+ || fileName === 'package-lock.json'
54
+ || fileName === 'pnpm-lock.yaml'
55
+ || fileName === 'bun.lockb';
56
+ }
57
+ function isGeneratedToolingPath(value) {
58
+ const path = normalizeRepoPath(value);
59
+ return path === 'AGENTS.md'
60
+ || path === 'CLAUDE.md'
61
+ || path === '.mcp.json'
62
+ || path.startsWith('.claude/')
63
+ || path.startsWith('.cursor/')
64
+ || path.startsWith('.codex/')
65
+ || path.startsWith('.kiro/')
66
+ || path.startsWith('.agents/')
67
+ || path.startsWith('.agent/')
68
+ || path.startsWith('.roo/')
69
+ || path.startsWith('.roo-code/')
70
+ || path.startsWith('.augment/')
71
+ || path.startsWith('.gemini/')
72
+ || path.startsWith('.vscode/')
73
+ || path.startsWith('.lingma/')
74
+ || path.startsWith('.windsurf/');
75
+ }
20
76
  function isAidaRuntimePath(value) {
21
77
  const path = normalizeRepoPath(value);
22
- return path.startsWith('.aida/runs/')
23
- || path.startsWith('.aida/memories/')
24
- || path === '.aida/bootstrap-state.local.json';
78
+ return path.startsWith('.aida/');
25
79
  }
26
80
  function filterMeaningfulPaths(values, limit) {
27
81
  const filtered = uniqueStrings(values.map((value) => normalizeRepoPath(value || '')))
28
- .filter((value) => value && !isAidaRuntimePath(value));
82
+ .filter((value) => value && !isAidaRuntimePath(value) && !isNoisePath(value) && !isGeneratedToolingPath(value));
29
83
  return typeof limit === 'number' ? filtered.slice(0, limit) : filtered;
30
84
  }
31
85
  function topItems(values, limit = 8) {
@@ -71,6 +125,46 @@ function walkMarkdownFiles(rootDir) {
71
125
  }
72
126
  return result.sort();
73
127
  }
128
+ function pruneEmptyModuleMemoryDirs(rootDir) {
129
+ if (!fileExists(rootDir))
130
+ return;
131
+ for (const name of readdirSync(rootDir)) {
132
+ const full = resolve(rootDir, name);
133
+ if (!statSync(full).isDirectory())
134
+ continue;
135
+ pruneEmptyModuleMemoryDirs(full);
136
+ if (readdirSync(full).length === 0) {
137
+ rmSync(full, { recursive: true, force: true });
138
+ }
139
+ }
140
+ }
141
+ function migrateLegacyNestedModuleMemoryLayout(projectRoot) {
142
+ const rootDir = moduleMemoriesDir(projectRoot);
143
+ if (!fileExists(rootDir))
144
+ return;
145
+ const files = [
146
+ ...walkJsonFiles(rootDir).filter((file) => basename(file) !== 'index.json'),
147
+ ...walkMarkdownFiles(rootDir),
148
+ ];
149
+ for (const file of files) {
150
+ const relative = file.slice(rootDir.length + 1).replace(/\\/g, '/');
151
+ if (!relative.includes('/'))
152
+ continue;
153
+ const moduleKey = relative.replace(/\.(json|md)$/u, '');
154
+ const target = file.endsWith('.json')
155
+ ? moduleMemoryPath(projectRoot, moduleKey)
156
+ : moduleMemoryViewPath(projectRoot, moduleKey);
157
+ if (target === file)
158
+ continue;
159
+ ensureDir(dirname(target));
160
+ if (fileExists(target)) {
161
+ rmSync(file, { force: true });
162
+ continue;
163
+ }
164
+ renameSync(file, target);
165
+ }
166
+ pruneEmptyModuleMemoryDirs(rootDir);
167
+ }
74
168
  function walkRunJsonFiles(rootDir) {
75
169
  if (!fileExists(rootDir))
76
170
  return [];
@@ -195,7 +289,7 @@ function pickKeyFiles(runs) {
195
289
  const score = new Map();
196
290
  for (const run of runs) {
197
291
  for (const file of run.files || []) {
198
- if (isAidaRuntimePath(file.path))
292
+ if (isAidaRuntimePath(file.path) || isNoisePath(file.path) || isGeneratedToolingPath(file.path))
199
293
  continue;
200
294
  const path = normalizeRepoPath(file.path);
201
295
  const weight = (file.linesAdded || 0) + (file.linesRemoved || 0) + ((file.changeCount || 1) * 5);
@@ -217,14 +311,54 @@ function deriveCurrentPhase(runs) {
217
311
  return 'Completed';
218
312
  return 'Not Started';
219
313
  }
220
- function inferModules(requirement, runs) {
314
+ function inferModuleDescriptors(requirement, runs) {
315
+ const byKey = new Map();
316
+ const meaningfulRunPaths = runs
317
+ .flatMap((run) => run.files || [])
318
+ .filter((file) => !isAidaRuntimePath(file.path) && !isNoisePath(file.path) && !isGeneratedToolingPath(file.path))
319
+ .map((file) => file.path);
320
+ const put = (descriptor) => {
321
+ const key = normalizeModuleKey(descriptor.key);
322
+ if (!key || key === 'default')
323
+ return;
324
+ const existing = byKey.get(key);
325
+ if (!existing) {
326
+ byKey.set(key, {
327
+ key,
328
+ title: descriptor.title || key,
329
+ description: descriptor.description || undefined,
330
+ });
331
+ return;
332
+ }
333
+ byKey.set(key, {
334
+ key,
335
+ title: existing.title || descriptor.title || key,
336
+ description: existing.description || descriptor.description || undefined,
337
+ });
338
+ };
221
339
  if (requirement?.modules?.length) {
222
- return uniqueStrings(requirement.modules.map((module) => module.name).filter(Boolean));
340
+ for (const module of requirement.modules) {
341
+ const candidatePaths = typeof module.file === 'string' && module.file.trim().length > 0
342
+ ? [module.file]
343
+ : meaningfulRunPaths;
344
+ put(deriveModuleDescriptor(module.name, candidatePaths, module.description || ''));
345
+ }
346
+ }
347
+ else {
348
+ const stageNames = uniqueStrings(runs
349
+ .flatMap((run) => run.tasks || [])
350
+ .map((task) => task.stageName)
351
+ .filter((stage) => stage && stage !== 'default'));
352
+ for (const stageName of stageNames) {
353
+ put(deriveModuleDescriptor(stageName, meaningfulRunPaths));
354
+ }
355
+ if (byKey.size === 0) {
356
+ for (const descriptor of inferModuleDescriptorsFromPaths(meaningfulRunPaths)) {
357
+ put(descriptor);
358
+ }
359
+ }
223
360
  }
224
- return uniqueStrings(runs
225
- .flatMap((run) => run.tasks || [])
226
- .map((task) => task.stageName)
227
- .filter((stage) => stage && stage !== 'default'));
361
+ return [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key));
228
362
  }
229
363
  function renderListSection(title, values) {
230
364
  if (values.length === 0)
@@ -274,44 +408,185 @@ export function normalizeModuleKey(value) {
274
408
  return value
275
409
  .trim()
276
410
  .toLowerCase()
411
+ .replace(/\s*\/\s*/g, '/')
412
+ .replace(/-\//g, '/')
413
+ .replace(/\/-/g, '/')
277
414
  .replace(/[^a-z0-9/_\u4e00-\u9fa5-]+/g, '-')
278
415
  .replace(/\/+/g, '/')
279
416
  .replace(/-+/g, '-')
280
417
  .replace(/^-|-$/g, '');
281
418
  }
419
+ function normalizeModuleSegment(value) {
420
+ return normalizeModuleKey(value).replace(/\//g, '-');
421
+ }
422
+ function isGenericPathStem(value) {
423
+ return new Set([
424
+ 'index',
425
+ 'page',
426
+ 'view',
427
+ 'component',
428
+ 'service',
429
+ 'services',
430
+ 'store',
431
+ 'model',
432
+ 'models',
433
+ 'type',
434
+ 'types',
435
+ 'utils',
436
+ 'util',
437
+ 'helper',
438
+ 'helpers',
439
+ 'api',
440
+ 'hooks',
441
+ 'hook',
442
+ ]).has(value);
443
+ }
444
+ function isGenericModuleName(value) {
445
+ return new Set([
446
+ 'module',
447
+ 'modules',
448
+ 'feature',
449
+ 'features',
450
+ 'page',
451
+ 'pages',
452
+ 'view',
453
+ 'views',
454
+ 'component',
455
+ 'components',
456
+ 'api',
457
+ 'utils',
458
+ 'util',
459
+ 'helper',
460
+ 'helpers',
461
+ '基础设施',
462
+ '视图层',
463
+ '集成',
464
+ '国际化',
465
+ '公共工具',
466
+ '工具',
467
+ '配置',
468
+ ]).has(value);
469
+ }
470
+ function areAllModulesGeneric(values) {
471
+ return values.length > 0 && values.every((value) => isGenericModuleName(normalizeModuleKey(value)));
472
+ }
473
+ function isPlaceholderBranchText(value, branchName) {
474
+ if (!value)
475
+ return true;
476
+ const normalized = value.trim();
477
+ if (!normalized)
478
+ return true;
479
+ return normalized === branchName || normalized === branchName.replace(/-/g, '/');
480
+ }
481
+ function inferModuleDescriptorsFromPaths(paths) {
482
+ const byKey = new Map();
483
+ for (const path of filterMeaningfulPaths(paths)) {
484
+ const key = deriveModuleKeyFromPaths([path]);
485
+ if (!key || byKey.has(key))
486
+ continue;
487
+ byKey.set(key, { key, title: key });
488
+ }
489
+ return [...byKey.values()];
490
+ }
491
+ export function deriveModuleKeyFromPaths(paths) {
492
+ for (const value of paths) {
493
+ const normalized = stripKnownSourcePrefix(value || '');
494
+ if (!normalized)
495
+ continue;
496
+ const segments = normalized
497
+ .split('/')
498
+ .map((segment, index, all) => {
499
+ if (index === all.length - 1)
500
+ return segment.replace(/\.[^.]+$/u, '');
501
+ return segment;
502
+ })
503
+ .map((segment) => normalizeModuleSegment(segment))
504
+ .filter(Boolean);
505
+ while (segments.length > 1 && isGenericPathStem(segments[segments.length - 1])) {
506
+ segments.pop();
507
+ }
508
+ if (segments.length >= 2)
509
+ return `${segments[0]}/${segments[1]}`;
510
+ if (segments.length === 1)
511
+ return segments[0];
512
+ }
513
+ return '';
514
+ }
515
+ function deriveModuleDescriptor(rawName, candidatePaths = [], description = '') {
516
+ const normalizedName = normalizeModuleKey(rawName);
517
+ const pathDerived = deriveModuleKeyFromPaths(candidatePaths);
518
+ const key = normalizedName.includes('/')
519
+ ? normalizedName
520
+ : (!normalizedName || isGenericModuleName(normalizedName))
521
+ ? (pathDerived || normalizedName)
522
+ : normalizedName;
523
+ return {
524
+ key: key || normalizedName || 'module',
525
+ title: rawName.trim() || key || 'module',
526
+ description: description.trim() || undefined,
527
+ };
528
+ }
529
+ function normalizeModuleMemoryRecord(record) {
530
+ return {
531
+ ...record,
532
+ moduleKey: normalizeModuleKey(record.moduleKey),
533
+ };
534
+ }
282
535
  export function loadMemoryIndex(projectRoot) {
283
536
  const path = memoryIndexPath(projectRoot);
284
537
  if (!fileExists(path)) {
285
538
  return {
539
+ schemaVersion: MEMORY_SCHEMA_VERSION,
286
540
  updatedAt: new Date().toISOString(),
287
- modules: [],
541
+ items: [],
288
542
  };
289
543
  }
290
- return readJson(path);
544
+ const raw = readJson(path);
545
+ const items = Array.isArray(raw?.items)
546
+ ? raw.items
547
+ : Array.isArray(raw?.modules)
548
+ ? raw.modules
549
+ : [];
550
+ return {
551
+ schemaVersion: raw?.schemaVersion || MEMORY_SCHEMA_VERSION,
552
+ updatedAt: raw?.updatedAt || new Date().toISOString(),
553
+ items,
554
+ };
291
555
  }
292
556
  export function saveMemoryIndex(projectRoot, index) {
293
557
  ensureDir(memoriesDir(projectRoot));
294
- writeJson(memoryIndexPath(projectRoot), index);
558
+ writeJson(memoryIndexPath(projectRoot), {
559
+ schemaVersion: index.schemaVersion || MEMORY_SCHEMA_VERSION,
560
+ updatedAt: index.updatedAt,
561
+ items: index.items,
562
+ });
295
563
  }
296
564
  export function loadModuleMemory(projectRoot, moduleKey) {
297
565
  const normalized = normalizeModuleKey(moduleKey);
298
566
  const path = moduleMemoryPath(projectRoot, normalized);
299
567
  if (fileExists(path))
300
568
  return readJson(path);
569
+ const legacyPath = legacyModuleMemoryPath(projectRoot, normalized);
570
+ if (fileExists(legacyPath))
571
+ return readJson(legacyPath);
301
572
  const viewPath = moduleMemoryViewPath(projectRoot, normalized);
302
- if (!fileExists(viewPath))
573
+ if (fileExists(viewPath))
574
+ return moduleMemoryRecordFromMarkdown(readText(viewPath));
575
+ const legacyViewPath = legacyModuleMemoryViewPath(projectRoot, normalized);
576
+ if (!fileExists(legacyViewPath))
303
577
  return null;
304
- return moduleMemoryRecordFromMarkdown(readText(viewPath));
578
+ return moduleMemoryRecordFromMarkdown(readText(legacyViewPath));
305
579
  }
306
580
  function upsertMemoryIndexEntry(projectRoot, record) {
307
581
  const index = loadMemoryIndex(projectRoot);
308
582
  const entry = memoryIndexEntryFromRecord(record);
309
- const next = index.modules.filter((item) => item.key !== record.moduleKey);
583
+ const next = index.items.filter((item) => item.key !== record.moduleKey);
310
584
  next.push(entry);
311
585
  next.sort((a, b) => a.key.localeCompare(b.key));
312
586
  saveMemoryIndex(projectRoot, {
587
+ schemaVersion: MEMORY_SCHEMA_VERSION,
313
588
  updatedAt: new Date().toISOString(),
314
- modules: next,
589
+ items: next,
315
590
  });
316
591
  }
317
592
  function memoryIndexEntryFromRecord(record) {
@@ -321,9 +596,72 @@ function memoryIndexEntryFromRecord(record) {
321
596
  summary: record.summary,
322
597
  keywords: topItems([record.moduleKey, record.title, ...record.keywords], 12),
323
598
  paths: filterMeaningfulPaths([...record.entryFiles, ...record.relatedPaths], 12),
599
+ tickets: topItems([...(record.tickets || [])]
600
+ .map((ticket) => ticket.ticket || ticket.branch || '')
601
+ .filter(Boolean), 8),
324
602
  updatedAt: record.updatedAt,
325
603
  };
326
604
  }
605
+ function mergeTicketReferences(existing, incoming) {
606
+ const byKey = new Map();
607
+ for (const ticket of [...existing, ...incoming]) {
608
+ const summary = ticket.summary.trim();
609
+ if (!summary)
610
+ continue;
611
+ const key = `${ticket.ticket || ''}|${ticket.branch || ''}`;
612
+ const current = byKey.get(key);
613
+ if (!current) {
614
+ byKey.set(key, {
615
+ ticket: ticket.ticket,
616
+ branch: ticket.branch,
617
+ summary,
618
+ updatedAt: ticket.updatedAt,
619
+ });
620
+ continue;
621
+ }
622
+ const useIncoming = latestIso(current.updatedAt, ticket.updatedAt) === ticket.updatedAt;
623
+ byKey.set(key, {
624
+ ticket: current.ticket || ticket.ticket,
625
+ branch: current.branch || ticket.branch,
626
+ summary: useIncoming ? summary : current.summary,
627
+ updatedAt: latestIso(current.updatedAt, ticket.updatedAt),
628
+ });
629
+ }
630
+ return [...byKey.values()].sort((a, b) => `${b.updatedAt || ''}`.localeCompare(a.updatedAt || '') || `${a.ticket || a.branch || ''}`.localeCompare(`${b.ticket || b.branch || ''}`));
631
+ }
632
+ function mergeChangeEntries(existing, incoming) {
633
+ const byKey = new Map();
634
+ for (const change of [...existing, ...incoming]) {
635
+ const summary = change.summary.trim();
636
+ if (!summary)
637
+ continue;
638
+ const key = change.ticket || change.branch
639
+ ? `${change.ticket || ''}|${change.branch || ''}`
640
+ : `${change.title || ''}|${summary}`;
641
+ const current = byKey.get(key);
642
+ if (!current) {
643
+ byKey.set(key, {
644
+ ticket: change.ticket,
645
+ branch: change.branch,
646
+ title: change.title,
647
+ summary,
648
+ updatedAt: change.updatedAt,
649
+ });
650
+ continue;
651
+ }
652
+ const useIncoming = latestIso(current.updatedAt, change.updatedAt) === change.updatedAt;
653
+ byKey.set(key, {
654
+ ticket: current.ticket || change.ticket,
655
+ branch: current.branch || change.branch,
656
+ title: useIncoming ? (change.title || current.title) : (current.title || change.title),
657
+ summary: useIncoming ? summary : current.summary,
658
+ updatedAt: latestIso(current.updatedAt, change.updatedAt),
659
+ });
660
+ }
661
+ return [...byKey.values()]
662
+ .sort((a, b) => `${b.updatedAt}`.localeCompare(`${a.updatedAt}`))
663
+ .slice(0, 20);
664
+ }
327
665
  function mergeMemoryIndexEntry(existing, next) {
328
666
  if (!existing)
329
667
  return next;
@@ -333,6 +671,7 @@ function mergeMemoryIndexEntry(existing, next) {
333
671
  summary: next.summary || existing.summary,
334
672
  keywords: topItems([...(existing.keywords || []), ...(next.keywords || [])], 12),
335
673
  paths: filterMeaningfulPaths([...(existing.paths || []), ...(next.paths || [])], 12),
674
+ tickets: topItems([...(existing.tickets || []), ...(next.tickets || [])], 8),
336
675
  updatedAt: next.updatedAt || existing.updatedAt,
337
676
  };
338
677
  }
@@ -346,6 +685,7 @@ function moduleMemoryRecordFromMarkdown(raw) {
346
685
  const sections = extractMarkdownSections(raw);
347
686
  const summary = sections.get('summary') || '';
348
687
  return {
688
+ schemaVersion: MEMORY_SCHEMA_VERSION,
349
689
  moduleKey,
350
690
  title: raw.match(/^- Title:\s+(.+)$/m)?.[1]?.trim() || moduleKey,
351
691
  summary,
@@ -358,32 +698,54 @@ function moduleMemoryRecordFromMarkdown(raw) {
358
698
  pitfalls: parseListSection(raw, 'pitfalls'),
359
699
  relatedRules: parseListSection(raw, 'related rules'),
360
700
  tickets: [],
701
+ changes: [],
361
702
  updatedAt: raw.match(/^- Updated At:\s+(.+)$/m)?.[1]?.trim() || new Date().toISOString(),
362
703
  };
363
704
  }
364
705
  export function rebuildMemoryIndexFromDisk(projectRoot) {
365
706
  ensureDir(moduleMemoriesDir(projectRoot));
707
+ migrateLegacyNestedModuleMemoryLayout(projectRoot);
366
708
  const byKey = new Map();
367
- for (const entry of loadMemoryIndex(projectRoot).modules || []) {
368
- byKey.set(entry.key, entry);
369
- }
709
+ const expectedMarkdownViews = new Set();
370
710
  for (const file of walkJsonFiles(moduleMemoriesDir(projectRoot))) {
371
711
  if (basename(file) === 'index.json')
372
712
  continue;
373
- const record = readJson(file);
713
+ const original = readJson(file);
714
+ const record = normalizeModuleMemoryRecord(original);
715
+ if (record.moduleKey !== original.moduleKey) {
716
+ saveModuleMemory(projectRoot, record);
717
+ rmSync(file, { force: true });
718
+ }
374
719
  const entry = memoryIndexEntryFromRecord(record);
375
720
  byKey.set(record.moduleKey, mergeMemoryIndexEntry(byKey.get(record.moduleKey), entry));
721
+ expectedMarkdownViews.add(moduleMemoryViewPath(projectRoot, record.moduleKey));
376
722
  }
377
723
  for (const file of walkMarkdownFiles(moduleMemoriesDir(projectRoot))) {
378
724
  const record = moduleMemoryRecordFromMarkdown(readText(file));
379
725
  if (!record)
380
726
  continue;
381
- const entry = memoryIndexEntryFromRecord(record);
382
- byKey.set(record.moduleKey, mergeMemoryIndexEntry(byKey.get(record.moduleKey), entry));
727
+ const normalized = normalizeModuleMemoryRecord(record);
728
+ if (normalized.moduleKey !== record.moduleKey) {
729
+ const targetViewPath = moduleMemoryViewPath(projectRoot, normalized.moduleKey);
730
+ ensureDir(dirname(targetViewPath));
731
+ if (!fileExists(targetViewPath)) {
732
+ writeText(targetViewPath, renderModuleMemoryMarkdown(normalized));
733
+ }
734
+ rmSync(file, { force: true });
735
+ }
736
+ const entry = memoryIndexEntryFromRecord(normalized);
737
+ byKey.set(normalized.moduleKey, mergeMemoryIndexEntry(byKey.get(normalized.moduleKey), entry));
738
+ expectedMarkdownViews.add(moduleMemoryViewPath(projectRoot, normalized.moduleKey));
739
+ }
740
+ for (const file of walkMarkdownFiles(moduleMemoriesDir(projectRoot))) {
741
+ if (!expectedMarkdownViews.has(file)) {
742
+ rmSync(file, { force: true });
743
+ }
383
744
  }
384
745
  const index = {
746
+ schemaVersion: MEMORY_SCHEMA_VERSION,
385
747
  updatedAt: new Date().toISOString(),
386
- modules: [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key)),
748
+ items: [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key)),
387
749
  };
388
750
  saveMemoryIndex(projectRoot, index);
389
751
  return index;
@@ -391,7 +753,10 @@ export function rebuildMemoryIndexFromDisk(projectRoot) {
391
753
  export function saveModuleMemory(projectRoot, record) {
392
754
  const path = moduleMemoryPath(projectRoot, record.moduleKey);
393
755
  ensureDir(dirname(path));
394
- writeJson(path, record);
756
+ writeJson(path, {
757
+ schemaVersion: record.schemaVersion || MEMORY_SCHEMA_VERSION,
758
+ ...record,
759
+ });
395
760
  upsertMemoryIndexEntry(projectRoot, record);
396
761
  }
397
762
  export function loadRunContext(projectRoot, branchName) {
@@ -432,6 +797,12 @@ export function renderModuleMemoryMarkdown(record) {
432
797
  '',
433
798
  renderListSection('Related Rules', record.relatedRules).trimEnd(),
434
799
  '',
800
+ '## Changes',
801
+ '',
802
+ ...((record.changes || []).length > 0
803
+ ? (record.changes || []).map((change) => `- ${[change.ticket || change.branch, change.title, change.summary].filter(Boolean).join(' | ')}`)
804
+ : ['- None']),
805
+ '',
435
806
  '## Related Tickets',
436
807
  '',
437
808
  ...(record.tickets.length > 0
@@ -492,50 +863,34 @@ export function renderRunMemoryPackMarkdown(context, modules) {
492
863
  }
493
864
  export function buildMemoryViews(projectRoot) {
494
865
  ensureDir(moduleMemoriesDir(projectRoot));
866
+ migrateLegacyNestedModuleMemoryLayout(projectRoot);
495
867
  let moduleViews = 0;
496
868
  for (const file of walkJsonFiles(moduleMemoriesDir(projectRoot))) {
497
869
  if (basename(file) === 'index.json')
498
870
  continue;
499
- const record = readJson(file);
871
+ const original = readJson(file);
872
+ const record = normalizeModuleMemoryRecord(original);
873
+ if (record.moduleKey !== original.moduleKey) {
874
+ saveModuleMemory(projectRoot, record);
875
+ rmSync(file, { force: true });
876
+ }
500
877
  const viewPath = moduleMemoryViewPath(projectRoot, record.moduleKey);
501
878
  ensureDir(dirname(viewPath));
502
879
  writeText(viewPath, renderModuleMemoryMarkdown(record));
503
880
  moduleViews++;
504
881
  }
505
- let contextViews = 0;
506
- let packViews = 0;
507
- const root = runsDir(projectRoot);
508
- if (!fileExists(root)) {
509
- rebuildMemoryIndexFromDisk(projectRoot);
510
- return { moduleViews, contextViews, packViews };
511
- }
512
- for (const branchName of readdirSync(root)) {
513
- const safeBranchDir = resolve(root, branchName);
514
- if (!fileExists(safeBranchDir) || !statSync(safeBranchDir).isDirectory())
515
- continue;
516
- const contextPath = resolve(safeBranchDir, 'context.json');
517
- if (!fileExists(contextPath))
518
- continue;
519
- const record = readJson(contextPath);
520
- writeText(runContextViewPath(projectRoot, record.branch), renderRunContextMarkdown(record));
521
- contextViews++;
522
- const modules = record.modules
523
- .map((moduleName) => loadModuleMemory(projectRoot, moduleName))
524
- .filter((item) => item !== null);
525
- writeText(runMemoryPackViewPath(projectRoot, record.branch), renderRunMemoryPackMarkdown(record, modules));
526
- packViews++;
527
- }
528
882
  rebuildMemoryIndexFromDisk(projectRoot);
529
- return { moduleViews, contextViews, packViews };
883
+ return { moduleViews, contextViews: 0, packViews: 0 };
530
884
  }
531
885
  export function buildRunContextFromBranch(projectRoot, branchName) {
886
+ const existing = loadRunContext(projectRoot, branchName);
532
887
  const requirement = loadRequirement(projectRoot, branchName);
533
888
  const analysis = loadAnalysis(projectRoot, branchName);
534
889
  const runs = loadBranchRuns(projectRoot, branchName);
535
890
  if (!requirement && !analysis && runs.length === 0) {
536
891
  return null;
537
892
  }
538
- const modules = inferModules(requirement, runs);
893
+ const moduleDescriptors = inferModuleDescriptors(requirement, runs);
539
894
  const completed = topItems(runs.flatMap((run) => run.tasks || []).filter((task) => task.status === 'done').map((task) => task.title));
540
895
  const inProgress = topItems(runs.flatMap((run) => run.tasks || []).filter((task) => task.status === 'in-progress').map((task) => task.title));
541
896
  const next = topItems(runs.flatMap((run) => run.tasks || []).filter((task) => task.status === 'pending').map((task) => task.title));
@@ -547,25 +902,44 @@ export function buildRunContextFromBranch(projectRoot, branchName) {
547
902
  ...runs.flatMap((run) => run.deviations || []).map((deviation) => deviation.title),
548
903
  ];
549
904
  const title = requirement?.title || extractTicket(branchName) || branchName;
905
+ const derivedPhase = deriveCurrentPhase(runs);
906
+ const derivedModules = moduleDescriptors.map((descriptor) => descriptor.key);
907
+ const derivedKeyFiles = pickKeyFiles(runs);
908
+ const derivedRisks = topItems([...analysisRisks, ...runRisks]);
909
+ const existingModules = existing?.modules || [];
910
+ const fallbackPathModules = inferModuleDescriptorsFromPaths([
911
+ ...derivedKeyFiles,
912
+ ...(existing?.keyFiles || []),
913
+ ]).map((descriptor) => descriptor.key);
914
+ const nextModules = derivedModules.length > 0
915
+ ? derivedModules
916
+ : areAllModulesGeneric(existingModules) && fallbackPathModules.length > 0
917
+ ? fallbackPathModules
918
+ : existingModules;
919
+ const nextTitle = isPlaceholderBranchText(existing?.title, branchName) ? title : (existing?.title || title);
920
+ const nextSummary = requirement?.summary
921
+ || extractSummaryFromAnalysis(analysis)
922
+ || (!isPlaceholderBranchText(existing?.summary, branchName) ? existing.summary : '')
923
+ || (nextModules.length > 0 ? `涉及模块: ${nextModules.join(', ')}` : title);
550
924
  return {
551
925
  branch: branchName,
552
926
  ticket: extractTicket(requirement?.title || '') || extractTicket(branchName),
553
- title,
554
- summary: requirement?.summary || extractSummaryFromAnalysis(analysis) || title,
555
- currentPhase: deriveCurrentPhase(runs),
556
- modules,
557
- completed,
558
- inProgress,
559
- next,
560
- decisions,
561
- constraints,
562
- keyFiles: pickKeyFiles(runs),
563
- risks: topItems([...analysisRisks, ...runRisks]),
927
+ title: nextTitle,
928
+ summary: nextSummary,
929
+ currentPhase: derivedPhase === 'Not Started' && existing?.currentPhase ? existing.currentPhase : derivedPhase,
930
+ modules: nextModules,
931
+ completed: completed.length > 0 ? completed : (existing?.completed || []),
932
+ inProgress: inProgress.length > 0 ? inProgress : (existing?.inProgress || []),
933
+ next: next.length > 0 ? next : (existing?.next || []),
934
+ decisions: decisions.length > 0 ? decisions : (existing?.decisions || []),
935
+ constraints: constraints.length > 0 ? constraints : (existing?.constraints || []),
936
+ keyFiles: derivedKeyFiles.length > 0 ? derivedKeyFiles : (existing?.keyFiles || []),
937
+ risks: derivedRisks.length > 0 ? derivedRisks : (existing?.risks || []),
564
938
  updatedAt: new Date().toISOString(),
565
939
  };
566
940
  }
567
- function collectRelatedPaths(moduleName, branchContext, runs) {
568
- const query = moduleName.toLowerCase();
941
+ function collectRelatedPaths(moduleDescriptor, branchContext, runs) {
942
+ const query = moduleDescriptor.key.toLowerCase();
569
943
  const tokens = splitQueryTokens(query);
570
944
  const scored = new Map();
571
945
  for (const path of branchContext.keyFiles) {
@@ -575,10 +949,11 @@ function collectRelatedPaths(moduleName, branchContext, runs) {
575
949
  }
576
950
  for (const run of runs) {
577
951
  for (const task of run.tasks || []) {
578
- if (task.stageName !== moduleName)
952
+ const taskDescriptor = deriveModuleDescriptor(task.stageName || '', (run.files || []).map((file) => file.path));
953
+ if (taskDescriptor.key !== moduleDescriptor.key)
579
954
  continue;
580
955
  for (const file of run.files || []) {
581
- if (isAidaRuntimePath(file.path))
956
+ if (isAidaRuntimePath(file.path) || isNoisePath(file.path) || isGeneratedToolingPath(file.path))
582
957
  continue;
583
958
  const path = normalizeRepoPath(file.path);
584
959
  const bonus = (file.linesAdded || 0) + (file.linesRemoved || 0) + 10;
@@ -592,24 +967,31 @@ function collectRelatedPaths(moduleName, branchContext, runs) {
592
967
  export function upsertModuleMemory(projectRoot, input) {
593
968
  const moduleKey = normalizeModuleKey(input.moduleKey);
594
969
  const existing = loadModuleMemory(projectRoot, moduleKey);
970
+ const existingEntryFiles = filterMeaningfulPaths(existing?.entryFiles || [], 8);
971
+ const existingRelatedPaths = filterMeaningfulPaths(existing?.relatedPaths || [], 12);
972
+ const nextChanges = ((input.tickets || []).filter((ticket) => ticket.summary.trim().length > 0))
973
+ .map((ticket) => ({
974
+ ticket: ticket.ticket,
975
+ branch: ticket.branch,
976
+ title: input.changeTitle || ticket.ticket || ticket.branch || input.title || existing?.title || moduleKey,
977
+ summary: ticket.summary,
978
+ updatedAt: ticket.updatedAt || new Date().toISOString(),
979
+ }));
595
980
  const record = {
981
+ schemaVersion: MEMORY_SCHEMA_VERSION,
596
982
  moduleKey,
597
983
  title: input.title || existing?.title || moduleKey,
598
984
  summary: input.summary || existing?.summary || '',
599
985
  keywords: topItems([...(existing?.keywords || []), ...(input.keywords || []), moduleKey, input.title || ''], 16),
600
- entryFiles: filterMeaningfulPaths([...(existing?.entryFiles || []), ...(input.entryFiles || [])], 8),
601
- relatedPaths: filterMeaningfulPaths([...(existing?.relatedPaths || []), ...(input.relatedPaths || [])], 12),
986
+ entryFiles: filterMeaningfulPaths([...existingEntryFiles, ...(input.entryFiles || [])], 8),
987
+ relatedPaths: filterMeaningfulPaths([...existingRelatedPaths, ...(input.relatedPaths || [])], 12),
602
988
  dataFlow: topItems([...(existing?.dataFlow || []), ...(input.dataFlow || [])]),
603
989
  decisions: topItems([...(existing?.decisions || []), ...(input.decisions || [])]),
604
990
  constraints: topItems([...(existing?.constraints || []), ...(input.constraints || [])]),
605
991
  pitfalls: topItems([...(existing?.pitfalls || []), ...(input.pitfalls || [])]),
606
992
  relatedRules: topItems([...(existing?.relatedRules || []), ...(input.relatedRules || [])], 12),
607
- tickets: [
608
- ...(existing?.tickets || []),
609
- ...((input.tickets || []).filter((ticket) => ticket.summary.trim().length > 0)),
610
- ].filter((ticket, index, array) => array.findIndex((item) => item.ticket === ticket.ticket
611
- && item.branch === ticket.branch
612
- && item.summary === ticket.summary) === index),
993
+ tickets: mergeTicketReferences(existing?.tickets || [], (input.tickets || []).filter((ticket) => ticket.summary.trim().length > 0)),
994
+ changes: mergeChangeEntries(existing?.changes || [], nextChanges),
613
995
  updatedAt: new Date().toISOString(),
614
996
  };
615
997
  saveModuleMemory(projectRoot, record);
@@ -643,10 +1025,10 @@ export function searchModuleMemories(projectRoot, query, pathHints = []) {
643
1025
  const tokens = splitQueryTokens(normalizedQuery);
644
1026
  const hints = pathHints.map((hint) => hint.toLowerCase());
645
1027
  let index = loadMemoryIndex(projectRoot);
646
- if (index.modules.length === 0 && fileExists(moduleMemoriesDir(projectRoot))) {
1028
+ if (index.items.length === 0 && fileExists(moduleMemoriesDir(projectRoot))) {
647
1029
  index = rebuildMemoryIndexFromDisk(projectRoot);
648
1030
  }
649
- return index.modules
1031
+ return index.items
650
1032
  .map((entry) => {
651
1033
  let score = 0;
652
1034
  score += scoreText(normalizedQuery, tokens, entry.key) * 2;
@@ -686,7 +1068,19 @@ export function migrateLegacyMemories(projectRoot) {
686
1068
  const requirementData = requirement
687
1069
  ? readJson(resolve(branchPath, 'requirement.json'))
688
1070
  : null;
689
- const branchName = requirementData?.branch || safeBranch.replace(/-/g, '/');
1071
+ const branchRunsFromFiles = runFiles
1072
+ .map((path) => {
1073
+ try {
1074
+ return readJson(path);
1075
+ }
1076
+ catch {
1077
+ return null;
1078
+ }
1079
+ })
1080
+ .filter((item) => item !== null);
1081
+ const branchName = requirementData?.branch
1082
+ || branchRunsFromFiles.find((run) => run.meta?.branch)?.meta?.branch
1083
+ || safeBranch;
690
1084
  const context = buildRunContextFromBranch(projectRoot, branchName);
691
1085
  if (!context)
692
1086
  continue;
@@ -695,24 +1089,22 @@ export function migrateLegacyMemories(projectRoot) {
695
1089
  contextsWritten++;
696
1090
  const runs = loadBranchRuns(projectRoot, branchName);
697
1091
  const req = loadRequirement(projectRoot, branchName);
698
- const moduleCandidates = req?.modules?.length
699
- ? req.modules.map((module) => ({ name: module.name, description: module.description }))
700
- : context.modules.map((name) => ({ name, description: '' }));
701
- for (const candidate of moduleCandidates) {
702
- if (!candidate.name.trim())
1092
+ const moduleDescriptors = inferModuleDescriptors(req, runs);
1093
+ for (const descriptor of moduleDescriptors) {
1094
+ if (!descriptor.key.trim())
703
1095
  continue;
704
- const moduleKey = normalizeModuleKey(candidate.name);
705
- const relatedPaths = collectRelatedPaths(candidate.name, context, runs);
1096
+ const relatedPaths = collectRelatedPaths(descriptor, context, runs);
706
1097
  upsertModuleMemory(projectRoot, {
707
- moduleKey,
708
- title: candidate.name,
709
- summary: candidate.description || context.summary,
710
- keywords: [candidate.name, moduleKey, context.title],
1098
+ moduleKey: descriptor.key,
1099
+ title: descriptor.title,
1100
+ summary: descriptor.description || context.summary,
1101
+ keywords: [descriptor.title, descriptor.key, context.title],
711
1102
  entryFiles: relatedPaths.slice(0, 5),
712
1103
  relatedPaths,
713
1104
  decisions: context.decisions,
714
1105
  constraints: context.constraints,
715
1106
  pitfalls: context.risks,
1107
+ changeTitle: context.title,
716
1108
  tickets: [{
717
1109
  ticket: context.ticket,
718
1110
  branch: branchName,
@@ -721,7 +1113,7 @@ export function migrateLegacyMemories(projectRoot) {
721
1113
  }],
722
1114
  });
723
1115
  moduleMemoriesWritten++;
724
- modulesTouched.add(moduleKey);
1116
+ modulesTouched.add(descriptor.key);
725
1117
  }
726
1118
  }
727
1119
  buildMemoryViews(projectRoot);
@@ -742,18 +1134,19 @@ export function rebuildCurrentBranchMemory(projectRoot, branchName) {
742
1134
  const req = loadRequirement(projectRoot, branchName);
743
1135
  const runs = loadBranchRuns(projectRoot, branchName);
744
1136
  const modules = [];
745
- for (const moduleName of inferModules(req, runs)) {
746
- const relatedPaths = collectRelatedPaths(moduleName, context, runs);
1137
+ for (const descriptor of inferModuleDescriptors(req, runs)) {
1138
+ const relatedPaths = collectRelatedPaths(descriptor, context, runs);
747
1139
  modules.push(upsertModuleMemory(projectRoot, {
748
- moduleKey: moduleName,
749
- title: moduleName,
750
- summary: req?.modules?.find((module) => module.name === moduleName)?.description || context.summary,
751
- keywords: [moduleName, context.title],
1140
+ moduleKey: descriptor.key,
1141
+ title: descriptor.title,
1142
+ summary: descriptor.description || context.summary,
1143
+ keywords: [descriptor.title, descriptor.key, context.title],
752
1144
  entryFiles: relatedPaths.slice(0, 5),
753
1145
  relatedPaths,
754
1146
  decisions: context.decisions,
755
1147
  constraints: context.constraints,
756
1148
  pitfalls: context.risks,
1149
+ changeTitle: context.title,
757
1150
  tickets: [{
758
1151
  ticket: context.ticket,
759
1152
  branch: branchName,