aislop 0.8.3 → 0.9.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.
package/dist/cli.js CHANGED
@@ -4,14 +4,14 @@ import { Command } from "commander";
4
4
  import os from "node:os";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
+ import crypto, { randomUUID } from "node:crypto";
8
+ import { performance } from "node:perf_hooks";
7
9
  import YAML from "yaml";
8
10
  import { z } from "zod/v4";
9
- import { performance } from "node:perf_hooks";
10
11
  import { execSync, spawn, spawnSync } from "node:child_process";
11
12
  import micromatch from "micromatch";
12
13
  import { fileURLToPath } from "node:url";
13
14
  import ts from "typescript";
14
- import crypto from "node:crypto";
15
15
  import { isCancel, multiselect, select, text } from "@clack/prompts";
16
16
  import pc from "picocolors";
17
17
  import wcwidth from "wcwidth";
@@ -32,6 +32,366 @@ var __exportAll = (all, no_symbols) => {
32
32
  return target;
33
33
  };
34
34
 
35
+ //#endregion
36
+ //#region src/version.ts
37
+ const APP_VERSION = "0.9.1";
38
+
39
+ //#endregion
40
+ //#region src/telemetry/env.ts
41
+ const detectPackageManager$1 = (env = process.env) => {
42
+ const execPath = env.npm_execpath ?? "";
43
+ if (execPath.includes("npx")) return "npx";
44
+ const userAgent = env.npm_config_user_agent ?? "";
45
+ if (userAgent.startsWith("pnpm/")) return "pnpm";
46
+ if (userAgent.startsWith("yarn/")) return "yarn";
47
+ if (userAgent.startsWith("bun/")) return "bun";
48
+ if (userAgent.startsWith("npm/")) return "npm";
49
+ if (execPath.includes("pnpm")) return "pnpm";
50
+ if (execPath.includes("yarn")) return "yarn";
51
+ if (execPath.includes("bun")) return "bun";
52
+ if (execPath.includes("npm")) return "npm";
53
+ return "unknown";
54
+ };
55
+ const CI_ENV_KEYS = [
56
+ "CI",
57
+ "GITHUB_ACTIONS",
58
+ "GITLAB_CI",
59
+ "CIRCLECI",
60
+ "TRAVIS",
61
+ "BUILDKITE",
62
+ "DRONE",
63
+ "TEAMCITY_VERSION",
64
+ "TF_BUILD"
65
+ ];
66
+ const isCiEnv = (env = process.env) => CI_ENV_KEYS.some((k) => {
67
+ const v = env[k];
68
+ return v === "true" || v === "1" || v != null && v.length > 0 && k !== "CI";
69
+ }) || env.CI === "true" || env.CI === "1";
70
+ const fileCountBucket = (count) => {
71
+ if (count < 10) return "0-10";
72
+ if (count < 50) return "10-50";
73
+ if (count < 100) return "50-100";
74
+ if (count < 500) return "100-500";
75
+ if (count < 1e3) return "500-1000";
76
+ return "1000+";
77
+ };
78
+ const scoreBucket = (score) => {
79
+ if (score >= 75) return "75-100";
80
+ if (score >= 50) return "50-75";
81
+ if (score >= 25) return "25-50";
82
+ return "0-25";
83
+ };
84
+
85
+ //#endregion
86
+ //#region src/telemetry/identity.ts
87
+ const FILE_BASENAME = "install_id";
88
+ const resolveInstallIdPath = (homedir = os.homedir(), env = process.env) => {
89
+ if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", FILE_BASENAME);
90
+ return path.join(homedir, ".aislop", FILE_BASENAME);
91
+ };
92
+ const ensureInstallId = (idPath = resolveInstallIdPath()) => {
93
+ if (fs.existsSync(idPath)) {
94
+ const existing = fs.readFileSync(idPath, "utf-8").trim();
95
+ if (existing.length > 0) return {
96
+ installId: existing,
97
+ created: false
98
+ };
99
+ }
100
+ const dir = path.dirname(idPath);
101
+ fs.mkdirSync(dir, { recursive: true });
102
+ const installId = randomUUID();
103
+ const tmpPath = `${idPath}.${process.pid}.tmp`;
104
+ fs.writeFileSync(tmpPath, `${installId}\n`, { mode: 384 });
105
+ try {
106
+ fs.renameSync(tmpPath, idPath);
107
+ return {
108
+ installId,
109
+ created: true
110
+ };
111
+ } catch {
112
+ fs.rmSync(tmpPath, { force: true });
113
+ return {
114
+ installId: fs.readFileSync(idPath, "utf-8").trim(),
115
+ created: false
116
+ };
117
+ }
118
+ };
119
+
120
+ //#endregion
121
+ //#region src/telemetry/redaction.ts
122
+ const SAFE_PROPERTY_NAMES = new Set([
123
+ "aislop_version",
124
+ "node_version",
125
+ "os",
126
+ "arch",
127
+ "schema_version",
128
+ "anonymous_install_id",
129
+ "package_manager",
130
+ "is_ci",
131
+ "command",
132
+ "language_summary",
133
+ "lang_typescript",
134
+ "lang_javascript",
135
+ "lang_python",
136
+ "lang_java",
137
+ "file_count_bucket",
138
+ "exit_code",
139
+ "duration_ms",
140
+ "error_kind",
141
+ "score",
142
+ "score_bucket",
143
+ "finding_count",
144
+ "error_count",
145
+ "warning_count",
146
+ "fixable_count",
147
+ "fix_steps",
148
+ "fix_resolved",
149
+ "fix_score_delta",
150
+ "engine_format_issues",
151
+ "engine_format_ms",
152
+ "engine_lint_issues",
153
+ "engine_lint_ms",
154
+ "engine_code_quality_issues",
155
+ "engine_code_quality_ms",
156
+ "engine_ai_slop_issues",
157
+ "engine_ai_slop_ms",
158
+ "engine_architecture_issues",
159
+ "engine_architecture_ms",
160
+ "engine_security_issues",
161
+ "engine_security_ms",
162
+ "tool",
163
+ "ok",
164
+ "agent",
165
+ "score_delta"
166
+ ]);
167
+ const redactProperties = (props) => {
168
+ const clean = {};
169
+ const dropped = [];
170
+ for (const [key, value] of Object.entries(props)) {
171
+ if (value === void 0) continue;
172
+ if (SAFE_PROPERTY_NAMES.has(key)) clean[key] = value;
173
+ else dropped.push(key);
174
+ }
175
+ return {
176
+ clean,
177
+ dropped
178
+ };
179
+ };
180
+
181
+ //#endregion
182
+ //#region src/telemetry/client.ts
183
+ const POSTHOG_HOST = "https://eu.i.posthog.com";
184
+ const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
185
+ const SCHEMA_VERSION = "v2";
186
+ const REQUEST_TIMEOUT_MS = 3e3;
187
+ const isTelemetryDisabled = (config) => {
188
+ const env = process.env;
189
+ if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
190
+ if (config?.enabled === false) return true;
191
+ if (config?.enabled === true) return false;
192
+ if (env.CI === "true" || env.CI === "1") return true;
193
+ return false;
194
+ };
195
+ const isDebug = () => process.env.AISLOP_TELEMETRY_DEBUG === "1";
196
+ const pendingRequests = /* @__PURE__ */ new Set();
197
+ let cachedInstallId = null;
198
+ let installCreated = false;
199
+ const baseProperties = (installId) => ({
200
+ aislop_version: APP_VERSION,
201
+ node_version: process.version,
202
+ os: os.platform(),
203
+ arch: os.arch(),
204
+ schema_version: SCHEMA_VERSION,
205
+ anonymous_install_id: installId,
206
+ package_manager: detectPackageManager$1(),
207
+ is_ci: isCiEnv()
208
+ });
209
+ const track = (input) => {
210
+ if (isTelemetryDisabled(input.config)) return { installCreated: false };
211
+ if (cachedInstallId == null) {
212
+ const ensured = ensureInstallId(resolveInstallIdPath());
213
+ cachedInstallId = ensured.installId;
214
+ installCreated = ensured.created;
215
+ }
216
+ const { clean, dropped } = redactProperties({
217
+ ...baseProperties(cachedInstallId),
218
+ ...input.properties
219
+ });
220
+ if (isDebug()) {
221
+ const compact = JSON.stringify({
222
+ event: input.event,
223
+ properties: clean
224
+ });
225
+ process.stderr.write(`[telemetry] ${compact}\n`);
226
+ if (dropped.length > 0) for (const key of dropped) process.stderr.write(`[telemetry] dropped non-allowlisted property: ${key}\n`);
227
+ }
228
+ if (process.env.AISLOP_TELEMETRY_DRY_RUN === "1") return { installCreated };
229
+ const payload = {
230
+ api_key: POSTHOG_KEY,
231
+ event: input.event,
232
+ distinct_id: cachedInstallId,
233
+ properties: clean,
234
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
235
+ };
236
+ const request = fetch(`${POSTHOG_HOST}/capture/`, {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify(payload),
240
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
241
+ }).then(() => {}).catch(() => {}).finally(() => {
242
+ pendingRequests.delete(request);
243
+ });
244
+ pendingRequests.add(request);
245
+ return { installCreated };
246
+ };
247
+ const flushTelemetry = async () => {
248
+ if (pendingRequests.size === 0) return;
249
+ await Promise.all(pendingRequests);
250
+ };
251
+
252
+ //#endregion
253
+ //#region src/telemetry/language.ts
254
+ const ALL_LANGUAGES = [
255
+ "typescript",
256
+ "javascript",
257
+ "python",
258
+ "java"
259
+ ];
260
+ const buildLanguageProperties = (detected) => {
261
+ const present = new Set(detected);
262
+ const summary = [...present].filter((l) => ALL_LANGUAGES.includes(l));
263
+ summary.sort();
264
+ return {
265
+ language_summary: summary.join(","),
266
+ lang_typescript: present.has("typescript"),
267
+ lang_javascript: present.has("javascript"),
268
+ lang_python: present.has("python"),
269
+ lang_java: present.has("java")
270
+ };
271
+ };
272
+
273
+ //#endregion
274
+ //#region src/telemetry/events.ts
275
+ const buildCommandStartedProps = (input) => {
276
+ const props = { command: input.command };
277
+ if (input.languages) Object.assign(props, buildLanguageProperties(input.languages));
278
+ if (typeof input.fileCount === "number") props.file_count_bucket = fileCountBucket(input.fileCount);
279
+ return props;
280
+ };
281
+ const ENGINE_KEY_MAP = {
282
+ format: "engine_format",
283
+ lint: "engine_lint",
284
+ "code-quality": "engine_code_quality",
285
+ "ai-slop": "engine_ai_slop",
286
+ architecture: "engine_architecture",
287
+ security: "engine_security"
288
+ };
289
+ const flattenEngineStats = (issues, timings) => {
290
+ const out = {};
291
+ for (const [engine, count] of Object.entries(issues)) {
292
+ const key = ENGINE_KEY_MAP[engine];
293
+ if (key != null && typeof count === "number") out[`${key}_issues`] = count;
294
+ }
295
+ for (const [engine, ms] of Object.entries(timings)) {
296
+ const key = ENGINE_KEY_MAP[engine];
297
+ if (key != null && typeof ms === "number") out[`${key}_ms`] = Math.round(ms);
298
+ }
299
+ return out;
300
+ };
301
+ const buildCommandCompletedProps = (input) => {
302
+ const props = {
303
+ ...input.startProps,
304
+ exit_code: input.exitCode,
305
+ duration_ms: Math.round(input.durationMs)
306
+ };
307
+ if (input.errorKind) props.error_kind = input.errorKind;
308
+ if (typeof input.score === "number") {
309
+ props.score = input.score;
310
+ props.score_bucket = scoreBucket(input.score);
311
+ }
312
+ if (typeof input.findingCount === "number") props.finding_count = input.findingCount;
313
+ if (typeof input.errorCount === "number") props.error_count = input.errorCount;
314
+ if (typeof input.warningCount === "number") props.warning_count = input.warningCount;
315
+ if (typeof input.fixableCount === "number") props.fixable_count = input.fixableCount;
316
+ if (input.engineIssues && input.engineTimings) Object.assign(props, flattenEngineStats(input.engineIssues, input.engineTimings));
317
+ if (typeof input.fixSteps === "number") props.fix_steps = input.fixSteps;
318
+ if (typeof input.fixResolved === "number") props.fix_resolved = input.fixResolved;
319
+ if (typeof input.fixScoreDelta === "number") props.fix_score_delta = input.fixScoreDelta;
320
+ return props;
321
+ };
322
+ const buildHookScanCompletedProps = (input) => {
323
+ const props = {
324
+ agent: input.agent,
325
+ score: input.score,
326
+ score_bucket: scoreBucket(input.score),
327
+ finding_count: input.findingCount,
328
+ file_count_bucket: fileCountBucket(input.fileCount)
329
+ };
330
+ if (typeof input.scoreDelta === "number") props.score_delta = input.scoreDelta;
331
+ return props;
332
+ };
333
+ const errorKindFromException = (error) => {
334
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
335
+ if (message.includes("timeout") || message.includes("timed out")) return "timeout";
336
+ if (message.includes("invalid config") || message.includes("config_invalid")) return "config_invalid";
337
+ if (message.includes("engine") && message.includes("crash")) return "engine_crash";
338
+ return "unknown";
339
+ };
340
+
341
+ //#endregion
342
+ //#region src/telemetry/lifecycle.ts
343
+ const withCommandLifecycle = async (start, run) => {
344
+ const startProps = buildCommandStartedProps({
345
+ command: start.command,
346
+ languages: start.languages,
347
+ fileCount: start.fileCount
348
+ });
349
+ track({
350
+ event: "cli_command_started",
351
+ properties: startProps,
352
+ config: start.config
353
+ });
354
+ const startedAt = performance.now();
355
+ try {
356
+ const result = await run();
357
+ const durationMs = performance.now() - startedAt;
358
+ track({
359
+ event: "cli_command_completed",
360
+ properties: buildCommandCompletedProps({
361
+ startProps,
362
+ exitCode: result.exitCode,
363
+ durationMs,
364
+ score: result.score,
365
+ findingCount: result.findingCount,
366
+ errorCount: result.errorCount,
367
+ warningCount: result.warningCount,
368
+ fixableCount: result.fixableCount,
369
+ engineIssues: result.engineIssues,
370
+ engineTimings: result.engineTimings,
371
+ fixSteps: result.fixSteps,
372
+ fixResolved: result.fixResolved,
373
+ fixScoreDelta: result.fixScoreDelta
374
+ }),
375
+ config: start.config
376
+ });
377
+ await flushTelemetry();
378
+ return result;
379
+ } catch (error) {
380
+ track({
381
+ event: "cli_command_completed",
382
+ properties: buildCommandCompletedProps({
383
+ startProps,
384
+ exitCode: 1,
385
+ durationMs: performance.now() - startedAt,
386
+ errorKind: errorKindFromException(error)
387
+ }),
388
+ config: start.config
389
+ });
390
+ await flushTelemetry();
391
+ throw error;
392
+ }
393
+ };
394
+
35
395
  //#endregion
