aspectcode 0.4.1 → 1.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.
- package/README.md +2 -2
- package/dist/agentsMdRenderer.d.ts +16 -0
- package/dist/agentsMdRenderer.d.ts.map +1 -0
- package/dist/agentsMdRenderer.js +137 -0
- package/dist/agentsMdRenderer.js.map +1 -0
- package/dist/auth.d.ts +31 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +385 -0
- package/dist/auth.js.map +1 -0
- package/dist/autoResolve.d.ts +41 -0
- package/dist/autoResolve.d.ts.map +1 -0
- package/dist/autoResolve.js +196 -0
- package/dist/autoResolve.js.map +1 -0
- package/dist/changeEvaluator.d.ts +56 -0
- package/dist/changeEvaluator.d.ts.map +1 -0
- package/dist/changeEvaluator.js +674 -0
- package/dist/changeEvaluator.js.map +1 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +37 -17
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +50 -2
- package/dist/config.js.map +1 -1
- package/dist/dreamCycle.d.ts +57 -0
- package/dist/dreamCycle.d.ts.map +1 -0
- package/dist/dreamCycle.js +334 -0
- package/dist/dreamCycle.js.map +1 -0
- package/dist/kbBuilder.d.ts +1 -2
- package/dist/kbBuilder.d.ts.map +1 -1
- package/dist/kbBuilder.js +1 -2
- package/dist/kbBuilder.js.map +1 -1
- package/dist/main.d.ts +2 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +148 -7
- package/dist/main.js.map +1 -1
- package/dist/optimize.d.ts +13 -6
- package/dist/optimize.d.ts.map +1 -1
- package/dist/optimize.js +433 -142
- package/dist/optimize.js.map +1 -1
- package/dist/pipeline.d.ts +19 -21
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +1093 -160
- package/dist/pipeline.js.map +1 -1
- package/dist/preferences.d.ts +80 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +238 -0
- package/dist/preferences.js.map +1 -0
- package/dist/runtimeState.d.ts +30 -0
- package/dist/runtimeState.d.ts.map +1 -0
- package/dist/runtimeState.js +39 -0
- package/dist/runtimeState.js.map +1 -0
- package/dist/scopedRules.d.ts +84 -0
- package/dist/scopedRules.d.ts.map +1 -0
- package/dist/scopedRules.js +449 -0
- package/dist/scopedRules.js.map +1 -0
- package/dist/ui/Dashboard.d.ts +4 -16
- package/dist/ui/Dashboard.d.ts.map +1 -1
- package/dist/ui/Dashboard.js +339 -140
- package/dist/ui/Dashboard.js.map +1 -1
- package/dist/ui/MemoryMap.d.ts +16 -0
- package/dist/ui/MemoryMap.d.ts.map +1 -0
- package/dist/ui/MemoryMap.js +266 -0
- package/dist/ui/MemoryMap.js.map +1 -0
- package/dist/ui/SettingsPanel.d.ts +18 -0
- package/dist/ui/SettingsPanel.d.ts.map +1 -0
- package/dist/ui/SettingsPanel.js +241 -0
- package/dist/ui/SettingsPanel.js.map +1 -0
- package/dist/ui/prompts.d.ts +7 -0
- package/dist/ui/prompts.d.ts.map +1 -1
- package/dist/ui/prompts.js +63 -0
- package/dist/ui/prompts.js.map +1 -1
- package/dist/ui/store.d.ts +154 -18
- package/dist/ui/store.d.ts.map +1 -1
- package/dist/ui/store.js +154 -24
- package/dist/ui/store.js.map +1 -1
- package/dist/ui/theme.d.ts +1 -8
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +2 -21
- package/dist/ui/theme.js.map +1 -1
- package/dist/updateChecker.d.ts +13 -0
- package/dist/updateChecker.d.ts.map +1 -0
- package/dist/updateChecker.js +66 -0
- package/dist/updateChecker.js.map +1 -0
- package/dist/usageTracker.d.ts +12 -0
- package/dist/usageTracker.d.ts.map +1 -0
- package/dist/usageTracker.js +89 -0
- package/dist/usageTracker.js.map +1 -0
- package/dist/writer.d.ts +1 -7
- package/dist/writer.d.ts.map +1 -1
- package/dist/writer.js +1 -11
- package/dist/writer.js.map +1 -1
- package/node_modules/@aspectcode/core/dist/analysis/repo.d.ts.map +1 -1
- package/node_modules/@aspectcode/core/dist/analysis/repo.js +13 -2
- package/node_modules/@aspectcode/core/dist/analysis/repo.js.map +1 -1
- package/node_modules/@aspectcode/core/dist/index.d.ts +1 -3
- package/node_modules/@aspectcode/core/dist/index.d.ts.map +1 -1
- package/node_modules/@aspectcode/core/dist/index.js +1 -3
- package/node_modules/@aspectcode/core/dist/index.js.map +1 -1
- package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.d.ts +14 -0
- package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.d.ts.map +1 -0
- package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.js +191 -0
- package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.js.map +1 -0
- package/node_modules/@aspectcode/core/dist/parsers/index.d.ts +1 -0
- package/node_modules/@aspectcode/core/dist/parsers/index.d.ts.map +1 -1
- package/node_modules/@aspectcode/core/dist/parsers/index.js +6 -1
- package/node_modules/@aspectcode/core/dist/parsers/index.js.map +1 -1
- package/node_modules/@aspectcode/core/dist/parsers/languages.d.ts +20 -0
- package/node_modules/@aspectcode/core/dist/parsers/languages.d.ts.map +1 -1
- package/node_modules/@aspectcode/core/dist/parsers/languages.js +25 -0
- package/node_modules/@aspectcode/core/dist/parsers/languages.js.map +1 -1
- package/node_modules/@aspectcode/core/dist/parsers/tsJsExtractors.d.ts.map +1 -1
- package/node_modules/@aspectcode/core/dist/parsers/tsJsExtractors.js +4 -1
- package/node_modules/@aspectcode/core/dist/parsers/tsJsExtractors.js.map +1 -1
- package/node_modules/@aspectcode/core/package.json +2 -2
- package/node_modules/@aspectcode/core/parsers/cpp.wasm +0 -0
- package/node_modules/@aspectcode/core/parsers/go.wasm +0 -0
- package/node_modules/@aspectcode/core/parsers/php.wasm +0 -0
- package/node_modules/@aspectcode/core/parsers/ruby.wasm +0 -0
- package/node_modules/@aspectcode/core/parsers/rust.wasm +0 -0
- package/node_modules/@aspectcode/emitters/dist/index.d.ts +1 -17
- package/node_modules/@aspectcode/emitters/dist/index.d.ts.map +1 -1
- package/node_modules/@aspectcode/emitters/dist/index.js +2 -90
- package/node_modules/@aspectcode/emitters/dist/index.js.map +1 -1
- package/node_modules/@aspectcode/emitters/dist/instructions/index.d.ts +0 -2
- package/node_modules/@aspectcode/emitters/dist/instructions/index.d.ts.map +1 -1
- package/node_modules/@aspectcode/emitters/dist/instructions/index.js +1 -7
- package/node_modules/@aspectcode/emitters/dist/instructions/index.js.map +1 -1
- package/node_modules/@aspectcode/emitters/dist/kb/analyzers.d.ts +0 -18
- package/node_modules/@aspectcode/emitters/dist/kb/analyzers.d.ts.map +1 -1
- package/node_modules/@aspectcode/emitters/dist/kb/analyzers.js +0 -57
- package/node_modules/@aspectcode/emitters/dist/kb/analyzers.js.map +1 -1
- package/node_modules/@aspectcode/emitters/dist/kb/conventions.d.ts +0 -18
- package/node_modules/@aspectcode/emitters/dist/kb/conventions.d.ts.map +1 -1
- package/node_modules/@aspectcode/emitters/dist/kb/conventions.js +0 -130
- package/node_modules/@aspectcode/emitters/dist/kb/conventions.js.map +1 -1
- package/node_modules/@aspectcode/emitters/dist/kb/index.d.ts +2 -4
- package/node_modules/@aspectcode/emitters/dist/kb/index.d.ts.map +1 -1
- package/node_modules/@aspectcode/emitters/dist/kb/index.js +1 -11
- package/node_modules/@aspectcode/emitters/dist/kb/index.js.map +1 -1
- package/node_modules/@aspectcode/emitters/package.json +3 -3
- package/node_modules/@aspectcode/evaluator/dist/apply.d.ts +55 -0
- package/node_modules/@aspectcode/evaluator/dist/apply.d.ts.map +1 -0
- package/node_modules/@aspectcode/evaluator/dist/apply.js +368 -0
- package/node_modules/@aspectcode/evaluator/dist/apply.js.map +1 -0
- package/node_modules/@aspectcode/evaluator/dist/diagnosis.d.ts +16 -25
- package/node_modules/@aspectcode/evaluator/dist/diagnosis.d.ts.map +1 -1
- package/node_modules/@aspectcode/evaluator/dist/diagnosis.js +115 -138
- package/node_modules/@aspectcode/evaluator/dist/diagnosis.js.map +1 -1
- package/node_modules/@aspectcode/evaluator/dist/index.d.ts +8 -43
- package/node_modules/@aspectcode/evaluator/dist/index.d.ts.map +1 -1
- package/node_modules/@aspectcode/evaluator/dist/index.js +15 -61
- package/node_modules/@aspectcode/evaluator/dist/index.js.map +1 -1
- package/node_modules/@aspectcode/evaluator/dist/judge.d.ts +32 -0
- package/node_modules/@aspectcode/evaluator/dist/judge.d.ts.map +1 -0
- package/node_modules/@aspectcode/evaluator/dist/judge.js +165 -0
- package/node_modules/@aspectcode/evaluator/dist/judge.js.map +1 -0
- package/node_modules/@aspectcode/evaluator/dist/llmUtil.d.ts +15 -0
- package/node_modules/@aspectcode/evaluator/dist/llmUtil.d.ts.map +1 -0
- package/node_modules/@aspectcode/evaluator/dist/llmUtil.js +41 -0
- package/node_modules/@aspectcode/evaluator/dist/llmUtil.js.map +1 -0
- package/node_modules/@aspectcode/evaluator/dist/probes.d.ts +20 -47
- package/node_modules/@aspectcode/evaluator/dist/probes.d.ts.map +1 -1
- package/node_modules/@aspectcode/evaluator/dist/probes.js +188 -278
- package/node_modules/@aspectcode/evaluator/dist/probes.js.map +1 -1
- package/node_modules/@aspectcode/evaluator/dist/runner.d.ts +7 -32
- package/node_modules/@aspectcode/evaluator/dist/runner.d.ts.map +1 -1
- package/node_modules/@aspectcode/evaluator/dist/runner.js +21 -146
- package/node_modules/@aspectcode/evaluator/dist/runner.js.map +1 -1
- package/node_modules/@aspectcode/evaluator/dist/types.d.ts +141 -99
- package/node_modules/@aspectcode/evaluator/dist/types.d.ts.map +1 -1
- package/node_modules/@aspectcode/evaluator/dist/types.js +10 -2
- package/node_modules/@aspectcode/evaluator/dist/types.js.map +1 -1
- package/node_modules/@aspectcode/evaluator/package.json +4 -4
- package/node_modules/@aspectcode/optimizer/dist/index.d.ts +3 -10
- package/node_modules/@aspectcode/optimizer/dist/index.d.ts.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/index.js +1 -19
- package/node_modules/@aspectcode/optimizer/dist/index.js.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/providers/anthropic.d.ts.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/providers/anthropic.js +40 -0
- package/node_modules/@aspectcode/optimizer/dist/providers/anthropic.js.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.d.ts +9 -0
- package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.d.ts.map +1 -0
- package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.js +83 -0
- package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.js.map +1 -0
- package/node_modules/@aspectcode/optimizer/dist/providers/index.d.ts +4 -3
- package/node_modules/@aspectcode/optimizer/dist/providers/index.d.ts.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/providers/index.js +24 -10
- package/node_modules/@aspectcode/optimizer/dist/providers/index.js.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/providers/openai.d.ts.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/providers/openai.js +22 -0
- package/node_modules/@aspectcode/optimizer/dist/providers/openai.js.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/providers/retry.d.ts +14 -0
- package/node_modules/@aspectcode/optimizer/dist/providers/retry.d.ts.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/providers/retry.js +1 -0
- package/node_modules/@aspectcode/optimizer/dist/providers/retry.js.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/types.d.ts +14 -0
- package/node_modules/@aspectcode/optimizer/dist/types.d.ts.map +1 -1
- package/node_modules/@aspectcode/optimizer/dist/types.js.map +1 -1
- package/node_modules/@aspectcode/optimizer/package.json +2 -2
- package/package.json +6 -7
- package/dist/complaintProcessor.d.ts +0 -16
- package/dist/complaintProcessor.d.ts.map +0 -1
- package/dist/complaintProcessor.js +0 -134
- package/dist/complaintProcessor.js.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/emitter.d.ts +0 -72
- package/node_modules/@aspectcode/emitters/dist/emitter.d.ts.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/emitter.js +0 -10
- package/node_modules/@aspectcode/emitters/dist/emitter.js.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/instructions/content.d.ts +0 -26
- package/node_modules/@aspectcode/emitters/dist/instructions/content.d.ts.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/instructions/content.js +0 -501
- package/node_modules/@aspectcode/emitters/dist/instructions/content.js.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/instructions/detection.d.ts +0 -13
- package/node_modules/@aspectcode/emitters/dist/instructions/detection.d.ts.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/instructions/detection.js +0 -55
- package/node_modules/@aspectcode/emitters/dist/instructions/detection.js.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.d.ts +0 -9
- package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.d.ts.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.js +0 -30
- package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.js.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.d.ts +0 -21
- package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.d.ts.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.js +0 -125
- package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.js.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/manifest.d.ts +0 -37
- package/node_modules/@aspectcode/emitters/dist/manifest.d.ts.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/manifest.js +0 -50
- package/node_modules/@aspectcode/emitters/dist/manifest.js.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/report.d.ts +0 -22
- package/node_modules/@aspectcode/emitters/dist/report.d.ts.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/report.js +0 -3
- package/node_modules/@aspectcode/emitters/dist/report.js.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/stableJson.d.ts +0 -14
- package/node_modules/@aspectcode/emitters/dist/stableJson.d.ts.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/stableJson.js +0 -40
- package/node_modules/@aspectcode/emitters/dist/stableJson.js.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/transaction.d.ts +0 -29
- package/node_modules/@aspectcode/emitters/dist/transaction.d.ts.map +0 -1
- package/node_modules/@aspectcode/emitters/dist/transaction.js +0 -104
- 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
|