aspectcode 0.4.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/README.md +2 -2
  2. package/dist/agentsMdRenderer.d.ts +16 -0
  3. package/dist/agentsMdRenderer.d.ts.map +1 -0
  4. package/dist/agentsMdRenderer.js +137 -0
  5. package/dist/agentsMdRenderer.js.map +1 -0
  6. package/dist/auth.d.ts +31 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +386 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/autoResolve.d.ts +41 -0
  11. package/dist/autoResolve.d.ts.map +1 -0
  12. package/dist/autoResolve.js +196 -0
  13. package/dist/autoResolve.js.map +1 -0
  14. package/dist/changeEvaluator.d.ts +56 -0
  15. package/dist/changeEvaluator.d.ts.map +1 -0
  16. package/dist/changeEvaluator.js +674 -0
  17. package/dist/changeEvaluator.js.map +1 -0
  18. package/dist/cli.d.ts +3 -1
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +1 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/config.d.ts +37 -17
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +50 -2
  25. package/dist/config.js.map +1 -1
  26. package/dist/dreamCycle.d.ts +57 -0
  27. package/dist/dreamCycle.d.ts.map +1 -0
  28. package/dist/dreamCycle.js +334 -0
  29. package/dist/dreamCycle.js.map +1 -0
  30. package/dist/kbBuilder.d.ts +1 -2
  31. package/dist/kbBuilder.d.ts.map +1 -1
  32. package/dist/kbBuilder.js +1 -2
  33. package/dist/kbBuilder.js.map +1 -1
  34. package/dist/main.d.ts +2 -1
  35. package/dist/main.d.ts.map +1 -1
  36. package/dist/main.js +148 -7
  37. package/dist/main.js.map +1 -1
  38. package/dist/optimize.d.ts +13 -6
  39. package/dist/optimize.d.ts.map +1 -1
  40. package/dist/optimize.js +433 -142
  41. package/dist/optimize.js.map +1 -1
  42. package/dist/pipeline.d.ts +19 -21
  43. package/dist/pipeline.d.ts.map +1 -1
  44. package/dist/pipeline.js +1093 -160
  45. package/dist/pipeline.js.map +1 -1
  46. package/dist/preferences.d.ts +80 -0
  47. package/dist/preferences.d.ts.map +1 -0
  48. package/dist/preferences.js +238 -0
  49. package/dist/preferences.js.map +1 -0
  50. package/dist/runtimeState.d.ts +30 -0
  51. package/dist/runtimeState.d.ts.map +1 -0
  52. package/dist/runtimeState.js +39 -0
  53. package/dist/runtimeState.js.map +1 -0
  54. package/dist/scopedRules.d.ts +84 -0
  55. package/dist/scopedRules.d.ts.map +1 -0
  56. package/dist/scopedRules.js +449 -0
  57. package/dist/scopedRules.js.map +1 -0
  58. package/dist/ui/Dashboard.d.ts +4 -16
  59. package/dist/ui/Dashboard.d.ts.map +1 -1
  60. package/dist/ui/Dashboard.js +339 -140
  61. package/dist/ui/Dashboard.js.map +1 -1
  62. package/dist/ui/MemoryMap.d.ts +16 -0
  63. package/dist/ui/MemoryMap.d.ts.map +1 -0
  64. package/dist/ui/MemoryMap.js +266 -0
  65. package/dist/ui/MemoryMap.js.map +1 -0
  66. package/dist/ui/SettingsPanel.d.ts +18 -0
  67. package/dist/ui/SettingsPanel.d.ts.map +1 -0
  68. package/dist/ui/SettingsPanel.js +241 -0
  69. package/dist/ui/SettingsPanel.js.map +1 -0
  70. package/dist/ui/prompts.d.ts +7 -0
  71. package/dist/ui/prompts.d.ts.map +1 -1
  72. package/dist/ui/prompts.js +63 -0
  73. package/dist/ui/prompts.js.map +1 -1
  74. package/dist/ui/store.d.ts +154 -18
  75. package/dist/ui/store.d.ts.map +1 -1
  76. package/dist/ui/store.js +154 -24
  77. package/dist/ui/store.js.map +1 -1
  78. package/dist/ui/theme.d.ts +1 -8
  79. package/dist/ui/theme.d.ts.map +1 -1
  80. package/dist/ui/theme.js +2 -21
  81. package/dist/ui/theme.js.map +1 -1
  82. package/dist/updateChecker.d.ts +13 -0
  83. package/dist/updateChecker.d.ts.map +1 -0
  84. package/dist/updateChecker.js +66 -0
  85. package/dist/updateChecker.js.map +1 -0
  86. package/dist/usageTracker.d.ts +12 -0
  87. package/dist/usageTracker.d.ts.map +1 -0
  88. package/dist/usageTracker.js +89 -0
  89. package/dist/usageTracker.js.map +1 -0
  90. package/dist/writer.d.ts +1 -7
  91. package/dist/writer.d.ts.map +1 -1
  92. package/dist/writer.js +1 -11
  93. package/dist/writer.js.map +1 -1
  94. package/node_modules/@aspectcode/core/dist/analysis/repo.d.ts.map +1 -1
  95. package/node_modules/@aspectcode/core/dist/analysis/repo.js +13 -2
  96. package/node_modules/@aspectcode/core/dist/analysis/repo.js.map +1 -1
  97. package/node_modules/@aspectcode/core/dist/index.d.ts +1 -3
  98. package/node_modules/@aspectcode/core/dist/index.d.ts.map +1 -1
  99. package/node_modules/@aspectcode/core/dist/index.js +1 -3
  100. package/node_modules/@aspectcode/core/dist/index.js.map +1 -1
  101. package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.d.ts +14 -0
  102. package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.d.ts.map +1 -0
  103. package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.js +191 -0
  104. package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.js.map +1 -0
  105. package/node_modules/@aspectcode/core/dist/parsers/index.d.ts +1 -0
  106. package/node_modules/@aspectcode/core/dist/parsers/index.d.ts.map +1 -1
  107. package/node_modules/@aspectcode/core/dist/parsers/index.js +6 -1
  108. package/node_modules/@aspectcode/core/dist/parsers/index.js.map +1 -1
  109. package/node_modules/@aspectcode/core/dist/parsers/languages.d.ts +20 -0
  110. package/node_modules/@aspectcode/core/dist/parsers/languages.d.ts.map +1 -1
  111. package/node_modules/@aspectcode/core/dist/parsers/languages.js +25 -0
  112. package/node_modules/@aspectcode/core/dist/parsers/languages.js.map +1 -1
  113. package/node_modules/@aspectcode/core/dist/parsers/tsJsExtractors.d.ts.map +1 -1
  114. package/node_modules/@aspectcode/core/dist/parsers/tsJsExtractors.js +4 -1
  115. package/node_modules/@aspectcode/core/dist/parsers/tsJsExtractors.js.map +1 -1
  116. package/node_modules/@aspectcode/core/package.json +2 -2
  117. package/node_modules/@aspectcode/core/parsers/cpp.wasm +0 -0
  118. package/node_modules/@aspectcode/core/parsers/go.wasm +0 -0
  119. package/node_modules/@aspectcode/core/parsers/php.wasm +0 -0
  120. package/node_modules/@aspectcode/core/parsers/ruby.wasm +0 -0
  121. package/node_modules/@aspectcode/core/parsers/rust.wasm +0 -0
  122. package/node_modules/@aspectcode/emitters/dist/index.d.ts +1 -17
  123. package/node_modules/@aspectcode/emitters/dist/index.d.ts.map +1 -1
  124. package/node_modules/@aspectcode/emitters/dist/index.js +2 -90
  125. package/node_modules/@aspectcode/emitters/dist/index.js.map +1 -1
  126. package/node_modules/@aspectcode/emitters/dist/instructions/index.d.ts +0 -2
  127. package/node_modules/@aspectcode/emitters/dist/instructions/index.d.ts.map +1 -1
  128. package/node_modules/@aspectcode/emitters/dist/instructions/index.js +1 -7
  129. package/node_modules/@aspectcode/emitters/dist/instructions/index.js.map +1 -1
  130. package/node_modules/@aspectcode/emitters/dist/kb/analyzers.d.ts +0 -18
  131. package/node_modules/@aspectcode/emitters/dist/kb/analyzers.d.ts.map +1 -1
  132. package/node_modules/@aspectcode/emitters/dist/kb/analyzers.js +0 -57
  133. package/node_modules/@aspectcode/emitters/dist/kb/analyzers.js.map +1 -1
  134. package/node_modules/@aspectcode/emitters/dist/kb/conventions.d.ts +0 -18
  135. package/node_modules/@aspectcode/emitters/dist/kb/conventions.d.ts.map +1 -1
  136. package/node_modules/@aspectcode/emitters/dist/kb/conventions.js +0 -130
  137. package/node_modules/@aspectcode/emitters/dist/kb/conventions.js.map +1 -1
  138. package/node_modules/@aspectcode/emitters/dist/kb/index.d.ts +2 -4
  139. package/node_modules/@aspectcode/emitters/dist/kb/index.d.ts.map +1 -1
  140. package/node_modules/@aspectcode/emitters/dist/kb/index.js +1 -11
  141. package/node_modules/@aspectcode/emitters/dist/kb/index.js.map +1 -1
  142. package/node_modules/@aspectcode/emitters/package.json +3 -3
  143. package/node_modules/@aspectcode/evaluator/dist/apply.d.ts +55 -0
  144. package/node_modules/@aspectcode/evaluator/dist/apply.d.ts.map +1 -0
  145. package/node_modules/@aspectcode/evaluator/dist/apply.js +368 -0
  146. package/node_modules/@aspectcode/evaluator/dist/apply.js.map +1 -0
  147. package/node_modules/@aspectcode/evaluator/dist/diagnosis.d.ts +16 -25
  148. package/node_modules/@aspectcode/evaluator/dist/diagnosis.d.ts.map +1 -1
  149. package/node_modules/@aspectcode/evaluator/dist/diagnosis.js +115 -138
  150. package/node_modules/@aspectcode/evaluator/dist/diagnosis.js.map +1 -1
  151. package/node_modules/@aspectcode/evaluator/dist/index.d.ts +8 -43
  152. package/node_modules/@aspectcode/evaluator/dist/index.d.ts.map +1 -1
  153. package/node_modules/@aspectcode/evaluator/dist/index.js +15 -61
  154. package/node_modules/@aspectcode/evaluator/dist/index.js.map +1 -1
  155. package/node_modules/@aspectcode/evaluator/dist/judge.d.ts +32 -0
  156. package/node_modules/@aspectcode/evaluator/dist/judge.d.ts.map +1 -0
  157. package/node_modules/@aspectcode/evaluator/dist/judge.js +165 -0
  158. package/node_modules/@aspectcode/evaluator/dist/judge.js.map +1 -0
  159. package/node_modules/@aspectcode/evaluator/dist/llmUtil.d.ts +15 -0
  160. package/node_modules/@aspectcode/evaluator/dist/llmUtil.d.ts.map +1 -0
  161. package/node_modules/@aspectcode/evaluator/dist/llmUtil.js +41 -0
  162. package/node_modules/@aspectcode/evaluator/dist/llmUtil.js.map +1 -0
  163. package/node_modules/@aspectcode/evaluator/dist/probes.d.ts +20 -47
  164. package/node_modules/@aspectcode/evaluator/dist/probes.d.ts.map +1 -1
  165. package/node_modules/@aspectcode/evaluator/dist/probes.js +188 -278
  166. package/node_modules/@aspectcode/evaluator/dist/probes.js.map +1 -1
  167. package/node_modules/@aspectcode/evaluator/dist/runner.d.ts +7 -32
  168. package/node_modules/@aspectcode/evaluator/dist/runner.d.ts.map +1 -1
  169. package/node_modules/@aspectcode/evaluator/dist/runner.js +21 -146
  170. package/node_modules/@aspectcode/evaluator/dist/runner.js.map +1 -1
  171. package/node_modules/@aspectcode/evaluator/dist/types.d.ts +141 -99
  172. package/node_modules/@aspectcode/evaluator/dist/types.d.ts.map +1 -1
  173. package/node_modules/@aspectcode/evaluator/dist/types.js +10 -2
  174. package/node_modules/@aspectcode/evaluator/dist/types.js.map +1 -1
  175. package/node_modules/@aspectcode/evaluator/package.json +4 -4
  176. package/node_modules/@aspectcode/optimizer/dist/index.d.ts +3 -10
  177. package/node_modules/@aspectcode/optimizer/dist/index.d.ts.map +1 -1
  178. package/node_modules/@aspectcode/optimizer/dist/index.js +1 -19
  179. package/node_modules/@aspectcode/optimizer/dist/index.js.map +1 -1
  180. package/node_modules/@aspectcode/optimizer/dist/providers/anthropic.d.ts.map +1 -1
  181. package/node_modules/@aspectcode/optimizer/dist/providers/anthropic.js +40 -0
  182. package/node_modules/@aspectcode/optimizer/dist/providers/anthropic.js.map +1 -1
  183. package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.d.ts +9 -0
  184. package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.d.ts.map +1 -0
  185. package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.js +83 -0
  186. package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.js.map +1 -0
  187. package/node_modules/@aspectcode/optimizer/dist/providers/index.d.ts +4 -3
  188. package/node_modules/@aspectcode/optimizer/dist/providers/index.d.ts.map +1 -1
  189. package/node_modules/@aspectcode/optimizer/dist/providers/index.js +24 -10
  190. package/node_modules/@aspectcode/optimizer/dist/providers/index.js.map +1 -1
  191. package/node_modules/@aspectcode/optimizer/dist/providers/openai.d.ts.map +1 -1
  192. package/node_modules/@aspectcode/optimizer/dist/providers/openai.js +22 -0
  193. package/node_modules/@aspectcode/optimizer/dist/providers/openai.js.map +1 -1
  194. package/node_modules/@aspectcode/optimizer/dist/providers/retry.d.ts +14 -0
  195. package/node_modules/@aspectcode/optimizer/dist/providers/retry.d.ts.map +1 -1
  196. package/node_modules/@aspectcode/optimizer/dist/providers/retry.js +1 -0
  197. package/node_modules/@aspectcode/optimizer/dist/providers/retry.js.map +1 -1
  198. package/node_modules/@aspectcode/optimizer/dist/types.d.ts +14 -0
  199. package/node_modules/@aspectcode/optimizer/dist/types.d.ts.map +1 -1
  200. package/node_modules/@aspectcode/optimizer/dist/types.js.map +1 -1
  201. package/node_modules/@aspectcode/optimizer/package.json +2 -2
  202. package/node_modules/web-tree-sitter/LICENSE +21 -0
  203. package/node_modules/web-tree-sitter/README.md +198 -0
  204. package/node_modules/web-tree-sitter/package.json +36 -0
  205. package/node_modules/web-tree-sitter/tree-sitter-web.d.ts +204 -0
  206. package/node_modules/web-tree-sitter/tree-sitter.js +1 -0
  207. package/node_modules/web-tree-sitter/tree-sitter.wasm +0 -0
  208. package/package.json +8 -8
  209. package/dist/complaintProcessor.d.ts +0 -16
  210. package/dist/complaintProcessor.d.ts.map +0 -1
  211. package/dist/complaintProcessor.js +0 -134
  212. package/dist/complaintProcessor.js.map +0 -1
  213. package/node_modules/@aspectcode/emitters/dist/emitter.d.ts +0 -72
  214. package/node_modules/@aspectcode/emitters/dist/emitter.d.ts.map +0 -1
  215. package/node_modules/@aspectcode/emitters/dist/emitter.js +0 -10
  216. package/node_modules/@aspectcode/emitters/dist/emitter.js.map +0 -1
  217. package/node_modules/@aspectcode/emitters/dist/instructions/content.d.ts +0 -26
  218. package/node_modules/@aspectcode/emitters/dist/instructions/content.d.ts.map +0 -1
  219. package/node_modules/@aspectcode/emitters/dist/instructions/content.js +0 -501
  220. package/node_modules/@aspectcode/emitters/dist/instructions/content.js.map +0 -1
  221. package/node_modules/@aspectcode/emitters/dist/instructions/detection.d.ts +0 -13
  222. package/node_modules/@aspectcode/emitters/dist/instructions/detection.d.ts.map +0 -1
  223. package/node_modules/@aspectcode/emitters/dist/instructions/detection.js +0 -55
  224. package/node_modules/@aspectcode/emitters/dist/instructions/detection.js.map +0 -1
  225. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.d.ts +0 -9
  226. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.d.ts.map +0 -1
  227. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.js +0 -30
  228. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.js.map +0 -1
  229. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.d.ts +0 -21
  230. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.d.ts.map +0 -1
  231. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.js +0 -125
  232. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.js.map +0 -1
  233. package/node_modules/@aspectcode/emitters/dist/manifest.d.ts +0 -37
  234. package/node_modules/@aspectcode/emitters/dist/manifest.d.ts.map +0 -1
  235. package/node_modules/@aspectcode/emitters/dist/manifest.js +0 -50
  236. package/node_modules/@aspectcode/emitters/dist/manifest.js.map +0 -1
  237. package/node_modules/@aspectcode/emitters/dist/report.d.ts +0 -22
  238. package/node_modules/@aspectcode/emitters/dist/report.d.ts.map +0 -1
  239. package/node_modules/@aspectcode/emitters/dist/report.js +0 -3
  240. package/node_modules/@aspectcode/emitters/dist/report.js.map +0 -1
  241. package/node_modules/@aspectcode/emitters/dist/stableJson.d.ts +0 -14
  242. package/node_modules/@aspectcode/emitters/dist/stableJson.d.ts.map +0 -1
  243. package/node_modules/@aspectcode/emitters/dist/stableJson.js +0 -40
  244. package/node_modules/@aspectcode/emitters/dist/stableJson.js.map +0 -1
  245. package/node_modules/@aspectcode/emitters/dist/transaction.d.ts +0 -29
  246. package/node_modules/@aspectcode/emitters/dist/transaction.d.ts.map +0 -1
  247. package/node_modules/@aspectcode/emitters/dist/transaction.js +0 -104
  248. package/node_modules/@aspectcode/emitters/dist/transaction.js.map +0 -1