36
396
  //#region src/hooks/feedback.ts
37
397
  const fingerprintFinding = (f) => `${f.file}:${f.line}:${f.ruleId}`;
@@ -104,7 +464,21 @@ const buildSuggestedActions = (diagnostics, findings, regressed, delta) => {
104
464
  });
105
465
  return actions;
106
466
  };
107
- const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
467
+ const buildAccountability = (meta, findings, regressed, newSinceBaseline) => {
468
+ if (!meta?.agent && (!meta?.touchedFiles || meta.touchedFiles.length === 0)) return void 0;
469
+ const touchedFiles = Array.from(new Set(meta.touchedFiles ?? []));
470
+ const newFindingCount = newSinceBaseline?.length ?? findings.length;
471
+ const mustFixBeforeDone = regressed || findings.some((f) => f.severity === "error");
472
+ const reason = mustFixBeforeDone ? regressed ? "Score regressed against the captured baseline. The agent should fix or justify the new findings before finishing." : "Error-severity findings remain in files touched by this agent turn." : "No blocking regression detected for this agent turn.";
473
+ return {
474
+ agent: meta.agent,
475
+ touchedFiles,
476
+ newFindingCount,
477
+ mustFixBeforeDone,
478
+ reason
479
+ };
480
+ };
481
+ const buildFeedback = (diagnostics, score, rootDirectory, baseline, meta) => {
108
482
  const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
109
483
  const capped = all.slice(0, MAX_FINDINGS);
110
484
  const elided = all.length > MAX_FINDINGS ? all.length - MAX_FINDINGS : void 0;
@@ -132,6 +506,7 @@ const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
132
506
  baseline: baselineScore,
133
507
  delta,
134
508
  regressed,
509
+ accountability: buildAccountability(meta, capped, regressed, newSinceBaseline),
135
510
  counts,
136
511
  findings: capped,
137
512
  elided,
@@ -188,6 +563,7 @@ const DEFAULT_CONFIG = {
188
563
  "build",
189
564
  "coverage"
190
565
  ],
566
+ include: [],
191
567
  engines: {
192
568
  format: true,
193
569
  lint: true,
@@ -223,7 +599,7 @@ const DEFAULT_CONFIG = {
223
599
  smoothing: 20
224
600
  },
225
601
  ci: {
226
- failBelow: 0,
602
+ failBelow: 70,
227
603
  format: "json"
228
604
  },
229
605
  telemetry: { enabled: true }
@@ -349,7 +725,7 @@ const ScoringSchema = z.object({
349
725
  smoothing: z.number().nonnegative().default(20)
350
726
  });
351
727
  const CiSchema = z.object({
352
- failBelow: z.number().default(0),
728
+ failBelow: z.number().default(70),
353
729
  format: z.enum(["json"]).default("json")
354
730
  });
355
731
  const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
@@ -383,7 +759,7 @@ const AislopConfigSchema = z.object({
383
759
  smoothing: 20
384
760
  })),
385
761
  ci: CiSchema.default(() => ({
386
- failBelow: 0,
762
+ failBelow: 70,
387
763
  format: "json"
388
764
  })),
389
765
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
@@ -393,7 +769,8 @@ const AislopConfigSchema = z.object({
393
769
  "dist",
394
770
  "build",
395
771
  "coverage"
396
- ])
772
+ ]),
773
+ include: z.array(z.string()).default(() => [])
397
774
  });
398
775
  const defaults = AislopConfigSchema.parse({});
399
776
  /**
@@ -473,31 +850,68 @@ const EXCLUDED_DIRS = [
473
850
  "dist",
474
851
  "build",
475
852
  ".git",
853
+ ".agents",
476
854
  "vendor",
855
+ "examples",
856
+ "example",
857
+ "demos",
858
+ "demo",
859
+ "bench",
860
+ "benches",
861
+ "benchmarks",
862
+ "fixtures",
863
+ "fixture",
864
+ "samples",
865
+ "sample",
866
+ "tutorials",
867
+ "tutorial",
868
+ "code_samples",
869
+ "code-samples",
870
+ "notebooks",
477
871
  "tests",
478
872
  "test",
479
873
  "__tests__",
480
874
  "__test__",
481
875
  "spec",
482
876
  "__mocks__",
483
- "fixtures",
484
877
  "test_data",
485
878
  ".next",
486
879
  ".nuxt",
487
880
  "coverage",
488
- ".turbo"
881
+ ".turbo",
882
+ "public"
489
883
  ];
490
884
  const FIND_PRUNE_DIRS = [
491
885
  "node_modules",
492
886
  "dist",
493
887
  "build",
494
888
  ".git",
889
+ ".agents",
495
890
  "vendor",
891
+ "examples",
892
+ "example",
893
+ "demos",
894
+ "demo",
895
+ "bench",
896
+ "benches",
897
+ "benchmarks",
898
+ "fixtures",
899
+ "fixture",
900
+ "samples",
901
+ "sample",
902
+ "tutorials",
903
+ "tutorial",
904
+ "code_samples",
905
+ "code-samples",
906
+ "notebooks",
496
907
  ".next",
497
908
  ".nuxt",
498
909
  "coverage",
499
- ".turbo"
910
+ ".turbo",
911
+ "public"
500
912
  ];
913
+ const BUILD_CACHE_FILE_PATTERNS = [/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i];
914
+ const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
501
915
  const TEST_FILE_PATTERNS = [
502
916
  /(?:^|\/).*\.test\.[^/]+$/i,
503
917
  /(?:^|\/).*\.spec\.[^/]+$/i,
@@ -522,6 +936,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
522
936
  return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
523
937
  };
524
938
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
939
+ const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
525
940
  const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
526
941
  const getIgnoredPaths = (rootDirectory, files) => {
527
942
  if (files.length === 0) return /* @__PURE__ */ new Set();
@@ -580,7 +995,7 @@ const normalizeExcludePatterns = (patterns) => {
580
995
  return [p];
581
996
  });
582
997
  };
