slopbrick 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -55
- package/bin/slopbrick.js +23 -2
- package/dist/engine/worker.cjs +198 -40
- package/dist/engine/worker.js +198 -40
- package/dist/index.cjs +1792 -715
- package/dist/index.d.cts +141 -15
- package/dist/index.d.ts +141 -15
- package/dist/index.js +1740 -664
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ For the prevention layer:
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
slopbrick watch # re-run scan on every file change
|
|
27
|
-
slopbrick
|
|
27
|
+
slopbrick install # install the Git pre-commit hook
|
|
28
28
|
slopbrick ci # CI gate: exit 1 on constitution violation
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -40,14 +40,15 @@ on every scan — your repository, encoded for the next agent.
|
|
|
40
40
|
inventory, constitution, health) make your codebase queryable by
|
|
41
41
|
any AI agent in O(read file) instead of O(parse AST).
|
|
42
42
|
- **LockBrick prevention** — `slopbrick watch` flags violations as you
|
|
43
|
-
write, `slopbrick
|
|
43
|
+
write, `slopbrick install` blocks AI-introduced slop at pre-commit,
|
|
44
44
|
`slopbrick ci` enforces the same in CI.
|
|
45
45
|
- **Constitution** — declare your canonical stack (state lib, form
|
|
46
46
|
lib, modal system, API client) once. The agent and the linter
|
|
47
47
|
enforce it together.
|
|
48
48
|
|
|
49
|
-
**Status:** v0.
|
|
50
|
-
|
|
49
|
+
**Status:** v0.43.0 (current) (current). Engine: 103 rules in 22 categories,
|
|
50
|
+
4 headline scores, 9+ language frontends, v10-calibrated against 576,750
|
|
51
|
+
real files. See [CHANGELOG](./CHANGELOG.md) for the full release notes.
|
|
51
52
|
|
|
52
53
|
---
|
|
53
54
|
|
|
@@ -78,27 +79,31 @@ For every other config question, see [`EXAMPLES.md`](./EXAMPLES.md).
|
|
|
78
79
|
|
|
79
80
|
## The headlines (4-score model, v0.21.0+)
|
|
80
81
|
|
|
81
|
-
> **v0.15.0 introduced the 4-score model
|
|
82
|
-
> to the natural-reading "raw amount" direction
|
|
83
|
-
>
|
|
84
|
-
>
|
|
85
|
-
>
|
|
82
|
+
> **v0.15.0 introduced the 4-score model. v0.21.0 FLIPPED `aiSlopScore`
|
|
83
|
+
> to the natural-reading "raw amount of slop" direction
|
|
84
|
+
> (0 = clean, 100 = saturated, **lower = cleaner**).** v0.18.0 added
|
|
85
|
+
> the 4th score (security). The other three scores stay "higher = better".
|
|
86
|
+
> The legacy `slopIndex` field is kept as optional on `ProjectReport`
|
|
87
|
+
> for backward compat with existing test fixtures; the v0.14-compat
|
|
86
88
|
> removal is tracked separately.
|
|
87
89
|
|
|
88
90
|
| Score | What it measures | Direction | CI gate? |
|
|
89
91
|
|-------|------------------|-----------|----------|
|
|
90
|
-
| **`aiSlopScore`** | AI-slop signatures (16 `ai/*` rules). | **lower = cleaner** (raw amount) | **Yes** (`≤ meanSlop
|
|
92
|
+
| **`aiSlopScore`** | AI-slop signatures (16 `ai/*` rules). | **lower = cleaner** (raw amount of slop) | **Yes** (`≤ meanSlop` passes; default 30) |
|
|
91
93
|
| **`engineeringHygiene`** | Average of 6 category scores: arch, logic, layout, visual, component, test | higher = better | No (informational) |
|
|
92
|
-
| **`security`** | AI Security Risk band: low=100, medium=
|
|
93
|
-
| **`repositoryHealth`**
|
|
94
|
+
| **`security`** | AI Security Risk band: low=100, medium=75, high=40, critical=10 | higher = better | No (informational) |
|
|
95
|
+
| **`repositoryHealth`** | Weighted average of 8 axes (slopIndex, architectureConsistency, aiSecurityRisk, designTokenViolations, testQuality, businessLogicCoherence, docFreshness, dbHealth). Default weights in `REPOSITORY_HEALTH_WEIGHTS`. | higher = better | No (informational) |
|
|
94
96
|
|
|
95
97
|
**Score-band messages** (v0.21.0+): every score ships with a one-line
|
|
96
98
|
verdict in the pretty output — e.g. `AI Slop Score: 25 → "low amount
|
|
97
|
-
of slop"`, `Security Risk:
|
|
99
|
+
of AI slop"`, `Security Risk: low`. The band mapping for aiSlopScore
|
|
100
|
+
is **0–9 no slop**, **10–29 low**, **30–49 medium**, **50–69 high**,
|
|
101
|
+
**70–100 saturated** (v0.21.0 lower-is-better direction). See
|
|
102
|
+
`src/report/pretty.ts`.
|
|
98
103
|
|
|
99
|
-
The same numbers
|
|
104
|
+
The same numbers live in `.slopbrick/health.json`.
|
|
100
105
|
|
|
101
|
-
For the full math, the
|
|
106
|
+
For the full math, the 4-score quadrant, and which one to focus on, see
|
|
102
107
|
[`docs/scoring-explained.md`](./docs/scoring-explained.md).
|
|
103
108
|
|
|
104
109
|
For per-rule precision/recall/FPR (auditable), see
|
|
@@ -110,8 +115,7 @@ For per-rule precision/recall/FPR (auditable), see
|
|
|
110
115
|
|
|
111
116
|
Starting in **v0.24.0**, slopbrick can send a single one-shot usage
|
|
112
117
|
ping after `slopbrick scan` completes. This is **opt-in** — the
|
|
113
|
-
default is OFF — and is intended for
|
|
114
|
-
and self-hosted CI.
|
|
118
|
+
default is OFF — and is intended for self-hosted CI telemetry.
|
|
115
119
|
|
|
116
120
|
### How to opt in
|
|
117
121
|
|
|
@@ -134,13 +138,13 @@ A single POST with `Content-Type: application/json` and exactly
|
|
|
134
138
|
| Field | Type | Example | Source |
|
|
135
139
|
|-------|------|---------|--------|
|
|
136
140
|
| `schema_version` | string | `"1"` | constant |
|
|
137
|
-
| `slopbrick_version` | string | `"0.
|
|
141
|
+
| `slopbrick_version` | string | `"0.42.0"` | `package.json` |
|
|
138
142
|
| `scan_id` | string (UUID v4) | `"f47ac10b-…"` | generated per run |
|
|
139
143
|
| `file_count` | int | `42` | `results.length` |
|
|
140
|
-
| `rule_count` | int | `
|
|
144
|
+
| `rule_count` | int | `103` | `builtinRules.length` |
|
|
141
145
|
| `duration_ms` | int | `1834` | wall-clock scan time |
|
|
142
146
|
| `platform` | string | `"darwin"` | `process.platform` |
|
|
143
|
-
| `node_version` | string | `"
|
|
147
|
+
| `node_version` | string | `"v24.15.0"` | `process.version` |
|
|
144
148
|
|
|
145
149
|
### Privacy promise
|
|
146
150
|
|
|
@@ -181,42 +185,27 @@ for the receiver.
|
|
|
181
185
|
## Example output
|
|
182
186
|
|
|
183
187
|
```text
|
|
184
|
-
$ npx slopbrick scan
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
Business Logic 0/100
|
|
200
|
-
Security Risk LOW
|
|
201
|
-
|
|
202
|
-
✓ 99 INVERTED/NOISY issues correctly suppressed from 24 default-off rules.
|
|
203
|
-
|
|
204
|
-
Category breakdown (what kind of issue, and how much):
|
|
205
|
-
AI patterns ████████████████████ 167 — signatures of LLM-generated code
|
|
206
|
-
visual style ████████░░░░░░░░░░░░ 70 — colors, spacing, font sizes, layout
|
|
207
|
-
logic patterns ████████░░░░░░░░░░░░ 68 — state, hooks, prop usage
|
|
208
|
-
13 other categories are clean
|
|
209
|
-
|
|
210
|
-
Next step:
|
|
211
|
-
→ `slopbrick scan --rule src/cli/scan.ts` to drill into the worst file (4 issues)
|
|
212
|
-
→ `slopbrick scan --suggest` for auto-fix advice
|
|
213
|
-
→ `slopbrick scan --baseline` to accept today's scores as the new floor
|
|
214
|
-
→ `slopbrick scan --why-failing` for the top 5 issues dragging the score down
|
|
188
|
+
$ npx slopbrick scan --brief
|
|
189
|
+
[v0.43.0] auto-suppressed 184 INVERTED/NOISY issue(s) from 18 default-off rule(s).
|
|
190
|
+
Memory persisted to .slopbrick/ (0 patterns, 0 components, 537 bytes of structure.md).
|
|
191
|
+
|
|
192
|
+
Repo is low (25/100). The biggest problem is AI patterns — worst file is packages/slopbrick/src/engine/parser-rust.ts.
|
|
193
|
+
|
|
194
|
+
AI Slop Score 25 low (aiSlopScore)
|
|
195
|
+
Engineering Hygiene 100 excellent (engineeringHygiene)
|
|
196
|
+
Security 100 excellent (security)
|
|
197
|
+
Repository Health 57 needs work (repositoryHealth)
|
|
198
|
+
|
|
199
|
+
CI gate: AI Slop Score <= 15 -> fail
|
|
200
|
+
|
|
201
|
+
Scanned 593 files, 346 issues. Run with --all for the full report.
|
|
202
|
+
1 threshold failed: meanSlop (score 25 > 15)
|
|
215
203
|
```
|
|
216
204
|
|
|
217
205
|
`--brief` (CI/scripts): same headline + threshold + delta in 4 lines.
|
|
218
206
|
`--why-failing`: top 5 rules ranked by weighted impact.
|
|
219
207
|
`--suggest`: per-rule auto-fix advice.
|
|
208
|
+
`--human-only` / `--ai-only`: filter issues by category.
|
|
220
209
|
|
|
221
210
|
---
|
|
222
211
|
|
|
@@ -226,14 +215,14 @@ Next step:
|
|
|
226
215
|
|-------------------|-----------|
|
|
227
216
|
| Add a new rule (most common contribution) | [`CONTRIBUTING.md`](./CONTRIBUTING.md) |
|
|
228
217
|
| Configure for strict CI, monorepo, Python, etc. | [`EXAMPLES.md`](./EXAMPLES.md) |
|
|
229
|
-
| Understand the Slop
|
|
218
|
+
| Understand the 4-score model (AI Slop Score, Engineering Hygiene, Security, Repository Health) | [`docs/scoring-explained.md`](./docs/scoring-explained.md) |
|
|
230
219
|
| Connect Claude Code / Cursor / Copilot | [`docs/MCP.md`](./docs/MCP.md) |
|
|
231
220
|
| See the 4 `.slopbrick/` artifacts (structure, inventory, ...) | [`docs/repository-structure.md`](./docs/repository-structure.md) |
|
|
232
221
|
| See the 103 rules (per-rule descriptions + citations) | [`docs/rule-catalog.md`](./docs/rule-catalog.md) |
|
|
233
222
|
| See how the engine works (parser → facts → rules) | [`docs/architecture.md`](./docs/architecture.md) |
|
|
234
223
|
| See which frameworks are supported | [`docs/framework-parity-matrix.md`](./docs/framework-parity-matrix.md) |
|
|
235
224
|
| See what's changed in each release | [`CHANGELOG.md`](./CHANGELOG.md) |
|
|
236
|
-
| See the strategic plan (
|
|
225
|
+
| See the strategic plan (v0.x → v1.0) | [`ROADMAP.md`](./ROADMAP.md) |
|
|
237
226
|
| See research behind the calibration | [`docs/research/`](./docs/research/) |
|
|
238
227
|
| Report a security vulnerability | [`SECURITY.md`](./SECURITY.md) |
|
|
239
228
|
| Run a CI gate | `slopbrick ci` (see [`EXAMPLES.md`](./EXAMPLES.md#strict-ci-gate)) |
|
|
@@ -249,8 +238,9 @@ The 19 subcommands are auto-generated from commander and run
|
|
|
249
238
|
npm install -D slopbrick
|
|
250
239
|
```
|
|
251
240
|
|
|
252
|
-
Requires Node
|
|
253
|
-
types, and is published to npm as
|
|
241
|
+
Requires Node 20+ (verified by `slopbrick doctor`). The package ships
|
|
242
|
+
ESM + CJS dual builds, TypeScript types, and is published to npm as
|
|
243
|
+
`slopbrick`.
|
|
254
244
|
|
|
255
245
|
For the MCP server, add to your AI agent's config:
|
|
256
246
|
|
package/bin/slopbrick.js
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Top-level error guards: converts unhandled exceptions into a
|
|
3
|
+
// human-readable message + a non-zero exit code, instead of the
|
|
4
|
+
// default Node "UnhandledPromiseRejectionWarning" + stack trace.
|
|
5
|
+
// Without this, a transitive module-throwing-on-load (e.g. a
|
|
6
|
+
// config importer with a syntax error) showed a stack trace and
|
|
7
|
+
// exited with code 1 + a useless warning, confusing new users.
|
|
2
8
|
(async () => {
|
|
3
9
|
const start = performance.now();
|
|
4
|
-
|
|
5
|
-
|
|
10
|
+
try {
|
|
11
|
+
const { runCli } = await import('../dist/index.js');
|
|
12
|
+
await runCli({ start });
|
|
13
|
+
} catch (err) {
|
|
14
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
15
|
+
process.stderr.write(`slopbrick: unhandled error — ${msg}\n`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
6
18
|
})();
|
|
19
|
+
process.on('unhandledRejection', (reason) => {
|
|
20
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
21
|
+
process.stderr.write(`slopbrick: unhandled rejection — ${msg}\n`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
24
|
+
process.on('uncaughtException', (err) => {
|
|
25
|
+
process.stderr.write(`slopbrick: uncaught exception — ${err.message}\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
package/dist/engine/worker.cjs
CHANGED
|
@@ -6897,6 +6897,26 @@ function dispatchNode(node, parent, path, vctx) {
|
|
|
6897
6897
|
// src/engine/parser-rust.ts
|
|
6898
6898
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
6899
6899
|
var import_tree_sitter_rust = __toESM(require("tree-sitter-rust"), 1);
|
|
6900
|
+
|
|
6901
|
+
// src/engine/parser-shared.ts
|
|
6902
|
+
function parseTreeSitterSource(parserResult, source) {
|
|
6903
|
+
if (!parserResult.ok) return null;
|
|
6904
|
+
if (!source || source.trim() === "") return null;
|
|
6905
|
+
try {
|
|
6906
|
+
const rawTree = parserResult.parser.parse(source);
|
|
6907
|
+
if (!rawTree) return null;
|
|
6908
|
+
const tree = rawTree;
|
|
6909
|
+
if (!tree || !tree.rootNode) return null;
|
|
6910
|
+
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
6911
|
+
return null;
|
|
6912
|
+
}
|
|
6913
|
+
return tree;
|
|
6914
|
+
} catch {
|
|
6915
|
+
return null;
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
// src/engine/parser-rust.ts
|
|
6900
6920
|
var cachedParser = null;
|
|
6901
6921
|
var rustLanguage = null;
|
|
6902
6922
|
var loadError = null;
|
|
@@ -6923,21 +6943,7 @@ function effectiveParser() {
|
|
|
6923
6943
|
return getRustParser();
|
|
6924
6944
|
}
|
|
6925
6945
|
function parseRust(source) {
|
|
6926
|
-
|
|
6927
|
-
if (!result.ok) return null;
|
|
6928
|
-
if (!source || source.trim() === "") return null;
|
|
6929
|
-
try {
|
|
6930
|
-
const rawTree = result.parser.parse(source);
|
|
6931
|
-
if (!rawTree) return null;
|
|
6932
|
-
const tree = rawTree;
|
|
6933
|
-
if (!tree || !tree.rootNode) return null;
|
|
6934
|
-
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
6935
|
-
return null;
|
|
6936
|
-
}
|
|
6937
|
-
return tree;
|
|
6938
|
-
} catch {
|
|
6939
|
-
return null;
|
|
6940
|
-
}
|
|
6946
|
+
return parseTreeSitterSource(effectiveParser(), source);
|
|
6941
6947
|
}
|
|
6942
6948
|
function isRustParserAvailable() {
|
|
6943
6949
|
const result = getRustParser();
|
|
@@ -7551,8 +7557,6 @@ function mergeTemplateClassNames(filePath, source, facts, templateClassNames) {
|
|
|
7551
7557
|
}
|
|
7552
7558
|
function extractFacts(filePath, ast, source, supportsRsc = true, framework = "react", config) {
|
|
7553
7559
|
const lineOffsets = buildLineOffsets(source);
|
|
7554
|
-
const isTypeScript = /\.tsx?$/i.test(filePath);
|
|
7555
|
-
const NON_JSX_FRAMEWORKS = /* @__PURE__ */ new Set(["vue", "svelte", "astro"]);
|
|
7556
7560
|
const facts = {
|
|
7557
7561
|
filePath,
|
|
7558
7562
|
components: [],
|
|
@@ -32976,7 +32980,7 @@ var aiCommentRatioRule = createRule({
|
|
|
32976
32980
|
message: `Comment-line ratio is ${direction} (${ratio.toFixed(2)} vs corpus mean ${mean3}). Reductive LLMs (GPT-3.5, Claude 3 Haiku) skip comments; expansive LLMs (Claude Haiku 4.5, GPT-OSS) over-comment when prompted for "well-commented code".`,
|
|
32977
32981
|
line: 1,
|
|
32978
32982
|
column: 1,
|
|
32979
|
-
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "
|
|
32983
|
+
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "High comment ratio \u2014 consider whether the comments are intentional (api docs, type defs, tutorials). For functional code, trim comments that just restate the next line. Keep comments that explain WHY, not WHAT."
|
|
32980
32984
|
}
|
|
32981
32985
|
];
|
|
32982
32986
|
return issues;
|
|
@@ -33750,7 +33754,6 @@ function buildTrigramLM(samples, alpha = 1) {
|
|
|
33750
33754
|
logProb(context, t) {
|
|
33751
33755
|
const ctxKey = context.slice(-2).join("|");
|
|
33752
33756
|
const triKey = `${ctxKey}|${t}`;
|
|
33753
|
-
const biKey = context.slice(-1).join("|") + "|" + t;
|
|
33754
33757
|
const triCount = tri.get(triKey) ?? 0;
|
|
33755
33758
|
const biCount = bi.get(ctxKey) ?? 0;
|
|
33756
33759
|
const V = uni.size;
|
|
@@ -35543,7 +35546,7 @@ var RESERVED = /* @__PURE__ */ new Set([
|
|
|
35543
35546
|
]);
|
|
35544
35547
|
var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
35545
35548
|
var SOURCE_ROOTS = ["src", "lib", "app", "components"];
|
|
35546
|
-
var CAP =
|
|
35549
|
+
var CAP = 4e3;
|
|
35547
35550
|
function walk(dir, out, cap) {
|
|
35548
35551
|
if (!(0, import_node_fs.existsSync)(dir) || out.length >= cap) return;
|
|
35549
35552
|
let entries;
|
|
@@ -35623,6 +35626,7 @@ var staleFunctionReferenceRule = createRule({
|
|
|
35623
35626
|
if (!source) return issues;
|
|
35624
35627
|
for (const span of extractInlineCodeSpans(source)) {
|
|
35625
35628
|
const text = span.text;
|
|
35629
|
+
if (span.inBlockComment || span.inComment) continue;
|
|
35626
35630
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
|
|
35627
35631
|
if (text.length < 3) continue;
|
|
35628
35632
|
if (RESERVED.has(text.toLowerCase())) continue;
|
|
@@ -35785,6 +35789,7 @@ var stalePackageReferenceRule = createRule({
|
|
|
35785
35789
|
if (!source) return issues;
|
|
35786
35790
|
const spans = extractInlineCodeSpans(source);
|
|
35787
35791
|
for (const span of spans) {
|
|
35792
|
+
if (span.inComment) continue;
|
|
35788
35793
|
const lineStart = source.lastIndexOf("\n", span.index) + 1;
|
|
35789
35794
|
const lineEnd = source.indexOf("\n", span.index);
|
|
35790
35795
|
const line = source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd);
|
|
@@ -35830,6 +35835,8 @@ var stalePackageReferenceRule = createRule({
|
|
|
35830
35835
|
function extractInlineCodeSpans(source) {
|
|
35831
35836
|
const hits = [];
|
|
35832
35837
|
const re = /`([^`\n]+?)`/g;
|
|
35838
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
35839
|
+
const commentLines = findCommentLineRanges(source);
|
|
35833
35840
|
let m;
|
|
35834
35841
|
while ((m = re.exec(source)) !== null) {
|
|
35835
35842
|
const text = m[1] ?? "";
|
|
@@ -35837,13 +35844,22 @@ function extractInlineCodeSpans(source) {
|
|
|
35837
35844
|
const line = upTo.split("\n").length;
|
|
35838
35845
|
const lastNl = upTo.lastIndexOf("\n");
|
|
35839
35846
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
35840
|
-
hits.push({
|
|
35847
|
+
hits.push({
|
|
35848
|
+
text,
|
|
35849
|
+
line,
|
|
35850
|
+
column,
|
|
35851
|
+
index: m.index,
|
|
35852
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
35853
|
+
inComment: commentLines.has(line)
|
|
35854
|
+
});
|
|
35841
35855
|
}
|
|
35842
35856
|
return hits;
|
|
35843
35857
|
}
|
|
35844
35858
|
function extractMarkdownLinks(source) {
|
|
35845
35859
|
const hits = [];
|
|
35846
35860
|
const re = /(?<!\!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
35861
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
35862
|
+
const commentLines = findCommentLineRanges(source);
|
|
35847
35863
|
let m;
|
|
35848
35864
|
while ((m = re.exec(source)) !== null) {
|
|
35849
35865
|
const target = m[2] ?? "";
|
|
@@ -35851,10 +35867,55 @@ function extractMarkdownLinks(source) {
|
|
|
35851
35867
|
const line = upTo.split("\n").length;
|
|
35852
35868
|
const lastNl = upTo.lastIndexOf("\n");
|
|
35853
35869
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
35854
|
-
hits.push({
|
|
35870
|
+
hits.push({
|
|
35871
|
+
target,
|
|
35872
|
+
line,
|
|
35873
|
+
column,
|
|
35874
|
+
index: m.index,
|
|
35875
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
35876
|
+
inComment: commentLines.has(line)
|
|
35877
|
+
});
|
|
35855
35878
|
}
|
|
35856
35879
|
return hits;
|
|
35857
35880
|
}
|
|
35881
|
+
function findBlockCommentRanges(source) {
|
|
35882
|
+
const ranges = [];
|
|
35883
|
+
const stack = [];
|
|
35884
|
+
let i = 0;
|
|
35885
|
+
while (i < source.length) {
|
|
35886
|
+
if (source[i] === "/" && source[i + 1] === "/") {
|
|
35887
|
+
const eol = source.indexOf("\n", i);
|
|
35888
|
+
i = eol === -1 ? source.length : eol + 1;
|
|
35889
|
+
continue;
|
|
35890
|
+
}
|
|
35891
|
+
if (source[i] === "/" && source[i + 1] === "*") {
|
|
35892
|
+
stack.push(i);
|
|
35893
|
+
i += 2;
|
|
35894
|
+
continue;
|
|
35895
|
+
}
|
|
35896
|
+
if (source[i] === "*" && source[i + 1] === "/" && stack.length > 0) {
|
|
35897
|
+
const start = stack.pop();
|
|
35898
|
+
ranges.push([start, i + 2]);
|
|
35899
|
+
i += 2;
|
|
35900
|
+
continue;
|
|
35901
|
+
}
|
|
35902
|
+
i += 1;
|
|
35903
|
+
}
|
|
35904
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
35905
|
+
return ranges;
|
|
35906
|
+
}
|
|
35907
|
+
function isInBlockComment(ranges, pos) {
|
|
35908
|
+
let lo = 0;
|
|
35909
|
+
let hi = ranges.length - 1;
|
|
35910
|
+
while (lo <= hi) {
|
|
35911
|
+
const mid = lo + hi >>> 1;
|
|
35912
|
+
const [start, end] = ranges[mid];
|
|
35913
|
+
if (pos < start) hi = mid - 1;
|
|
35914
|
+
else if (pos >= end) lo = mid + 1;
|
|
35915
|
+
else return true;
|
|
35916
|
+
}
|
|
35917
|
+
return false;
|
|
35918
|
+
}
|
|
35858
35919
|
function declaredPackages(cwd) {
|
|
35859
35920
|
const out = /* @__PURE__ */ new Set();
|
|
35860
35921
|
const pkgPath = (0, import_node_path3.join)(cwd, "package.json");
|
|
@@ -35874,8 +35935,63 @@ function declaredPackages(cwd) {
|
|
|
35874
35935
|
}
|
|
35875
35936
|
return out;
|
|
35876
35937
|
}
|
|
35938
|
+
function findCommentLineRanges(source) {
|
|
35939
|
+
const out = /* @__PURE__ */ new Set();
|
|
35940
|
+
const stack = [];
|
|
35941
|
+
let inLineComment = false;
|
|
35942
|
+
const lines = source.split("\n");
|
|
35943
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35944
|
+
const line = lines[i];
|
|
35945
|
+
const lineNo = i + 1;
|
|
35946
|
+
let j = 0;
|
|
35947
|
+
while (j < line.length) {
|
|
35948
|
+
const c = line[j];
|
|
35949
|
+
const next = line[j + 1];
|
|
35950
|
+
if (!stack.length && c === "/" && next === "/") {
|
|
35951
|
+
inLineComment = true;
|
|
35952
|
+
out.add(lineNo);
|
|
35953
|
+
break;
|
|
35954
|
+
}
|
|
35955
|
+
if (c === "/" && next === "*") {
|
|
35956
|
+
stack.push(lineNo);
|
|
35957
|
+
out.add(lineNo);
|
|
35958
|
+
j += 2;
|
|
35959
|
+
continue;
|
|
35960
|
+
}
|
|
35961
|
+
if (c === "*" && next === "/" && stack.length > 0) {
|
|
35962
|
+
stack.pop();
|
|
35963
|
+
j += 2;
|
|
35964
|
+
continue;
|
|
35965
|
+
}
|
|
35966
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
35967
|
+
const quote = c;
|
|
35968
|
+
j += 1;
|
|
35969
|
+
while (j < line.length) {
|
|
35970
|
+
if (line[j] === "\\") {
|
|
35971
|
+
j += 2;
|
|
35972
|
+
continue;
|
|
35973
|
+
}
|
|
35974
|
+
if (line[j] === quote) {
|
|
35975
|
+
j += 1;
|
|
35976
|
+
break;
|
|
35977
|
+
}
|
|
35978
|
+
j += 1;
|
|
35979
|
+
}
|
|
35980
|
+
continue;
|
|
35981
|
+
}
|
|
35982
|
+
j += 1;
|
|
35983
|
+
}
|
|
35984
|
+
if (stack.length > 0) {
|
|
35985
|
+
out.add(lineNo);
|
|
35986
|
+
}
|
|
35987
|
+
}
|
|
35988
|
+
return out;
|
|
35989
|
+
}
|
|
35877
35990
|
|
|
35878
35991
|
// src/rules/docs/broken-link.ts
|
|
35992
|
+
function looksLikeRegexSyntax(target) {
|
|
35993
|
+
return /\[[^\]]*\]/.test(target) || /^\s*[?:=!]/.test(target) || /\\(?:s|d|w|S|D|W|t|n|r|[^\\])/.test(target) || /[{}|]/.test(target);
|
|
35994
|
+
}
|
|
35879
35995
|
var brokenLinkRule = createRule({
|
|
35880
35996
|
id: "docs/broken-link",
|
|
35881
35997
|
category: "docs",
|
|
@@ -35898,6 +36014,8 @@ var brokenLinkRule = createRule({
|
|
|
35898
36014
|
if (target.startsWith("#")) continue;
|
|
35899
36015
|
if (target.startsWith("//")) continue;
|
|
35900
36016
|
if (target.startsWith("/")) continue;
|
|
36017
|
+
if (link.inBlockComment) continue;
|
|
36018
|
+
if (looksLikeRegexSyntax(target)) continue;
|
|
35901
36019
|
const filePart = target.split("#")[0] ?? target;
|
|
35902
36020
|
if (filePart === "") continue;
|
|
35903
36021
|
const resolved = (0, import_node_path4.join)(docDir, filePart);
|
|
@@ -37222,8 +37340,16 @@ var DEFAULT_CONFIG3 = {
|
|
|
37222
37340
|
// rule definitions are meta-code
|
|
37223
37341
|
"**/snippet/**",
|
|
37224
37342
|
// RULE_HINTS examples are intentional bad code
|
|
37225
|
-
"**/tests/**"
|
|
37343
|
+
"**/tests/**",
|
|
37226
37344
|
// all test files (fixtures + unit + integration)
|
|
37345
|
+
// v0.42.0 (post-self-scan cleanup): language visitors are
|
|
37346
|
+
// intentional clones — see the §3 v0.42.0 release notes for
|
|
37347
|
+
// the design rationale. `dup/identical-block` fires 100+
|
|
37348
|
+
// times during self-scan because each visitor shares the
|
|
37349
|
+
// same service-suffix arrays and import-walking preamble.
|
|
37350
|
+
// Suppress these from the self-scan noise floor; they
|
|
37351
|
+
// remain on for user codebases.
|
|
37352
|
+
"**/src/engine/visitors/**"
|
|
37227
37353
|
]
|
|
37228
37354
|
}
|
|
37229
37355
|
};
|
|
@@ -41124,23 +41250,16 @@ var builtinRules = [
|
|
|
41124
41250
|
// src/rules/registry.ts
|
|
41125
41251
|
var RuleRegistry = class {
|
|
41126
41252
|
rules = /* @__PURE__ */ new Map();
|
|
41127
|
-
|
|
41128
|
-
|
|
41129
|
-
|
|
41130
|
-
|
|
41131
|
-
|
|
41132
|
-
|
|
41133
|
-
|
|
41134
|
-
|
|
41135
|
-
|
|
41136
|
-
|
|
41137
|
-
create: factory.create,
|
|
41138
|
-
analyze: factory.analyze
|
|
41139
|
-
};
|
|
41140
|
-
this.rules.set(ruleOrId, rule);
|
|
41141
|
-
} else {
|
|
41142
|
-
this.rules.set(ruleOrId.id, ruleOrId);
|
|
41143
|
-
}
|
|
41253
|
+
/**
|
|
41254
|
+
* Register a rule. The 2-arg overload `register(id, factory)` was
|
|
41255
|
+
* deleted in v0.42.0 (architecture review F6): no callers used it,
|
|
41256
|
+
* every `Rule` carries its own `id`, and the per-id registration
|
|
41257
|
+
* was ergonomically redundant with the 1-arg form (`register(rule)`).
|
|
41258
|
+
* External consumers that needed `register-by-id` semantics can
|
|
41259
|
+
* construct a `Rule` directly and pass it in.
|
|
41260
|
+
*/
|
|
41261
|
+
register(rule) {
|
|
41262
|
+
this.rules.set(rule.id, rule);
|
|
41144
41263
|
}
|
|
41145
41264
|
loadBuiltins(onlyRuleId) {
|
|
41146
41265
|
for (const rule of builtinRules) {
|
|
@@ -41169,6 +41288,15 @@ var RuleRegistry = class {
|
|
|
41169
41288
|
all() {
|
|
41170
41289
|
return Array.from(this.rules.values());
|
|
41171
41290
|
}
|
|
41291
|
+
/** v0.42.0 (§3b.5): look up a registered rule by id.
|
|
41292
|
+
* Returns undefined if not registered. */
|
|
41293
|
+
get(id) {
|
|
41294
|
+
return this.rules.get(id);
|
|
41295
|
+
}
|
|
41296
|
+
/** v0.42.0 (§3b.5): check whether a rule id is registered. */
|
|
41297
|
+
has(id) {
|
|
41298
|
+
return this.rules.has(id);
|
|
41299
|
+
}
|
|
41172
41300
|
createContexts(config, filePath, cwd, hotspotIssues = []) {
|
|
41173
41301
|
const context = {
|
|
41174
41302
|
config,
|
|
@@ -44863,6 +44991,36 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
|
44863
44991
|
const issues = applyRuleOverrides(rawIssues, config.rules);
|
|
44864
44992
|
const triggeredRuleIds = Array.from(new Set(issues.map((i) => i.ruleId)));
|
|
44865
44993
|
const compScore = compositeScore(triggeredRuleIds, loadSignalStrength());
|
|
44994
|
+
const fireSet = new Set(triggeredRuleIds);
|
|
44995
|
+
const factsWithFireSet = Object.assign(facts, {
|
|
44996
|
+
compositeFireSet: fireSet
|
|
44997
|
+
});
|
|
44998
|
+
for (const { rule } of rules) {
|
|
44999
|
+
if (!rule.id.startsWith("composite/")) continue;
|
|
45000
|
+
try {
|
|
45001
|
+
const compositeIssues = rule.analyze(
|
|
45002
|
+
rule.create({
|
|
45003
|
+
config,
|
|
45004
|
+
filePath,
|
|
45005
|
+
cwd,
|
|
45006
|
+
framework: config.framework,
|
|
45007
|
+
uiLibraries: config.uiLibraries,
|
|
45008
|
+
hasTailwind: config.hasTailwind,
|
|
45009
|
+
supportsRsc: config.supportsRsc,
|
|
45010
|
+
hotspotIssues: []
|
|
45011
|
+
}),
|
|
45012
|
+
factsWithFireSet
|
|
45013
|
+
);
|
|
45014
|
+
if (compositeIssues.length > 0) {
|
|
45015
|
+
issues.push(...compositeIssues);
|
|
45016
|
+
for (const ci of compositeIssues) fireSet.add(ci.ruleId);
|
|
45017
|
+
}
|
|
45018
|
+
} catch (err) {
|
|
45019
|
+
if (process.env.SLOP_AUDIT_DEBUG === "1") {
|
|
45020
|
+
console.error(`[worker] composite ${rule.id} threw: ${err.message}`);
|
|
45021
|
+
}
|
|
45022
|
+
}
|
|
45023
|
+
}
|
|
44866
45024
|
const gapValues = collectGapValues(facts);
|
|
44867
45025
|
const styleSources = collectStyleSources(facts);
|
|
44868
45026
|
const elementTags = facts.v2.jsx.elements.map((e) => e.tag);
|