deep-slop 1.4.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/.deep-slop/.deep-slop-ignore +13 -0
- package/LICENSE +21 -0
- package/README.md +1170 -0
- package/dist/arch-constraints-C7s1E_bc.js +450 -0
- package/dist/arch-rules-DI1SYPqu.js +358 -0
- package/dist/ast-slop-BGdr58wZ.js +1839 -0
- package/dist/config-lint-ph3vMUbg.js +371 -0
- package/dist/dead-flow-DHRkyxZT.js +1422 -0
- package/dist/deep-slop-bundled.js +33140 -0
- package/dist/discover-B_S_Fy2S.js +164 -0
- package/dist/dup-detect-DKRXM04q.js +709 -0
- package/dist/file-utils-B_HFXhCs.js +93 -0
- package/dist/format-lint-DeElllNm.js +445 -0
- package/dist/framework-lint-CqdlF9hX.js +782 -0
- package/dist/i18n-lint-CPzx7V8Q.js +605 -0
- package/dist/import-intelligence-SK4F7XpL.js +966 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +1030 -0
- package/dist/knip-CgxnnTBZ.js +93 -0
- package/dist/lint-external-ZbW3jGvB.js +326 -0
- package/dist/markup-lint-DKVEDz9M.js +805 -0
- package/dist/mcp.js +35939 -0
- package/dist/meta-quality-Dai1W5iC.js +224 -0
- package/dist/perf-hints-BnWFMFff.js +500 -0
- package/dist/security-deep-DJRINs10.js +1198 -0
- package/dist/syntax-deep-ZQYMutky.js +624 -0
- package/dist/tree-sitter-CM-cP0nl.js +661 -0
- package/dist/type-safety-Dboj2C1t.js +519 -0
- package/package.json +92 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
import { n as detectFrameworks, r as detectLanguages, t as collectFiles } from "./discover-B_S_Fy2S.js";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
//#region src/config/defaults.ts
|
|
8
|
+
/** Default configuration matching the canonical values */
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
engines: {
|
|
11
|
+
"ast-slop": true,
|
|
12
|
+
"import-intelligence": true,
|
|
13
|
+
"dead-flow": true,
|
|
14
|
+
"type-safety": true,
|
|
15
|
+
"syntax-deep": true,
|
|
16
|
+
"security-deep": true,
|
|
17
|
+
"arch-constraints": true,
|
|
18
|
+
"dup-detect": true,
|
|
19
|
+
"perf-hints": true,
|
|
20
|
+
"i18n-lint": true,
|
|
21
|
+
"config-lint": true,
|
|
22
|
+
"meta-quality": true,
|
|
23
|
+
"arch-rules": true,
|
|
24
|
+
"lint-external": true,
|
|
25
|
+
"knip": true,
|
|
26
|
+
"format-lint": true,
|
|
27
|
+
"framework-lint": true,
|
|
28
|
+
"markup-lint": true
|
|
29
|
+
},
|
|
30
|
+
quality: {
|
|
31
|
+
maxFunctionLoc: 50,
|
|
32
|
+
maxFileLoc: 300,
|
|
33
|
+
maxNesting: 4,
|
|
34
|
+
maxParams: 5,
|
|
35
|
+
maxCyclomatic: 10,
|
|
36
|
+
maxCoupling: 10
|
|
37
|
+
},
|
|
38
|
+
security: {
|
|
39
|
+
audit: false,
|
|
40
|
+
auditTimeout: 25e3,
|
|
41
|
+
owasp: true
|
|
42
|
+
},
|
|
43
|
+
imports: {
|
|
44
|
+
suggestAlternatives: true,
|
|
45
|
+
optimizeBarrels: true,
|
|
46
|
+
validateAliases: true,
|
|
47
|
+
buildGraph: true,
|
|
48
|
+
maxCircularDepth: 5
|
|
49
|
+
},
|
|
50
|
+
types: {
|
|
51
|
+
flagAsAny: true,
|
|
52
|
+
suggestTypes: true,
|
|
53
|
+
flagDoubleAssertion: true
|
|
54
|
+
},
|
|
55
|
+
deadCode: {
|
|
56
|
+
unreachableBranches: true,
|
|
57
|
+
unusedExports: true,
|
|
58
|
+
unusedVariables: true
|
|
59
|
+
},
|
|
60
|
+
i18n: {
|
|
61
|
+
hardcodedStrings: true,
|
|
62
|
+
validateKeys: false
|
|
63
|
+
},
|
|
64
|
+
scoring: {
|
|
65
|
+
mode: "logarithmic",
|
|
66
|
+
smoothing: 20,
|
|
67
|
+
maxPerRule: 40
|
|
68
|
+
},
|
|
69
|
+
telemetry: { enabled: false },
|
|
70
|
+
exclude: [
|
|
71
|
+
"node_modules",
|
|
72
|
+
".git",
|
|
73
|
+
"dist",
|
|
74
|
+
"build",
|
|
75
|
+
"coverage",
|
|
76
|
+
"tmp-*"
|
|
77
|
+
],
|
|
78
|
+
ignore: [],
|
|
79
|
+
ci: {
|
|
80
|
+
failBelow: 70,
|
|
81
|
+
format: "json",
|
|
82
|
+
failOnErrors: true
|
|
83
|
+
},
|
|
84
|
+
rules: {}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/types/index.ts
|
|
89
|
+
/** Runtime list of all 18 built-in engine names */
|
|
90
|
+
const ALL_ENGINE_NAMES = [
|
|
91
|
+
"ast-slop",
|
|
92
|
+
"import-intelligence",
|
|
93
|
+
"dead-flow",
|
|
94
|
+
"type-safety",
|
|
95
|
+
"syntax-deep",
|
|
96
|
+
"security-deep",
|
|
97
|
+
"arch-constraints",
|
|
98
|
+
"dup-detect",
|
|
99
|
+
"perf-hints",
|
|
100
|
+
"i18n-lint",
|
|
101
|
+
"config-lint",
|
|
102
|
+
"meta-quality",
|
|
103
|
+
"arch-rules",
|
|
104
|
+
"lint-external",
|
|
105
|
+
"knip",
|
|
106
|
+
"format-lint",
|
|
107
|
+
"framework-lint",
|
|
108
|
+
"markup-lint"
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/scoring/rule-impact.ts
|
|
113
|
+
/** Tier defaults: multiplier and per-rule cap */
|
|
114
|
+
const TIER_DEFAULTS = {
|
|
115
|
+
strict: {
|
|
116
|
+
multiplier: 1,
|
|
117
|
+
cap: 40
|
|
118
|
+
},
|
|
119
|
+
standard: {
|
|
120
|
+
multiplier: 1,
|
|
121
|
+
cap: 30
|
|
122
|
+
},
|
|
123
|
+
maintainability: {
|
|
124
|
+
multiplier: .75,
|
|
125
|
+
cap: 24
|
|
126
|
+
},
|
|
127
|
+
mechanical: {
|
|
128
|
+
multiplier: .5,
|
|
129
|
+
cap: 16
|
|
130
|
+
},
|
|
131
|
+
style: {
|
|
132
|
+
multiplier: .5,
|
|
133
|
+
cap: 8
|
|
134
|
+
},
|
|
135
|
+
advisory: {
|
|
136
|
+
multiplier: .25,
|
|
137
|
+
cap: 8
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/** Helper to build a RuleImpact from tier name + rationale */
|
|
141
|
+
function tier(t, rationale) {
|
|
142
|
+
const def = TIER_DEFAULTS[t];
|
|
143
|
+
return {
|
|
144
|
+
tier: t,
|
|
145
|
+
multiplier: def.multiplier,
|
|
146
|
+
cap: def.cap,
|
|
147
|
+
rationale
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Map of all 60+ rule IDs to their impact tier/multiplier/cap/rationale.
|
|
152
|
+
* Rule IDs use the format "engine/rule-name" matching Diagnostic.rule.
|
|
153
|
+
*/
|
|
154
|
+
const RULE_IMPACT = {
|
|
155
|
+
"ast-slop/narrative-comment": tier("strict", "Narrative comments are a hallmark of AI slop — authoritative, misleading, and dangerous"),
|
|
156
|
+
"ast-slop/trivial-comment": tier("mechanical", "Trivial comments add noise without value, easily bulk-removed"),
|
|
157
|
+
"ast-slop/decorative-comment": tier("style", "Decorative comments are cosmetic — section dividers, banners"),
|
|
158
|
+
"ast-slop/console-leftover": tier("style", "Console leftovers are debug debris, low impact but sloppy"),
|
|
159
|
+
"ast-slop/generic-naming": tier("advisory", "Generic naming reduces readability but is subjective"),
|
|
160
|
+
"ast-slop/hallucinated-import": tier("standard", "Hallucinated imports will cause runtime errors"),
|
|
161
|
+
"ast-slop/as-any-cast": tier("standard", "as-any casts disable type safety — tracked here for ast-slop context"),
|
|
162
|
+
"ast-slop/empty-catch": tier("strict", "Empty catch blocks silently swallow errors, hiding bugs"),
|
|
163
|
+
"ast-slop/todo-leftover": tier("mechanical", "TODO leftovers are technical debt markers, not critical"),
|
|
164
|
+
"import-intelligence/alternative-path": tier("mechanical", "Non-optimal import paths increase bundle size"),
|
|
165
|
+
"import-intelligence/barrel-optimization": tier("mechanical", "Barrel files cause unnecessary re-export overhead"),
|
|
166
|
+
"import-intelligence/circular-dependency": tier("strict", "Circular dependencies create unstable module graphs and runtime errors"),
|
|
167
|
+
"import-intelligence/unused-import": tier("mechanical", "Unused imports are dead code that increases bundle size"),
|
|
168
|
+
"import-intelligence/duplicate-import": tier("mechanical", "Duplicate imports are redundant and indicate copy-paste"),
|
|
169
|
+
"import-intelligence/broken-alias": tier("standard", "Broken path aliases cause module resolution failures"),
|
|
170
|
+
"dead-flow/unreachable-after-terminator": tier("strict", "Unreachable code after return/throw is definitively dead"),
|
|
171
|
+
"dead-flow/unused-export": tier("standard", "Unused exports bloat the public API surface"),
|
|
172
|
+
"dead-flow/unused-variable": tier("standard", "Unused variables are dead code increasing cognitive load"),
|
|
173
|
+
"dead-flow/empty-block": tier("mechanical", "Empty blocks suggest missing implementation"),
|
|
174
|
+
"dead-flow/dead-conditional": tier("standard", "Dead conditionals indicate logic errors or AI artifacts"),
|
|
175
|
+
"dead-flow/dead-switch-case": tier("standard", "Dead switch cases are unreachable branches"),
|
|
176
|
+
"type-safety/as-any-cast": tier("standard", "as-any casts circumvent the type system"),
|
|
177
|
+
"type-safety/double-assertion": tier("strict", "Double assertions (as unknown as X) are type-system abuse"),
|
|
178
|
+
"type-safety/ts-suppress": tier("standard", "@ts-ignore/@ts-expect-error suppress real type errors"),
|
|
179
|
+
"type-safety/non-null-assertion": tier("standard", "Non-null assertions (!) bypass null checks"),
|
|
180
|
+
"type-safety/generic-any": tier("mechanical", "Generic <any> parameters lose type information"),
|
|
181
|
+
"type-safety/missing-return-type": tier("mechanical", "Missing return types reduce type inference reliability"),
|
|
182
|
+
"syntax-deep/bom-present": tier("mechanical", "BOM characters cause encoding issues across tools"),
|
|
183
|
+
"syntax-deep/crlf-line-endings": tier("mechanical", "CRLF line endings cause cross-platform diff noise"),
|
|
184
|
+
"syntax-deep/mixed-line-endings": tier("mechanical", "Mixed line endings break tooling expectations"),
|
|
185
|
+
"syntax-deep/escape-sequence": tier("mechanical", "Unusual escape sequences may indicate encoding bugs"),
|
|
186
|
+
"syntax-deep/regex-issue": tier("mechanical", "Regex issues can cause silent match failures"),
|
|
187
|
+
"syntax-deep/precision-loss": tier("style", "Floating-point precision loss is cosmetic in most contexts"),
|
|
188
|
+
"syntax-deep/unicode-anomaly": tier("standard", "Unicode anomalies may hide invisible characters or homoglyphs"),
|
|
189
|
+
"security-deep/eval-usage": tier("strict", "eval() enables arbitrary code execution"),
|
|
190
|
+
"security-deep/innerhtml-usage": tier("strict", "innerHTML enables XSS injection"),
|
|
191
|
+
"security-deep/sql-injection": tier("strict", "SQL injection enables database compromise"),
|
|
192
|
+
"security-deep/shell-injection": tier("strict", "Shell injection enables OS command execution"),
|
|
193
|
+
"security-deep/prototype-pollution": tier("strict", "Prototype pollution corrupts all object instances"),
|
|
194
|
+
"security-deep/ssrf-risk": tier("strict", "SSRF enables internal network access from external input"),
|
|
195
|
+
"security-deep/hardcoded-secret": tier("strict", "Hardcoded secrets leak credentials into source control"),
|
|
196
|
+
"security-deep/dependency-vulnerability": tier("strict", "Dependency vulnerabilities expose known exploit vectors"),
|
|
197
|
+
"security-deep/xss-risk": tier("strict", "XSS risks enable cross-site scripting attacks"),
|
|
198
|
+
"security-deep/unsafe-html": tier("strict", "Unsafe HTML operations enable injection attacks"),
|
|
199
|
+
"arch-constraints/high-coupling": tier("maintainability", "High coupling makes modules hard to change independently"),
|
|
200
|
+
"arch-constraints/layer-violation": tier("maintainability", "Layer violations break architectural boundaries"),
|
|
201
|
+
"arch-constraints/god-file": tier("maintainability", "God files concentrate too many responsibilities"),
|
|
202
|
+
"arch-constraints/circular-dependency": tier("strict", "Arch-level circular deps create build and runtime instability"),
|
|
203
|
+
"arch-constraints/deep-nesting": tier("maintainability", "Deep nesting reduces readability and increases bug surface"),
|
|
204
|
+
"arch-constraints/unstable-dependency": tier("style", "Unstable dependencies increase fragility"),
|
|
205
|
+
"dup-detect/identical-blocks": tier("maintainability", "Identical code blocks violate DRY and increase maintenance cost"),
|
|
206
|
+
"dup-detect/similar-blocks": tier("style", "Similar blocks may indicate copy-paste with minor edits"),
|
|
207
|
+
"dup-detect/duplicate-imports": tier("mechanical", "Duplicate imports are redundant overhead"),
|
|
208
|
+
"dup-detect/repeated-constants": tier("mechanical", "Repeated constants should be extracted to shared definitions"),
|
|
209
|
+
"dup-detect/copy-paste": tier("style", "Copy-paste patterns increase divergence risk"),
|
|
210
|
+
"perf-hints/n-plus-one": tier("maintainability", "N+1 queries cause severe performance degradation at scale"),
|
|
211
|
+
"perf-hints/react-missing-memo": tier("advisory", "Missing React memoization causes unnecessary re-renders"),
|
|
212
|
+
"perf-hints/sync-in-async": tier("standard", "Synchronous calls in async context block the event loop"),
|
|
213
|
+
"perf-hints/large-loop-allocation": tier("style", "Large allocations in loops increase GC pressure"),
|
|
214
|
+
"perf-hints/unnecessary-await": tier("advisory", "Unnecessary awaits add microtask overhead"),
|
|
215
|
+
"perf-hints/string-concat": tier("style", "String concatenation in loops is slower than array join"),
|
|
216
|
+
"i18n-lint/hardcoded-string": tier("advisory", "Hardcoded strings prevent localization"),
|
|
217
|
+
"i18n-lint/missing-key": tier("mechanical", "Missing translation keys cause fallback or errors"),
|
|
218
|
+
"i18n-lint/locale-mismatch": tier("mechanical", "Locale mismatches cause inconsistent user experience"),
|
|
219
|
+
"i18n-lint/untranslated": tier("advisory", "Untranslated strings break multi-language support"),
|
|
220
|
+
"config-lint/tsconfig-issue": tier("mechanical", "Misconfigured tsconfig causes type-check gaps"),
|
|
221
|
+
"config-lint/eslint-issue": tier("mechanical", "ESLint misconfig leaves lint rules ineffective"),
|
|
222
|
+
"config-lint/package-scripts": tier("mechanical", "Package script issues break CI/CD workflows"),
|
|
223
|
+
"config-lint/prettier-issue": tier("mechanical", "Prettier misconfig causes formatting inconsistencies"),
|
|
224
|
+
"config-lint/vite-config": tier("mechanical", "Vite misconfig affects build performance and output"),
|
|
225
|
+
"config-lint/editorconfig": tier("mechanical", "EditorConfig issues cause cross-editor inconsistencies"),
|
|
226
|
+
"meta-quality/score-report": tier("advisory", "Score report quality is meta — it affects trust in the tool itself"),
|
|
227
|
+
"meta-quality/trend-analysis": tier("advisory", "Trend analysis is meta — helps track regressions over time"),
|
|
228
|
+
"meta-quality/quality-gate": tier("standard", "Quality gate failures block CI, directly impacting delivery"),
|
|
229
|
+
"meta-quality/config-check": tier("mechanical", "Config check issues are operational, not code quality"),
|
|
230
|
+
"lint-external/ruff": tier("mechanical", "Ruff findings are external Python lint issues"),
|
|
231
|
+
"lint-external/golangci": tier("mechanical", "golangci-lint findings are external Go lint issues"),
|
|
232
|
+
"lint-external/clippy": tier("mechanical", "Clippy findings are external Rust lint issues"),
|
|
233
|
+
"format-lint/inconsistent-indent": tier("mechanical", "Mixed indentation breaks rendering and tooling across editors"),
|
|
234
|
+
"format-lint/inconsistent-quotes": tier("style", "Mixed quote styles create diff noise and inconsistency"),
|
|
235
|
+
"format-lint/max-line-length": tier("style", "Long lines require scrolling and reduce readability"),
|
|
236
|
+
"format-lint/inconsistent-semicolons": tier("style", "Inconsistent semicolons create diff noise and style ambiguity"),
|
|
237
|
+
"format-lint/blank-line-cluster": tier("style", "Excessive blank lines waste vertical space"),
|
|
238
|
+
"format-lint/trailing-comma-inconsistency": tier("mechanical", "Inconsistent trailing commas create diff noise and refactoring errors"),
|
|
239
|
+
"nextjs/misplaced-use-client": tier("mechanical", "Unnecessary 'use client' increases client bundle size"),
|
|
240
|
+
"nextjs/missing-use-client": tier("strict", "Missing 'use client' causes runtime errors with React hooks"),
|
|
241
|
+
"nextjs/pages-router-in-app": tier("maintainability", "Pages Router APIs are deprecated in App Router projects"),
|
|
242
|
+
"nextjs/next-router-vs-navigation": tier("maintainability", "next/router is deprecated in App Router — use next/navigation"),
|
|
243
|
+
"nextjs/image-missing-dimensions": tier("mechanical", "Missing Image dimensions cause layout shift"),
|
|
244
|
+
"nextjs/metadata-in-client": tier("strict", "Metadata exports in client components are ignored by Next.js"),
|
|
245
|
+
"nextjs/hardcoded-env": tier("mechanical", "Hardcoded URLs should use environment variables for portability"),
|
|
246
|
+
"nextjs/link-without-aria": tier("advisory", "Links without accessible text fail a11y standards"),
|
|
247
|
+
"tailwind/apply-anti-pattern": tier("maintainability", "@apply negates Tailwind utility-first approach"),
|
|
248
|
+
"tailwind/inline-style-conflict": tier("maintainability", "Mixing inline styles with Tailwind creates conflicting styling sources"),
|
|
249
|
+
"tailwind/important-modifier": tier("style", "!important modifier in Tailwind indicates specificity issues"),
|
|
250
|
+
"tailwind/duplicate-utilities": tier("maintainability", "Conflicting utilities in same className indicate copy-paste errors"),
|
|
251
|
+
"tailwind/magic-values": tier("advisory", "Arbitrary values should use theme scale for consistency"),
|
|
252
|
+
"tailwind/incomplete-flex": tier("style", "Bare flex without alignment likely incomplete"),
|
|
253
|
+
"tailwind/overloaded-classname": tier("advisory", "Overloaded className strings should be extracted into components"),
|
|
254
|
+
"json/trailing-comma": tier("strict", "Trailing commas make JSON invalid per RFC 8259"),
|
|
255
|
+
"json/duplicate-keys": tier("strict", "Duplicate keys in JSON objects cause silent data loss"),
|
|
256
|
+
"json/inconsistent-spacing": tier("style", "Mixed compact/expanded formatting is cosmetic"),
|
|
257
|
+
"json/deep-nesting": tier("maintainability", "Deep nesting makes JSON hard to read and maintain"),
|
|
258
|
+
"yaml/tab-indent": tier("strict", "Tab indentation is invalid in YAML — parsers reject it"),
|
|
259
|
+
"yaml/duplicate-keys": tier("strict", "Duplicate keys in YAML mappings cause silent data loss"),
|
|
260
|
+
"yaml/complex-anchor": tier("advisory", "Complex anchor/alias chains are hard to trace"),
|
|
261
|
+
"yaml/multi-doc-unseparated": tier("mechanical", "Missing --- separator between documents causes ambiguity"),
|
|
262
|
+
"css/unused-selector": tier("mechanical", "Unused CSS selectors bloat stylesheets"),
|
|
263
|
+
"css/important-overuse": tier("maintainability", "!important overuse indicates specificity conflicts"),
|
|
264
|
+
"css/duplicate-property": tier("mechanical", "Duplicate properties in same rule are accidental"),
|
|
265
|
+
"css/universal-selector": tier("advisory", "Universal selector * has performance impact"),
|
|
266
|
+
"html/missing-alt": tier("strict", "Missing alt on <img> fails WCAG 1.1.1 accessibility"),
|
|
267
|
+
"html/missing-lang": tier("maintainability", "Missing lang on <html> fails WCAG 3.1.1"),
|
|
268
|
+
"html/deprecated-tag": tier("mechanical", "Deprecated HTML tags are removed from HTML5 spec"),
|
|
269
|
+
"html/inline-event-handler": tier("maintainability", "Inline handlers violate CSP and mix behavior with structure"),
|
|
270
|
+
"md/broken-link": tier("mechanical", "Empty or # links are broken references"),
|
|
271
|
+
"md/inconsistent-heading": tier("style", "Mixed heading styles reduce document consistency"),
|
|
272
|
+
"md/todo-in-doc": tier("advisory", "TODO/FIXME markers in docs should be tracked externally"),
|
|
273
|
+
"md/missing-fenced-lang": tier("advisory", "Missing language on fenced blocks disables syntax highlighting")
|
|
274
|
+
};
|
|
275
|
+
/** Fallback impact for unknown rules */
|
|
276
|
+
const DEFAULT_IMPACT = {
|
|
277
|
+
tier: "mechanical",
|
|
278
|
+
multiplier: .5,
|
|
279
|
+
cap: 16,
|
|
280
|
+
rationale: "Unknown rule — defaulting to mechanical tier"
|
|
281
|
+
};
|
|
282
|
+
/** Get the RuleImpact for a given rule ID, falling back to default */
|
|
283
|
+
function getRuleImpact(ruleId) {
|
|
284
|
+
return RULE_IMPACT[ruleId] ?? DEFAULT_IMPACT;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/scoring/rule-severity.ts
|
|
289
|
+
/**
|
|
290
|
+
* Per-severity weight mapping.
|
|
291
|
+
* Errors hurt most, suggestions are mild.
|
|
292
|
+
* These weights are multiplied by the rule's impact multiplier
|
|
293
|
+
* to get the per-diagnostic deduction.
|
|
294
|
+
*/
|
|
295
|
+
const SEVERITY_WEIGHTS = {
|
|
296
|
+
error: 10,
|
|
297
|
+
warning: 1,
|
|
298
|
+
info: 0,
|
|
299
|
+
suggestion: 0
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/scoring/index.ts
|
|
304
|
+
/** Default scoring configuration */
|
|
305
|
+
const DEFAULT_SCORING_CONFIG = {
|
|
306
|
+
mode: "logarithmic",
|
|
307
|
+
severityWeights: { ...SEVERITY_WEIGHTS },
|
|
308
|
+
defaultEngineWeight: 1,
|
|
309
|
+
smoothing: 2e4,
|
|
310
|
+
maxPerRule: 5,
|
|
311
|
+
tierDefaults: { ...TIER_DEFAULTS }
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* Classify a score into a human-readable label.
|
|
315
|
+
* Healthy: >= 75
|
|
316
|
+
* Needs Work: >= 50
|
|
317
|
+
* Critical: < 50
|
|
318
|
+
*/
|
|
319
|
+
function scoreLabel(score) {
|
|
320
|
+
if (score >= 75) return "Healthy";
|
|
321
|
+
if (score >= 50) return "Needs Work";
|
|
322
|
+
return "Critical";
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Density-aware logarithmic scoring (aislop formula).
|
|
326
|
+
*
|
|
327
|
+
* density = min(1, diagnostics.length / (fileCount + smoothing))
|
|
328
|
+
* totalDeduction = sum(severityWeight * ruleMultiplier * engineWeight) [skip if rule count > cap]
|
|
329
|
+
* scaledDeduction = totalDeduction * density
|
|
330
|
+
* score = round(100 - (100 * log1p(scaledDeduction)) / log1p(100 + scaledDeduction))
|
|
331
|
+
*/
|
|
332
|
+
function calculateLogarithmic(diagnostics, fileCount, config) {
|
|
333
|
+
const smoothing = config.smoothing;
|
|
334
|
+
const maxPerRule = config.maxPerRule;
|
|
335
|
+
const actionableCount = diagnostics.filter((d) => d.severity === "error" || d.severity === "warning").length;
|
|
336
|
+
const density = Math.min(1, actionableCount / (fileCount + smoothing));
|
|
337
|
+
const ruleCounts = /* @__PURE__ */ new Map();
|
|
338
|
+
const ruleDeductions = /* @__PURE__ */ new Map();
|
|
339
|
+
let totalDeduction = 0;
|
|
340
|
+
let cappedCount = 0;
|
|
341
|
+
for (const d of diagnostics) {
|
|
342
|
+
const impact = getRuleImpact(d.rule);
|
|
343
|
+
const prev = ruleCounts.get(d.rule) ?? 0;
|
|
344
|
+
ruleCounts.set(d.rule, prev + 1);
|
|
345
|
+
if (prev >= impact.cap) {
|
|
346
|
+
cappedCount++;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const severityWeight = config.severityWeights[d.severity];
|
|
350
|
+
const ruleMultiplier = impact.multiplier;
|
|
351
|
+
const engineWeight = config.defaultEngineWeight;
|
|
352
|
+
const deduction = severityWeight * ruleMultiplier * engineWeight;
|
|
353
|
+
const prevDeduction = ruleDeductions.get(d.rule) ?? 0;
|
|
354
|
+
if (prevDeduction + deduction > maxPerRule) {
|
|
355
|
+
const allowed = maxPerRule - prevDeduction;
|
|
356
|
+
if (allowed > 0) {
|
|
357
|
+
totalDeduction += allowed;
|
|
358
|
+
ruleDeductions.set(d.rule, maxPerRule);
|
|
359
|
+
}
|
|
360
|
+
cappedCount++;
|
|
361
|
+
} else {
|
|
362
|
+
totalDeduction += deduction;
|
|
363
|
+
ruleDeductions.set(d.rule, prevDeduction + deduction);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const scaledDeduction = totalDeduction * density;
|
|
367
|
+
const rawScore = 100 - 100 * Math.log1p(scaledDeduction) / Math.log1p(100 + scaledDeduction);
|
|
368
|
+
const score = Math.round(Math.max(0, Math.min(100, rawScore)));
|
|
369
|
+
return {
|
|
370
|
+
score,
|
|
371
|
+
label: scoreLabel(score),
|
|
372
|
+
density,
|
|
373
|
+
totalDeduction,
|
|
374
|
+
scaledDeduction,
|
|
375
|
+
cappedCount,
|
|
376
|
+
mode: "logarithmic"
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Legacy linear scoring (old formula).
|
|
381
|
+
* score = max(0, 100 - sum(penalties))
|
|
382
|
+
* Each diagnostic contributes severityWeight * ruleMultiplier.
|
|
383
|
+
*/
|
|
384
|
+
function calculateLinear(diagnostics, config) {
|
|
385
|
+
const ruleCounts = /* @__PURE__ */ new Map();
|
|
386
|
+
let totalPenalty = 0;
|
|
387
|
+
let cappedCount = 0;
|
|
388
|
+
for (const d of diagnostics) {
|
|
389
|
+
const impact = getRuleImpact(d.rule);
|
|
390
|
+
const prev = ruleCounts.get(d.rule) ?? 0;
|
|
391
|
+
ruleCounts.set(d.rule, prev + 1);
|
|
392
|
+
if (prev >= impact.cap) {
|
|
393
|
+
cappedCount++;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const severityWeight = config.severityWeights[d.severity];
|
|
397
|
+
const ruleMultiplier = impact.multiplier;
|
|
398
|
+
totalPenalty += severityWeight * ruleMultiplier;
|
|
399
|
+
}
|
|
400
|
+
const score = Math.max(0, Math.round(100 - totalPenalty));
|
|
401
|
+
return {
|
|
402
|
+
score,
|
|
403
|
+
label: scoreLabel(score),
|
|
404
|
+
density: 0,
|
|
405
|
+
totalDeduction: totalPenalty,
|
|
406
|
+
scaledDeduction: totalPenalty,
|
|
407
|
+
cappedCount,
|
|
408
|
+
mode: "linear"
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Calculate score using the configured mode.
|
|
413
|
+
*
|
|
414
|
+
* @param diagnostics - All diagnostics from the scan
|
|
415
|
+
* @param fileCount - Number of files scanned (for density calculation)
|
|
416
|
+
* @param config - Scoring configuration (uses defaults if omitted)
|
|
417
|
+
* @returns Detailed scoring result
|
|
418
|
+
*/
|
|
419
|
+
function calculateScore(diagnostics, fileCount, config = DEFAULT_SCORING_CONFIG) {
|
|
420
|
+
if (config.mode === "linear") return calculateLinear(diagnostics, config);
|
|
421
|
+
return calculateLogarithmic(diagnostics, fileCount, config);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/scoring/rule-overrides.ts
|
|
426
|
+
/**
|
|
427
|
+
* Apply per-rule severity overrides to a list of diagnostics.
|
|
428
|
+
*
|
|
429
|
+
* - If a rule's override is "off": the diagnostic is removed (filtered out).
|
|
430
|
+
* - If a rule's override is "error"/"warning"/"info": the diagnostic's severity is rewritten.
|
|
431
|
+
* - Override keys ending with "/*" act as wildcard prefixes, matching all rules
|
|
432
|
+
* that start with that prefix (e.g. "ast-slop/*" matches "ast-slop/narrative-comment").
|
|
433
|
+
*
|
|
434
|
+
* Exact-match overrides take precedence over wildcard overrides.
|
|
435
|
+
* CLI overrides (passed separately) should be merged into the overrides map
|
|
436
|
+
* before calling this function, with CLI values taking priority.
|
|
437
|
+
*
|
|
438
|
+
* @param diagnostics - Raw diagnostics from all engines
|
|
439
|
+
* @param overrides - Map of rule-id (or prefix with /*) to severity override
|
|
440
|
+
* @returns Filtered and re-severitized diagnostics
|
|
441
|
+
*/
|
|
442
|
+
function applyRuleSeverities(diagnostics, overrides) {
|
|
443
|
+
if (!overrides || Object.keys(overrides).length === 0) return diagnostics;
|
|
444
|
+
const wildcardPrefixes = [];
|
|
445
|
+
const exactOverrides = {};
|
|
446
|
+
for (const [key, severity] of Object.entries(overrides)) if (key.endsWith("/*")) wildcardPrefixes.push({
|
|
447
|
+
prefix: key.slice(0, -2),
|
|
448
|
+
severity
|
|
449
|
+
});
|
|
450
|
+
else exactOverrides[key] = severity;
|
|
451
|
+
const severityMap = {
|
|
452
|
+
error: "error",
|
|
453
|
+
warning: "warning",
|
|
454
|
+
info: "info"
|
|
455
|
+
};
|
|
456
|
+
const result = [];
|
|
457
|
+
for (const d of diagnostics) {
|
|
458
|
+
let override = exactOverrides[d.rule];
|
|
459
|
+
if (!override) {
|
|
460
|
+
for (const wc of wildcardPrefixes) if (d.rule.startsWith(wc.prefix)) {
|
|
461
|
+
override = wc.severity;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (!override) {
|
|
466
|
+
result.push(d);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (override === "off") continue;
|
|
470
|
+
const newSeverity = severityMap[override];
|
|
471
|
+
if (newSeverity && newSeverity !== d.severity) result.push({
|
|
472
|
+
...d,
|
|
473
|
+
severity: newSeverity
|
|
474
|
+
});
|
|
475
|
+
else result.push(d);
|
|
476
|
+
}
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/history/store.ts
|
|
482
|
+
const HISTORY_DIR = ".deep-slop";
|
|
483
|
+
const HISTORY_FILE = "history.jsonl";
|
|
484
|
+
function historyPath(rootDir) {
|
|
485
|
+
return join(rootDir, HISTORY_DIR, HISTORY_FILE);
|
|
486
|
+
}
|
|
487
|
+
function appendRecord(rootDir, record) {
|
|
488
|
+
const dir = join(rootDir, HISTORY_DIR);
|
|
489
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
490
|
+
const line = JSON.stringify(record) + "\n";
|
|
491
|
+
appendFileSync(historyPath(rootDir), line, "utf8");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/utils/file-cache.ts
|
|
496
|
+
/** In-memory file content cache — avoids re-reading the same file across engines */
|
|
497
|
+
const cache = /* @__PURE__ */ new Map();
|
|
498
|
+
/** Read file with caching. Checks mtime to invalidate stale entries. */
|
|
499
|
+
async function readFileCached(filePath) {
|
|
500
|
+
await readFile(filePath).then((b) => b);
|
|
501
|
+
const cached = cache.get(filePath);
|
|
502
|
+
if (cached) return cached.content;
|
|
503
|
+
let content = (await readFile(filePath)).toString("utf-8");
|
|
504
|
+
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
505
|
+
cache.set(filePath, {
|
|
506
|
+
content,
|
|
507
|
+
mtimeMs: Date.now()
|
|
508
|
+
});
|
|
509
|
+
return content;
|
|
510
|
+
}
|
|
511
|
+
/** Preload multiple files into cache in parallel */
|
|
512
|
+
async function preloadFiles(filePaths) {
|
|
513
|
+
await Promise.all(filePaths.map(async (fp) => {
|
|
514
|
+
try {
|
|
515
|
+
await readFileCached(fp);
|
|
516
|
+
} catch {}
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
/** Clear the file cache (call between scans) */
|
|
520
|
+
function clearFileCache() {
|
|
521
|
+
cache.clear();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/plugins/loader.ts
|
|
526
|
+
/**
|
|
527
|
+
* Load a plugin from a file path.
|
|
528
|
+
* The module must default-export an Engine-compatible object.
|
|
529
|
+
* Returns null on any failure (missing file, bad export, etc).
|
|
530
|
+
*/
|
|
531
|
+
async function loadPlugin(pluginPath) {
|
|
532
|
+
try {
|
|
533
|
+
const engine = (await import(pluginPath)).default;
|
|
534
|
+
if (!engine || typeof engine !== "object") return null;
|
|
535
|
+
if (typeof engine.name === "string" && typeof engine.description === "string" && Array.isArray(engine.supportedLanguages) && typeof engine.run === "function") return engine;
|
|
536
|
+
return null;
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Load multiple plugins from an array of paths.
|
|
543
|
+
* Returns successfully loaded engines, skipping failures.
|
|
544
|
+
*/
|
|
545
|
+
async function loadPlugins(paths) {
|
|
546
|
+
const results = await Promise.allSettled(paths.map((p) => loadPlugin(p)));
|
|
547
|
+
const engines = [];
|
|
548
|
+
for (const r of results) if (r.status === "fulfilled" && r.value !== null) engines.push(r.value);
|
|
549
|
+
return engines;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
//#endregion
|
|
553
|
+
//#region src/plugins/registry.ts
|
|
554
|
+
/** Plugin registry singleton */
|
|
555
|
+
var PluginRegistry = class {
|
|
556
|
+
entries = /* @__PURE__ */ new Map();
|
|
557
|
+
loaded = false;
|
|
558
|
+
/** Get all registered plugins */
|
|
559
|
+
getAll() {
|
|
560
|
+
return [...this.entries.values()];
|
|
561
|
+
}
|
|
562
|
+
/** Get all successfully loaded engines */
|
|
563
|
+
getEngines() {
|
|
564
|
+
return [...this.entries.values()].filter((e) => e.loaded).map((e) => e.engine);
|
|
565
|
+
}
|
|
566
|
+
/** Get a specific plugin by id */
|
|
567
|
+
get(id) {
|
|
568
|
+
return this.entries.get(id);
|
|
569
|
+
}
|
|
570
|
+
/** Get a loaded engine by name */
|
|
571
|
+
getEngine(name) {
|
|
572
|
+
const entry = this.entries.get(name);
|
|
573
|
+
return entry?.loaded ? entry.engine : void 0;
|
|
574
|
+
}
|
|
575
|
+
/** Register a plugin entry */
|
|
576
|
+
register(entry) {
|
|
577
|
+
this.entries.set(entry.id, entry);
|
|
578
|
+
}
|
|
579
|
+
/** Remove a plugin by id */
|
|
580
|
+
remove(id) {
|
|
581
|
+
return this.entries.delete(id);
|
|
582
|
+
}
|
|
583
|
+
/** Check if a plugin is registered */
|
|
584
|
+
has(id) {
|
|
585
|
+
return this.entries.has(id);
|
|
586
|
+
}
|
|
587
|
+
/** Number of registered plugins */
|
|
588
|
+
get size() {
|
|
589
|
+
return this.entries.size;
|
|
590
|
+
}
|
|
591
|
+
/** Whether the registry has been loaded from disk */
|
|
592
|
+
get isLoaded() {
|
|
593
|
+
return this.loaded;
|
|
594
|
+
}
|
|
595
|
+
/** Mark the registry as loaded */
|
|
596
|
+
setLoaded(loaded) {
|
|
597
|
+
this.loaded = loaded;
|
|
598
|
+
}
|
|
599
|
+
/** Clear all entries */
|
|
600
|
+
clear() {
|
|
601
|
+
this.entries.clear();
|
|
602
|
+
this.loaded = false;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
/** Global plugin registry instance */
|
|
606
|
+
const pluginRegistry = new PluginRegistry();
|
|
607
|
+
/** Default plugin directory name */
|
|
608
|
+
const PLUGIN_DIR = ".deep-slop/plugins";
|
|
609
|
+
/**
|
|
610
|
+
* Resolve the plugins directory for a given project root.
|
|
611
|
+
*/
|
|
612
|
+
function getPluginDir(rootDir) {
|
|
613
|
+
return join(rootDir, PLUGIN_DIR);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Discover and load all plugins from the project's plugin directory.
|
|
617
|
+
* Returns the loaded engines. Safe to call multiple times.
|
|
618
|
+
*/
|
|
619
|
+
async function discoverAndLoadPlugins(rootDir) {
|
|
620
|
+
if (pluginRegistry.isLoaded) return pluginRegistry.getEngines();
|
|
621
|
+
const pluginDir = getPluginDir(rootDir);
|
|
622
|
+
if (!existsSync(pluginDir)) {
|
|
623
|
+
pluginRegistry.setLoaded(true);
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
const pluginPaths = (await readdir(pluginDir)).filter((f) => f.endsWith(".js") || f.endsWith(".mjs")).map((f) => join(pluginDir, f));
|
|
628
|
+
const engines = await loadPlugins(pluginPaths);
|
|
629
|
+
for (let i = 0; i < engines.length; i++) {
|
|
630
|
+
const engine = engines[i];
|
|
631
|
+
pluginRegistry.register({
|
|
632
|
+
id: engine.name,
|
|
633
|
+
path: pluginPaths[i],
|
|
634
|
+
engine,
|
|
635
|
+
loaded: true
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
for (let i = 0; i < pluginPaths.length; i++) {
|
|
639
|
+
const path = pluginPaths[i];
|
|
640
|
+
if (!engines.find((e) => {
|
|
641
|
+
return pluginRegistry.getAll().find((ent) => ent.path === path)?.engine;
|
|
642
|
+
}) && !pluginRegistry.has(path)) pluginRegistry.register({
|
|
643
|
+
id: `plugin-${i}`,
|
|
644
|
+
path,
|
|
645
|
+
engine: null,
|
|
646
|
+
loaded: false,
|
|
647
|
+
error: "Failed to load plugin module"
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
pluginRegistry.setLoaded(true);
|
|
651
|
+
return engines;
|
|
652
|
+
} catch {
|
|
653
|
+
pluginRegistry.setLoaded(true);
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
//#endregion
|
|
659
|
+
//#region src/utils/suppress.ts
|
|
660
|
+
/** Parse suppress directives from source content */
|
|
661
|
+
function parseSuppressDirectives(content) {
|
|
662
|
+
const entries = [];
|
|
663
|
+
const lines = content.split("\n");
|
|
664
|
+
for (let i = 0; i < lines.length; i++) {
|
|
665
|
+
const line = lines[i].trim();
|
|
666
|
+
const lineNum = i + 1;
|
|
667
|
+
const ignoreNextMatch = line.match(/\/\/\s*deep-slop-ignore-next(?:\s+(.+))?$/);
|
|
668
|
+
if (ignoreNextMatch) {
|
|
669
|
+
entries.push({
|
|
670
|
+
directiveLine: lineNum,
|
|
671
|
+
targetLine: lineNum + 1,
|
|
672
|
+
rules: parseRuleList(ignoreNextMatch[1]),
|
|
673
|
+
type: "next-line"
|
|
674
|
+
});
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const ignoreLineMatch = line.match(/\/\/\s*deep-slop-ignore-line(?:\s+(.+))?$/);
|
|
678
|
+
if (ignoreLineMatch) {
|
|
679
|
+
entries.push({
|
|
680
|
+
directiveLine: lineNum,
|
|
681
|
+
targetLine: lineNum,
|
|
682
|
+
rules: parseRuleList(ignoreLineMatch[1]),
|
|
683
|
+
type: "line"
|
|
684
|
+
});
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const ignoreRuleMatch = line.match(/\/\/\s*deep-slop-ignore(?:\s+(.+))?$/);
|
|
688
|
+
if (ignoreRuleMatch) {
|
|
689
|
+
const rulePart = ignoreRuleMatch[1];
|
|
690
|
+
if (rulePart !== "start" && rulePart !== "end") {
|
|
691
|
+
entries.push({
|
|
692
|
+
directiveLine: lineNum,
|
|
693
|
+
targetLine: lineNum + 1,
|
|
694
|
+
rules: parseRuleList(rulePart),
|
|
695
|
+
type: "next-line"
|
|
696
|
+
});
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const ignoreStartMatch = line.match(/\/\/\s*deep-slop-ignore-start(?:\s+(.+))?$/);
|
|
701
|
+
if (ignoreStartMatch) {
|
|
702
|
+
entries.push({
|
|
703
|
+
directiveLine: lineNum,
|
|
704
|
+
targetLine: lineNum,
|
|
705
|
+
rules: parseRuleList(ignoreStartMatch[1]),
|
|
706
|
+
type: "block-start"
|
|
707
|
+
});
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (line.match(/\/\/\s*deep-slop-ignore-end/)) {
|
|
711
|
+
entries.push({
|
|
712
|
+
directiveLine: lineNum,
|
|
713
|
+
targetLine: lineNum,
|
|
714
|
+
rules: /* @__PURE__ */ new Set(),
|
|
715
|
+
type: "block-end"
|
|
716
|
+
});
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const nextLineMatch = line.match(/\/\/\s*deep-slop-disable-next-line(?:\s+(.+))?$/);
|
|
720
|
+
if (nextLineMatch) {
|
|
721
|
+
entries.push({
|
|
722
|
+
directiveLine: lineNum,
|
|
723
|
+
targetLine: lineNum + 1,
|
|
724
|
+
rules: parseRuleList(nextLineMatch[1]),
|
|
725
|
+
type: "next-line"
|
|
726
|
+
});
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const lineMatch = line.match(/\/\/\s*deep-slop-disable-line(?:\s+(.+))?$/);
|
|
730
|
+
if (lineMatch) {
|
|
731
|
+
entries.push({
|
|
732
|
+
directiveLine: lineNum,
|
|
733
|
+
targetLine: lineNum,
|
|
734
|
+
rules: parseRuleList(lineMatch[1]),
|
|
735
|
+
type: "line"
|
|
736
|
+
});
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const blockStart = line.match(/\/\*\s*deep-slop-disable(?:\s+(.+))?\s*\//);
|
|
740
|
+
if (blockStart) {
|
|
741
|
+
entries.push({
|
|
742
|
+
directiveLine: lineNum,
|
|
743
|
+
targetLine: lineNum,
|
|
744
|
+
rules: parseRuleList(blockStart[1]),
|
|
745
|
+
type: "block-start"
|
|
746
|
+
});
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (line.match(/\/\*\s*deep-slop-enable\s*\//)) entries.push({
|
|
750
|
+
directiveLine: lineNum,
|
|
751
|
+
targetLine: lineNum,
|
|
752
|
+
rules: /* @__PURE__ */ new Set(),
|
|
753
|
+
type: "block-end"
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
return entries;
|
|
757
|
+
}
|
|
758
|
+
/** Parse comma/space-separated rule list from directive */
|
|
759
|
+
function parseRuleList(rulesStr) {
|
|
760
|
+
if (!rulesStr) return /* @__PURE__ */ new Set();
|
|
761
|
+
const rules = rulesStr.split(/[,\\s]+/).map((r) => r.trim()).filter(Boolean);
|
|
762
|
+
return new Set(rules);
|
|
763
|
+
}
|
|
764
|
+
/** Build a suppress check function from parsed directives */
|
|
765
|
+
function buildSuppressChecker(entries) {
|
|
766
|
+
const blockRanges = [];
|
|
767
|
+
let currentStart = null;
|
|
768
|
+
for (const entry of entries) if (entry.type === "block-start") currentStart = {
|
|
769
|
+
line: entry.directiveLine,
|
|
770
|
+
rules: entry.rules
|
|
771
|
+
};
|
|
772
|
+
else if (entry.type === "block-end" && currentStart) {
|
|
773
|
+
blockRanges.push({
|
|
774
|
+
startLine: currentStart.line,
|
|
775
|
+
endLine: entry.directiveLine,
|
|
776
|
+
rules: currentStart.rules
|
|
777
|
+
});
|
|
778
|
+
currentStart = null;
|
|
779
|
+
}
|
|
780
|
+
if (currentStart) blockRanges.push({
|
|
781
|
+
startLine: currentStart.line,
|
|
782
|
+
endLine: 999999,
|
|
783
|
+
rules: currentStart.rules
|
|
784
|
+
});
|
|
785
|
+
return (line, rule) => {
|
|
786
|
+
for (const entry of entries) if (entry.type === "next-line" || entry.type === "line") {
|
|
787
|
+
if (entry.targetLine === line) {
|
|
788
|
+
if (entry.rules.size === 0 || entry.rules.has(rule)) return true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
for (const range of blockRanges) if (line >= range.startLine && line <= range.endLine) {
|
|
792
|
+
if (range.rules.size === 0 || range.rules.has(rule)) return true;
|
|
793
|
+
}
|
|
794
|
+
return false;
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Build a full suppress map for a file's content.
|
|
799
|
+
* Returns both the entries and a checker function.
|
|
800
|
+
*/
|
|
801
|
+
function buildSuppressMap(content) {
|
|
802
|
+
const entries = parseSuppressDirectives(content);
|
|
803
|
+
return {
|
|
804
|
+
entries,
|
|
805
|
+
isSuppressed: buildSuppressChecker(entries)
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Load global suppression rules from `.deep-slop/.deep-slop-ignore`.
|
|
810
|
+
* Returns a list of rule IDs to suppress across the whole project.
|
|
811
|
+
* Missing or unreadable files are treated as "no global rules".
|
|
812
|
+
*/
|
|
813
|
+
function loadIgnoreFile(rootDir) {
|
|
814
|
+
const ignorePath = join(rootDir, ".deep-slop", ".deep-slop-ignore");
|
|
815
|
+
try {
|
|
816
|
+
return readFileSync(ignorePath, "utf-8").split("\n").map((line) => line.split("#")[0].trim()).filter((line) => line && !line.startsWith("file:"));
|
|
817
|
+
} catch {
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Filter diagnostics using suppress directives.
|
|
823
|
+
* Returns the filtered list and the count of suppressed diagnostics.
|
|
824
|
+
*/
|
|
825
|
+
function applySuppressDirectives(diagnostics, fileContents, globallySuppressed = /* @__PURE__ */ new Set()) {
|
|
826
|
+
const suppressMaps = /* @__PURE__ */ new Map();
|
|
827
|
+
for (const [filePath, content] of fileContents) suppressMaps.set(filePath, buildSuppressMap(content));
|
|
828
|
+
const filtered = [];
|
|
829
|
+
let suppressedCount = 0;
|
|
830
|
+
for (const diag of diagnostics) {
|
|
831
|
+
if (globallySuppressed.has(diag.rule)) {
|
|
832
|
+
suppressedCount++;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const map = suppressMaps.get(diag.filePath);
|
|
836
|
+
if (map && map.isSuppressed(diag.line, diag.rule)) suppressedCount++;
|
|
837
|
+
else filtered.push(diag);
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
filtered,
|
|
841
|
+
suppressedCount
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
//#endregion
|
|
846
|
+
//#region src/engines/orchestrator.ts
|
|
847
|
+
/** File extension to Language mapping (for scoreability check) */
|
|
848
|
+
const EXT_TO_LANG = {
|
|
849
|
+
".ts": "typescript",
|
|
850
|
+
".tsx": "tsx",
|
|
851
|
+
".js": "javascript",
|
|
852
|
+
".jsx": "jsx",
|
|
853
|
+
".mjs": "javascript",
|
|
854
|
+
".cjs": "javascript",
|
|
855
|
+
".py": "python",
|
|
856
|
+
".go": "go",
|
|
857
|
+
".rs": "rust",
|
|
858
|
+
".rb": "ruby",
|
|
859
|
+
".php": "php",
|
|
860
|
+
".java": "java",
|
|
861
|
+
".cs": "csharp",
|
|
862
|
+
".swift": "swift"
|
|
863
|
+
};
|
|
864
|
+
/** Registry of all 18 engines (loaded lazily) */
|
|
865
|
+
const ENGINE_REGISTRY = {
|
|
866
|
+
"ast-slop": () => import("./ast-slop-BGdr58wZ.js").then((m) => m.astSlopEngine),
|
|
867
|
+
"import-intelligence": () => import("./import-intelligence-SK4F7XpL.js").then((m) => m.importIntelligenceEngine),
|
|
868
|
+
"dead-flow": () => import("./dead-flow-DHRkyxZT.js").then((m) => m.deadFlowEngine),
|
|
869
|
+
"type-safety": () => import("./type-safety-Dboj2C1t.js").then((m) => m.typeSafetyEngine),
|
|
870
|
+
"syntax-deep": () => import("./syntax-deep-ZQYMutky.js").then((m) => m.syntaxDeepEngine),
|
|
871
|
+
"security-deep": () => import("./security-deep-DJRINs10.js").then((m) => m.securityDeepEngine),
|
|
872
|
+
"arch-constraints": () => import("./arch-constraints-C7s1E_bc.js").then((m) => m.archConstraintsEngine),
|
|
873
|
+
"dup-detect": () => import("./dup-detect-DKRXM04q.js").then((m) => m.dupDetectEngine),
|
|
874
|
+
"perf-hints": () => import("./perf-hints-BnWFMFff.js").then((m) => m.perfHintsEngine),
|
|
875
|
+
"i18n-lint": () => import("./i18n-lint-CPzx7V8Q.js").then((m) => m.i18nLintEngine),
|
|
876
|
+
"config-lint": () => import("./config-lint-ph3vMUbg.js").then((m) => m.configLintEngine),
|
|
877
|
+
"meta-quality": () => import("./meta-quality-Dai1W5iC.js").then((m) => m.metaQualityEngine),
|
|
878
|
+
"lint-external": () => import("./lint-external-ZbW3jGvB.js").then((m) => m.lintExternalEngine),
|
|
879
|
+
"arch-rules": () => import("./arch-rules-DI1SYPqu.js").then((m) => m.archRulesEngine),
|
|
880
|
+
"knip": () => import("./knip-CgxnnTBZ.js").then((m) => m.knipEngine),
|
|
881
|
+
"format-lint": () => import("./format-lint-DeElllNm.js").then((m) => m.formatLintEngine),
|
|
882
|
+
"framework-lint": () => import("./framework-lint-CqdlF9hX.js").then((m) => m.frameworkLintEngine),
|
|
883
|
+
"markup-lint": () => import("./markup-lint-DKVEDz9M.js").then((m) => m.markupLintEngine)
|
|
884
|
+
};
|
|
885
|
+
/** Run selected engines and produce aggregated scan result */
|
|
886
|
+
async function runScan(context, callbacks) {
|
|
887
|
+
const startTotal = performance.now();
|
|
888
|
+
clearFileCache();
|
|
889
|
+
if (context.files?.length) await preloadFiles(context.files);
|
|
890
|
+
const pluginEngines = await discoverAndLoadPlugins(context.rootDirectory);
|
|
891
|
+
const enabledEngines = Object.entries(ENGINE_REGISTRY).filter(([name]) => context.config.engines[name] !== false);
|
|
892
|
+
for (const pluginEngine of pluginEngines) if (context.config.engines[pluginEngine.name] !== false) enabledEngines.push([pluginEngine.name, () => Promise.resolve(pluginEngine)]);
|
|
893
|
+
const results = [];
|
|
894
|
+
let completed = 0;
|
|
895
|
+
const settled = await Promise.allSettled(enabledEngines.map(async ([name, loader]) => {
|
|
896
|
+
callbacks?.onEngineStart?.(name);
|
|
897
|
+
try {
|
|
898
|
+
const engine = await loader();
|
|
899
|
+
const start = performance.now();
|
|
900
|
+
if (!engine.supportedLanguages.some((l) => context.languages.includes(l))) {
|
|
901
|
+
const result = {
|
|
902
|
+
engine: name,
|
|
903
|
+
diagnostics: [],
|
|
904
|
+
elapsed: 0,
|
|
905
|
+
skipped: true,
|
|
906
|
+
skipReason: `No supported language found (engine supports: ${engine.supportedLanguages.join(", ")})`
|
|
907
|
+
};
|
|
908
|
+
callbacks?.onEngineComplete?.(result);
|
|
909
|
+
completed++;
|
|
910
|
+
callbacks?.onProgress?.(completed, enabledEngines.length);
|
|
911
|
+
return result;
|
|
912
|
+
}
|
|
913
|
+
const result = await engine.run(context);
|
|
914
|
+
result.elapsed = performance.now() - start;
|
|
915
|
+
callbacks?.onEngineComplete?.(result);
|
|
916
|
+
completed++;
|
|
917
|
+
callbacks?.onProgress?.(completed, enabledEngines.length);
|
|
918
|
+
return result;
|
|
919
|
+
} catch (error) {
|
|
920
|
+
const result = {
|
|
921
|
+
engine: name,
|
|
922
|
+
diagnostics: [],
|
|
923
|
+
elapsed: 0,
|
|
924
|
+
skipped: true,
|
|
925
|
+
skipReason: error instanceof Error ? error.message : String(error)
|
|
926
|
+
};
|
|
927
|
+
callbacks?.onEngineComplete?.(result);
|
|
928
|
+
completed++;
|
|
929
|
+
callbacks?.onProgress?.(completed, enabledEngines.length);
|
|
930
|
+
return result;
|
|
931
|
+
}
|
|
932
|
+
}));
|
|
933
|
+
for (const r of settled) results.push(r.status === "fulfilled" ? r.value : {
|
|
934
|
+
engine: "ast-slop",
|
|
935
|
+
diagnostics: [],
|
|
936
|
+
elapsed: 0,
|
|
937
|
+
skipped: true,
|
|
938
|
+
skipReason: r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
939
|
+
});
|
|
940
|
+
let allDiagnostics = applyRuleSeverities(results.flatMap((r) => r.diagnostics), context.config.rules || {});
|
|
941
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
942
|
+
if (context.files?.length) {
|
|
943
|
+
const { readFile: fsReadFile } = await import("node:fs/promises");
|
|
944
|
+
const { join } = await import("node:path");
|
|
945
|
+
const uniquePaths = new Set(allDiagnostics.map((d) => d.filePath));
|
|
946
|
+
for (const relPath of uniquePaths) try {
|
|
947
|
+
const content = await fsReadFile(join(context.rootDirectory, relPath), "utf-8");
|
|
948
|
+
fileContents.set(relPath, content);
|
|
949
|
+
} catch {}
|
|
950
|
+
}
|
|
951
|
+
const globallySuppressed = new Set(loadIgnoreFile(context.rootDirectory));
|
|
952
|
+
const { filtered, suppressedCount } = applySuppressDirectives(allDiagnostics, fileContents, globallySuppressed);
|
|
953
|
+
allDiagnostics = filtered;
|
|
954
|
+
for (const r of results) r.diagnostics = allDiagnostics.filter((d) => d.engine === r.engine);
|
|
955
|
+
const bySeverity = {
|
|
956
|
+
error: 0,
|
|
957
|
+
warning: 0,
|
|
958
|
+
info: 0,
|
|
959
|
+
suggestion: 0
|
|
960
|
+
};
|
|
961
|
+
const byEngine = {};
|
|
962
|
+
const categoryScores = {};
|
|
963
|
+
for (const d of allDiagnostics) {
|
|
964
|
+
bySeverity[d.severity]++;
|
|
965
|
+
byEngine[d.engine] = (byEngine[d.engine] ?? 0) + 1;
|
|
966
|
+
}
|
|
967
|
+
const fileCount = context.files?.length ?? 0;
|
|
968
|
+
let score = calculateScore(allDiagnostics, fileCount).score;
|
|
969
|
+
const supportedLangs = /* @__PURE__ */ new Set();
|
|
970
|
+
for (const r of results) if (!r.skipped) {
|
|
971
|
+
const engineLoader = ENGINE_REGISTRY[r.engine];
|
|
972
|
+
if (engineLoader) try {
|
|
973
|
+
const engine = await engineLoader();
|
|
974
|
+
for (const l of engine.supportedLanguages) supportedLangs.add(l);
|
|
975
|
+
} catch {}
|
|
976
|
+
}
|
|
977
|
+
const unsupportedFileCount = context.files ? context.files.filter((f) => {
|
|
978
|
+
const lang = EXT_TO_LANG[`.${f.split(".").pop()?.toLowerCase() ?? ""}`];
|
|
979
|
+
return lang && !supportedLangs.has(lang);
|
|
980
|
+
}).length : 0;
|
|
981
|
+
const totalFileCount = context.files?.length ?? 0;
|
|
982
|
+
const scoreable = (totalFileCount > 0 ? unsupportedFileCount / totalFileCount : 0) < .8;
|
|
983
|
+
if (!scoreable) score = null;
|
|
984
|
+
const scanResult = {
|
|
985
|
+
engines: results,
|
|
986
|
+
score,
|
|
987
|
+
scoreable,
|
|
988
|
+
categoryScores,
|
|
989
|
+
totalDiagnostics: allDiagnostics.length,
|
|
990
|
+
bySeverity,
|
|
991
|
+
byEngine,
|
|
992
|
+
suppressedCount,
|
|
993
|
+
meta: {
|
|
994
|
+
rootDirectory: context.rootDirectory,
|
|
995
|
+
languages: context.languages,
|
|
996
|
+
frameworks: context.frameworks,
|
|
997
|
+
filesScanned: context.files?.length ?? 0,
|
|
998
|
+
elapsed: performance.now() - startTotal,
|
|
999
|
+
diffScope: context.diffScope
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
if (!context.diffScope) {
|
|
1003
|
+
const record = {
|
|
1004
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1005
|
+
score: scanResult.score,
|
|
1006
|
+
errors: bySeverity.error,
|
|
1007
|
+
warnings: bySeverity.warning,
|
|
1008
|
+
info: bySeverity.info,
|
|
1009
|
+
suggestions: bySeverity.suggestion,
|
|
1010
|
+
filesScanned: scanResult.meta.filesScanned,
|
|
1011
|
+
engines: results.filter((r) => !r.skipped).map((r) => r.engine),
|
|
1012
|
+
durationMs: scanResult.meta.elapsed
|
|
1013
|
+
};
|
|
1014
|
+
try {
|
|
1015
|
+
appendRecord(context.rootDirectory, record);
|
|
1016
|
+
} catch {}
|
|
1017
|
+
}
|
|
1018
|
+
return scanResult;
|
|
1019
|
+
}
|
|
1020
|
+
/** Run auto-fix for a specific engine */
|
|
1021
|
+
async function runFix(engineName, diagnostics, context) {
|
|
1022
|
+
const loader = ENGINE_REGISTRY[engineName];
|
|
1023
|
+
if (!loader) return null;
|
|
1024
|
+
const engine = await loader();
|
|
1025
|
+
if (!engine.fix) return null;
|
|
1026
|
+
return engine.fix(diagnostics, context);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
//#endregion
|
|
1030
|
+
export { ALL_ENGINE_NAMES, DEFAULT_CONFIG, collectFiles, detectFrameworks, detectLanguages, runFix, runScan };
|