ai-dev-analytics 1.1.12 → 2.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.
Files changed (192) hide show
  1. package/README.en.md +51 -57
  2. package/README.md +68 -485
  3. package/dist/cli/commands/doctor.d.ts +2 -0
  4. package/dist/cli/commands/doctor.d.ts.map +1 -0
  5. package/dist/cli/commands/doctor.js +125 -0
  6. package/dist/cli/commands/doctor.js.map +1 -0
  7. package/dist/cli/commands/init.d.ts.map +1 -1
  8. package/dist/cli/commands/init.js +19 -176
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/commands/memory.d.ts.map +1 -1
  11. package/dist/cli/commands/memory.js +3 -13
  12. package/dist/cli/commands/memory.js.map +1 -1
  13. package/dist/cli/commands/rules.d.ts +0 -10
  14. package/dist/cli/commands/rules.d.ts.map +1 -1
  15. package/dist/cli/commands/rules.js +9 -34
  16. package/dist/cli/commands/rules.js.map +1 -1
  17. package/dist/cli/commands/skills.d.ts +0 -5
  18. package/dist/cli/commands/skills.d.ts.map +1 -1
  19. package/dist/cli/commands/skills.js +5 -24
  20. package/dist/cli/commands/skills.js.map +1 -1
  21. package/dist/cli/commands/sync.d.ts +2 -0
  22. package/dist/cli/commands/sync.d.ts.map +1 -0
  23. package/dist/cli/commands/sync.js +28 -0
  24. package/dist/cli/commands/sync.js.map +1 -0
  25. package/dist/cli/index.js +26 -85
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/{schemas/run-json.d.ts → internal/runtime/schema.d.ts} +4 -18
  28. package/dist/internal/runtime/schema.d.ts.map +1 -0
  29. package/dist/{schemas/run-json.js → internal/runtime/schema.js} +5 -9
  30. package/dist/internal/runtime/schema.js.map +1 -0
  31. package/dist/{utils/run-data.d.ts → internal/runtime/state.d.ts} +7 -8
  32. package/dist/internal/runtime/state.d.ts.map +1 -0
  33. package/dist/{utils/run-data.js → internal/runtime/state.js} +11 -12
  34. package/dist/internal/runtime/state.js.map +1 -0
  35. package/dist/internal/runtime/summary.d.ts +8 -0
  36. package/dist/internal/runtime/summary.d.ts.map +1 -0
  37. package/dist/internal/runtime/summary.js +260 -0
  38. package/dist/internal/runtime/summary.js.map +1 -0
  39. package/dist/internal/runtime/tokens.d.ts.map +1 -0
  40. package/dist/internal/runtime/tokens.js.map +1 -0
  41. package/dist/mcp/server.js +9 -8
  42. package/dist/mcp/server.js.map +1 -1
  43. package/dist/schemas/aida-project.d.ts +27 -1
  44. package/dist/schemas/aida-project.d.ts.map +1 -1
  45. package/dist/schemas/rules.d.ts +15 -0
  46. package/dist/schemas/rules.d.ts.map +1 -0
  47. package/dist/schemas/rules.js +5 -0
  48. package/dist/schemas/rules.js.map +1 -0
  49. package/dist/services/project-build.d.ts +44 -0
  50. package/dist/services/project-build.d.ts.map +1 -0
  51. package/dist/services/project-build.js +32 -0
  52. package/dist/services/project-build.js.map +1 -0
  53. package/dist/services/project-health.d.ts +14 -0
  54. package/dist/services/project-health.d.ts.map +1 -0
  55. package/dist/services/project-health.js +19 -0
  56. package/dist/services/project-health.js.map +1 -0
  57. package/dist/services/security-audit.d.ts +59 -0
  58. package/dist/services/security-audit.d.ts.map +1 -0
  59. package/dist/services/security-audit.js +638 -0
  60. package/dist/services/security-audit.js.map +1 -0
  61. package/dist/utils/ai-build.d.ts +3 -8
  62. package/dist/utils/ai-build.d.ts.map +1 -1
  63. package/dist/utils/ai-build.js +31 -117
  64. package/dist/utils/ai-build.js.map +1 -1
  65. package/dist/utils/guide.d.ts +3 -3
  66. package/dist/utils/guide.d.ts.map +1 -1
  67. package/dist/utils/guide.js +91 -68
  68. package/dist/utils/guide.js.map +1 -1
  69. package/dist/utils/import.d.ts.map +1 -1
  70. package/dist/utils/import.js +4 -15
  71. package/dist/utils/import.js.map +1 -1
  72. package/dist/utils/memory.d.ts +2 -0
  73. package/dist/utils/memory.d.ts.map +1 -1
  74. package/dist/utils/memory.js +440 -95
  75. package/dist/utils/memory.js.map +1 -1
  76. package/dist/utils/paths.d.ts +2 -10
  77. package/dist/utils/paths.d.ts.map +1 -1
  78. package/dist/utils/paths.js +5 -17
  79. package/dist/utils/paths.js.map +1 -1
  80. package/dist/utils/project-health.d.ts +39 -0
  81. package/dist/utils/project-health.d.ts.map +1 -0
  82. package/dist/utils/project-health.js +286 -0
  83. package/dist/utils/project-health.js.map +1 -0
  84. package/dist/utils/registry.d.ts +11 -0
  85. package/dist/utils/registry.d.ts.map +1 -0
  86. package/dist/utils/registry.js +65 -0
  87. package/dist/utils/registry.js.map +1 -0
  88. package/dist/utils/rules.d.ts +11 -1
  89. package/dist/utils/rules.d.ts.map +1 -1
  90. package/dist/utils/rules.js +76 -8
  91. package/dist/utils/rules.js.map +1 -1
  92. package/dist/utils/skills.d.ts +6 -9
  93. package/dist/utils/skills.d.ts.map +1 -1
  94. package/dist/utils/skills.js +26 -54
  95. package/dist/utils/skills.js.map +1 -1
  96. package/package.json +12 -14
  97. package/dist/cli/commands/build.d.ts +0 -2
  98. package/dist/cli/commands/build.d.ts.map +0 -1
  99. package/dist/cli/commands/build.js +0 -56
  100. package/dist/cli/commands/build.js.map +0 -1
  101. package/dist/cli/commands/dashboard.d.ts +0 -2
  102. package/dist/cli/commands/dashboard.d.ts.map +0 -1
  103. package/dist/cli/commands/dashboard.js +0 -70
  104. package/dist/cli/commands/dashboard.js.map +0 -1
  105. package/dist/cli/commands/import.d.ts +0 -2
  106. package/dist/cli/commands/import.d.ts.map +0 -1
  107. package/dist/cli/commands/import.js +0 -71
  108. package/dist/cli/commands/import.js.map +0 -1
  109. package/dist/cli/commands/log.d.ts +0 -2
  110. package/dist/cli/commands/log.d.ts.map +0 -1
  111. package/dist/cli/commands/log.js +0 -440
  112. package/dist/cli/commands/log.js.map +0 -1
  113. package/dist/cli/commands/merge-data.d.ts +0 -20
  114. package/dist/cli/commands/merge-data.d.ts.map +0 -1
  115. package/dist/cli/commands/merge-data.js +0 -574
  116. package/dist/cli/commands/merge-data.js.map +0 -1
  117. package/dist/cli/commands/merge.d.ts +0 -2
  118. package/dist/cli/commands/merge.d.ts.map +0 -1
  119. package/dist/cli/commands/merge.js +0 -82
  120. package/dist/cli/commands/merge.js.map +0 -1
  121. package/dist/cli/commands/migrate-dir.d.ts +0 -6
  122. package/dist/cli/commands/migrate-dir.d.ts.map +0 -1
  123. package/dist/cli/commands/migrate-dir.js +0 -125
  124. package/dist/cli/commands/migrate-dir.js.map +0 -1
  125. package/dist/cli/commands/migrate-legacy.d.ts +0 -2
  126. package/dist/cli/commands/migrate-legacy.d.ts.map +0 -1
  127. package/dist/cli/commands/migrate-legacy.js +0 -143
  128. package/dist/cli/commands/migrate-legacy.js.map +0 -1
  129. package/dist/cli/commands/migrate.d.ts +0 -2
  130. package/dist/cli/commands/migrate.d.ts.map +0 -1
  131. package/dist/cli/commands/migrate.js +0 -300
  132. package/dist/cli/commands/migrate.js.map +0 -1
  133. package/dist/cli/commands/reindex.d.ts +0 -14
  134. package/dist/cli/commands/reindex.d.ts.map +0 -1
  135. package/dist/cli/commands/reindex.js +0 -139
  136. package/dist/cli/commands/reindex.js.map +0 -1
  137. package/dist/cli/commands/report.d.ts +0 -2
  138. package/dist/cli/commands/report.d.ts.map +0 -1
  139. package/dist/cli/commands/report.js +0 -219
  140. package/dist/cli/commands/report.js.map +0 -1
  141. package/dist/cli/commands/start.d.ts +0 -2
  142. package/dist/cli/commands/start.d.ts.map +0 -1
  143. package/dist/cli/commands/start.js +0 -155
  144. package/dist/cli/commands/start.js.map +0 -1
  145. package/dist/cli/commands/status.d.ts +0 -2
  146. package/dist/cli/commands/status.d.ts.map +0 -1
  147. package/dist/cli/commands/status.js +0 -70
  148. package/dist/cli/commands/status.js.map +0 -1
  149. package/dist/cli/commands/update.d.ts +0 -2
  150. package/dist/cli/commands/update.d.ts.map +0 -1
  151. package/dist/cli/commands/update.js +0 -73
  152. package/dist/cli/commands/update.js.map +0 -1
  153. package/dist/schemas/run-json.d.ts.map +0 -1
  154. package/dist/schemas/run-json.js.map +0 -1
  155. package/dist/server/api.d.ts +0 -30
  156. package/dist/server/api.d.ts.map +0 -1
  157. package/dist/server/api.js +0 -239
  158. package/dist/server/api.js.map +0 -1
  159. package/dist/server/index.d.ts +0 -2
  160. package/dist/server/index.d.ts.map +0 -1
  161. package/dist/server/index.js +0 -228
  162. package/dist/server/index.js.map +0 -1
  163. package/dist/utils/run-data.d.ts.map +0 -1
  164. package/dist/utils/run-data.js.map +0 -1
  165. package/dist/utils/tokens.d.ts.map +0 -1
  166. package/dist/utils/tokens.js.map +0 -1
  167. package/src/assets/skills/audit.md +0 -98
  168. package/src/assets/skills/bug-fixer.md +0 -43
  169. package/src/assets/skills/code-generator.md +0 -71
  170. package/src/assets/skills/commit-code.md +0 -67
  171. package/src/assets/skills/dashboard-generator.md +0 -65
  172. package/src/assets/skills/dev-flower.md +0 -85
  173. package/src/assets/skills/deviation-recorder.md +0 -83
  174. package/src/assets/skills/docx-to-markdown.md +0 -69
  175. package/src/assets/skills/mcp-reviewer.md +0 -38
  176. package/src/assets/skills/requirement-analyzer.md +0 -103
  177. package/src/assets/skills/rules-evolver.md +0 -47
  178. package/src/assets/skills/self-reviewer.md +0 -49
  179. package/src/assets/skills/task-splitter.md +0 -60
  180. package/src/assets/skills/workflow-orchestrator.md +0 -209
  181. package/src/assets/templates/demo-run.json +0 -910
  182. package/src/assets/templates/run.json +0 -63
  183. package/src/dashboard/assets/index-B8QcPcg7.css +0 -1
  184. package/src/dashboard/assets/index-DcAl6lhS.js +0 -111
  185. package/src/dashboard/demo/overview.json +0 -71
  186. package/src/dashboard/demo/run.en.json +0 -1169
  187. package/src/dashboard/demo/run.json +0 -2667
  188. package/src/dashboard/demo/run.zh.json +0 -1169
  189. package/src/dashboard/demo/runs.json +0 -19
  190. package/src/dashboard/index.html +0 -13
  191. /package/dist/{utils → internal/runtime}/tokens.d.ts +0 -0
  192. /package/dist/{utils → internal/runtime}/tokens.js +0 -0