@@ -0,0 +1,674 @@
1
+ "use strict";
2
+ /**
3
+ * Change evaluator — real-time assessment of file changes against the
4
+ * current AnalysisModel and learned preferences.
5
+ *
6
+ * All checks are pure functions. No LLM calls, no file reads beyond
7
+ * what's already in RuntimeState, no tree-sitter. Fast enough to run
8
+ * on every file save.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.trackChange = trackChange;
45
+ exports.getRecentChanges = getRecentChanges;
46
+ exports.clearRecentChanges = clearRecentChanges;
47
+ exports.evaluateChange = evaluateChange;
48
+ exports.extractExportNames = extractExportNames;
49
+ exports.hasPathInGraph = hasPathInGraph;
50
+ const path = __importStar(require("path"));
51
+ const preferences_1 = require("./preferences");
52
+ // ── Burst tracker ────────────────────────────────────────────
53
+ const BURST_WINDOW_MS = 60000;
54
+ const recentChanges = [];
55
+ function trackChange(event) {
56
+ recentChanges.push({
57
+ type: event.type,
58
+ path: event.path,
59
+ timestamp: Date.now(),
60
+ });
61
+ const cutoff = Date.now() - BURST_WINDOW_MS;
62
+ while (recentChanges.length > 0 && recentChanges[0].timestamp < cutoff) {
63
+ recentChanges.shift();
64
+ }
65
+ }
66
+ function getRecentChanges() {
67
+ const cutoff = Date.now() - BURST_WINDOW_MS;
68
+ while (recentChanges.length > 0 && recentChanges[0].timestamp < cutoff) {
69
+ recentChanges.shift();
70
+ }
71
+ return [...recentChanges];
72
+ }
73
+ function clearRecentChanges() {
74
+ recentChanges.length = 0;
75
+ }
76
+ // ── Main evaluator ───────────────────────────────────────────
77
+ function evaluateChange(event, ctx) {
78
+ const assessments = [];
79
+ // Deleted files don't need convention/naming checks
80
+ if (event.type !== 'unlink') {
81
+ assessments.push(...checkCoChange(event.path, ctx));
82
+ if (event.type === 'add') {
83
+ assessments.push(...checkDirectoryConvention(event.path, ctx));
84
+ assessments.push(...checkNamingConvention(event.path, ctx));
85
+ }
86
+ if (event.type === 'change') {
87
+ assessments.push(...checkImportPattern(event.path, ctx));
88
+ assessments.push(...checkExportContract(event.path, ctx));
89
+ assessments.push(...checkCircularDependency(event.path, ctx));
90
+ assessments.push(...checkTestCoverageGap(event.path, ctx));
91
+ assessments.push(...checkFileSize(event.path, ctx));
92
+ assessments.push(...checkNewHub(event.path, ctx));
93
+ assessments.push(...checkCrossBoundary(event.path, ctx));
94
+ assessments.push(...checkStaleImport(event.path, ctx));
95
+ assessments.push(...checkInheritanceChange(event.path, ctx));
96
+ }
97
+ }
98
+ // Apply preference overrides
99
+ return applyPreferences(assessments, ctx.preferences);
100
+ }
101
+ // ── Check 1: Co-change detection ─────────────────────────────
102
+ function checkCoChange(file, ctx) {
103
+ // Find all files that depend on this file (import from it), weighted by strength
104
+ const dependents = [];
105
+ for (const edge of ctx.model.graph.edges) {
106
+ if (edge.type === 'import' || edge.type === 'call') {
107
+ if (edge.target === file && edge.source !== file) {
108
+ dependents.push({ file: edge.source, strength: edge.strength });
109
+ }
110
+ if (edge.bidirectional && edge.source === file && edge.target !== file) {
111
+ dependents.push({ file: edge.target, strength: edge.strength });
112
+ }
113
+ }
114
+ }
115
+ // Need at least 2 dependents to be meaningful
116
+ if (dependents.length < 2)
117
+ return [];
118
+ const strongDependents = dependents.filter((d) => d.strength >= 0.5);
119
+ if (strongDependents.length === 0)
120
+ return [];
121
+ // Check which dependents have been changed recently
122
+ const recentPaths = new Set(ctx.recentChanges.map((c) => c.path));
123
+ const updatedStrong = strongDependents.filter((d) => recentPaths.has(d.file));
124
+ const missingStrong = strongDependents.filter((d) => !recentPaths.has(d.file));
125
+ const depCtx = `${strongDependents.length} strong dependents, ${updatedStrong.length} updated` +
126
+ (missingStrong.length > 0 ? `, ${missingStrong.length} missing: [${missingStrong.map((d) => d.file).join(', ')}]` : '');
127
+ if (missingStrong.length === 0) {
128
+ return [{
129
+ file,
130
+ type: 'ok',
131
+ rule: 'co-change',
132
+ message: `All ${dependents.length} dependents updated`,
133
+ dependencyContext: depCtx,
134
+ dismissable: false,
135
+ }];
136
+ }
137
+ const shown = missingStrong.slice(0, 3).map((d) => d.file);
138
+ const moreCount = missingStrong.length - shown.length;
139
+ const fileList = shown.join(', ') + (moreCount > 0 ? `, +${moreCount} more` : '');
140
+ return [{
141
+ file,
142
+ type: 'warning',
143
+ rule: 'co-change',
144
+ message: `${dependents.length} dependents, ${updatedStrong.length} of ${strongDependents.length} strong dependents updated`,
145
+ details: `Not yet updated: ${fileList}`,
146
+ suggestion: `You modified ${file} which has ${strongDependents.length} strong dependents. Please verify and update: ${missingStrong.map((d) => d.file).join(', ')}`,
147
+ dependencyContext: depCtx,
148
+ dismissable: true,
149
+ }];
150
+ }
151
+ // ── Check 2: Directory convention ────────────────────────────
152
+ function checkDirectoryConvention(file, ctx) {
153
+ const dir = path.dirname(file);
154
+ if (dir === '.')
155
+ return []; // root-level file, no convention to check
156
+ // Check if this directory already has files in the model
157
+ const existingInDir = ctx.model.files.filter((f) => path.dirname(f.relativePath) === dir);
158
+ if (existingInDir.length > 0)
159
+ return []; // known directory
160
+ // New directory — check if the file type matches where similar files live
161
+ const basename = path.basename(file).toLowerCase();
162
+ const assessments = [];
163
+ // Check test file placement
164
+ if (isTestFile(basename)) {
165
+ const testDirs = findDirsMatching(ctx.model, isTestFile);
166
+ if (testDirs.length > 0 && !testDirs.includes(dir)) {
167
+ assessments.push({
168
+ file,
169
+ type: 'warning',
170
+ rule: 'directory-convention',
171
+ message: `Test file in unexpected directory`,
172
+ details: `Tests usually live in: ${testDirs.slice(0, 3).join(', ')}`,
173
+ suggestion: `This test file was created in ${dir}/ but existing tests are in ${testDirs[0]}/. Consider moving it.`,
174
+ dismissable: true,
175
+ });
176
+ }
177
+ }
178
+ // Check route/controller/api file placement
179
+ if (isRouteFile(basename)) {
180
+ const routeDirs = findDirsMatching(ctx.model, isRouteFile);
181
+ if (routeDirs.length > 0 && !routeDirs.includes(dir)) {
182
+ assessments.push({
183
+ file,
184
+ type: 'warning',
185
+ rule: 'directory-convention',
186
+ message: `Route/API file in unexpected directory`,
187
+ details: `Route files usually live in: ${routeDirs.slice(0, 3).join(', ')}`,
188
+ suggestion: `This route file was created in ${dir}/ but existing routes are in ${routeDirs[0]}/. Consider moving it.`,
189
+ dismissable: true,
190
+ });
191
+ }
192
+ }
193
+ return assessments;
194
+ }
195
+ function isTestFile(name) {
196
+ return /\.(test|spec)\.[^.]+$/.test(name) || /^test_/.test(name) || /_test\.[^.]+$/.test(name);
197
+ }
198
+ function isRouteFile(name) {
199
+ return /route|controller|endpoint|handler|api/i.test(name);
200
+ }
201
+ /** Find directories that contain files matching a predicate. */
202
+ function findDirsMatching(model, predicate) {
203
+ const dirs = new Map();
204
+ for (const f of model.files) {
205
+ if (predicate(path.basename(f.relativePath).toLowerCase())) {
206
+ const d = path.dirname(f.relativePath);
207
+ dirs.set(d, (dirs.get(d) ?? 0) + 1);
208
+ }
209
+ }
210
+ // Sort by count descending
211
+ return [...dirs.entries()]
212
+ .sort((a, b) => b[1] - a[1])
213
+ .map(([d]) => d);
214
+ }
215
+ // ── Check 3: Naming convention ───────────────────────────────
216
+ function checkNamingConvention(file, ctx) {
217
+ const dir = path.dirname(file);
218
+ const basename = path.basename(file);
219
+ const nameOnly = basename.replace(/\.[^.]+$/, ''); // strip extension
220
+ // Find siblings in the same directory
221
+ const siblings = ctx.model.files
222
+ .filter((f) => path.dirname(f.relativePath) === dir)
223
+ .map((f) => path.basename(f.relativePath).replace(/\.[^.]+$/, ''));
224
+ if (siblings.length < 2)
225
+ return []; // not enough data to detect a pattern
226
+ const dominant = detectNamingPattern(siblings);
227
+ if (!dominant)
228
+ return [];
229
+ const filePattern = classifyName(nameOnly);
230
+ if (!filePattern || filePattern === dominant)
231
+ return [];
232
+ return [{
233
+ file,
234
+ type: 'warning',
235
+ rule: 'naming-convention',
236
+ message: `Naming doesn't match directory convention`,
237
+ details: `"${basename}" is ${filePattern} but ${dir}/ uses ${dominant}`,
238
+ dismissable: true,
239
+ }];
240
+ }
241
+ function classifyName(name) {
242
+ if (/^[a-z][a-zA-Z0-9]*$/.test(name) && /[A-Z]/.test(name))
243
+ return 'camelCase';
244
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(name))
245
+ return 'PascalCase';
246
+ if (/^[a-z][a-z0-9]*(_[a-z0-9]+)+$/.test(name))
247
+ return 'snake_case';
248
+ if (/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(name))
249
+ return 'kebab-case';
250
+ return null;
251
+ }
252
+ function detectNamingPattern(names) {
253
+ const counts = {};
254
+ for (const name of names) {
255
+ const pattern = classifyName(name);
256
+ if (pattern)
257
+ counts[pattern] = (counts[pattern] ?? 0) + 1;
258
+ }
259
+ const entries = Object.entries(counts);
260
+ if (entries.length === 0)
261
+ return null;
262
+ entries.sort((a, b) => b[1] - a[1]);
263
+ const [topPattern, topCount] = entries[0];
264
+ // Require >50% dominance
265
+ const total = entries.reduce((sum, [, c]) => sum + c, 0);
266
+ if (topCount / total <= 0.5)
267
+ return null;
268
+ return topPattern;
269
+ }
270
+ // ── Check 4: Import pattern ──────────────────────────────────
271
+ function checkImportPattern(file, ctx) {
272
+ if (!ctx.fileContents)
273
+ return [];
274
+ const content = ctx.fileContents.get(file);
275
+ if (!content)
276
+ return [];
277
+ // Simple regex-based import extraction
278
+ const currentImports = extractImports(content);
279
+ const hubFiles = new Set(ctx.model.metrics.hubs.map((h) => h.file));
280
+ // Get previous imports from the model
281
+ const modelFile = ctx.model.files.find((f) => f.relativePath === file);
282
+ const previousImports = new Set(modelFile?.imports ?? []);
283
+ const assessments = [];
284
+ for (const imp of currentImports) {
285
+ if (previousImports.has(imp))
286
+ continue; // not new
287
+ // Resolve relative imports to check against hub paths
288
+ const resolved = resolveRelativeImport(file, imp);
289
+ if (resolved && hubFiles.has(resolved)) {
290
+ const hub = ctx.model.metrics.hubs.find((h) => h.file === resolved);
291
+ assessments.push({
292
+ file,
293
+ type: 'warning',
294
+ rule: 'import-hub',
295
+ message: `New import from high-risk hub`,
296
+ details: `${resolved} (${hub?.inDegree ?? 0} dependents)`,
297
+ dismissable: true,
298
+ });
299
+ }
300
+ }
301
+ return assessments;
302
+ }
303
+ const IMPORT_PATTERNS = [
304
+ /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g, // ES import
305
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, // CommonJS
306
+ /from\s+(\S+)\s+import/g, // Python
307
+ /^import\s+(\S+)/gm, // Python bare import
308
+ ];
309
+ function extractImports(content) {
310
+ const imports = new Set();
311
+ for (const pattern of IMPORT_PATTERNS) {
312
+ pattern.lastIndex = 0;
313
+ let match;
314
+ while ((match = pattern.exec(content)) !== null) {
315
+ imports.add(match[1]);
316
+ }
317
+ }
318
+ return [...imports];
319
+ }
320
+ function resolveRelativeImport(fromFile, specifier) {
321
+ if (!specifier.startsWith('.'))
322
+ return null;
323
+ const dir = path.dirname(fromFile);
324
+ let resolved = path.posix.join(dir, specifier);
325
+ // Try common extensions
326
+ for (const ext of ['', '.ts', '.tsx', '.js', '.jsx', '.py', '/index.ts', '/index.js']) {
327
+ const candidate = resolved + ext;
328
+ if (candidate === resolved && !resolved.includes('.'))
329
+ continue;
330
+ // We can't check if file exists, but return the posix-normalized path
331
+ if (ext === '' && resolved.includes('.'))
332
+ return resolved;
333
+ }
334
+ // Return with .ts as best guess for relative imports
335
+ if (!resolved.includes('.')) {
336
+ return resolved + '.ts';
337
+ }
338
+ return resolved;
339
+ }
340
+ // ── Check 5: Export contract breakage ─────────────────────────
341
+ function extractExportNames(content, _language) {
342
+ const exports = new Set();
343
+ // export function/class/const/let/var/type/interface/enum Name
344
+ const namedRe = /export\s+(?:function|class|const|let|var|type|interface|enum)\s+(\w+)/g;
345
+ let m;
346
+ while ((m = namedRe.exec(content)) !== null)
347
+ exports.add(m[1]);
348
+ // export { A, B, C }
349
+ const braceRe = /export\s*\{([^}]+)\}/g;
350
+ while ((m = braceRe.exec(content)) !== null) {
351
+ for (const part of m[1].split(',')) {
352
+ const name = part.trim().split(/\s+as\s+/)[0].trim();
353
+ if (name)
354
+ exports.add(name);
355
+ }
356
+ }
357
+ // export default function/class Name
358
+ const defaultRe = /export\s+default\s+(?:function|class)\s+(\w+)/g;
359
+ while ((m = defaultRe.exec(content)) !== null)
360
+ exports.add(m[1]);
361
+ return [...exports];
362
+ }
363
+ function checkExportContract(file, ctx) {
364
+ if (!ctx.fileContents)
365
+ return [];
366
+ const content = ctx.fileContents.get(file);
367
+ if (!content)
368
+ return [];
369
+ const modelFile = ctx.model.files.find((f) => f.relativePath === file);
370
+ if (!modelFile)
371
+ return [];
372
+ const previousExports = new Set(modelFile.exports);
373
+ if (previousExports.size === 0)
374
+ return [];
375
+ const currentExports = new Set(extractExportNames(content, modelFile.language));
376
+ const removedExports = [...previousExports].filter((e) => !currentExports.has(e));
377
+ if (removedExports.length === 0)
378
+ return [];
379
+ // Find consumers of removed exports
380
+ const affectedConsumers = new Set();
381
+ for (const removed of removedExports) {
382
+ for (const edge of ctx.model.graph.edges) {
383
+ if (edge.target === file && edge.symbols?.includes(removed)) {
384
+ affectedConsumers.add(edge.source);
385
+ }
386
+ }
387
+ }
388
+ if (affectedConsumers.size === 0)
389
+ return [];
390
+ const consumers = [...affectedConsumers];
391
+ const shown = consumers.slice(0, 3);
392
+ const moreCount = consumers.length - shown.length;
393
+ const consumerList = shown.join(', ') + (moreCount > 0 ? `, +${moreCount} more` : '');
394
+ const depCtx = `Removed exports: [${removedExports.join(', ')}], ${consumers.length} affected consumers: [${consumers.join(', ')}]`;
395
+ return [{
396
+ file,
397
+ type: 'warning',
398
+ rule: 'export-contract',
399
+ message: `Removed export${removedExports.length > 1 ? 's' : ''} ${removedExports.join(', ')} — ${consumers.length} consumer${consumers.length > 1 ? 's' : ''} may break`,
400
+ details: `Affected: ${consumerList}`,
401
+ suggestion: `Verify these files still compile: ${consumers.join(', ')}`,
402
+ dependencyContext: depCtx,
403
+ dismissable: true,
404
+ }];
405
+ }
406
+ // ── Check 6: Circular dependency introduction ────────────────
407
+ function hasPathInGraph(from, to, edges, maxDepth = 20) {
408
+ const visited = new Set();
409
+ const queue = [{ node: from, path: [from] }];
410
+ while (queue.length > 0) {
411
+ const { node, path: currentPath } = queue.shift();
412
+ if (currentPath.length > maxDepth)
413
+ continue;
414
+ if (node === to)
415
+ return currentPath;
416
+ for (const edge of edges) {
417
+ if (edge.source === node && !visited.has(edge.target)) {
418
+ visited.add(edge.target);
419
+ queue.push({ node: edge.target, path: [...currentPath, edge.target] });
420
+ }
421
+ }
422
+ }
423
+ return null;
424
+ }
425
+ function checkCircularDependency(file, ctx) {
426
+ if (!ctx.fileContents)
427
+ return [];
428
+ const content = ctx.fileContents.get(file);
429
+ if (!content)
430
+ return [];
431
+ const modelFile = ctx.model.files.find((f) => f.relativePath === file);
432
+ const previousImports = new Set(modelFile?.imports ?? []);
433
+ const currentImports = extractImports(content);
434
+ const newImports = currentImports.filter((imp) => !previousImports.has(imp));
435
+ const assessments = [];
436
+ for (const imp of newImports) {
437
+ const resolved = resolveRelativeImport(file, imp);
438
+ if (!resolved)
439
+ continue;
440
+ // Check if target already has a path back to this file (would create a cycle)
441
+ const cyclePath = hasPathInGraph(resolved, file, ctx.model.graph.edges);
442
+ if (cyclePath) {
443
+ const fullCycle = [file, ...cyclePath];
444
+ const depCtx = `Cycle: ${fullCycle.join(' → ')}`;
445
+ assessments.push({
446
+ file,
447
+ type: 'warning',
448
+ rule: 'circular-dependency',
449
+ message: `New import creates circular dependency`,
450
+ details: `Cycle: ${fullCycle.join(' → ')}`,
451
+ suggestion: `Consider restructuring to break the cycle between ${file} and ${resolved}`,
452
+ dependencyContext: depCtx,
453
+ dismissable: true,
454
+ });
455
+ }
456
+ }
457
+ return assessments;
458
+ }
459
+ // ── Check 7: Test coverage gap ───────────────────────────────
460
+ function checkTestCoverageGap(file, ctx) {
461
+ const basename = path.basename(file).toLowerCase();
462
+ if (isTestFile(basename))
463
+ return [];
464
+ const dir = path.dirname(file);
465
+ const nameNoExt = path.basename(file).replace(/\.[^.]+$/, '');
466
+ const ext = path.extname(file);
467
+ // Generate candidate test paths
468
+ const candidates = [
469
+ path.posix.join(dir, `${nameNoExt}.test${ext}`),
470
+ path.posix.join(dir, `${nameNoExt}.spec${ext}`),
471
+ path.posix.join(dir, 'test', `${nameNoExt}.test${ext}`),
472
+ path.posix.join(dir, '__tests__', `${nameNoExt}.test${ext}`),
473
+ ];
474
+ const modelPaths = new Set(ctx.model.files.map((f) => f.relativePath));
475
+ const matchedTestFile = candidates.find((c) => modelPaths.has(c));
476
+ if (!matchedTestFile)
477
+ return []; // no test file exists — skip silently
478
+ const recentPaths = new Set(ctx.recentChanges.map((c) => c.path));
479
+ if (recentPaths.has(matchedTestFile))
480
+ return []; // test was updated recently
481
+ return [{
482
+ file,
483
+ type: 'warning',
484
+ rule: 'test-coverage-gap',
485
+ message: `Test file exists but wasn't updated`,
486
+ details: `${matchedTestFile} may need updates`,
487
+ suggestion: `Consider updating ${matchedTestFile} to cover the changes in ${file}`,
488
+ dependencyContext: `Source: ${file}, test: ${matchedTestFile}`,
489
+ dismissable: true,
490
+ }];
491
+ }
492
+ // ── Check 8: File size growth ────────────────────────────────
493
+ const FILE_SIZE_THRESHOLD = 500;
494
+ const FILE_SIZE_GROWTH = 100;
495
+ function checkFileSize(file, ctx) {
496
+ if (!ctx.fileContents)
497
+ return [];
498
+ const content = ctx.fileContents.get(file);
499
+ if (!content)
500
+ return [];
501
+ const currentLines = content.split('\n').length;
502
+ const modelFile = ctx.model.files.find((f) => f.relativePath === file);
503
+ const previousLines = modelFile?.lineCount ?? 0;
504
+ // Only fire if over threshold OR grew by a lot
505
+ if (currentLines < FILE_SIZE_THRESHOLD && currentLines - previousLines < FILE_SIZE_GROWTH)
506
+ return [];
507
+ // Don't fire if file was already large (only on crossing threshold or big growth)
508
+ if (previousLines >= FILE_SIZE_THRESHOLD && currentLines - previousLines < FILE_SIZE_GROWTH)
509
+ return [];
510
+ return [{
511
+ file,
512
+ type: 'warning',
513
+ rule: 'file-size',
514
+ message: currentLines >= FILE_SIZE_THRESHOLD
515
+ ? `File is ${currentLines} lines (threshold: ${FILE_SIZE_THRESHOLD})`
516
+ : `File grew by ${currentLines - previousLines} lines (now ${currentLines})`,
517
+ suggestion: `Consider splitting ${file} into smaller modules.`,
518
+ dismissable: true,
519
+ }];
520
+ }
521
+ // ── Check 9: New hub detection ──────────────────────────────
522
+ const HUB_THRESHOLD = 3;
523
+ function checkNewHub(file, ctx) {
524
+ // Count current inDegree for this file
525
+ let inDegree = 0;
526
+ for (const edge of ctx.model.graph.edges) {
527
+ if ((edge.type === 'import' || edge.type === 'call') && edge.target === file) {
528
+ inDegree++;
529
+ }
530
+ }
531
+ if (inDegree < HUB_THRESHOLD)
532
+ return [];
533
+ const previousInDegree = ctx.previousHubCounts?.get(file) ?? 0;
534
+ if (previousInDegree >= HUB_THRESHOLD)
535
+ return []; // was already a hub
536
+ return [{
537
+ file,
538
+ type: 'warning',
539
+ rule: 'new-hub',
540
+ message: `Now imported by ${inDegree} files — becoming a hub`,
541
+ details: `Changes to this file will affect ${inDegree} dependents.`,
542
+ suggestion: `${file} is becoming a shared dependency. Ensure its API is stable and well-tested.`,
543
+ dismissable: true,
544
+ }];
545
+ }
546
+ // ── Check 10: Cross-boundary import ─────────────────────────
547
+ function checkCrossBoundary(file, ctx) {
548
+ if (!ctx.fileContents)
549
+ return [];
550
+ const content = ctx.fileContents.get(file);
551
+ if (!content)
552
+ return [];
553
+ const fileSegment = file.split('/')[0];
554
+ if (!fileSegment)
555
+ return [];
556
+ // Only check if the top-level dir has enough files to be a real boundary
557
+ const dirFileCounts = new Map();
558
+ for (const f of ctx.model.files) {
559
+ const seg = f.relativePath.split('/')[0];
560
+ dirFileCounts.set(seg, (dirFileCounts.get(seg) ?? 0) + 1);
561
+ }
562
+ if ((dirFileCounts.get(fileSegment) ?? 0) < 3)
563
+ return [];
564
+ const modelFile = ctx.model.files.find((f) => f.relativePath === file);
565
+ const previousImports = new Set(modelFile?.imports ?? []);
566
+ const currentImports = extractImports(content);
567
+ const newImports = currentImports.filter((imp) => !previousImports.has(imp));
568
+ const assessments = [];
569
+ for (const imp of newImports) {
570
+ const resolved = resolveRelativeImport(file, imp);
571
+ if (!resolved)
572
+ continue;
573
+ const targetSegment = resolved.split('/')[0];
574
+ if (!targetSegment || targetSegment === fileSegment)
575
+ continue;
576
+ if ((dirFileCounts.get(targetSegment) ?? 0) < 3)
577
+ continue;
578
+ assessments.push({
579
+ file,
580
+ type: 'warning',
581
+ rule: 'cross-boundary',
582
+ message: `New import crosses ${fileSegment}/${targetSegment} boundary`,
583
+ details: `${file} now imports from ${resolved}`,
584
+ suggestion: `Verify this cross-boundary import is intentional (${fileSegment} → ${targetSegment}).`,
585
+ dependencyContext: `boundary: ${fileSegment} → ${targetSegment}`,
586
+ dismissable: true,
587
+ });
588
+ break; // One warning per boundary crossing is enough
589
+ }
590
+ return assessments;
591
+ }
592
+ // ── Check 11: Stale import (deleted target) ─────────────────
593
+ function checkStaleImport(file, ctx) {
594
+ // Check if any recently deleted files were imported by this file
595
+ const recentDeletes = ctx.recentChanges
596
+ .filter((c) => c.type === 'unlink')
597
+ .map((c) => c.path);
598
+ if (recentDeletes.length === 0)
599
+ return [];
600
+ const modelFile = ctx.model.files.find((f) => f.relativePath === file);
601
+ if (!modelFile)
602
+ return [];
603
+ // Check graph edges: does this file import any recently deleted file?
604
+ const importTargets = new Set();
605
+ for (const edge of ctx.model.graph.edges) {
606
+ if (edge.source === file && (edge.type === 'import' || edge.type === 'call')) {
607
+ importTargets.add(edge.target);
608
+ }
609
+ }
610
+ const assessments = [];
611
+ for (const deleted of recentDeletes) {
612
+ if (importTargets.has(deleted)) {
613
+ assessments.push({
614
+ file,
615
+ type: 'warning',
616
+ rule: 'stale-import',
617
+ message: `Imports from ${deleted} which was just deleted`,
618
+ suggestion: `Update or remove the import from ${deleted} in ${file}.`,
619
+ dependencyContext: `deleted: ${deleted}, importer: ${file}`,
620
+ dismissable: true,
621
+ });
622
+ }
623
+ }
624
+ return assessments;
625
+ }
626
+ // ── Check 12: Inheritance change propagation ────────────────
627
+ function checkInheritanceChange(file, ctx) {
628
+ // Find files that inherit from symbols in this file
629
+ const children = [];
630
+ for (const edge of ctx.model.graph.edges) {
631
+ if (edge.type === 'inherit' && edge.target === file && edge.source !== file) {
632
+ children.push(edge.source);
633
+ }
634
+ }
635
+ if (children.length === 0)
636
+ return [];
637
+ // Check if children were recently updated
638
+ const recentPaths = new Set(ctx.recentChanges.map((c) => c.path));
639
+ const missingChildren = children.filter((c) => !recentPaths.has(c));
640
+ if (missingChildren.length === 0)
641
+ return [];
642
+ const shown = missingChildren.slice(0, 3);
643
+ const moreCount = missingChildren.length - shown.length;
644
+ const childList = shown.join(', ') + (moreCount > 0 ? `, +${moreCount} more` : '');
645
+ return [{
646
+ file,
647
+ type: 'warning',
648
+ rule: 'inheritance-change',
649
+ message: `Base class/interface modified — ${missingChildren.length} child${missingChildren.length === 1 ? '' : 'ren'} may need updates`,
650
+ details: `Not yet updated: ${childList}`,
651
+ suggestion: `Verify child implementations: ${missingChildren.join(', ')}`,
652
+ dependencyContext: `${children.length} children, ${missingChildren.length} not updated: [${missingChildren.join(', ')}]`,
653
+ dismissable: true,
654
+ }];
655
+ }
656
+ // ── Preference override ──────────────────────────────────────
657
+ function applyPreferences(assessments, preferences) {
658
+ return assessments.filter((a) => {
659
+ const dir = path.dirname(a.file) + '/';
660
+ const pref = (0, preferences_1.findMatchingPreference)(preferences, a.rule, a.file, dir);
661
+ if (!pref)
662
+ return true;
663
+ if (pref.disposition === 'allow') {
664
+ (0, preferences_1.bumpPreferenceHit)(preferences, pref.id);
665
+ return false; // suppress
666
+ }
667
+ if (pref.disposition === 'deny') {
668
+ (0, preferences_1.bumpPreferenceHit)(preferences, pref.id);
669
+ a.type = 'violation'; // upgrade to violation
670
+ }
671
+ return true;
672
+ });
673
+ }
674
+ //# sourceMappingURL=changeEvaluator.js.map