583
- const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = []) => {
998
+ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = [], include = []) => {
584
999
  const extraSet = new Set(extraExtensions);
585
1000
  const normalizedFiles = files.map((file) => {
586
1001
  const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
@@ -595,8 +1010,16 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
595
1010
  if (!normalizedExcludePatterns.length) return false;
596
1011
  return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
597
1012
  };
1013
+ const hasIncludePatterns = include.length > 0;
1014
+ const isUserIncluded = (relativePath) => {
1015
+ if (!hasIncludePatterns) return true;
1016
+ return micromatch.isMatch(relativePath, include, { dot: true });
1017
+ };
598
1018
  return normalizedFiles.filter(({ absolutePath, relativePath }) => {
599
- return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile$2(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
1019
+ if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || ignoredPaths.has(relativePath)) return false;
1020
+ if (!isUserIncluded(relativePath)) return false;
1021
+ if (isUserExcluded(relativePath)) return false;
1022
+ return hasAllowedExtension(relativePath, extraSet);
600
1023
  }).map(({ absolutePath }) => absolutePath);
601
1024
  };
602
1025
  const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
@@ -1326,6 +1749,86 @@ const PYTHON_IMPORT_TO_PIP = {
1326
1749
  redis: "redis"
1327
1750
  };
1328
1751
 
1752
+ //#endregion
1753
+ //#region src/engines/ai-slop/python-manifest.ts
1754
+ const addPyDep = (pyDeps, name) => {
1755
+ const normalized = name.toLowerCase().replace(/_/g, "-");
1756
+ pyDeps.add(normalized);
1757
+ };
1758
+ const collectFromRequirementsTxt = (rootDir, pyDeps) => {
1759
+ const reqPath = path.join(rootDir, "requirements.txt");
1760
+ if (!fs.existsSync(reqPath)) return false;
1761
+ try {
1762
+ const content = fs.readFileSync(reqPath, "utf-8");
1763
+ for (const line of content.split("\n")) {
1764
+ const trimmed = line.trim();
1765
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
1766
+ const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
1767
+ if (match) addPyDep(pyDeps, match[1]);
1768
+ }
1769
+ return true;
1770
+ } catch {
1771
+ return false;
1772
+ }
1773
+ };
1774
+ const collectFromPyproject = (rootDir, pyDeps) => {
1775
+ const pyprojPath = path.join(rootDir, "pyproject.toml");
1776
+ if (!fs.existsSync(pyprojPath)) return false;
1777
+ try {
1778
+ const content = fs.readFileSync(pyprojPath, "utf-8");
1779
+ const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1780
+ if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
1781
+ const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1782
+ if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
1783
+ const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
1784
+ if (pep621) for (const line of pep621[1].split("\n")) {
1785
+ const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
1786
+ if (m) addPyDep(pyDeps, m[1]);
1787
+ }
1788
+ const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
1789
+ let match = poetryRe.exec(content);
1790
+ while (match !== null) {
1791
+ for (const line of match[1].split("\n")) {
1792
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1793
+ if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
1794
+ }
1795
+ match = poetryRe.exec(content);
1796
+ }
1797
+ return true;
1798
+ } catch {
1799
+ return false;
1800
+ }
1801
+ };
1802
+ const collectFromPipfile = (rootDir, pyDeps) => {
1803
+ const pipfilePath = path.join(rootDir, "Pipfile");
1804
+ if (!fs.existsSync(pipfilePath)) return false;
1805
+ try {
1806
+ const content = fs.readFileSync(pipfilePath, "utf-8");
1807
+ const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
1808
+ let match = sectionRe.exec(content);
1809
+ while (match !== null) {
1810
+ for (const line of match[2].split("\n")) {
1811
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1812
+ if (m) addPyDep(pyDeps, m[1]);
1813
+ }
1814
+ match = sectionRe.exec(content);
1815
+ }
1816
+ return true;
1817
+ } catch {
1818
+ return false;
1819
+ }
1820
+ };
1821
+ const collectPythonDeps = (rootDir) => {
1822
+ const pyDeps = /* @__PURE__ */ new Set();
1823
+ const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
1824
+ const hasPyproject = collectFromPyproject(rootDir, pyDeps);
1825
+ const hasPipfile = collectFromPipfile(rootDir, pyDeps);
1826
+ return {
1827
+ pyDeps,
1828
+ hasPyManifest: hasReq || hasPyproject || hasPipfile
1829
+ };
1830
+ };
1831
+
1329
1832
  //#endregion
1330
1833
  //#region src/engines/ai-slop/hallucinated-imports.ts
1331
1834
  const JS_EXTENSIONS$2 = new Set([
@@ -1462,10 +1965,26 @@ const buildAliasMatcher = (key) => {
1462
1965
  };
1463
1966
  const collectAliasMatchersFromConfig = (configPath, matchers) => {
1464
1967
  const opts = readJson(configPath)?.compilerOptions;
1465
- if (!opts || typeof opts !== "object") return;
1968
+ if (!opts) return;
1466
1969
  const paths = opts.paths;
1467
- if (!paths || typeof paths !== "object") return;
1468
- for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
1970
+ if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
1971
+ const baseUrl = opts.baseUrl;
1972
+ if (typeof baseUrl === "string") {
1973
+ const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
1974
+ let entries;
1975
+ try {
1976
+ entries = fs.readdirSync(baseUrlDir);
1977
+ } catch {
1978
+ return;
1979
+ }
1980
+ const baseSpecifiers = /* @__PURE__ */ new Set();
1981
+ for (const entry of entries) {
1982
+ if (entry.startsWith(".") || entry === "node_modules") continue;
1983
+ const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
1984
+ if (base.length > 0) baseSpecifiers.add(base);
1985
+ }
1986
+ for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
1987
+ }
1469
1988
  };
1470
1989
  const collectTsPathAliases = (rootDir) => {
1471
1990
  const matchers = [];
@@ -1473,97 +1992,35 @@ const collectTsPathAliases = (rootDir) => {
1473
1992
  for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
1474
1993
  return matchers;
1475
1994
  };
1476
- const addPyDep = (pyDeps, name) => {
1477
- const normalized = name.toLowerCase().replace(/_/g, "-");
1478
- pyDeps.add(normalized);
1479
- };
1480
- const collectFromRequirementsTxt = (rootDir, pyDeps) => {
1481
- const reqPath = path.join(rootDir, "requirements.txt");
1482
- if (!fs.existsSync(reqPath)) return false;
1483
- try {
1484
- const content = fs.readFileSync(reqPath, "utf-8");
1485
- for (const line of content.split("\n")) {
1486
- const trimmed = line.trim();
1487
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
1488
- const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
1489
- if (match) addPyDep(pyDeps, match[1]);
1490
- }
1491
- return true;
1492
- } catch {
1493
- return false;
1494
- }
1495
- };
1496
- const collectFromPyproject = (rootDir, pyDeps) => {
1497
- const pyprojPath = path.join(rootDir, "pyproject.toml");
1498
- if (!fs.existsSync(pyprojPath)) return false;
1499
- try {
1500
- const content = fs.readFileSync(pyprojPath, "utf-8");
1501
- const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1502
- if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
1503
- const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1504
- if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
1505
- const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
1506
- if (pep621) for (const line of pep621[1].split("\n")) {
1507
- const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
1508
- if (m) addPyDep(pyDeps, m[1]);
1509
- }
1510
- const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
1511
- let match = poetryRe.exec(content);
1512
- while (match !== null) {
1513
- for (const line of match[1].split("\n")) {
1514
- const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1515
- if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
1516
- }
1517
- match = poetryRe.exec(content);
1518
- }
1519
- return true;
1520
- } catch {
1521
- return false;
1522
- }
1523
- };
1524
- const collectFromPipfile = (rootDir, pyDeps) => {
1525
- const pipfilePath = path.join(rootDir, "Pipfile");
1526
- if (!fs.existsSync(pipfilePath)) return false;
1527
- try {
1528
- const content = fs.readFileSync(pipfilePath, "utf-8");
1529
- const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
1530
- let match = sectionRe.exec(content);
1531
- while (match !== null) {
1532
- for (const line of match[2].split("\n")) {
1533
- const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1534
- if (m) addPyDep(pyDeps, m[1]);
1535
- }
1536
- match = sectionRe.exec(content);
1537
- }
1538
- return true;
1539
- } catch {
1540
- return false;
1541
- }
1542
- };
1543
1995
  const loadManifest = (rootDir) => {
1544
1996
  const jsDeps = /* @__PURE__ */ new Set();
1545
- const pyDeps = /* @__PURE__ */ new Set();
1546
1997
  const hasJsManifest = collectJsDeps(rootDir, jsDeps);
1547
- const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
1548
- const hasPyproject = collectFromPyproject(rootDir, pyDeps);
1549
- const hasPipfile = collectFromPipfile(rootDir, pyDeps);
1998
+ const { pyDeps, hasPyManifest } = collectPythonDeps(rootDir);
1550
1999
  return {
1551
2000
  jsDeps,
1552
2001
  pyDeps,
1553
2002
  hasJsManifest,
1554
- hasPyManifest: hasReq || hasPyproject || hasPipfile
2003
+ hasPyManifest
1555
2004
  };
1556
2005
  };
1557
2006
  const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
2007
+ const RUNTIME_BUILTINS = new Set(["bun"]);
1558
2008
  const isJsBuiltin = (spec) => {
2009
+ if (RUNTIME_BUILTINS.has(spec)) return true;
1559
2010
  return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
1560
2011
  };
1561
2012
  const VIRTUAL_MODULE_PREFIXES = [
1562
2013
  "astro:",
1563
2014
  "virtual:",
1564
- "bun:"
2015
+ "bun:",
2016
+ "~icons/"
1565
2017
  ];
1566
2018
  const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
2019
+ const stripImportQuery = (spec) => {
2020
+ const idx = spec.indexOf("?");
2021
+ return idx === -1 ? spec : spec.slice(0, idx);
2022
+ };
2023
+ const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
1567
2024
  const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
1568
2025
  const isLikelyRealImportSpec = (spec) => {
1569
2026
  if (spec.length === 0) return false;
@@ -1630,10 +2087,14 @@ const extractPyImports = (content) => {
1630
2087
  }
1631
2088
  return results;
1632
2089
  };
1633
- const checkJsImport = (spec, manifest, tsAliasMatchers) => {
2090
+ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
2091
+ const spec = stripImportQuery(rawSpec);
2092
+ if (spec.length === 0) return null;
1634
2093
  if (isJsRelativeOrAbsolute(spec)) return null;
1635
2094
  if (isJsBuiltin(spec)) return null;
1636
2095
  if (isJsVirtualModule(spec)) return null;
2096
+ const virtualOwner = VIRTUAL_ASSET_FILES[spec];
2097
+ if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
1637
2098
  if (tsAliasMatchers.some((m) => m(spec))) return null;
1638
2099
  const pkg = packageNameFromImport(spec);
1639
2100
  if (manifest.jsDeps.has(pkg)) return null;
@@ -2989,64 +3450,88 @@ const analyzeFunctions = (content, ext) => {
2989
3450
  }
2990
3451
  return functions;
2991
3452
  };
2992
- const JSX_FILE_LOC_MULTIPLIER = 1.5;
3453
+ const FILE_LOC_MULTIPLIERS = {
3454
+ ".tsx": 1.5,
3455
+ ".jsx": 1.5,
3456
+ ".rs": 2.5,
3457
+ ".go": 1.5
3458
+ };
3459
+ const DECLARATION_FILE_RE = /\.d\.ts$/i;
3460
+ const fileLocBudget = (ext, relativePath, base) => {
3461
+ if (DECLARATION_FILE_RE.test(relativePath)) return Number.POSITIVE_INFINITY;
3462
+ const multiplier = FILE_LOC_MULTIPLIERS[ext] ?? 1;
3463
+ return Math.ceil(base * multiplier);
3464
+ };
2993
3465
  const checkFileDiagnostics = (relativePath, content, limits) => {
2994
3466
  const results = [];
2995
3467
  const lineCount = content.split("\n").length;
2996
3468
  const ext = path.extname(relativePath).toLowerCase();
2997
3469
  if (isDataFile(content)) return results;
2998
- const configuredMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
3470
+ const configuredMax = fileLocBudget(ext, relativePath, limits.maxFileLoc);
3471
+ if (!Number.isFinite(configuredMax)) return results;
2999
3472
  if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
3000
3473
  filePath: relativePath,
3001
3474
  engine: "code-quality",
3002
3475
  rule: "complexity/file-too-large",
3003
3476
  severity: "warning",
3004
- message: `File has ${lineCount} lines (max: ${configuredMax})`,
3477
+ message: `File too large (max: ${configuredMax})`,
3005
3478
  help: "Consider splitting this file into smaller modules",
3006
3479
  line: 0,
3007
3480
  column: 0,
3008
3481
  category: "Complexity",
3009
- fixable: false
3482
+ fixable: false,
3483
+ detail: `${lineCount} lines`
3010
3484
  });
3011
3485
  return results;
3012
3486
  };
3013
- const checkFunctionDiagnostics = (relativePath, fn, limits) => {
3487
+ const JSX_EXTENSIONS = new Set([".tsx", ".jsx"]);
3488
+ const isComponentFunction = (name, ext) => JSX_EXTENSIONS.has(ext) && /^[A-Z]/.test(name);
3489
+ const functionLocBudget = (fn, ext, base) => {
3490
+ if (isComponentFunction(fn.name, ext)) return Math.ceil(base * 2);
3491
+ if (ext === ".rs") return Math.ceil(base * 1.5);
3492
+ return base;
3493
+ };
3494
+ const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
3014
3495
  const results = [];
3015
- if (fn.lineCount - fn.templateLines > Math.ceil(limits.maxFunctionLoc * 1.1)) results.push({
3496
+ const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
3497
+ if (fn.lineCount - fn.templateLines > Math.ceil(fnMax * 1.1)) results.push({
3016
3498
  filePath: relativePath,
3017
3499
  engine: "code-quality",
3018
3500
  rule: "complexity/function-too-long",
3019
3501
  severity: "warning",
3020
- message: `Function '${fn.name}' has ${fn.lineCount} lines (max: ${limits.maxFunctionLoc})`,
3502
+ message: `Function too long (max: ${fnMax})`,
3021
3503
  help: "Consider breaking this function into smaller pieces",
3022
3504
  line: fn.startLine,
3023
3505
  column: 0,
3024
3506
  category: "Complexity",
3025
- fixable: false
3507
+ fixable: false,
3508
+ detail: `${fn.name} · ${fn.lineCount} lines`
3026
3509
  });
3027
3510
  if (fn.maxNesting > limits.maxNesting) results.push({
3028
3511
  filePath: relativePath,
3029
3512
  engine: "code-quality",
3030
3513
  rule: "complexity/deep-nesting",
3031
3514
  severity: "warning",
3032
- message: `Function '${fn.name}' has nesting depth ${fn.maxNesting} (max: ${limits.maxNesting})`,
3515
+ message: `Function nested too deeply (max: ${limits.maxNesting})`,
3033
3516
  help: "Consider using early returns or extracting nested logic",
3034
3517
  line: fn.startLine,
3035
3518
  column: 0,
3036
3519
  category: "Complexity",
3037
- fixable: false
3520
+ fixable: false,
3521
+ detail: `${fn.name} · depth ${fn.maxNesting}`
3038
3522
  });
3039
3523
  if (fn.paramCount > limits.maxParams) results.push({
3040
3524
  filePath: relativePath,
3041
3525
  engine: "code-quality",
3042
3526
  rule: "complexity/too-many-params",
3043
3527
  severity: "warning",
3044
- message: `Function '${fn.name}' has ${fn.paramCount} parameters (max: ${limits.maxParams})`,
3528
+ message: `Function has too many parameters (max: ${limits.maxParams})`,
3045
3529
  help: "Consider using an options object parameter",
3046
3530
  line: fn.startLine,
3047
3531
  column: 0,
3048
3532
  category: "Complexity",
3049
- fixable: false
3533
+ fixable: false,
3534
+ detail: `${fn.name} · ${fn.paramCount} params`
3050
3535
  });
3051
3536
  return results;
3052
3537
  };
@@ -3061,7 +3546,7 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
3061
3546
  }
3062
3547
  const ext = path.extname(filePath).toLowerCase();
3063
3548
  const diagnostics = checkFileDiagnostics(relativePath, content, limits);
3064
- for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
3549
+ for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits, ext));
3065
3550
  return diagnostics;
3066
3551
  };