@@ -1,7 +1,8 @@
1
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, legacyModuleMemoryPath, legacyModuleMemoryViewPath, 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) {
@@ -235,7 +289,7 @@ function pickKeyFiles(runs) {
235
289
  const score = new Map();
236
290
  for (const run of runs) {
237
291
  for (const file of run.files || []) {
238
- if (isAidaRuntimePath(file.path))
292
+ if (isAidaRuntimePath(file.path) || isNoisePath(file.path) || isGeneratedToolingPath(file.path))
239
293
  continue;
240
294
  const path = normalizeRepoPath(file.path);
241
295
  const weight = (file.linesAdded || 0) + (file.linesRemoved || 0) + ((file.changeCount || 1) * 5);
@@ -257,14 +311,54 @@ function deriveCurrentPhase(runs) {
257
311
  return 'Completed';
258
312
  return 'Not Started';
259
313
  }
260
- 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
+ };
261
339
  if (requirement?.modules?.length) {
262
- 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
+ }
263
360
  }
264
- return uniqueStrings(runs
265
- .flatMap((run) => run.tasks || [])
266
- .map((task) => task.stageName)
267
- .filter((stage) => stage && stage !== 'default'));
361
+ return [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key));
268
362
  }
269
363
  function renderListSection(title, values) {
270
364
  if (values.length === 0)
@@ -314,24 +408,158 @@ export function normalizeModuleKey(value) {
314
408
  return value
315
409
  .trim()
316
410
  .toLowerCase()
411
+ .replace(/\s*\/\s*/g, '/')
412
+ .replace(/-\//g, '/')
413
+ .replace(/\/-/g, '/')
317
414
  .replace(/[^a-z0-9/_\u4e00-\u9fa5-]+/g, '-')
318
415
  .replace(/\/+/g, '/')
319
416
  .replace(/-+/g, '-')
320
417
  .replace(/^-|-$/g, '');
321
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
+ }
322
535
  export function loadMemoryIndex(projectRoot) {
323
536
  const path = memoryIndexPath(projectRoot);
324
537
  if (!fileExists(path)) {
325
538
  return {
539
+ schemaVersion: MEMORY_SCHEMA_VERSION,
326
540
  updatedAt: new Date().toISOString(),
327
- modules: [],
541
+ items: [],
328
542
  };
329
543
  }
330
- 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
+ };
331
555
  }
332
556
  export function saveMemoryIndex(projectRoot, index) {
333
557
  ensureDir(memoriesDir(projectRoot));
334
- writeJson(memoryIndexPath(projectRoot), index);
558
+ writeJson(memoryIndexPath(projectRoot), {
559
+ schemaVersion: index.schemaVersion || MEMORY_SCHEMA_VERSION,
560
+ updatedAt: index.updatedAt,
561
+ items: index.items,
562
+ });
335
563
  }
336
564
  export function loadModuleMemory(projectRoot, moduleKey) {
337
565
  const normalized = normalizeModuleKey(moduleKey);
@@ -352,12 +580,13 @@ export function loadModuleMemory(projectRoot, moduleKey) {
352
580
  function upsertMemoryIndexEntry(projectRoot, record) {
353
581
  const index = loadMemoryIndex(projectRoot);
354
582
  const entry = memoryIndexEntryFromRecord(record);
355
- const next = index.modules.filter((item) => item.key !== record.moduleKey);
583
+ const next = index.items.filter((item) => item.key !== record.moduleKey);
356
584
  next.push(entry);
357
585
  next.sort((a, b) => a.key.localeCompare(b.key));
358
586
  saveMemoryIndex(projectRoot, {
587
+ schemaVersion: MEMORY_SCHEMA_VERSION,
359
588
  updatedAt: new Date().toISOString(),
360
- modules: next,
589
+ items: next,
361
590
  });
362
591
  }
363
592
  function memoryIndexEntryFromRecord(record) {
@@ -367,9 +596,72 @@ function memoryIndexEntryFromRecord(record) {
367
596
  summary: record.summary,
368
597
  keywords: topItems([record.moduleKey, record.title, ...record.keywords], 12),
369
598
  paths: filterMeaningfulPaths([...record.entryFiles, ...record.relatedPaths], 12),
599
+ tickets: topItems([...(record.tickets || [])]
600
+ .map((ticket) => ticket.ticket || ticket.branch || '')
601
+ .filter(Boolean), 8),
370
602
  updatedAt: record.updatedAt,
371
603
  };
372
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
+ }
373
665
  function mergeMemoryIndexEntry(existing, next) {
374
666
  if (!existing)
375
667
  return next;
@@ -379,6 +671,7 @@ function mergeMemoryIndexEntry(existing, next) {
379
671
  summary: next.summary || existing.summary,
380
672
  keywords: topItems([...(existing.keywords || []), ...(next.keywords || [])], 12),
381
673
  paths: filterMeaningfulPaths([...(existing.paths || []), ...(next.paths || [])], 12),
674
+ tickets: topItems([...(existing.tickets || []), ...(next.tickets || [])], 8),
382
675
  updatedAt: next.updatedAt || existing.updatedAt,
383
676
  };
384
677
  }
@@ -392,6 +685,7 @@ function moduleMemoryRecordFromMarkdown(raw) {
392
685
  const sections = extractMarkdownSections(raw);
393
686
  const summary = sections.get('summary') || '';
394
687
  return {
688
+ schemaVersion: MEMORY_SCHEMA_VERSION,
395
689
  moduleKey,
396
690
  title: raw.match(/^- Title:\s+(.+)$/m)?.[1]?.trim() || moduleKey,
397
691
  summary,
@@ -404,6 +698,7 @@ function moduleMemoryRecordFromMarkdown(raw) {
404
698
  pitfalls: parseListSection(raw, 'pitfalls'),
405
699
  relatedRules: parseListSection(raw, 'related rules'),
406
700
  tickets: [],
701
+ changes: [],
407
702
  updatedAt: raw.match(/^- Updated At:\s+(.+)$/m)?.[1]?.trim() || new Date().toISOString(),
408
703
  };
409
704
  }
@@ -411,26 +706,46 @@ export function rebuildMemoryIndexFromDisk(projectRoot) {
411
706
  ensureDir(moduleMemoriesDir(projectRoot));
412
707
  migrateLegacyNestedModuleMemoryLayout(projectRoot);
413
708
  const byKey = new Map();
414
- for (const entry of loadMemoryIndex(projectRoot).modules || []) {
415
- byKey.set(entry.key, entry);
416
- }
709
+ const expectedMarkdownViews = new Set();
417
710
  for (const file of walkJsonFiles(moduleMemoriesDir(projectRoot))) {
418
711
  if (basename(file) === 'index.json')
419
712
  continue;
420
- 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
+ }
421
719
  const entry = memoryIndexEntryFromRecord(record);
422
720
  byKey.set(record.moduleKey, mergeMemoryIndexEntry(byKey.get(record.moduleKey), entry));
721
+ expectedMarkdownViews.add(moduleMemoryViewPath(projectRoot, record.moduleKey));
423
722
  }
424
723
  for (const file of walkMarkdownFiles(moduleMemoriesDir(projectRoot))) {
425
724
  const record = moduleMemoryRecordFromMarkdown(readText(file));
426
725
  if (!record)
427
726
  continue;
428
- const entry = memoryIndexEntryFromRecord(record);
429
- 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
+ }
430
744
  }
431
745
  const index = {
746
+ schemaVersion: MEMORY_SCHEMA_VERSION,
432
747
  updatedAt: new Date().toISOString(),
433
- modules: [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key)),
748
+ items: [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key)),
434
749
  };
435
750
  saveMemoryIndex(projectRoot, index);
436
751
  return index;
@@ -438,7 +753,10 @@ export function rebuildMemoryIndexFromDisk(projectRoot) {
438
753
  export function saveModuleMemory(projectRoot, record) {
439
754
  const path = moduleMemoryPath(projectRoot, record.moduleKey);
440
755
  ensureDir(dirname(path));
441
- writeJson(path, record);
756
+ writeJson(path, {
757
+ schemaVersion: record.schemaVersion || MEMORY_SCHEMA_VERSION,
758
+ ...record,
759
+ });
442
760
  upsertMemoryIndexEntry(projectRoot, record);
443
761
  }
444
762
  export function loadRunContext(projectRoot, branchName) {
@@ -479,6 +797,12 @@ export function renderModuleMemoryMarkdown(record) {
479
797
  '',
480
798
  renderListSection('Related Rules', record.relatedRules).trimEnd(),
481
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
+ '',
482
806
  '## Related Tickets',
483
807
  '',
484
808
  ...(record.tickets.length > 0
@@ -544,46 +868,29 @@ export function buildMemoryViews(projectRoot) {
544
868
  for (const file of walkJsonFiles(moduleMemoriesDir(projectRoot))) {
545
869
  if (basename(file) === 'index.json')
546
870
  continue;
547
- 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
+ }
548
877
  const viewPath = moduleMemoryViewPath(projectRoot, record.moduleKey);
549
878
  ensureDir(dirname(viewPath));
550
879
  writeText(viewPath, renderModuleMemoryMarkdown(record));
551
880
  moduleViews++;
552
881
  }
553
- let contextViews = 0;
554
- let packViews = 0;
555
- const root = runsDir(projectRoot);
556
- if (!fileExists(root)) {
557
- rebuildMemoryIndexFromDisk(projectRoot);
558
- return { moduleViews, contextViews, packViews };
559
- }
560
- for (const branchName of readdirSync(root)) {
561
- const safeBranchDir = resolve(root, branchName);
562
- if (!fileExists(safeBranchDir) || !statSync(safeBranchDir).isDirectory())
563
- continue;
564
- const contextPath = resolve(safeBranchDir, 'context.json');
565
- if (!fileExists(contextPath))
566
- continue;
567
- const record = readJson(contextPath);
568
- writeText(runContextViewPath(projectRoot, record.branch), renderRunContextMarkdown(record));
569
- contextViews++;
570
- const modules = record.modules
571
- .map((moduleName) => loadModuleMemory(projectRoot, moduleName))
572
- .filter((item) => item !== null);
573
- writeText(runMemoryPackViewPath(projectRoot, record.branch), renderRunMemoryPackMarkdown(record, modules));
574
- packViews++;
575
- }
576
882
  rebuildMemoryIndexFromDisk(projectRoot);
577
- return { moduleViews, contextViews, packViews };
883
+ return { moduleViews, contextViews: 0, packViews: 0 };
578
884
  }
579
885
  export function buildRunContextFromBranch(projectRoot, branchName) {
886
+ const existing = loadRunContext(projectRoot, branchName);
580
887
  const requirement = loadRequirement(projectRoot, branchName);
581
888
  const analysis = loadAnalysis(projectRoot, branchName);
582
889
  const runs = loadBranchRuns(projectRoot, branchName);
583
890
  if (!requirement && !analysis && runs.length === 0) {
584
891
  return null;
585
892
  }
586
- const modules = inferModules(requirement, runs);
893
+ const moduleDescriptors = inferModuleDescriptors(requirement, runs);
587
894
  const completed = topItems(runs.flatMap((run) => run.tasks || []).filter((task) => task.status === 'done').map((task) => task.title));
588
895
  const inProgress = topItems(runs.flatMap((run) => run.tasks || []).filter((task) => task.status === 'in-progress').map((task) => task.title));
589
896
  const next = topItems(runs.flatMap((run) => run.tasks || []).filter((task) => task.status === 'pending').map((task) => task.title));
@@ -595,25 +902,44 @@ export function buildRunContextFromBranch(projectRoot, branchName) {
595
902
  ...runs.flatMap((run) => run.deviations || []).map((deviation) => deviation.title),
596
903
  ];
597
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);
598
924
  return {
599
925
  branch: branchName,
600
926
  ticket: extractTicket(requirement?.title || '') || extractTicket(branchName),
601
- title,
602
- summary: requirement?.summary || extractSummaryFromAnalysis(analysis) || title,
603
- currentPhase: deriveCurrentPhase(runs),
604
- modules,
605
- completed,
606
- inProgress,
607
- next,
608
- decisions,
609
- constraints,
610
- keyFiles: pickKeyFiles(runs),
611
- 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 || []),
612
938
  updatedAt: new Date().toISOString(),
613
939
  };
614
940
  }
615
- function collectRelatedPaths(moduleName, branchContext, runs) {
616
- const query = moduleName.toLowerCase();
941
+ function collectRelatedPaths(moduleDescriptor, branchContext, runs) {
942
+ const query = moduleDescriptor.key.toLowerCase();
617
943
  const tokens = splitQueryTokens(query);
618
944
  const scored = new Map();
619
945
  for (const path of branchContext.keyFiles) {
@@ -623,10 +949,11 @@ function collectRelatedPaths(moduleName, branchContext, runs) {
623
949
  }
624
950
  for (const run of runs) {
625
951
  for (const task of run.tasks || []) {
626
- if (task.stageName !== moduleName)
952
+ const taskDescriptor = deriveModuleDescriptor(task.stageName || '', (run.files || []).map((file) => file.path));
953
+ if (taskDescriptor.key !== moduleDescriptor.key)
627
954
  continue;
628
955
  for (const file of run.files || []) {
629
- if (isAidaRuntimePath(file.path))
956
+ if (isAidaRuntimePath(file.path) || isNoisePath(file.path) || isGeneratedToolingPath(file.path))
630
957
  continue;
631
958
  const path = normalizeRepoPath(file.path);
632
959
  const bonus = (file.linesAdded || 0) + (file.linesRemoved || 0) + 10;
@@ -640,24 +967,31 @@ function collectRelatedPaths(moduleName, branchContext, runs) {
640
967
  export function upsertModuleMemory(projectRoot, input) {
641
968
  const moduleKey = normalizeModuleKey(input.moduleKey);
642
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
+ }));
643
980
  const record = {
981
+ schemaVersion: MEMORY_SCHEMA_VERSION,
644
982
  moduleKey,
645
983
  title: input.title || existing?.title || moduleKey,
646
984
  summary: input.summary || existing?.summary || '',
647
985
  keywords: topItems([...(existing?.keywords || []), ...(input.keywords || []), moduleKey, input.title || ''], 16),
648
- entryFiles: filterMeaningfulPaths([...(existing?.entryFiles || []), ...(input.entryFiles || [])], 8),
649
- relatedPaths: filterMeaningfulPaths([...(existing?.relatedPaths || []), ...(input.relatedPaths || [])], 12),
986
+ entryFiles: filterMeaningfulPaths([...existingEntryFiles, ...(input.entryFiles || [])], 8),
987
+ relatedPaths: filterMeaningfulPaths([...existingRelatedPaths, ...(input.relatedPaths || [])], 12),
650
988
  dataFlow: topItems([...(existing?.dataFlow || []), ...(input.dataFlow || [])]),
651
989
  decisions: topItems([...(existing?.decisions || []), ...(input.decisions || [])]),
652
990
  constraints: topItems([...(existing?.constraints || []), ...(input.constraints || [])]),
653
991
  pitfalls: topItems([...(existing?.pitfalls || []), ...(input.pitfalls || [])]),
654
992
  relatedRules: topItems([...(existing?.relatedRules || []), ...(input.relatedRules || [])], 12),
655
- tickets: [
656
- ...(existing?.tickets || []),
657
- ...((input.tickets || []).filter((ticket) => ticket.summary.trim().length > 0)),
658
- ].filter((ticket, index, array) => array.findIndex((item) => item.ticket === ticket.ticket
659
- && item.branch === ticket.branch
660
- && 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),
661
995
  updatedAt: new Date().toISOString(),
662
996
  };
663
997
  saveModuleMemory(projectRoot, record);
@@ -691,10 +1025,10 @@ export function searchModuleMemories(projectRoot, query, pathHints = []) {
691
1025
  const tokens = splitQueryTokens(normalizedQuery);
692
1026
  const hints = pathHints.map((hint) => hint.toLowerCase());
693
1027
  let index = loadMemoryIndex(projectRoot);
694
- if (index.modules.length === 0 && fileExists(moduleMemoriesDir(projectRoot))) {
1028
+ if (index.items.length === 0 && fileExists(moduleMemoriesDir(projectRoot))) {
695
1029
  index = rebuildMemoryIndexFromDisk(projectRoot);
696
1030
  }
697
- return index.modules
1031
+ return index.items
698
1032
  .map((entry) => {
699
1033
  let score = 0;
700
1034
  score += scoreText(normalizedQuery, tokens, entry.key) * 2;
@@ -734,7 +1068,19 @@ export function migrateLegacyMemories(projectRoot) {
734
1068
  const requirementData = requirement
735
1069
  ? readJson(resolve(branchPath, 'requirement.json'))
736
1070
  : null;
737
- 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;
738
1084
  const context = buildRunContextFromBranch(projectRoot, branchName);
739
1085
  if (!context)
740
1086
  continue;
@@ -743,24 +1089,22 @@ export function migrateLegacyMemories(projectRoot) {
743
1089
  contextsWritten++;
744
1090
  const runs = loadBranchRuns(projectRoot, branchName);
745
1091
  const req = loadRequirement(projectRoot, branchName);
746
- const moduleCandidates = req?.modules?.length
747
- ? req.modules.map((module) => ({ name: module.name, description: module.description }))
748
- : context.modules.map((name) => ({ name, description: '' }));
749
- for (const candidate of moduleCandidates) {
750
- if (!candidate.name.trim())
1092
+ const moduleDescriptors = inferModuleDescriptors(req, runs);
1093
+ for (const descriptor of moduleDescriptors) {
1094
+ if (!descriptor.key.trim())
751
1095
  continue;
752
- const moduleKey = normalizeModuleKey(candidate.name);
753
- const relatedPaths = collectRelatedPaths(candidate.name, context, runs);
1096
+ const relatedPaths = collectRelatedPaths(descriptor, context, runs);
754
1097
  upsertModuleMemory(projectRoot, {
755
- moduleKey,
756
- title: candidate.name,
757
- summary: candidate.description || context.summary,
758
- 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],
759
1102
  entryFiles: relatedPaths.slice(0, 5),
760
1103
  relatedPaths,
761
1104
  decisions: context.decisions,
762
1105
  constraints: context.constraints,
763
1106
  pitfalls: context.risks,
1107
+ changeTitle: context.title,
764
1108
  tickets: [{
765
1109
  ticket: context.ticket,
766
1110
  branch: branchName,
@@ -769,7 +1113,7 @@ export function migrateLegacyMemories(projectRoot) {
769
1113
  }],
770
1114
  });
771
1115
  moduleMemoriesWritten++;
772
- modulesTouched.add(moduleKey);
1116
+ modulesTouched.add(descriptor.key);
773
1117
  }
774
1118
  }
775
1119
  buildMemoryViews(projectRoot);
@@ -790,18 +1134,19 @@ export function rebuildCurrentBranchMemory(projectRoot, branchName) {
790
1134
  const req = loadRequirement(projectRoot, branchName);
791
1135
  const runs = loadBranchRuns(projectRoot, branchName);
792
1136
  const modules = [];
793
- for (const moduleName of inferModules(req, runs)) {
794
- const relatedPaths = collectRelatedPaths(moduleName, context, runs);
1137
+ for (const descriptor of inferModuleDescriptors(req, runs)) {
1138
+ const relatedPaths = collectRelatedPaths(descriptor, context, runs);
795
1139
  modules.push(upsertModuleMemory(projectRoot, {
796
- moduleKey: moduleName,
797
- title: moduleName,
798
- summary: req?.modules?.find((module) => module.name === moduleName)?.description || context.summary,
799
- 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],
800
1144
  entryFiles: relatedPaths.slice(0, 5),
801
1145
  relatedPaths,
802
1146
  decisions: context.decisions,
803
1147
  constraints: context.constraints,
804
1148
  pitfalls: context.risks,
1149
+ changeTitle: context.title,
805
1150
  tickets: [{
806
1151
  ticket: context.ticket,
807
1152
  branch: branchName,