pi-lens 2.0.37 → 2.0.39

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 (46) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/clients/architect-client.js +50 -20
  4. package/clients/architect-client.ts +61 -22
  5. package/clients/ast-grep-client.js +32 -127
  6. package/clients/ast-grep-client.test.js +2 -14
  7. package/clients/ast-grep-client.test.ts +2 -16
  8. package/clients/ast-grep-client.ts +34 -150
  9. package/clients/auto-loop.js +117 -0
  10. package/clients/auto-loop.ts +171 -0
  11. package/clients/biome-client.js +25 -22
  12. package/clients/biome-client.ts +28 -25
  13. package/clients/complexity-client.js +111 -74
  14. package/clients/complexity-client.ts +149 -105
  15. package/clients/dependency-checker.js +16 -30
  16. package/clients/dependency-checker.ts +15 -35
  17. package/clients/fix-scanners.js +195 -0
  18. package/clients/fix-scanners.ts +297 -0
  19. package/clients/interviewer-templates.js +75 -0
  20. package/clients/interviewer-templates.ts +90 -0
  21. package/clients/interviewer.js +73 -101
  22. package/clients/interviewer.ts +195 -140
  23. package/clients/knip-client.js +12 -4
  24. package/clients/knip-client.ts +21 -16
  25. package/clients/metrics-history.js +215 -0
  26. package/clients/metrics-history.ts +300 -0
  27. package/clients/scan-architectural-debt.js +62 -72
  28. package/clients/scan-architectural-debt.ts +79 -64
  29. package/clients/scan-utils.js +98 -0
  30. package/clients/scan-utils.ts +112 -0
  31. package/clients/sg-runner.js +138 -0
  32. package/clients/sg-runner.ts +168 -0
  33. package/clients/subprocess-client.js +0 -37
  34. package/clients/subprocess-client.ts +0 -60
  35. package/clients/ts-service.ts +1 -7
  36. package/clients/type-safety-client.js +3 -8
  37. package/clients/type-safety-client.ts +10 -14
  38. package/clients/typescript-client.js +55 -56
  39. package/clients/typescript-client.ts +94 -52
  40. package/default-architect.yaml +87 -0
  41. package/index.ts +143 -1165
  42. package/package.json +2 -1
  43. package/rules/ast-grep-rules/rules/large-class.yml +6 -2
  44. package/rules/ast-grep-rules/rules/long-method.yml +1 -1
  45. package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
  46. package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Metrics History Tracker for pi-lens