3067
3552
  const checkComplexity = async (context) => {
@@ -3166,17 +3651,19 @@ const findDuplicateBlocks = (content, relativePath) => {
3166
3651
  });
3167
3652
  }
3168
3653
  return reports.map((r) => {
3654
+ const span = r.currentEnd - r.currentStart + 1;
3169
3655
  return {
3170
3656
  filePath: relativePath,
3171
3657
  engine: "code-quality",
3172
3658
  rule: "code-quality/duplicate-block",
3173
3659
  severity: "warning",
3174
- message: `${r.currentEnd - r.currentStart + 1}-line block at line ${r.currentStart} duplicates a block starting at line ${r.priorStart}. Extract a shared helper.`,
3660
+ message: "Duplicate code block extract a shared helper",
3175
3661
  help: `Pull the shared logic into a function both sites can call. Keeps one version of the truth and makes future changes one-shot instead of N-shot.`,
3176
3662
  line: r.currentStart,
3177
3663
  column: 0,
3178
3664
  category: "Complexity",
3179
- fixable: false
3665
+ fixable: false,
3666
+ detail: `${span} lines duplicate block at L${r.priorStart}`
3180
3667
  };
3181
3668
  });
3182
3669
  };
@@ -3842,16 +4329,34 @@ const isToolAvailable = async (toolName) => {
3842
4329
  return isToolInstalled(toolName);
3843
4330
  };
3844
4331
 
4332
+ //#endregion
4333
+ //#region src/engines/python-targets.ts
4334
+ const PYTHON_EXTENSIONS = new Set([".py", ".pyi"]);
4335
+ const normalizeProjectPath = (filePath) => filePath.split(path.sep).join("/");
4336
+ const getPythonTargets = (context) => {
4337
+ const targets = (context.files ?? getSourceFiles(context)).filter((filePath) => PYTHON_EXTENSIONS.has(path.extname(filePath).toLowerCase())).map((filePath) => {
4338
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(context.rootDirectory, filePath);
4339
+ return normalizeProjectPath(path.relative(context.rootDirectory, absolutePath));
4340
+ }).filter((filePath) => filePath.length > 0 && !filePath.startsWith(".."));
4341
+ return [...new Set(targets)];
4342
+ };
4343
+ const getRuffDiagnosticPath = (rootDirectory, filePath) => {
4344
+ const normalizedPath = filePath.replace(/^a\//, "");
4345
+ return normalizeProjectPath(path.isAbsolute(normalizedPath) ? path.relative(rootDirectory, normalizedPath) : normalizedPath);
4346
+ };
4347
+
3845
4348
  //#endregion
3846
4349
  //#region src/engines/format/ruff-format.ts
3847
4350
  const runRuffFormat = async (context) => {
3848
4351
  const ruffBinary = resolveToolBinary("ruff");
4352
+ const targets = getPythonTargets(context);
4353
+ if (targets.length === 0) return [];
3849
4354
  try {
3850
4355
  const result = await runSubprocess(ruffBinary, [
3851
4356
  "format",
3852
4357
  "--check",
3853
4358
  "--diff",
3854
- context.rootDirectory
4359
+ ...targets
3855
4360
  ], {
3856
4361
  cwd: context.rootDirectory,
3857
4362
  timeout: 6e4
@@ -3867,9 +4372,9 @@ const parseRuffFormatOutput = (output, rootDir) => {
3867
4372
  const filePattern = /^--- (.+)$/gm;
3868
4373
  let match;
3869
4374
  while ((match = filePattern.exec(output)) !== null) {
3870
- const filePath = match[1].replace(/^a\//, "");
4375
+ const filePath = getRuffDiagnosticPath(rootDir, match[1]);
3871
4376
  diagnostics.push({
3872
- filePath: path.relative(rootDir, filePath),
4377
+ filePath,
3873
4378
  engine: "format",
3874
4379
  rule: "python-formatting",
3875
4380
  severity: "warning",
@@ -4378,6 +4883,95 @@ const resolveOxlintBinary = () => {
4378
4883
  return "oxlint";
4379
4884
  }
4380
4885
  };
4886
+ const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
4887
+ const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
4888
+ const AMBIENT_GLOBAL_DEPS = [
4889
+ "unplugin-icons",
4890
+ "@types/bun",
4891
+ "bun-types"
4892
+ ];
4893
+ const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
4894
+ const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
4895
+ const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
4896
+ const detectAmbientSources = (rootDir) => {
4897
+ const found = /* @__PURE__ */ new Set();
4898
+ const skipDirs = new Set([
4899
+ "node_modules",
4900
+ ".git",
4901
+ "dist",
4902
+ "build",
4903
+ "out",
4904
+ "target",
4905
+ "coverage",
4906
+ ".next",
4907
+ ".turbo"
4908
+ ]);
4909
+ const walk = (dir, depth) => {
4910
+ if (depth > 4 || found.size === AMBIENT_GLOBAL_DEPS.length) return;
4911
+ let entries;
4912
+ try {
4913
+ entries = fs.readdirSync(dir, { withFileTypes: true });
4914
+ } catch {
4915
+ return;
4916
+ }
4917
+ for (const entry of entries) {
4918
+ if (found.size === AMBIENT_GLOBAL_DEPS.length) return;
4919
+ if (entry.name.startsWith(".") && entry.name !== ".github") continue;
4920
+ if (skipDirs.has(entry.name)) continue;
4921
+ const full = path.join(dir, entry.name);
4922
+ if (entry.isDirectory()) walk(full, depth + 1);
4923
+ else if (entry.name === "package.json") try {
4924
+ const pkg = JSON.parse(fs.readFileSync(full, "utf-8"));
4925
+ const allDeps = {
4926
+ ...pkg.dependencies ?? {},
4927
+ ...pkg.devDependencies ?? {},
4928
+ ...pkg.peerDependencies ?? {}
4929
+ };
4930
+ for (const dep of AMBIENT_GLOBAL_DEPS) if (dep in allDeps) found.add(dep);
4931
+ } catch {}
4932
+ }
4933
+ };
4934
+ walk(rootDir, 0);
4935
+ return found;
4936
+ };
4937
+ const extractNoUndefIdentifier = (message) => {
4938
+ return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
4939
+ };
4940
+ const isAmbientFalsePositive = (rule, message, sources) => {
4941
+ if (rule !== "eslint/no-undef") return false;
4942
+ const ident = extractNoUndefIdentifier(message);
4943
+ if (!ident) return false;
4944
+ if (sources.has("unplugin-icons") && ICON_AUTOIMPORT_RE.test(ident)) return true;
4945
+ if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
4946
+ return false;
4947
+ };
4948
+ const sstReferencedFiles = /* @__PURE__ */ new Map();
4949
+ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4950
+ const cached = sstReferencedFiles.get(relativeFilePath);
4951
+ if (cached !== void 0) return cached;
4952
+ const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
4953
+ let referenced = false;
4954
+ try {
4955
+ const fd = fs.openSync(absolute, "r");
4956
+ try {
4957
+ const buf = Buffer.alloc(512);
4958
+ const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
4959
+ referenced = SST_PLATFORM_REF_RE.test(buf.toString("utf-8", 0, bytesRead));
4960
+ } finally {
4961
+ fs.closeSync(fd);
4962
+ }
4963
+ } catch {
4964
+ referenced = false;
4965
+ }
4966
+ sstReferencedFiles.set(relativeFilePath, referenced);
4967
+ return referenced;
4968
+ };
4969
+ const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
4970
+ const isUnderscoreUnusedVar = (rule, message) => {
4971
+ if (rule !== "eslint/no-unused-vars") return false;
4972
+ const match = UNUSED_VAR_IDENT_RE.exec(message);
4973
+ return match ? match[1].startsWith("_") : false;
4974
+ };
4381
4975
  const parseRuleCode = (code) => {
4382
4976
  if (!code) return {
4383
4977
  plugin: "eslint",
@@ -4474,6 +5068,8 @@ const runOxlint = async (context) => {
4474
5068
  framework: context.frameworks.find((f) => f !== "none"),
4475
5069
  testFramework: detectTestFramework(context.rootDirectory)
4476
5070
  });
5071
+ const ambientSources = detectAmbientSources(context.rootDirectory);
5072
+ sstReferencedFiles.clear();
4477
5073
  try {
4478
5074
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
4479
5075
  const args = [
@@ -4513,6 +5109,11 @@ const runOxlint = async (context) => {
4513
5109
  fixable: false
4514
5110
  };
4515
5111
  }).filter((d) => {
5112
+ if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
5113
+ if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
5114
+ if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
5115
+ if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
5116
+ if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
4516
5117
  const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
4517
5118
  if (seen.has(key)) return false;
4518
5119
  seen.add(key);
@@ -4576,18 +5177,20 @@ const fixOxlint = async (context, options = {}) => {
4576
5177
  //#region src/engines/lint/ruff.ts
4577
5178
  const runRuffLint = async (context) => {
4578
5179
  const ruffBinary = resolveToolBinary("ruff");
5180
+ const targets = getPythonTargets(context);
5181
+ if (targets.length === 0) return [];
4579
5182
  try {
4580
5183
  const output = (await runSubprocess(ruffBinary, [
4581
5184
  "check",
4582
5185
  "--output-format=json",
4583
- context.rootDirectory
5186
+ ...targets
4584
5187
  ], {
4585
5188
  cwd: context.rootDirectory,
4586
5189
  timeout: 6e4
4587
5190
  })).stdout;
4588
5191
  if (!output) return [];
4589
5192
  return JSON.parse(output).map((d) => ({
4590
- filePath: path.relative(context.rootDirectory, d.filename),
5193
+ filePath: getRuffDiagnosticPath(context.rootDirectory, d.filename),
4591
5194
  engine: "lint",
4592
5195
  rule: `ruff/${d.code}`,
4593
5196
  severity: d.code.startsWith("E") || d.code.startsWith("F") ? "error" : "warning",
@@ -4636,7 +5239,7 @@ const lintEngine = {
4636
5239
  const promises = [];
4637
5240
  if (languages.includes("typescript") || languages.includes("javascript")) {
4638
5241
  promises.push(runOxlint(context));
4639
- if (context.config.lint.typecheck) promises.push(import("./typecheck-B1MXNAy-.js").then((mod) => mod.runTypecheck(context)));
5242
+ if (context.config.lint.typecheck) promises.push(import("./typecheck-wVSohmOX.js").then((mod) => mod.runTypecheck(context)));
4640
5243
  }
4641
5244
  if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
4642
5245
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
@@ -4705,56 +5308,94 @@ const runPnpmAuditWithFallback = async (rootDir, timeout) => {
4705
5308
  return [];
4706
5309
  }
4707
5310
  };
5311
+ const SEVERITY_RANK = {
5312
+ critical: 4,
5313
+ high: 3,
5314
+ moderate: 2,
5315
+ low: 1
5316
+ };
4708
5317
  const toSeverity = (value) => value === "critical" || value === "high" ? "error" : "warning";
4709
- const defaultAuditFixCommand = (source) => source === "pnpm audit" ? "pnpm audit --fix" : "npm audit fix";
5318
+ const upsertVuln = (bucket, packageName, severity, recommendation) => {
5319
+ const existing = bucket.get(packageName);
5320
+ if (existing) {
5321
+ existing.advisories++;
5322
+ if ((SEVERITY_RANK[severity] ?? 0) > (SEVERITY_RANK[existing.worstSeverity] ?? 0)) existing.worstSeverity = severity;
5323
+ if (recommendation) existing.recommendations.add(recommendation);
5324
+ } else bucket.set(packageName, {
5325
+ packageName,
5326
+ worstSeverity: severity,
5327
+ advisories: 1,
5328
+ recommendations: recommendation ? new Set([recommendation]) : /* @__PURE__ */ new Set()
5329
+ });
5330
+ };
5331
+ const SEMVER_RE = /(\d+)\.(\d+)\.(\d+)/;
5332
+ const cmpSemver = (a, b) => {
5333
+ const [, a1, a2, a3] = SEMVER_RE.exec(a) ?? [
5334
+ "",
5335
+ "0",
5336
+ "0",
5337
+ "0"
5338
+ ];
5339
+ const [, b1, b2, b3] = SEMVER_RE.exec(b) ?? [
5340
+ "",
5341
+ "0",
5342
+ "0",
5343
+ "0"
5344
+ ];
5345
+ if (Number(a1) !== Number(b1)) return Number(a1) - Number(b1);
5346
+ if (Number(a2) !== Number(b2)) return Number(a2) - Number(b2);
5347
+ return Number(a3) - Number(b3);
5348
+ };
5349
+ const pickBestRecommendation = (recs) => {
5350
+ if (recs.length <= 1) return recs[0] ?? "";
5351
+ const versioned = recs.filter((r) => SEMVER_RE.test(r));
5352
+ if (versioned.length === 0) return recs[0];
5353
+ return versioned.reduce((best, r) => cmpSemver(r, best) > 0 ? r : best);
5354
+ };
5355
+ const cleanRecommendation = (raw) => {
5356
+ const t = raw.trim();
5357
+ if (!t || t.toLowerCase() === "none") return "no fix available";
5358
+ return t;
5359
+ };
5360
+ const aggregateToDiagnostic = (agg, source) => {
5361
+ const best = cleanRecommendation(pickBestRecommendation([...agg.recommendations]));
5362
+ const countLabel = agg.advisories > 1 ? ` (${agg.advisories} advisories)` : "";
5363
+ const recLabel = best ? ` — ${best}` : "";
5364
+ return {
5365
+ filePath: "package.json",
5366
+ engine: "security",
5367
+ rule: "security/vulnerable-dependency",
5368
+ severity: toSeverity(agg.worstSeverity),
5369
+ message: `${agg.packageName} (${agg.worstSeverity})${recLabel}${countLabel}`,
5370
+ help: "",
5371
+ line: 0,
5372
+ column: 0,
5373
+ category: "Security",
5374
+ fixable: false,
5375
+ detail: source === "npm audit" ? "npm" : "pnpm"
5376
+ };
5377
+ };
4710
5378
  const parseLegacyAdvisories = (advisories, source) => {
4711
- const diagnostics = [];
4712
- for (const [key, advisory] of Object.entries(advisories)) {
4713
- const packageName = advisory.module_name ?? advisory.name ?? advisory.package ?? key;
4714
- const severity = (advisory.severity ?? "moderate").toLowerCase();
4715
- const recommendation = advisory.recommendation ?? advisory.title ?? `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
4716
- diagnostics.push({
4717
- filePath: "package.json",
4718
- engine: "security",
4719
- rule: "security/vulnerable-dependency",
4720
- severity: toSeverity(severity),
4721
- message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
4722
- help: withFixHint(recommendation),
4723
- line: 0,
4724
- column: 0,
4725
- category: "Security",
4726
- fixable: false
4727
- });
4728
- }
4729
- return diagnostics;
5379
+ const bucket = /* @__PURE__ */ new Map();
5380
+ for (const [key, advisory] of Object.entries(advisories)) upsertVuln(bucket, advisory.module_name ?? advisory.name ?? advisory.package ?? key, (advisory.severity ?? "moderate").toLowerCase(), advisory.recommendation ?? advisory.title ?? "");
5381
+ return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
4730
5382
  };
4731
5383
  const parseModernVulnerabilities = (vulnerabilities, source) => {
4732
- const diagnostics = [];
5384
+ const bucket = /* @__PURE__ */ new Map();
4733
5385
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
4734
5386
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
4735
5387
  const fixAvailable = vulnerability.fixAvailable;
4736
5388
  const isDirect = vulnerability.isDirect === true;
4737
- let recommendation = `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
4738
- if (fixAvailable === false) recommendation = isDirect ? "No automatic fix — check for a newer major version" : "Transitive with no fix add an override or upgrade the parent";
4739
- else if (!isDirect && fixAvailable === true) recommendation = "Transitive dep — may need an override or parent upgrade";
5389
+ let recommendation = "";
5390
+ if (fixAvailable === false) recommendation = isDirect ? "no automatic fix" : "transitiveneeds override or parent upgrade";
5391
+ else if (!isDirect && fixAvailable === true) recommendation = "transitive — may need override or parent upgrade";
4740
5392
  else if (fixAvailable && typeof fixAvailable === "object" && "name" in fixAvailable && "version" in fixAvailable) {
4741
5393
  const target = fixAvailable;
4742
- if (target.name && target.version) recommendation = `Upgrade to ${target.name}@${target.version}.`;
5394
+ if (target.name && target.version) recommendation = `upgrade to ${target.name}@${target.version}`;
4743
5395
  }
4744
- diagnostics.push({
4745
- filePath: "package.json",
4746
- engine: "security",
4747
- rule: "security/vulnerable-dependency",
4748
- severity: toSeverity(severity),
4749
- message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
4750
- help: withFixHint(recommendation),
4751
- line: 0,
4752
- column: 0,
4753
- category: "Security",
4754
- fixable: false
4755
- });
5396
+ upsertVuln(bucket, packageName, severity, recommendation);
4756
5397
  }
4757
- return diagnostics;
5398
+ return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
4758
5399
  };
4759
5400
  const parseJsAudit = (output, source) => {
4760
5401
  if (!output) return [];
@@ -5854,7 +6495,20 @@ const runClaudeHook = async (deps = {}) => {
5854
6495
  const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline ? {
5855
6496
  score: baseline.score,
5856
6497
  findingFingerprints: baseline.findingFingerprints
5857
- } : void 0);
6498
+ } : void 0, {
6499
+ agent: "claude",
6500
+ touchedFiles: files
6501
+ });
6502
+ track({
6503
+ event: "hook_scan_completed",
6504
+ properties: buildHookScanCompletedProps({
6505
+ agent: "claude",
6506
+ score,
6507
+ scoreDelta: baseline ? score - baseline.score : null,
6508
+ findingCount: diagnostics.length,
6509
+ fileCount: files.length
6510
+ })
6511
+ });
5858
6512
  const envelope = renderClaudeOutput(JSON.stringify(feedback));
5859
6513
  write(JSON.stringify(envelope));
5860
6514
  return 0;
@@ -5925,6 +6579,9 @@ const runClaudeStopHook = async (deps = {}) => {
5925
6579
  const feedback = buildFeedback(diagnostics, score, rootDirectory, {
5926
6580
  score: baseline.score,
5927
6581
  findingFingerprints: baseline.findingFingerprints
6582
+ }, {
6583
+ agent: "claude",
6584
+ touchedFiles: sessionFiles
5928
6585
  });
5929
6586
  if (!feedback.regressed) {
5930
6587
  clearSessionFiles(cwd);
@@ -5985,7 +6642,19 @@ const runCursorHook = async (deps = {}) => {
5985
6642
  if (!release) return 0;
5986
6643
  try {
5987
6644
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
5988
- const feedback = buildFeedback(diagnostics, score, rootDirectory);
6645
+ const feedback = buildFeedback(diagnostics, score, rootDirectory, void 0, {
6646
+ agent: "cursor",
6647
+ touchedFiles: files
6648
+ });
6649
+ track({
6650
+ event: "hook_scan_completed",
6651
+ properties: buildHookScanCompletedProps({
6652
+ agent: "cursor",
6653
+ score,
6654
+ findingCount: diagnostics.length,
6655
+ fileCount: files.length
6656
+ })
6657
+ });
5989
6658
  const serialized = JSON.stringify(feedback);
5990
6659
  write(JSON.stringify(renderCursorOutput(serialized)));
5991
6660
  writeErr(`${serialized}\n`);
@@ -6035,7 +6704,19 @@ const runGeminiHook = async (deps = {}) => {
6035
6704
  if (!release) return 0;
6036
6705
  try {
6037
6706
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
6038
- const feedback = buildFeedback(diagnostics, score, rootDirectory);
6707
+ const feedback = buildFeedback(diagnostics, score, rootDirectory, void 0, {
6708
+ agent: "gemini",
6709
+ touchedFiles: files
6710
+ });
6711
+ track({
6712
+ event: "hook_scan_completed",
6713
+ properties: buildHookScanCompletedProps({
6714
+ agent: "gemini",
6715
+ score,
6716
+ findingCount: diagnostics.length,
6717
+ fileCount: files.length
6718
+ })
6719
+ });
6039
6720
  write(JSON.stringify(renderGeminiOutput(JSON.stringify(feedback))));
6040
6721
  return 0;
6041
6722
  } catch {
@@ -7050,12 +7731,18 @@ const registerInstall = (hook) => {
7050
7731
  install.action(async (positional, opts) => {
7051
7732
  const agents = await pickAgents("install", opts, positional);
7052
7733
  if (agents === null || agents.length === 0) return;
7053
- await hookInstall({
7054
- agents,
7055
- scope: resolveScope(opts),
7056
- dryRun: Boolean(opts.dryRun),
7057
- yes: Boolean(opts.yes),
7058
- qualityGate: Boolean(opts.qualityGate)
7734
+ await withCommandLifecycle({
7735
+ command: "hook_install",
7736
+ config: loadConfig(process.cwd()).telemetry
7737
+ }, async () => {
7738
+ await hookInstall({
7739
+ agents,
7740
+ scope: resolveScope(opts),
7741
+ dryRun: Boolean(opts.dryRun),
7742
+ yes: Boolean(opts.yes),
7743
+ qualityGate: Boolean(opts.qualityGate)
7744
+ });
7745
+ return { exitCode: 0 };
7059
7746
  });
7060
7747
  });
7061
7748
  };
@@ -7065,21 +7752,39 @@ const registerUninstall = (hook) => {
7065
7752
  uninstall.action(async (positional, opts) => {
7066
7753
  const agents = await pickAgents("uninstall", opts, positional);
7067
7754
  if (agents === null || agents.length === 0) return;
7068
- await hookUninstall({
7069
- agents,
7070
- scope: resolveScope(opts),
7071
- dryRun: Boolean(opts.dryRun),
7072
- yes: true,
7073
- qualityGate: false
7755
+ await withCommandLifecycle({
7756
+ command: "hook_uninstall",
7757
+ config: loadConfig(process.cwd()).telemetry
7758
+ }, async () => {
7759
+ await hookUninstall({
7760
+ agents,
7761
+ scope: resolveScope(opts),
7762
+ dryRun: Boolean(opts.dryRun),
7763
+ yes: true,
7764
+ qualityGate: false
7765
+ });
7766
+ return { exitCode: 0 };
7074
7767
  });
7075
7768
  });
7076
7769
  };
7077
7770
  const registerCallbacks = (hook) => {
7078
7771
  hook.command("status").description("Show which agent hooks are installed").action(async () => {
7079
- await hookStatus();
7772
+ await withCommandLifecycle({
7773
+ command: "hook_status",
7774
+ config: loadConfig(process.cwd()).telemetry
7775
+ }, async () => {
7776
+ await hookStatus();
7777
+ return { exitCode: 0 };
7778
+ });
7080
7779
  });
7081
7780
  hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
7082
- await hookBaseline();
7781
+ await withCommandLifecycle({
7782
+ command: "hook_baseline",
7783
+ config: loadConfig(process.cwd()).telemetry
7784
+ }, async () => {
7785
+ await hookBaseline();
7786
+ return { exitCode: 0 };
7787
+ });
7083
7788
  });
7084
7789
  hook.command("claude").description("Internal: Claude Code PostToolUse / Stop / FileChanged callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").option("--on-file-changed", "run in FileChanged mode (refresh baseline on watched file change)").action(async (opts) => {
7085
7790
  await hookRun("claude", {
@@ -7333,8 +8038,53 @@ const wrapHelpText = (text, maxWidth, indent) => {
7333
8038
  };
7334
8039
  const terminalWidth = () => {
7335
8040
  const raw = process.stdout.columns;
7336
- if (typeof raw !== "number" || raw <= 0) return 100;
7337
- return Math.min(raw, 100);
8041
+ if (typeof raw !== "number" || raw <= 0) return 120;
8042
+ return Math.min(raw, 120);
8043
+ };
8044
+ const renderRuleHeader = (first, count, lines) => {
8045
+ const level = toSeverityLabel(first.severity);
8046
+ const countLabel = count > 1 ? ` (${count})` : "";
8047
+ const status = colorBySeverity(level, first.severity);
8048
+ const fixableTag = first.fixable ? ` ${style(theme, "muted", "[auto]")}` : "";
8049
+ const fixableWidth = first.fixable ? 7 : 0;
8050
+ const badgePrefix = ` [${status}]${fixableTag} `;
8051
+ const badgePrefixWidth = 5 + level.length + 1 + fixableWidth + 1;
8052
+ const wrapped = wrapText(`${first.message}${countLabel}`, terminalWidth(), badgePrefixWidth, " ");
8053
+ lines.push(`${badgePrefix}${wrapped[0]}`);
8054
+ for (let i = 1; i < wrapped.length; i++) lines.push(wrapped[i]);
8055
+ };
8056
+ const renderLocations = (ruleDiags, verbose, lines) => {
8057
+ const unique = [];
8058
+ const seen = /* @__PURE__ */ new Set();
8059
+ for (const d of ruleDiags) {
8060
+ const label = toLocationLabel(d);
8061
+ const detail = d.detail ?? "";
8062
+ const key = `${label}|${detail}`;
8063
+ if (seen.has(key)) continue;
8064
+ seen.add(key);
8065
+ unique.push({
8066
+ label,
8067
+ detail
8068
+ });
8069
+ }
8070
+ const shown = verbose ? unique : unique.slice(0, 3);
8071
+ const maxLabel = shown.reduce((w, l) => Math.max(w, l.label.length), 0);
8072
+ for (const { label, detail } of shown) {
8073
+ const padded = detail ? `${label.padEnd(maxLabel)} ${detail}` : label;
8074
+ lines.push(style(theme, "muted", ` ${padded}`));
8075
+ }
8076
+ if (!verbose && unique.length > shown.length) lines.push(style(theme, "muted", ` +${unique.length - shown.length} more location(s), use -d for full list`));
8077
+ };
8078
+ const renderHiddenFooter = (sorted, maxRules, lines) => {
8079
+ const hidden = sorted.slice(maxRules);
8080
+ const hiddenErrors = hidden.reduce((acc, [, diags]) => acc + (diags[0].severity === "error" ? diags.length : 0), 0);
8081
+ const hiddenWarnings = hidden.reduce((acc, [, diags]) => acc + (diags[0].severity === "warning" ? diags.length : 0), 0);
8082
+ const parts = [];
8083
+ if (hiddenErrors > 0) parts.push(`${hiddenErrors} error${hiddenErrors === 1 ? "" : "s"}`);
8084
+ if (hiddenWarnings > 0) parts.push(`${hiddenWarnings} warning${hiddenWarnings === 1 ? "" : "s"}`);
8085
+ const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
8086
+ lines.push(style(theme, "muted", ` ... and ${hidden.length} more rules hidden${detail}. Run with -v or --verbose to see full output.`));
8087
+ lines.push("");
7338
8088
  };
7339
8089
  const renderDiagnostics = (diagnostics, verbose) => {
7340
8090
  const lines = [];
@@ -7343,29 +8093,23 @@ const renderDiagnostics = (diagnostics, verbose) => {
7343
8093
  const label = getEngineLabel(engine);
7344
8094
  lines.push(` ${style(theme, "bold", `${symbols.engineActive} ${label}`)}`);
7345
8095
  const sorted = [...groupBy(engineDiags, (d) => `${d.rule}:${d.message}`).entries()].sort(([, a], [, b]) => {
7346
- return (a[0].severity === "error" ? 0 : a[0].severity === "warning" ? 1 : 2) - (b[0].severity === "error" ? 0 : b[0].severity === "warning" ? 1 : 2);
8096
+ const sa = a[0].severity === "error" ? 0 : a[0].severity === "warning" ? 1 : 2;
8097
+ const sb = b[0].severity === "error" ? 0 : b[0].severity === "warning" ? 1 : 2;
8098
+ if (sa !== sb) return sa - sb;
8099
+ return b.length - a.length;
7347
8100
  });
7348
- for (const [, ruleDiags] of sorted) {
8101
+ const maxRules = verbose ? Infinity : 40;
8102
+ for (const [, ruleDiags] of sorted.slice(0, maxRules)) {
7349
8103
  const first = ruleDiags[0];
7350
- const level = toSeverityLabel(first.severity);
7351
- const count = ruleDiags.length > 1 ? ` (${ruleDiags.length})` : "";
7352
- const status = colorBySeverity(level, first.severity);
7353
- const fixableTag = first.fixable ? ` ${style(theme, "muted", "[auto]")}` : "";
7354
- const fixableWidth = first.fixable ? 7 : 0;
7355
- const badgePrefix = ` [${status}]${fixableTag} `;
7356
- const badgePrefixWidth = 5 + level.length + 1 + fixableWidth + 1;
7357
- const wrappedMsg = wrapText(`${first.message}${count}`, terminalWidth(), badgePrefixWidth, " ");
7358
- lines.push(`${badgePrefix}${wrappedMsg[0]}`);
7359
- for (let i = 1; i < wrappedMsg.length; i++) lines.push(wrappedMsg[i]);
7360
- const locations = verbose ? ruleDiags : ruleDiags.slice(0, 3);
7361
- for (const diagnostic of locations) lines.push(style(theme, "muted", ` ${toLocationLabel(diagnostic)}`));
7362
- if (!verbose && ruleDiags.length > locations.length) lines.push(style(theme, "muted", ` +${ruleDiags.length - locations.length} more location(s), use -d for full list`));
8104
+ renderRuleHeader(first, ruleDiags.length, lines);
8105
+ renderLocations(ruleDiags, verbose, lines);
7363
8106
  if (first.help) {
7364
8107
  const wrapped = wrapHelpText(first.help, terminalWidth(), " ");
7365
8108
  for (const line of wrapped) lines.push(style(theme, "muted", line));
7366
8109
  }
7367
8110
  lines.push("");
7368
8111
  }
8112
+ if (sorted.length > maxRules) renderHiddenFooter(sorted, maxRules, lines);
7369
8113
  }
7370
8114
  return `${lines.join("\n")}\n`;
7371
8115
  };
@@ -7544,6 +8288,81 @@ var LiveGrid = class {
7544
8288
  }
7545
8289
  };
7546
8290
 
8291
+ //#endregion
8292
+ //#region src/output/rule-labels.ts
8293
+ const RULE_LABELS = {
8294
+ formatting: "Code not formatted",
8295
+ "code-quality/duplicate-block": "Duplicate code block",
8296
+ "complexity/file-too-large": "File too large",
8297
+ "complexity/function-too-long": "Function too long",
8298
+ "complexity/deep-nesting": "Deeply nested code",
8299
+ "complexity/too-many-params": "Too many parameters",
8300
+ "knip/files": "Unused file",
8301
+ "knip/dependencies": "Unused dependency",
8302
+ "knip/devDependencies": "Unused dev dependency",
8303
+ "knip/unlisted": "Used but not in package.json",
8304
+ "knip/unresolved": "Unresolved import",
8305
+ "knip/binaries": "Unused binary",
8306
+ "knip/exports": "Unused export",
8307
+ "knip/types": "Unused type",
8308
+ "ai-slop/trivial-comment": "Trivial restating comment",
8309
+ "ai-slop/swallowed-exception": "Empty catch (swallowed error)",
8310
+ "ai-slop/thin-wrapper": "Thin function wrapper",
8311
+ "ai-slop/generic-naming": "Generic/vague identifier name",
8312
+ "ai-slop/unused-import": "Unused import",
8313
+ "ai-slop/console-leftover": "console.log left in code",
8314
+ "ai-slop/todo-stub": "Unresolved TODO/FIXME",
8315
+ "ai-slop/unreachable-code": "Unreachable code",
8316
+ "ai-slop/constant-condition": "Constant condition",
8317
+ "ai-slop/empty-function": "Empty function body",
8318
+ "ai-slop/unsafe-type-assertion": "Unsafe type cast",
8319
+ "ai-slop/double-type-assertion": "Double type cast",
8320
+ "ai-slop/ts-directive": "@ts-ignore / @ts-expect-error",
8321
+ "ai-slop/narrative-comment": "Narrative comment block",
8322
+ "ai-slop/duplicate-import": "Duplicate import statement",
8323
+ "ai-slop/python-bare-except": "Bare except",
8324
+ "ai-slop/python-broad-except": "Broad except",
8325
+ "ai-slop/python-mutable-default": "Mutable default argument",
8326
+ "ai-slop/python-print-debug": "print() left in code",
8327
+ "ai-slop/go-library-panic": "panic() in Go library code",
8328
+ "ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
8329
+ "ai-slop/rust-todo-stub": "Rust todo!() stub",
8330
+ "ai-slop/hallucinated-import": "Import not in package.json",
8331
+ "security/hardcoded-secret": "Possible hardcoded secret",
8332
+ "security/vulnerable-dependency": "Vulnerable dependency",
8333
+ "security/eval": "eval() usage",
8334
+ "security/innerhtml": "innerHTML assignment",
8335
+ "security/dangerously-set-innerhtml": "dangerouslySetInnerHTML (XSS risk)",
8336
+ "security/sql-injection": "Possible SQL injection",
8337
+ "security/shell-injection": "Possible shell injection",
8338
+ "eslint/no-undef": "Undefined identifier",
8339
+ "eslint/no-unused-vars": "Unused variable",
8340
+ "eslint/no-unassigned-vars": "Variable never assigned",
8341
+ "eslint/no-empty": "Empty block statement",
8342
+ "eslint/no-unused-expressions": "Unused expression",
8343
+ "eslint/no-shadow-restricted-names": "Shadowing restricted name",
8344
+ "eslint/no-constant-binary-expression": "Constant binary expression",
8345
+ "eslint/no-unsafe-optional-chaining": "Unsafe optional chaining",
8346
+ "eslint/require-yield": "Generator with no yield",
8347
+ "import/no-duplicates": "Duplicate import path",
8348
+ "import/default": "Missing default export",
8349
+ "import/named": "Missing named export",
8350
+ "import/namespace": "Invalid namespace import",
8351
+ "typescript-eslint/triple-slash-reference": "Triple-slash reference",
8352
+ "unicorn/no-useless-fallback-in-spread": "Useless spread fallback",
8353
+ "unicorn/no-invalid-remove-event-listener": "Invalid removeEventListener",
8354
+ "unicorn/no-empty-file": "Empty file",
8355
+ "unicorn/no-useless-length-check": "Useless array length check",
8356
+ "unicorn/no-new-array": "Avoid new Array(n)",
8357
+ "unicorn/no-useless-spread": "Useless spread",
8358
+ "unicorn/no-single-promise-in-promise-methods": "Single-element Promise.all"
8359
+ };
8360
+ const prettifyFallback = (ruleId) => {
8361
+ const spaced = (ruleId.includes("/") ? ruleId.slice(ruleId.indexOf("/") + 1) : ruleId).replace(/[-_]/g, " ").replace(/\//g, " · ");
8362
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
8363
+ };
8364
+ const labelForRule = (ruleId) => RULE_LABELS[ruleId] ?? prettifyFallback(ruleId);
8365
+
7547
8366
  //#endregion
7548
8367
  //#region src/ui/summary.ts
7549
8368
  const elapsed = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
@@ -7570,6 +8389,34 @@ const renderSummary = (input, deps = {}) => {
7570
8389
  ` ${style(t, "muted", `${input.files} files`)} ${sep} ${style(t, "muted", `${input.engines} engines`)} ${sep} ${style(t, "muted", elapsed(input.elapsedMs))}`,
7571
8390
  ""
7572
8391
  ];
8392
+ if (input.breakdown && input.breakdown.rows.length > 0) {
8393
+ lines.push(` ${style(t, "bold", "Top findings")}`);
8394
+ const maxCountWidth = input.breakdown.rows.reduce((w, r) => Math.max(w, String(r.errors + r.warnings + r.info).length), 0);
8395
+ const labels = input.breakdown.rows.map((r) => labelForRule(r.rule));
8396
+ const maxLabelWidth = labels.reduce((w, l) => Math.max(w, l.length), 0);
8397
+ for (let i = 0; i < input.breakdown.rows.length; i++) {
8398
+ const row = input.breakdown.rows[i];
8399
+ const total = row.errors + row.warnings + row.info;
8400
+ const count = String(total).padStart(maxCountWidth);
8401
+ const label = padEnd(labels[i], maxLabelWidth);
8402
+ const tags = [];
8403
+ if (row.errors > 0) tags.push(style(t, "danger", `${row.errors} err`));
8404
+ if (row.warnings > 0) tags.push(style(t, "warn", `${row.warnings} warn`));
8405
+ if (row.info > 0) tags.push(style(t, "muted", `${row.info} info`));
8406
+ if (row.fixable > 0) tags.push(style(t, "success", `${row.fixable} fix`));
8407
+ const tagBlock = tags.length > 0 ? ` ${style(t, "muted", "·")} ${tags.join(" ")}` : "";
8408
+ const ruleHint = style(t, "muted", `(${row.rule})`);
8409
+ lines.push(` ${style(t, "muted", count)} ${label} ${ruleHint}${tagBlock}`);
8410
+ }
8411
+ if (input.breakdown.hiddenRules > 0) {
8412
+ const hiddenParts = [];
8413
+ if (input.breakdown.hiddenErrors > 0) hiddenParts.push(`${input.breakdown.hiddenErrors} error${input.breakdown.hiddenErrors === 1 ? "" : "s"}`);
8414
+ if (input.breakdown.hiddenWarnings > 0) hiddenParts.push(`${input.breakdown.hiddenWarnings} warning${input.breakdown.hiddenWarnings === 1 ? "" : "s"}`);
8415
+ const detail = hiddenParts.length > 0 ? ` (${hiddenParts.join(", ")})` : "";
8416
+ lines.push(style(t, "muted", ` +${input.breakdown.hiddenRules} more rule${input.breakdown.hiddenRules === 1 ? "" : "s"}${detail}. Run with -v for the full list.`));
8417
+ }
8418
+ lines.push("");
8419
+ }
7573
8420
  if (input.nextSteps.length > 0) {
7574
8421
  for (const step of input.nextSteps) {
7575
8422
  const glyph = step.emphasis === "primary" ? s.hint : s.bullet;
@@ -7592,77 +8439,43 @@ const renderCleanRun = (input, deps = {}) => {
7592
8439
  return `\n ${parts.join(` ${sep} `)}\n`;
7593
8440
  };
7594
8441
 
7595
- //#endregion
7596
- //#region src/version.ts
7597
- const APP_VERSION = "0.8.3";
7598
-
7599
- //#endregion
7600
- //#region src/utils/telemetry.ts
7601
- const POSTHOG_HOST = "https://eu.i.posthog.com";
7602
- const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
7603
- /**
7604
- * Returns true if telemetry should be disabled.
7605
- * Telemetry is opt-out: it runs unless explicitly disabled.
7606
- */
7607
- const isTelemetryDisabled = (configEnabled) => {
7608
- if (process.env.AISLOP_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1") return true;
7609
- if (process.env.CI === "true" || process.env.CI === "1") return true;
7610
- if (configEnabled === false) return true;
7611
- return false;
7612
- };
7613
- const getScoreBucket = (score) => {
7614
- if (score >= 75) return "75-100";
7615
- if (score >= 50) return "50-75";
7616
- if (score >= 25) return "25-50";
7617
- return "0-25";
7618
- };
7619
- const getAnonymousId = () => {
7620
- const raw = `${os.hostname()}-${os.platform()}-${os.arch()}`;
7621
- let hash = 5381;
7622
- for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
7623
- return `aislop_${(hash >>> 0).toString(36)}`;
7624
- };
7625
- /** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
7626
- let pendingRequest = null;
7627
- const trackEvent = (event) => {
7628
- const payload = {
7629
- api_key: POSTHOG_KEY,
7630
- event: `cli_${event.command}`,
7631
- distinct_id: getAnonymousId(),
7632
- properties: {
7633
- version: APP_VERSION,
7634
- node_version: process.version,
7635
- os: os.platform(),
7636
- arch: os.arch(),
7637
- languages: event.languages,
7638
- score_bucket: event.scoreBucket,
7639
- engine_issues: event.engineIssues,
7640
- engine_timings: event.engineTimings,
7641
- elapsed_ms: event.elapsedMs,
7642
- file_count: event.fileCount,
7643
- fix_steps: event.fixSteps,
7644
- fix_resolved: event.fixResolved
7645
- },
7646
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
7647
- };
7648
- pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
7649
- method: "POST",
7650
- headers: { "Content-Type": "application/json" },
7651
- body: JSON.stringify(payload),
7652
- signal: AbortSignal.timeout(3e3)
7653
- }).then(() => {}).catch(() => {});
7654
- };
7655
- const flushTelemetry = async () => {
7656
- if (pendingRequest) {
7657
- await pendingRequest;
7658
- pendingRequest = null;
7659
- }
7660
- };
7661
-
7662
8442
  //#endregion
7663
8443
  //#region src/commands/scan.ts
7664
8444
  const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
7665
8445
  const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
8446
+ const BREAKDOWN_TOP_N = 10;
8447
+ const computeBreakdown = (diagnostics) => {
8448
+ const byRule = /* @__PURE__ */ new Map();
8449
+ for (const d of diagnostics) {
8450
+ const row = byRule.get(d.rule) ?? {
8451
+ rule: d.rule,
8452
+ errors: 0,
8453
+ warnings: 0,
8454
+ info: 0,
8455
+ fixable: 0
8456
+ };
8457
+ if (d.severity === "error") row.errors++;
8458
+ else if (d.severity === "warning") row.warnings++;
8459
+ else row.info++;
8460
+ if (d.fixable) row.fixable++;
8461
+ byRule.set(d.rule, row);
8462
+ }
8463
+ const sorted = [...byRule.values()].sort((a, b) => {
8464
+ const aTotal = a.errors + a.warnings + a.info;
8465
+ const bTotal = b.errors + b.warnings + b.info;
8466
+ if (aTotal !== bTotal) return bTotal - aTotal;
8467
+ if (a.errors !== b.errors) return b.errors - a.errors;
8468
+ return a.rule.localeCompare(b.rule);
8469
+ });
8470
+ const rows = sorted.slice(0, BREAKDOWN_TOP_N);
8471
+ const hidden = sorted.slice(BREAKDOWN_TOP_N);
8472
+ return {
8473
+ rows,
8474
+ hiddenRules: hidden.length,
8475
+ hiddenErrors: hidden.reduce((acc, r) => acc + r.errors, 0),
8476
+ hiddenWarnings: hidden.reduce((acc, r) => acc + r.warnings, 0)
8477
+ };
8478
+ };
7666
8479
  const buildScanRender = (input) => {
7667
8480
  const deps = {
7668
8481
  theme: createTheme(),
@@ -7712,11 +8525,11 @@ const buildScanRender = (input) => {
7712
8525
  engines: input.results.length,
7713
8526
  elapsedMs: input.elapsedMs,
7714
8527
  nextSteps,
8528
+ breakdown: computeBreakdown(input.diagnostics),
7715
8529
  thresholds: input.thresholds
7716
8530
  }, deps)}`;
7717
8531
  };
7718
8532
  const scanCommand = async (directory, config, options) => {
7719
- const startTime = performance.now();
7720
8533
  const resolvedDir = path.resolve(directory);
7721
8534
  if (!fs.existsSync(resolvedDir)) {
7722
8535
  const msg = `Path does not exist: ${resolvedDir}`;
@@ -7730,9 +8543,18 @@ const scanCommand = async (directory, config, options) => {
7730
8543
  else log.error(msg);
7731
8544
  return { exitCode: 1 };
7732
8545
  }
8546
+ const projectInfo = await discoverProject(resolvedDir);
8547
+ return withCommandLifecycle({
8548
+ command: options.command ?? "scan",
8549
+ config: config.telemetry,
8550
+ languages: projectInfo.languages,
8551
+ fileCount: projectInfo.sourceFileCount
8552
+ }, () => runScanBody(resolvedDir, config, options, projectInfo));
8553
+ };
8554
+ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
8555
+ const startTime = performance.now();
7733
8556
  const showHeader = options.showHeader !== false;
7734
8557
  const useLiveProgress = !options.json && shouldUseSpinner();
7735
- const projectInfo = await discoverProject(resolvedDir);
7736
8558
  let files;
7737
8559
  if (options.staged) {
7738
8560
  files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
@@ -7799,28 +8621,27 @@ const scanCommand = async (directory, config, options) => {
7799
8621
  const elapsedMs = performance.now() - startTime;
7800
8622
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
7801
8623
  const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
7802
- if (!isTelemetryDisabled(config.telemetry?.enabled)) {
7803
- const engineIssues = {};
7804
- const engineTimings = {};
7805
- for (const r of results) {
7806
- engineIssues[r.engine] = r.diagnostics.length;
7807
- engineTimings[r.engine] = Math.round(r.elapsed);
7808
- }
7809
- trackEvent({
7810
- command: options.command ?? "scan",
7811
- languages: projectInfo.languages,
7812
- scoreBucket: getScoreBucket(scoreResult.score),
7813
- engineIssues,
7814
- engineTimings,
7815
- elapsedMs: Math.round(elapsedMs),
7816
- fileCount: projectInfo.sourceFileCount
7817
- });
7818
- }
8624
+ const engineIssues = {};
8625
+ const engineTimings = {};
8626
+ for (const r of results) {
8627
+ engineIssues[r.engine] = r.diagnostics.length;
8628
+ engineTimings[r.engine] = Math.round(r.elapsed);
8629
+ }
8630
+ const completion = {
8631
+ exitCode,
8632
+ score: scoreResult.score,
8633
+ findingCount: allDiagnostics.length,
8634
+ errorCount: allDiagnostics.filter((d) => d.severity === "error").length,
8635
+ warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
8636
+ fixableCount: allDiagnostics.filter((d) => d.fixable).length,
8637
+ engineIssues,
8638
+ engineTimings
8639
+ };
7819
8640
  if (options.json) {
7820
- const { buildJsonOutput } = await import("./json-BbMwrgyd.js");
8641
+ const { buildJsonOutput } = await import("./json-OIzja7OM.js");
7821
8642
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
7822
8643
  console.log(JSON.stringify(jsonOut, null, 2));
7823
- return { exitCode };
8644
+ return completion;
7824
8645
  }
7825
8646
  const projectName = projectInfo.projectName ?? "project";
7826
8647
  const language = projectInfo.languages[0] ?? "unknown";
@@ -7837,7 +8658,7 @@ const scanCommand = async (directory, config, options) => {
7837
8658
  includeHeader: showHeader,
7838
8659
  printBrand: options.printBrand
7839
8660
  }));
7840
- return { exitCode };
8661
+ return completion;
7841
8662
  };
7842
8663
 
7843
8664
  //#endregion
@@ -9785,15 +10606,23 @@ const fixCommand = async (directory, config, options = {
9785
10606
  verbose: false,
9786
10607
  showHeader: true
9787
10608
  }) => {
9788
- const startTime = performance.now();
9789
10609
  const resolvedDir = path.resolve(directory);
9790
10610
  if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
9791
10611
  const msg = !fs.existsSync(resolvedDir) ? `Path does not exist: ${resolvedDir}` : `Not a directory: ${resolvedDir}`;
9792
10612
  log.error(msg);
9793
10613
  return;
9794
10614
  }
9795
- const showHeader = options.showHeader !== false;
9796
10615
  const projectInfo = await discoverProject(resolvedDir);
10616
+ await withCommandLifecycle({
10617
+ command: "fix",
10618
+ config: config.telemetry,
10619
+ languages: projectInfo.languages,
10620
+ fileCount: projectInfo.sourceFileCount
10621
+ }, () => runFixBody(resolvedDir, config, options, projectInfo));
10622
+ };
10623
+ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
10624
+ const startTime = performance.now();
10625
+ const showHeader = options.showHeader !== false;
9797
10626
  const projectName = projectInfo.projectName ?? "project";
9798
10627
  if (showHeader) process.stdout.write(renderHeader({
9799
10628
  version: APP_VERSION,
@@ -9830,12 +10659,6 @@ const fixCommand = async (directory, config, options = {
9830
10659
  await runFormattingStep(pipelineDeps);
9831
10660
  await runForceSteps(pipelineDeps);
9832
10661
  const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
9833
- if (!isTelemetryDisabled(config.telemetry?.enabled)) trackEvent({
9834
- command: "fix",
9835
- languages: projectInfo.languages,
9836
- fixSteps: steps.length,
9837
- fixResolved: totalResolved
9838
- });
9839
10662
  const configDir = findConfigDir(resolvedDir);
9840
10663
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
9841
10664
  const engineConfig = {
@@ -9858,7 +10681,9 @@ const fixCommand = async (directory, config, options = {
9858
10681
  });
9859
10682
  const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
9860
10683
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
9861
- const remaining = allDiagnostics.filter((d) => d.severity === "error").length + allDiagnostics.filter((d) => d.severity === "warning").length;
10684
+ const errors = allDiagnostics.filter((d) => d.severity === "error").length;
10685
+ const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
10686
+ const remaining = errors + warnings;
9862
10687
  if (steps.length === 0) rail.complete({
9863
10688
  status: "skipped",
9864
10689
  label: "No applicable auto-fixers found"
@@ -9887,12 +10712,31 @@ const fixCommand = async (directory, config, options = {
9887
10712
  }
9888
10713
  if (options.agent) {
9889
10714
  launchAgent(options.agent, resolvedDir, allDiagnostics, scoreResult.score);
9890
- return;
10715
+ return {
10716
+ exitCode: 0,
10717
+ score: scoreResult.score,
10718
+ fixSteps: steps.length,
10719
+ fixResolved: totalResolved
10720
+ };
9891
10721
  }
9892
10722
  if (options.prompt) {
9893
10723
  printPrompt(resolvedDir, allDiagnostics, scoreResult.score);
9894
- return;
10724
+ return {
10725
+ exitCode: 0,
10726
+ score: scoreResult.score,
10727
+ fixSteps: steps.length,
10728
+ fixResolved: totalResolved
10729
+ };
9895
10730
  }
10731
+ return {
10732
+ exitCode: 0,
10733
+ score: scoreResult.score,
10734
+ findingCount: allDiagnostics.length,
10735
+ errorCount: errors,
10736
+ warningCount: warnings,
10737
+ fixSteps: steps.length,
10738
+ fixResolved: totalResolved
10739
+ };
9896
10740
  };
9897
10741
 
9898
10742
  //#endregion
@@ -10008,10 +10852,18 @@ const promptForConfigChoices = async () => {
10008
10852
  return {
10009
10853
  engines: enginesSelection,
10010
10854
  failBelow: Number(failBelowRaw),
10855
+ typecheck: DEFAULT_CONFIG.lint.typecheck,
10011
10856
  telemetryEnabled: telemetryChoice === "enabled",
10012
10857
  writeGithubWorkflow: workflowChoice === "yes"
10013
10858
  };
10014
10859
  };
10860
+ const strictChoices = () => ({
10861
+ engines: Object.keys(DEFAULT_CONFIG.engines),
10862
+ failBelow: 85,
10863
+ typecheck: true,
10864
+ telemetryEnabled: DEFAULT_CONFIG.telemetry.enabled,
10865
+ writeGithubWorkflow: true
10866
+ });
10015
10867
  const writeAislopConfig = (configDir, configPath, choices) => {
10016
10868
  const selected = new Set(choices.engines);
10017
10869
  const engines = {
@@ -10026,6 +10878,7 @@ const writeAislopConfig = (configDir, configPath, choices) => {
10026
10878
  version: DEFAULT_CONFIG.version,
10027
10879
  engines,
10028
10880
  quality: { ...DEFAULT_CONFIG.quality },
10881
+ lint: { typecheck: choices.typecheck },
10029
10882
  security: { ...DEFAULT_CONFIG.security },
10030
10883
  scoring: {
10031
10884
  weights: { ...DEFAULT_CONFIG.scoring.weights },
@@ -10078,7 +10931,7 @@ const initCommand = async (directory, options = {}) => {
10078
10931
  return;
10079
10932
  }
10080
10933
  }
10081
- const choices = await promptForConfigChoices();
10934
+ const choices = options.strict ? strictChoices() : await promptForConfigChoices();
10082
10935
  if (!choices) return;
10083
10936
  writeAislopConfig(configDir, configPath, choices);
10084
10937
  const steps = [{
@@ -10375,29 +11228,38 @@ const interactiveCommand = async (directory, config) => {
10375
11228
  //#region src/cli.ts
10376
11229
  process.on("SIGINT", () => process.exit(0));
10377
11230
  process.on("SIGTERM", () => process.exit(0));
10378
- const excludeParser = (value, previous = []) => {
11231
+ const fireInstalledOnce = () => {
11232
+ if (isTelemetryDisabled(loadConfig(process.cwd()).telemetry)) return;
11233
+ if (ensureInstallId(resolveInstallIdPath()).created) track({
11234
+ event: "cli_installed",
11235
+ config: loadConfig(process.cwd()).telemetry
11236
+ });
11237
+ };
11238
+ const commaSeparatedParser = (value, previous = []) => {
10379
11239
  const parts = value.split(",").map((v) => v.trim()).filter(Boolean);
10380
11240
  return [...previous, ...parts];
10381
11241
  };
10382
11242
  const runScan = async (directory, flags) => {
10383
11243
  const config = loadConfig(directory);
10384
- const { exitCode } = await scanCommand(directory, flags.exclude?.length ? {
11244
+ const { exitCode } = await scanCommand(directory, {
10385
11245
  ...config,
10386
- exclude: [...config.exclude ?? [], ...flags.exclude]
10387
- } : config, {
11246
+ exclude: [...config.exclude ?? [], ...flags.exclude ?? []],
11247
+ include: [...config.include ?? [], ...flags.include ?? []]
11248
+ }, {
10388
11249
  changes: Boolean(flags.changes),
10389
11250
  staged: Boolean(flags.staged),
10390
11251
  verbose: Boolean(flags.verbose),
10391
11252
  json: Boolean(flags.json),
10392
- exclude: flags.exclude
11253
+ exclude: flags.exclude,
11254
+ include: flags.include
10393
11255
  });
10394
11256
  if (exitCode !== 0) {
10395
11257
  await flushTelemetry();
10396
- process.exit(exitCode);
11258
+ process.exitCode = exitCode;
10397
11259
  }
10398
11260
  };
10399
- const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !(flags.exclude && flags.exclude.length > 0);
10400
- const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", excludeParser, []).action(async (directory, flags) => {
11261
+ const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !(flags.exclude && flags.exclude.length > 0) && !(flags.include && flags.include.length > 0);
11262
+ const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory, flags) => {
10401
11263
  if (noFlagsPassed(flags) && process.stdin.isTTY) try {
10402
11264
  await interactiveCommand(directory, loadConfig(directory));
10403
11265
  return;
@@ -10433,7 +11295,7 @@ ${style(theme, "dim", "Examples:")}
10433
11295
  npx aislop scan --exclude node_modules --exclude dist --exclude **/*.ts
10434
11296
  ${renderHintLine("Run npx aislop scan to scan your project").trimEnd()}
10435
11297
  `);
10436
- program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", excludeParser, []).action(async (directory = ".", _flags, command) => {
11298
+ program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory = ".", _flags, command) => {
10437
11299
  await runScan(directory, command.optsWithGlobals());
10438
11300
  });
10439
11301
  const FIX_AGENT_FLAGS = [
@@ -10522,43 +11384,70 @@ fixProgram.action(async (directory = ".", _flags, command) => {
10522
11384
  agent: matchFixAgent(flags)
10523
11385
  });
10524
11386
  });
10525
- program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {
10526
- await initCommand(directory);
11387
+ program.command("init [directory]").description("Initialize aislop config in project").option("--strict", "write an enterprise-grade default config: all engines, typecheck on, CI failBelow 85, workflow included").action(async (directory = ".", _flags, command) => {
11388
+ const flags = command.optsWithGlobals();
11389
+ await withCommandLifecycle({
11390
+ command: "init",
11391
+ config: loadConfig(directory).telemetry
11392
+ }, async () => {
11393
+ await initCommand(directory, { strict: Boolean(flags.strict) });
11394
+ return { exitCode: 0 };
11395
+ });
10527
11396
  });
10528
11397
  program.command("doctor [directory]").description("Check installed tools and environment").action(async (directory = ".") => {
10529
- await doctorCommand(directory);
11398
+ await withCommandLifecycle({
11399
+ command: "doctor",
11400
+ config: loadConfig(directory).telemetry
11401
+ }, async () => {
11402
+ await doctorCommand(directory);
11403
+ return { exitCode: 0 };
11404
+ });
10530
11405
  });
10531
11406
  program.command("ci [directory]").description("CI-friendly JSON output with exit codes").option("--human", "render the human-friendly scan design instead of JSON").action(async (directory = ".", _flags, command) => {
10532
11407
  const flags = command.optsWithGlobals();
10533
11408
  const { exitCode } = await ciCommand(directory, loadConfig(directory), { human: Boolean(flags.human) });
10534
11409
  if (exitCode !== 0) {
10535
11410
  await flushTelemetry();
10536
- process.exit(exitCode);
11411
+ process.exitCode = exitCode;
10537
11412
  }
10538
11413
  });
10539
11414
  program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
10540
- await rulesCommand(directory);
11415
+ await withCommandLifecycle({
11416
+ command: "rules",
11417
+ config: loadConfig(directory).telemetry
11418
+ }, async () => {
11419
+ await rulesCommand(directory);
11420
+ return { exitCode: 0 };
11421
+ });
10541
11422
  });
10542
11423
  program.command("badge [directory]").description("Print the public score badge URL + README markdown for this repo").option("--owner <owner>", "GitHub owner (auto-detected from git remote if omitted)").option("--repo <repo>", "GitHub repo name (auto-detected from git remote if omitted)").option("--json", "emit machine-readable JSON instead of the rendered output").action(async (directory = ".", _flags, command) => {
10543
11424
  const flags = command.optsWithGlobals();
10544
11425
  try {
10545
- await badgeCommand({
10546
- directory,
10547
- owner: flags.owner,
10548
- repo: flags.repo,
10549
- json: Boolean(flags.json)
11426
+ await withCommandLifecycle({
11427
+ command: "badge",
11428
+ config: loadConfig(directory).telemetry
11429
+ }, async () => {
11430
+ await badgeCommand({
11431
+ directory,
11432
+ owner: flags.owner,
11433
+ repo: flags.repo,
11434
+ json: Boolean(flags.json)
11435
+ });
11436
+ return { exitCode: 0 };
10550
11437
  });
10551
11438
  } catch (err) {
10552
- process.stderr.write(`${err?.message ?? "Failed to print badge"}\n`);
11439
+ const message = err instanceof Error ? err.message : "Failed to print badge";
11440
+ process.stderr.write(`${message}\n`);
10553
11441
  process.exit(1);
10554
11442
  }
10555
11443
  });
10556
11444
  registerHookCommand(program);
10557
11445
  const main = async () => {
11446
+ fireInstalledOnce();
10558
11447
  await program.parseAsync();
10559
11448
  await flushTelemetry();
10560
11449
  };
10561
11450
  main();
10562
11451
 
10563
11452
  //#endregion
10564
- export { ENGINE_INFO as n, runSubprocess as r, APP_VERSION as t };
11453
+ export { runSubprocess as n, APP_VERSION as r, ENGINE_INFO as t };