3
+ *
4
+ * Persists complexity metrics per commit to track trends over time.
5
+ * Captures snapshots passively (session start) and explicitly (/lens-metrics).
6
+ *
7
+ * Storage: .pi-lens/metrics-history.json
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ // --- Constants ---
12
+ const HISTORY_FILE = ".pi-lens/metrics-history.json";
13
+ const MAX_HISTORY_PER_FILE = 20;
14
+ // --- Git Helpers ---
15
+ /**
16
+ * Get current git commit hash (short)
17
+ */
18
+ function getCurrentCommit() {
19
+ try {
20
+ const { execSync } = require("node:child_process");
21
+ return execSync("git rev-parse --short HEAD", {
22
+ encoding: "utf-8",
23
+ timeout: 5000,
24
+ }).trim();
25
+ }
26
+ catch {
27
+ return "unknown";
28
+ }
29
+ }
30
+ // --- History Management ---
31
+ /**
32
+ * Load history from disk (or return empty)
33
+ */
34
+ export function loadHistory() {
35
+ const historyPath = path.join(process.cwd(), HISTORY_FILE);
36
+ if (!fs.existsSync(historyPath)) {
37
+ return {
38
+ version: 1,
39
+ files: {},
40
+ capturedAt: new Date().toISOString(),
41
+ };
42
+ }
43
+ try {
44
+ const content = fs.readFileSync(historyPath, "utf-8");
45
+ return JSON.parse(content);
46
+ }
47
+ catch {
48
+ return {
49
+ version: 1,
50
+ files: {},
51
+ capturedAt: new Date().toISOString(),
52
+ };
53
+ }
54
+ }
55
+ /**
56
+ * Save history to disk
57
+ */
58
+ export function saveHistory(history) {
59
+ const historyDir = path.join(process.cwd(), ".pi-lens");
60
+ if (!fs.existsSync(historyDir)) {
61
+ fs.mkdirSync(historyDir, { recursive: true });
62
+ }
63
+ history.capturedAt = new Date().toISOString();
64
+ const historyPath = path.join(historyDir, "metrics-history.json");
65
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
66
+ }
67
+ /**
68
+ * Capture a snapshot for a file's current metrics
69
+ */
70
+ export function captureSnapshot(filePath, metrics, history) {
71
+ const hist = history ?? loadHistory();
72
+ const relativePath = path.relative(process.cwd(), filePath);
73
+ const commit = getCurrentCommit();
74
+ const snapshot = {
75
+ commit,
76
+ timestamp: new Date().toISOString(),
77
+ mi: Math.round(metrics.maintainabilityIndex * 10) / 10,
78
+ cognitive: metrics.cognitiveComplexity,
79
+ nesting: metrics.maxNestingDepth,
80
+ lines: metrics.linesOfCode,
81
+ };
82
+ const existing = hist.files[relativePath];
83
+ if (existing) {
84
+ // Append to history (cap at MAX_HISTORY_PER_FILE)
85
+ existing.history.push(snapshot);
86
+ if (existing.history.length > MAX_HISTORY_PER_FILE) {
87
+ existing.history = existing.history.slice(-MAX_HISTORY_PER_FILE);
88
+ }
89
+ existing.latest = snapshot;
90
+ existing.trend = computeTrend(existing.history);
91
+ }
92
+ else {
93
+ // New file
94
+ hist.files[relativePath] = {
95
+ latest: snapshot,
96
+ history: [snapshot],
97
+ trend: "stable",
98
+ };
99
+ }
100
+ return hist;
101
+ }
102
+ /**
103
+ * Capture snapshots for multiple files
104
+ */
105
+ export function captureSnapshots(files) {
106
+ let history = loadHistory();
107
+ for (const file of files) {
108
+ history = captureSnapshot(file.filePath, file.metrics, history);
109
+ }
110
+ saveHistory(history);
111
+ return history;
112
+ }
113
+ // --- Trend Analysis ---
114
+ /**
115
+ * Compute trend direction from history snapshots
116
+ * Uses last 3 snapshots for stability (or 2 if only 2 available)
117
+ */
118
+ export function computeTrend(history) {
119
+ if (history.length < 2)
120
+ return "stable";
121
+ const recent = history.slice(-3);
122
+ const first = recent[0];
123
+ const last = recent[recent.length - 1];
124
+ // Use MI as primary indicator, cognitive as secondary
125
+ const miDelta = last.mi - first.mi;
126
+ const cogDelta = last.cognitive - first.cognitive;
127
+ // Thresholds (MI changes < 2 are noise)
128
+ if (miDelta > 2)
129
+ return "improving";
130
+ if (miDelta < -2)
131
+ return "regressing";
132
+ // If MI is stable, check cognitive
133
+ if (cogDelta < -10)
134
+ return "improving";
135
+ if (cogDelta > 10)
136
+ return "regressing";
137
+ return "stable";
138
+ }
139
+ /**
140
+ * Get delta between current snapshot and previous
141
+ */
142
+ export function getDelta(history) {
143
+ if (!history || history.history.length < 2)
144
+ return null;
145
+ const current = history.history[history.history.length - 1];
146
+ const previous = history.history[history.history.length - 2];
147
+ return {
148
+ mi: Math.round((current.mi - previous.mi) * 10) / 10,
149
+ cognitive: current.cognitive - previous.cognitive,
150
+ trend: history.trend,
151
+ };
152
+ }
153
+ /**
154
+ * Get trend emoji for display
155
+ */
156
+ export function getTrendEmoji(trend) {
157
+ switch (trend) {
158
+ case "improving":
159
+ return "📈";
160
+ case "regressing":
161
+ return "📉";
162
+ default:
163
+ return "➡️";
164
+ }
165
+ }
166
+ /**
167
+ * Get trend summary across all files
168
+ */
169
+ export function getTrendSummary(history) {
170
+ let improving = 0;
171
+ let regressing = 0;
172
+ let stable = 0;
173
+ const regressions = [];
174
+ for (const [file, fileHistory] of Object.entries(history.files)) {
175
+ switch (fileHistory.trend) {
176
+ case "improving":
177
+ improving++;
178
+ break;
179
+ case "regressing":
180
+ regressing++;
181
+ const delta = getDelta(fileHistory);
182
+ if (delta) {
183
+ regressions.push({ file, miDelta: delta.mi });
184
+ }
185
+ break;
186
+ default:
187
+ stable++;
188
+ }
189
+ }
190
+ // Sort regressions by MI delta (worst first)
191
+ regressions.sort((a, b) => a.miDelta - b.miDelta);
192
+ return {
193
+ improving,
194
+ regressing,
195
+ stable,
196
+ worstRegressions: regressions.slice(0, 5),
197
+ };
198
+ }
199
+ /**
200
+ * Format trend for metrics table
201
+ */
202
+ export function formatTrendCell(filePath, history) {
203
+ const relativePath = path.relative(process.cwd(), filePath);
204
+ const fileHistory = history.files[relativePath];
205
+ if (!fileHistory || fileHistory.history.length < 2) {
206
+ return "—"; // No history
207
+ }
208
+ const delta = getDelta(fileHistory);
209
+ if (!delta)
210
+ return "—";
211
+ const emoji = getTrendEmoji(delta.trend);
212
+ const miSign = delta.mi > 0 ? "+" : "";
213
+ const miColor = delta.mi > 0 ? "🟢" : delta.mi < 0 ? "🔴" : "⚪";
214
+ return `${emoji} ${miColor}${miSign}${delta.mi}`;
215
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Metrics History Tracker for pi-lens
3
+ *
4
+ * Persists complexity metrics per commit to track trends over time.
5
+ * Captures snapshots passively (session start) and explicitly (/lens-metrics).
6
+ *
7
+ * Storage: .pi-lens/metrics-history.json
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+
13
+ // --- Types ---
14
+
15
+ export interface MetricSnapshot {
16
+ commit: string;
17
+ timestamp: string;
18
+ mi: number;
19
+ cognitive: number;
20
+ nesting: number;
21
+ lines: number;
22
+ }
23
+
24
+ export interface FileHistory {
25
+ latest: MetricSnapshot;
26
+ history: MetricSnapshot[];
27
+ trend: "improving" | "stable" | "regressing";
28
+ }
29
+
30
+ export interface MetricsHistory {
31
+ version: number;
32
+ files: Record<string, FileHistory>;
33
+ capturedAt: string;
34
+ }
35
+
36
+ export type TrendDirection = "improving" | "stable" | "regressing";
37
+
38
+ // --- Constants ---
39
+
40
+ const HISTORY_FILE = ".pi-lens/metrics-history.json";
41
+ const MAX_HISTORY_PER_FILE = 20;
42
+
43
+ // --- Git Helpers ---
44
+
45
+ /**
46
+ * Get current git commit hash (short)
47
+ */
48
+ function getCurrentCommit(): string {
49
+ try {
50
+ const { execSync } = require("node:child_process");
51
+ return execSync("git rev-parse --short HEAD", {
52
+ encoding: "utf-8",
53
+ timeout: 5000,
54
+ }).trim();
55
+ } catch {
56
+ return "unknown";
57
+ }
58
+ }
59
+
60
+ // --- History Management ---
61
+
62
+ /**
63
+ * Load history from disk (or return empty)
64
+ */
65
+ export function loadHistory(): MetricsHistory {
66
+ const historyPath = path.join(process.cwd(), HISTORY_FILE);
67
+
68
+ if (!fs.existsSync(historyPath)) {
69
+ return {
70
+ version: 1,
71
+ files: {},
72
+ capturedAt: new Date().toISOString(),
73
+ };
74
+ }
75
+
76
+ try {
77
+ const content = fs.readFileSync(historyPath, "utf-8");
78
+ return JSON.parse(content);
79
+ } catch {
80
+ return {
81
+ version: 1,
82
+ files: {},
83
+ capturedAt: new Date().toISOString(),
84
+ };
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Save history to disk
90
+ */
91
+ export function saveHistory(history: MetricsHistory): void {
92
+ const historyDir = path.join(process.cwd(), ".pi-lens");
93
+ if (!fs.existsSync(historyDir)) {
94
+ fs.mkdirSync(historyDir, { recursive: true });
95
+ }
96
+
97
+ history.capturedAt = new Date().toISOString();
98
+ const historyPath = path.join(historyDir, "metrics-history.json");
99
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
100
+ }
101
+
102
+ /**
103
+ * Capture a snapshot for a file's current metrics
104
+ */
105
+ export function captureSnapshot(
106
+ filePath: string,
107
+ metrics: {
108
+ maintainabilityIndex: number;
109
+ cognitiveComplexity: number;
110
+ maxNestingDepth: number;
111
+ linesOfCode: number;
112
+ },
113
+ history?: MetricsHistory,
114
+ ): MetricsHistory {
115
+ const hist = history ?? loadHistory();
116
+ const relativePath = path.relative(process.cwd(), filePath);
117
+ const commit = getCurrentCommit();
118
+
119
+ const snapshot: MetricSnapshot = {
120
+ commit,
121
+ timestamp: new Date().toISOString(),
122
+ mi: Math.round(metrics.maintainabilityIndex * 10) / 10,
123
+ cognitive: metrics.cognitiveComplexity,
124
+ nesting: metrics.maxNestingDepth,
125
+ lines: metrics.linesOfCode,
126
+ };
127
+
128
+ const existing = hist.files[relativePath];
129
+
130
+ if (existing) {
131
+ // Append to history (cap at MAX_HISTORY_PER_FILE)
132
+ existing.history.push(snapshot);
133
+ if (existing.history.length > MAX_HISTORY_PER_FILE) {
134
+ existing.history = existing.history.slice(-MAX_HISTORY_PER_FILE);
135
+ }
136
+ existing.latest = snapshot;
137
+ existing.trend = computeTrend(existing.history);
138
+ } else {
139
+ // New file
140
+ hist.files[relativePath] = {
141
+ latest: snapshot,
142
+ history: [snapshot],
143
+ trend: "stable",
144
+ };
145
+ }
146
+
147
+ return hist;
148
+ }
149
+
150
+ /**
151
+ * Capture snapshots for multiple files
152
+ */
153
+ export function captureSnapshots(
154
+ files: Array<{
155
+ filePath: string;
156
+ metrics: {
157
+ maintainabilityIndex: number;
158
+ cognitiveComplexity: number;
159
+ maxNestingDepth: number;
160
+ linesOfCode: number;
161
+ };
162
+ }>,
163
+ ): MetricsHistory {
164
+ let history = loadHistory();
165
+
166
+ for (const file of files) {
167
+ history = captureSnapshot(file.filePath, file.metrics, history);
168
+ }
169
+
170
+ saveHistory(history);
171
+ return history;
172
+ }
173
+
174
+ // --- Trend Analysis ---
175
+
176
+ /**
177
+ * Compute trend direction from history snapshots
178
+ * Uses last 3 snapshots for stability (or 2 if only 2 available)
179
+ */
180
+ export function computeTrend(history: MetricSnapshot[]): TrendDirection {
181
+ if (history.length < 2) return "stable";
182
+
183
+ const recent = history.slice(-3);
184
+ const first = recent[0];
185
+ const last = recent[recent.length - 1];
186
+
187
+ // Use MI as primary indicator, cognitive as secondary
188
+ const miDelta = last.mi - first.mi;
189
+ const cogDelta = last.cognitive - first.cognitive;
190
+
191
+ // Thresholds (MI changes < 2 are noise)
192
+ if (miDelta > 2) return "improving";
193
+ if (miDelta < -2) return "regressing";
194
+
195
+ // If MI is stable, check cognitive
196
+ if (cogDelta < -10) return "improving";
197
+ if (cogDelta > 10) return "regressing";
198
+
199
+ return "stable";
200
+ }
201
+
202
+ /**
203
+ * Get delta between current snapshot and previous
204
+ */
205
+ export function getDelta(history: FileHistory | null): {
206
+ mi: number;
207
+ cognitive: number;
208
+ trend: TrendDirection;
209
+ } | null {
210
+ if (!history || history.history.length < 2) return null;
211
+
212
+ const current = history.history[history.history.length - 1];
213
+ const previous = history.history[history.history.length - 2];
214
+
215
+ return {
216
+ mi: Math.round((current.mi - previous.mi) * 10) / 10,
217
+ cognitive: current.cognitive - previous.cognitive,
218
+ trend: history.trend,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Get trend emoji for display
224
+ */
225
+ export function getTrendEmoji(trend: TrendDirection): string {
226
+ switch (trend) {
227
+ case "improving":
228
+ return "📈";
229
+ case "regressing":
230
+ return "📉";
231
+ default:
232
+ return "➡️";
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Get trend summary across all files
238
+ */
239
+ export function getTrendSummary(history: MetricsHistory): {
240
+ improving: number;
241
+ regressing: number;
242
+ stable: number;
243
+ worstRegressions: Array<{ file: string; miDelta: number }>;
244
+ } {
245
+ let improving = 0;
246
+ let regressing = 0;
247
+ let stable = 0;
248
+ const regressions: Array<{ file: string; miDelta: number }> = [];
249
+
250
+ for (const [file, fileHistory] of Object.entries(history.files)) {
251
+ switch (fileHistory.trend) {
252
+ case "improving":
253
+ improving++;
254
+ break;
255
+ case "regressing":
256
+ regressing++;
257
+ const delta = getDelta(fileHistory);
258
+ if (delta) {
259
+ regressions.push({ file, miDelta: delta.mi });
260
+ }
261
+ break;
262
+ default:
263
+ stable++;
264
+ }
265
+ }
266
+
267
+ // Sort regressions by MI delta (worst first)
268
+ regressions.sort((a, b) => a.miDelta - b.miDelta);
269
+
270
+ return {
271
+ improving,
272
+ regressing,
273
+ stable,
274
+ worstRegressions: regressions.slice(0, 5),
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Format trend for metrics table
280
+ */
281
+ export function formatTrendCell(
282
+ filePath: string,
283
+ history: MetricsHistory,
284
+ ): string {
285
+ const relativePath = path.relative(process.cwd(), filePath);
286
+ const fileHistory = history.files[relativePath];
287
+
288
+ if (!fileHistory || fileHistory.history.length < 2) {
289
+ return "—"; // No history
290
+ }
291
+
292
+ const delta = getDelta(fileHistory);
293
+ if (!delta) return "—";
294
+
295
+ const emoji = getTrendEmoji(delta.trend);
296
+ const miSign = delta.mi > 0 ? "+" : "";
297
+ const miColor = delta.mi > 0 ? "🟢" : delta.mi < 0 ? "🔴" : "⚪";
298
+
299
+ return `${emoji} ${miColor}${miSign}${delta.mi}`;
300
+ }
@@ -2,8 +2,10 @@
2
2
  * Shared architectural debt scanning — used by booboo-fix and booboo-refactor.
3
3
  * Scans ast-grep skip rules + complexity metrics + architect.yaml rules.
4
4
  */
5
+ import { spawnSync } from "node:child_process";
5
6
  import * as fs from "node:fs";
6
7
  import * as path from "node:path";
8
+ import { getSourceFiles, parseAstGrepJson } from "./scan-utils.js";
7
9
  /**
8
10
  * Scan for skip-category ast-grep violations grouped by absolute file path.
9
11
  */
@@ -11,34 +13,35 @@ export function scanSkipViolations(astGrepClient, configPath, targetPath, isTsPr
11
13
  const skipByFile = new Map();
12
14
  if (!astGrepClient.isAvailable())
13
15
  return skipByFile;
14
- const { spawnSync } = require("node:child_process");
15
16
  const sgResult = spawnSync("npx", [
16
- "sg", "scan", "--config", configPath, "--json",
17
- "--globs", "!**/*.test.ts", "--globs", "!**/*.spec.ts",
18
- "--globs", "!**/test-utils.ts", "--globs", "!**/.pi-lens/**",
17
+ "sg",
18
+ "scan",
19
+ "--config",
20
+ configPath,
21
+ "--json",
22
+ "--globs",
23
+ "!**/*.test.ts",
24
+ "--globs",
25
+ "!**/*.spec.ts",
26
+ "--globs",
27
+ "!**/test-utils.ts",
28
+ "--globs",
29
+ "!**/.pi-lens/**",
19
30
  ...(isTsProject ? ["--globs", "!**/*.js"] : []),
20
31
  targetPath,
21
- ], { encoding: "utf-8", timeout: 30000, shell: true, maxBuffer: 32 * 1024 * 1024 });
22
- const raw = sgResult.stdout?.trim() ?? "";
23
- // biome-ignore lint/suspicious/noExplicitAny: ast-grep JSON output is untyped
24
- const items = raw.startsWith("[")
25
- ? (() => { try {
26
- return JSON.parse(raw);
27
- }
28
- catch {
29
- return [];
30
- } })()
31
- : raw.split("\n").flatMap((l) => { try {
32
- return [JSON.parse(l)];
33
- }
34
- catch {
35
- return [];
36
- } });
32
+ ], {
33
+ encoding: "utf-8",
34
+ timeout: 30000,
35
+ shell: true,
36
+ maxBuffer: 32 * 1024 * 1024,
37
+ });
38
+ const items = parseAstGrepJson(sgResult.stdout?.trim() ?? "");
37
39
  for (const item of items) {
38
40
  const rule = item.ruleId || item.rule?.title || item.name || "unknown";
39
41
  if (!skipRules.has(rule))
40
42
  continue;
41
- const line = (item.labels?.[0]?.range?.start?.line ?? item.range?.start?.line ?? 0) + 1;
43
+ const line = (item.labels?.[0]?.range?.start?.line ?? item.range?.start?.line ?? 0) +
44
+ 1;
42
45
  const absFile = path.resolve(item.file ?? "");
43
46
  const list = skipByFile.get(absFile) ?? [];
44
47
  list.push({ rule, line, note: ruleActions[rule]?.note ?? "" });
@@ -51,26 +54,19 @@ export function scanSkipViolations(astGrepClient, configPath, targetPath, isTsPr
51
54
  */
52
55
  export function scanComplexityMetrics(complexityClient, targetPath, isTsProject) {
53
56
  const metricsByFile = new Map();
54
- const scanDir = (dir) => {
55
- if (!fs.existsSync(dir))
56
- return;
57
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
58
- const full = path.join(dir, entry.name);
59
- if (entry.isDirectory()) {
60
- if (["node_modules", ".git", "dist", "build", ".next", ".pi-lens"].includes(entry.name))
61
- continue;
62
- scanDir(full);
63
- }
64
- else if (complexityClient.isSupportedFile(full) &&
65
- !/\.(test|spec)\.[jt]sx?$/.test(entry.name) &&
66
- !(isTsProject && /\.js$/.test(entry.name))) {
67
- const m = complexityClient.analyzeFile(full);
68
- if (m)
69
- metricsByFile.set(full, { mi: m.maintainabilityIndex, cognitive: m.cognitiveComplexity, nesting: m.maxNestingDepth });
70
- }
57
+ const files = getSourceFiles(targetPath, isTsProject);
58
+ for (const full of files) {
59
+ if (complexityClient.isSupportedFile(full) &&
60
+ !/\.(test|spec)\.[jt]sx?$/.test(path.basename(full))) {
61
+ const m = complexityClient.analyzeFile(full);
62
+ if (m)
63
+ metricsByFile.set(full, {
64
+ mi: m.maintainabilityIndex,
65
+ cognitive: m.cognitiveComplexity,
66
+ nesting: m.maxNestingDepth,
67
+ });
71
68
  }
72
- };
73
- scanDir(targetPath);
69
+ }
74
70
  return metricsByFile;
75
71
  }
76
72
  /**
@@ -81,37 +77,27 @@ export function scanArchitectViolations(architectClient, targetPath) {
81
77
  const violationsByFile = new Map();
82
78
  if (!architectClient.hasConfig())
83
79
  return violationsByFile;
84
- const scanDir = (dir) => {
85
- if (!fs.existsSync(dir))
86
- return;
87
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
88
- const full = path.join(dir, entry.name);
89
- if (entry.isDirectory()) {
90
- if (["node_modules", ".git", "dist", "build", ".next", ".pi-lens"].includes(entry.name))
91
- continue;
92
- scanDir(full);
93
- }
94
- else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
95
- const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
96
- const content = fs.readFileSync(full, "utf-8");
97
- const lineCount = content.split("\n").length;
98
- const msgs = [];
99
- // Check pattern violations
100
- for (const v of architectClient.checkFile(relPath, content)) {
101
- msgs.push(v.message);
102
- }
103
- // Check file size
104
- const sizeV = architectClient.checkFileSize(relPath, lineCount);
105
- if (sizeV) {
106
- msgs.push(sizeV.message);
107
- }
108
- if (msgs.length > 0) {
109
- violationsByFile.set(full, msgs);
110
- }
111
- }
80
+ const isTsProject = fs.existsSync(path.join(targetPath, "tsconfig.json"));
81
+ const files = getSourceFiles(targetPath, isTsProject);
82
+ for (const full of files) {
83
+ const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
84
+ const content = fs.readFileSync(full, "utf-8");
85
+ const lineCount = content.split("\n").length;
86
+ const msgs = [];
87
+ // Check pattern violations
88
+ for (const v of architectClient.checkFile(relPath, content)) {
89
+ const lineStr = v.line ? `L${v.line}: ` : "";
90
+ msgs.push(`${lineStr}${v.message}`);
112
91
  }
113
- };
114
- scanDir(targetPath);
92
+ // Check file size
93
+ const sizeV = architectClient.checkFileSize(relPath, lineCount);
94
+ if (sizeV) {
95
+ msgs.push(sizeV.message);
96
+ }
97
+ if (msgs.length > 0) {
98
+ violationsByFile.set(full, msgs);
99
+ }
100
+ }
115
101
  return violationsByFile;
116
102
  }
117
103
  /**
@@ -170,9 +156,13 @@ export function scoreFiles(skipByFile, metricsByFile, architectViolations) {
170
156
  export function extractCodeSnippet(filePath, firstLine, contextLines = 2, maxLines = 45) {
171
157
  try {
172
158
  const fileLines = fs.readFileSync(filePath, "utf-8").split("\n");
173
- const start = Math.max(0, (firstLine - 1) - contextLines);
159
+ const start = Math.max(0, firstLine - 1 - contextLines);
174
160
  const end = Math.min(fileLines.length, start + maxLines);
175
- return { snippet: fileLines.slice(start, end).join("\n"), start: start + 1, end };
161
+ return {
162
+ snippet: fileLines.slice(start, end).join("\n"),
163
+ start: start + 1,
164
+ end,
165
+ };
176
166
  }
177
167
  catch {
178
168
  return null;