slopbrick 0.21.0 → 0.21.2
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 +16 -12
- package/dist/engine/worker.cjs +42 -12
- package/dist/engine/worker.js +42 -12
- package/dist/index.cjs +46 -17
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +44 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ on every scan — your repository, encoded for the next agent.
|
|
|
46
46
|
lib, modal system, API client) once. The agent and the linter
|
|
47
47
|
enforce it together.
|
|
48
48
|
|
|
49
|
-
**Status:** v0.
|
|
49
|
+
**Status:** v0.21.0 (current). See the [CHANGELOG](./CHANGELOG.md) for
|
|
50
50
|
the full release notes.
|
|
51
51
|
|
|
52
52
|
---
|
|
@@ -76,21 +76,25 @@ For every other config question, see [`EXAMPLES.md`](./EXAMPLES.md).
|
|
|
76
76
|
|
|
77
77
|
---
|
|
78
78
|
|
|
79
|
-
## The headlines (4-score model, v0.
|
|
79
|
+
## The headlines (4-score model, v0.21.0+)
|
|
80
80
|
|
|
81
|
-
> **v0.15.0 introduced the 4-score model; v0.
|
|
82
|
-
>
|
|
83
|
-
>
|
|
84
|
-
> is kept as optional on `ProjectReport` for backward compat with
|
|
81
|
+
> **v0.15.0 introduced the 4-score model; v0.21.0 FLIPPED `aiSlopScore`
|
|
82
|
+
> to the natural-reading "raw amount" direction (0=clean, 100=saturated).**
|
|
83
|
+
> The other three scores stay "higher = better". The legacy `slopIndex`
|
|
84
|
+
> field is kept as optional on `ProjectReport` for backward compat with
|
|
85
85
|
> existing test fixtures and historical telemetry; the v0.14-compat
|
|
86
86
|
> removal is tracked separately.
|
|
87
87
|
|
|
88
|
-
| Score | What it measures | CI gate? |
|
|
89
|
-
|
|
90
|
-
| **`aiSlopScore`** | AI-slop signatures (16 `ai/*` rules).
|
|
91
|
-
| **`engineeringHygiene`** | Average of 6 category scores: arch, logic, layout, visual, component, test | No (informational) |
|
|
92
|
-
| **`security`** | AI Security Risk band: low=100, medium=67, high=33, critical=0 | No (informational) |
|
|
93
|
-
| **`repositoryHealth`** (composite) | Weighted: 0.4
|
|
88
|
+
| Score | What it measures | Direction | CI gate? |
|
|
89
|
+
|-------|------------------|-----------|----------|
|
|
90
|
+
| **`aiSlopScore`** | AI-slop signatures (16 `ai/*` rules). | **lower = cleaner** (raw amount) | **Yes** (`≤ meanSlop: 30` passes) |
|
|
91
|
+
| **`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=67, high=33, critical=0 | higher = better | No (informational) |
|
|
93
|
+
| **`repositoryHealth`** (composite) | Weighted: `0.4 × (100 − aiSlopScore) + 0.3 × eng + 0.2 × sec + 0.1 × test` | higher = better (inverts `aiSlopScore` internally) | No (informational) |
|
|
94
|
+
|
|
95
|
+
**Score-band messages** (v0.21.0+): every score ships with a one-line
|
|
96
|
+
verdict in the pretty output — e.g. `AI Slop Score: 25 → "low amount
|
|
97
|
+
of slop"`, `Security Risk: 33 → "high risk"`. See `src/report/pretty.ts`.
|
|
94
98
|
|
|
95
99
|
The same numbers are in `.slopbrick/health.json`.
|
|
96
100
|
|
package/dist/engine/worker.cjs
CHANGED
|
@@ -6424,6 +6424,7 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
6424
6424
|
if (source) {
|
|
6425
6425
|
const { line, column } = positionFrom(node, vctx.lineOffsets);
|
|
6426
6426
|
const importedNames = [];
|
|
6427
|
+
const isTypeOnly = node.typeOnly === true;
|
|
6427
6428
|
const specifiers = node.specifiers;
|
|
6428
6429
|
if (Array.isArray(specifiers)) {
|
|
6429
6430
|
for (const specifier of specifiers) {
|
|
@@ -6439,7 +6440,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
6439
6440
|
line,
|
|
6440
6441
|
column,
|
|
6441
6442
|
source,
|
|
6442
|
-
isReferenced: false
|
|
6443
|
+
isReferenced: false,
|
|
6444
|
+
isTypeOnly
|
|
6443
6445
|
});
|
|
6444
6446
|
}
|
|
6445
6447
|
} else if (specifier.type === "ImportSpecifier") {
|
|
@@ -6454,7 +6456,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
6454
6456
|
line,
|
|
6455
6457
|
column,
|
|
6456
6458
|
source,
|
|
6457
|
-
isReferenced: false
|
|
6459
|
+
isReferenced: false,
|
|
6460
|
+
isTypeOnly
|
|
6458
6461
|
});
|
|
6459
6462
|
} else if (isObject(local) && local.type === "Identifier" && typeof local.value === "string") {
|
|
6460
6463
|
const name = local.value;
|
|
@@ -6465,7 +6468,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
6465
6468
|
line,
|
|
6466
6469
|
column,
|
|
6467
6470
|
source,
|
|
6468
|
-
isReferenced: false
|
|
6471
|
+
isReferenced: false,
|
|
6472
|
+
isTypeOnly
|
|
6469
6473
|
});
|
|
6470
6474
|
}
|
|
6471
6475
|
}
|
|
@@ -6758,7 +6762,7 @@ function handleJSXOpeningElement(node, _parent, _path, vctx) {
|
|
|
6758
6762
|
}
|
|
6759
6763
|
return false;
|
|
6760
6764
|
}
|
|
6761
|
-
function handleVariableDeclarator(node,
|
|
6765
|
+
function handleVariableDeclarator(node, parent, path, vctx) {
|
|
6762
6766
|
if (!isObject(node)) return false;
|
|
6763
6767
|
const init = node.init;
|
|
6764
6768
|
if (init && vctx.visit) {
|
|
@@ -6773,16 +6777,18 @@ function handleVariableDeclarator(node, _parent, path, vctx) {
|
|
|
6773
6777
|
}
|
|
6774
6778
|
}
|
|
6775
6779
|
if (bindingNames.length > 0) {
|
|
6776
|
-
const
|
|
6777
|
-
const kind = isObject(
|
|
6780
|
+
const declParent = isObject(parent) && parent.type === "ExportNamedDeclaration" ? parent.declaration ?? parent : parent;
|
|
6781
|
+
const kind = isObject(declParent) && declParent.type === "VariableDeclaration" ? String(declParent.kind ?? "var") : "var";
|
|
6778
6782
|
const { line, column } = positionFrom(id, vctx.lineOffsets);
|
|
6783
|
+
const scope = vctx.ctx.stack.length === 0 ? "module" : "function";
|
|
6779
6784
|
for (const bindingName of bindingNames) {
|
|
6780
6785
|
vctx.facts.deadCode.bindings.push({
|
|
6781
6786
|
name: bindingName,
|
|
6782
6787
|
kind,
|
|
6783
6788
|
line,
|
|
6784
6789
|
column,
|
|
6785
|
-
isReferenced: false
|
|
6790
|
+
isReferenced: false,
|
|
6791
|
+
scope
|
|
6786
6792
|
});
|
|
6787
6793
|
}
|
|
6788
6794
|
}
|
|
@@ -33850,6 +33856,16 @@ var aiSegmentSurprisalCvRule = createRule({
|
|
|
33850
33856
|
category: "ai",
|
|
33851
33857
|
severity: "medium",
|
|
33852
33858
|
aiSpecific: true,
|
|
33859
|
+
// v0.21.0: defaultOff. The rule fires on 250/250 files in the
|
|
33860
|
+
// self-scan (1 per file), which means it's not discriminating
|
|
33861
|
+
// between AI and non-AI text — it fires on every file. The
|
|
33862
|
+
// Binoculars-style per-segment entropy CV is too coarse a
|
|
33863
|
+
// signal at the file level; the entropy floor is hit by most
|
|
33864
|
+
// structured TypeScript regardless of whether the author was
|
|
33865
|
+
// an LLM. Re-enable per-file via
|
|
33866
|
+
// `rules: { 'ai/segment-surprisal-cv': 'medium' }` in
|
|
33867
|
+
// slopbrick.config.mjs once a tighter threshold is calibrated.
|
|
33868
|
+
defaultOff: true,
|
|
33853
33869
|
description: "Per-segment cross-entropy CV is suspiciously low \u2014 Binoculars (Hans 2024): AI text has near-constant per-window entropy",
|
|
33854
33870
|
create(context) {
|
|
33855
33871
|
return context;
|
|
@@ -34990,6 +35006,7 @@ var unusedImportRule = createRule({
|
|
|
34990
35006
|
if (binding.kind !== "import-specifier" && binding.kind !== "import-default" && binding.kind !== "import-namespace") {
|
|
34991
35007
|
continue;
|
|
34992
35008
|
}
|
|
35009
|
+
if (binding.isTypeOnly) continue;
|
|
34993
35010
|
if (binding.isReferenced) continue;
|
|
34994
35011
|
if (!binding.name) continue;
|
|
34995
35012
|
const source = binding.source ? ` from '${binding.source}'` : "";
|
|
@@ -35063,6 +35080,9 @@ var unusedLocalRule = createRule({
|
|
|
35063
35080
|
if (binding.isReferenced) continue;
|
|
35064
35081
|
if (SKIP_NAMES.has(binding.name)) continue;
|
|
35065
35082
|
if (binding.name.startsWith("_")) continue;
|
|
35083
|
+
if (binding.scope === "module" && (binding.kind === "const" || binding.kind === "function" || binding.kind === "class" || binding.kind === "type" || binding.kind === "interface" || binding.kind === "enum")) {
|
|
35084
|
+
continue;
|
|
35085
|
+
}
|
|
35066
35086
|
issues.push({
|
|
35067
35087
|
ruleId: "dead/unused-local",
|
|
35068
35088
|
category: "logic",
|
|
@@ -36059,7 +36079,7 @@ var brokenLinkRule = createRule({
|
|
|
36059
36079
|
|
|
36060
36080
|
// src/rules/dup/identical-block.ts
|
|
36061
36081
|
var crypto = __toESM(require("crypto"), 1);
|
|
36062
|
-
var WINDOW_SIZE =
|
|
36082
|
+
var WINDOW_SIZE = 20;
|
|
36063
36083
|
var MIN_NORMALIZED_LENGTH = 40;
|
|
36064
36084
|
var HASH_PREFIX_LENGTH = 16;
|
|
36065
36085
|
var DEDUP_CACHE = /* @__PURE__ */ new Map();
|
|
@@ -36284,6 +36304,7 @@ var javaArraylistVsLinkedlistRule = createRule({
|
|
|
36284
36304
|
const issues = [];
|
|
36285
36305
|
const source = facts.v2?._source;
|
|
36286
36306
|
if (!source) return issues;
|
|
36307
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36287
36308
|
let m;
|
|
36288
36309
|
NEW_LINKED_LIST_REGEX.lastIndex = 0;
|
|
36289
36310
|
while ((m = NEW_LINKED_LIST_REGEX.exec(source)) !== null) {
|
|
@@ -36318,6 +36339,7 @@ var javaEmptyCatchBlockRule = createRule({
|
|
|
36318
36339
|
const issues = [];
|
|
36319
36340
|
const source = facts.v2?._source;
|
|
36320
36341
|
if (!source) return issues;
|
|
36342
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36321
36343
|
let m;
|
|
36322
36344
|
SINGLE_LINE_EMPTY_CATCH_REGEX.lastIndex = 0;
|
|
36323
36345
|
while ((m = SINGLE_LINE_EMPTY_CATCH_REGEX.exec(source)) !== null) {
|
|
@@ -36354,6 +36376,7 @@ var javaLegacyDateApiRule = createRule({
|
|
|
36354
36376
|
const issues = [];
|
|
36355
36377
|
const source = facts.v2?._source;
|
|
36356
36378
|
if (!source) return issues;
|
|
36379
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36357
36380
|
const flagged = /* @__PURE__ */ new Set();
|
|
36358
36381
|
let m;
|
|
36359
36382
|
LEGACY_IMPORT_REGEX.lastIndex = 0;
|
|
@@ -36420,6 +36443,7 @@ var javaRawTypeOveruseRule = createRule({
|
|
|
36420
36443
|
const issues = [];
|
|
36421
36444
|
const source = facts.v2?._source;
|
|
36422
36445
|
if (!source) return issues;
|
|
36446
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36423
36447
|
let m;
|
|
36424
36448
|
RAW_TYPE_REGEX.lastIndex = 0;
|
|
36425
36449
|
while ((m = RAW_TYPE_REGEX.exec(source)) !== null) {
|
|
@@ -36455,6 +36479,7 @@ var javaStringConcatLoopRule = createRule({
|
|
|
36455
36479
|
const issues = [];
|
|
36456
36480
|
const source = facts.v2?._source;
|
|
36457
36481
|
if (!source) return issues;
|
|
36482
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36458
36483
|
if (!/\b(for|while|do)\b/.test(source)) return issues;
|
|
36459
36484
|
let m;
|
|
36460
36485
|
STRING_CONCAT_REGEX.lastIndex = 0;
|
|
@@ -36491,6 +36516,7 @@ var javaSystemOutPrintlnRule = createRule({
|
|
|
36491
36516
|
const issues = [];
|
|
36492
36517
|
const source = facts.v2?._source;
|
|
36493
36518
|
if (!source) return issues;
|
|
36519
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36494
36520
|
const matches = [];
|
|
36495
36521
|
let m;
|
|
36496
36522
|
PRINTLN_REGEX.lastIndex = 0;
|
|
@@ -42017,7 +42043,8 @@ var signal_strength_default = {
|
|
|
42017
42043
|
precision: 0.6803,
|
|
42018
42044
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42019
42045
|
verdict: "USEFUL",
|
|
42020
|
-
|
|
42046
|
+
defaultOff: true,
|
|
42047
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=27874, FP=13097, P=68.0%, FPR=5.20%, lift=13.09. v7 was USEFUL (TP=16673, FP=10112, lift=11.29). v8 was USEFUL (TP=11201, FP=2985). v0.21.0: marked defaultOff because the v0.21.0 self-scan fires 109 times across 47 files \u2014 the heuristic counts `{`, `}`, `(`, `)` as raw characters without skipping string contents, template literals, regex literals, or comments. Files with normal closing-brace tails (the last 4-5 lines of a function definition) consistently fire because the tail has 0 opens and 3-4 closes. The corpus-level 13.09 lift doesn't translate to per-file discrimination. v0.22.0 should re-implement the count with a token-aware state machine (string/comment/regex skip) or switch to a parse-error-based signal.",
|
|
42021
42048
|
aiSpecific: true,
|
|
42022
42049
|
_v7Verdict: "USEFUL",
|
|
42023
42050
|
_v7Lift: 11.29,
|
|
@@ -42120,7 +42147,8 @@ var signal_strength_default = {
|
|
|
42120
42147
|
precision: 0.7495,
|
|
42121
42148
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42122
42149
|
verdict: "USEFUL",
|
|
42123
|
-
|
|
42150
|
+
defaultOff: true,
|
|
42151
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=59390, FP=19849, P=75.0%, FPR=7.87%, lift=9.52. v7 was USEFUL (TP=43498, FP=14983, lift=9.11). v8 was USEFUL (TP=15892, FP=4866). v0.21.0: marked defaultOff because the v0.21.0 self-scan fires 250 times across 250 files (1 per file) \u2014 the Binoculars-style per-segment entropy CV is too coarse a signal at the file level; the entropy floor is hit by most structured TypeScript regardless of whether the author was an LLM. The v8.5 corpus-level lift of 9.52 doesn't translate to per-file discrimination. v0.22.0 should tighten the threshold or re-calibrate against the per-file (not per-corpus) lift.",
|
|
42124
42152
|
aiSpecific: true,
|
|
42125
42153
|
_v7Verdict: "USEFUL",
|
|
42126
42154
|
_v7Lift: 9.11,
|
|
@@ -42188,7 +42216,8 @@ var signal_strength_default = {
|
|
|
42188
42216
|
precision: 0.5583,
|
|
42189
42217
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42190
42218
|
verdict: "USEFUL",
|
|
42191
|
-
|
|
42219
|
+
defaultOff: true,
|
|
42220
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=21567, FP=17061, P=55.8%, FPR=6.77%, lift=8.25. v7 was USEFUL (TP=17466, FP=13438, lift=7.71). v8 was USEFUL (TP=4101, FP=3623). v0.21.2: marked defaultOff because precision 0.5583 < 0.60 floor (the v9.5 methodology in `docs/research/methodology-minimum-sample-size.md` sets the default-on threshold at P >= 0.60). At 55.8% precision, 44% of fires are false positives \u2014 too noisy to gate on. The rule stays available for opt-in via `rules: { 'ai/whitespace-regularity': 'low' }` in slopbrick.config.mjs. The 18 src/ self-scan fires are the typical noise profile.",
|
|
42192
42221
|
aiSpecific: true,
|
|
42193
42222
|
_v7Verdict: "USEFUL",
|
|
42194
42223
|
_v7Lift: 7.71,
|
|
@@ -42536,7 +42565,8 @@ var signal_strength_default = {
|
|
|
42536
42565
|
precision: 0,
|
|
42537
42566
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42538
42567
|
verdict: "DORMANT",
|
|
42539
|
-
|
|
42568
|
+
defaultOff: false,
|
|
42569
|
+
_calibrationNote: "v0.19: new rule, not yet calibrated. v8.5 calibration does not run dup/* rules (the calibration script analyzes each file in isolation, but dup/* rules require cross-file state \u2014 the module-scope dedup cache is per-process, not per-file, so per-file analysis never produces cross-file matches and the rule's recall reads as 0). Scheduled for v0.22 calibration on the v9 corpus with a per-project dedup cache. v0.21.1 self-scan: 207 fires in src/ after WINDOW_SIZE 10\u219220 (was 575) \u2014 69% reduction. The actual precision/recall of these 207 fires is unverified ground truth. Default stays on because the rule's value is real (block-level duplications are a classic AI-fingerprint) and the corpus-measured 0.0 is a methodology artifact, not a real measurement.",
|
|
42540
42570
|
aiSpecific: false,
|
|
42541
42571
|
_v7Verdict: "DORMANT",
|
|
42542
42572
|
_v7Lift: 1,
|
package/dist/engine/worker.js
CHANGED
|
@@ -6395,6 +6395,7 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
6395
6395
|
if (source) {
|
|
6396
6396
|
const { line, column } = positionFrom(node, vctx.lineOffsets);
|
|
6397
6397
|
const importedNames = [];
|
|
6398
|
+
const isTypeOnly = node.typeOnly === true;
|
|
6398
6399
|
const specifiers = node.specifiers;
|
|
6399
6400
|
if (Array.isArray(specifiers)) {
|
|
6400
6401
|
for (const specifier of specifiers) {
|
|
@@ -6410,7 +6411,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
6410
6411
|
line,
|
|
6411
6412
|
column,
|
|
6412
6413
|
source,
|
|
6413
|
-
isReferenced: false
|
|
6414
|
+
isReferenced: false,
|
|
6415
|
+
isTypeOnly
|
|
6414
6416
|
});
|
|
6415
6417
|
}
|
|
6416
6418
|
} else if (specifier.type === "ImportSpecifier") {
|
|
@@ -6425,7 +6427,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
6425
6427
|
line,
|
|
6426
6428
|
column,
|
|
6427
6429
|
source,
|
|
6428
|
-
isReferenced: false
|
|
6430
|
+
isReferenced: false,
|
|
6431
|
+
isTypeOnly
|
|
6429
6432
|
});
|
|
6430
6433
|
} else if (isObject(local) && local.type === "Identifier" && typeof local.value === "string") {
|
|
6431
6434
|
const name = local.value;
|
|
@@ -6436,7 +6439,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
6436
6439
|
line,
|
|
6437
6440
|
column,
|
|
6438
6441
|
source,
|
|
6439
|
-
isReferenced: false
|
|
6442
|
+
isReferenced: false,
|
|
6443
|
+
isTypeOnly
|
|
6440
6444
|
});
|
|
6441
6445
|
}
|
|
6442
6446
|
}
|
|
@@ -6729,7 +6733,7 @@ function handleJSXOpeningElement(node, _parent, _path, vctx) {
|
|
|
6729
6733
|
}
|
|
6730
6734
|
return false;
|
|
6731
6735
|
}
|
|
6732
|
-
function handleVariableDeclarator(node,
|
|
6736
|
+
function handleVariableDeclarator(node, parent, path, vctx) {
|
|
6733
6737
|
if (!isObject(node)) return false;
|
|
6734
6738
|
const init = node.init;
|
|
6735
6739
|
if (init && vctx.visit) {
|
|
@@ -6744,16 +6748,18 @@ function handleVariableDeclarator(node, _parent, path, vctx) {
|
|
|
6744
6748
|
}
|
|
6745
6749
|
}
|
|
6746
6750
|
if (bindingNames.length > 0) {
|
|
6747
|
-
const
|
|
6748
|
-
const kind = isObject(
|
|
6751
|
+
const declParent = isObject(parent) && parent.type === "ExportNamedDeclaration" ? parent.declaration ?? parent : parent;
|
|
6752
|
+
const kind = isObject(declParent) && declParent.type === "VariableDeclaration" ? String(declParent.kind ?? "var") : "var";
|
|
6749
6753
|
const { line, column } = positionFrom(id, vctx.lineOffsets);
|
|
6754
|
+
const scope = vctx.ctx.stack.length === 0 ? "module" : "function";
|
|
6750
6755
|
for (const bindingName of bindingNames) {
|
|
6751
6756
|
vctx.facts.deadCode.bindings.push({
|
|
6752
6757
|
name: bindingName,
|
|
6753
6758
|
kind,
|
|
6754
6759
|
line,
|
|
6755
6760
|
column,
|
|
6756
|
-
isReferenced: false
|
|
6761
|
+
isReferenced: false,
|
|
6762
|
+
scope
|
|
6757
6763
|
});
|
|
6758
6764
|
}
|
|
6759
6765
|
}
|
|
@@ -33821,6 +33827,16 @@ var aiSegmentSurprisalCvRule = createRule({
|
|
|
33821
33827
|
category: "ai",
|
|
33822
33828
|
severity: "medium",
|
|
33823
33829
|
aiSpecific: true,
|
|
33830
|
+
// v0.21.0: defaultOff. The rule fires on 250/250 files in the
|
|
33831
|
+
// self-scan (1 per file), which means it's not discriminating
|
|
33832
|
+
// between AI and non-AI text — it fires on every file. The
|
|
33833
|
+
// Binoculars-style per-segment entropy CV is too coarse a
|
|
33834
|
+
// signal at the file level; the entropy floor is hit by most
|
|
33835
|
+
// structured TypeScript regardless of whether the author was
|
|
33836
|
+
// an LLM. Re-enable per-file via
|
|
33837
|
+
// `rules: { 'ai/segment-surprisal-cv': 'medium' }` in
|
|
33838
|
+
// slopbrick.config.mjs once a tighter threshold is calibrated.
|
|
33839
|
+
defaultOff: true,
|
|
33824
33840
|
description: "Per-segment cross-entropy CV is suspiciously low \u2014 Binoculars (Hans 2024): AI text has near-constant per-window entropy",
|
|
33825
33841
|
create(context) {
|
|
33826
33842
|
return context;
|
|
@@ -34961,6 +34977,7 @@ var unusedImportRule = createRule({
|
|
|
34961
34977
|
if (binding.kind !== "import-specifier" && binding.kind !== "import-default" && binding.kind !== "import-namespace") {
|
|
34962
34978
|
continue;
|
|
34963
34979
|
}
|
|
34980
|
+
if (binding.isTypeOnly) continue;
|
|
34964
34981
|
if (binding.isReferenced) continue;
|
|
34965
34982
|
if (!binding.name) continue;
|
|
34966
34983
|
const source = binding.source ? ` from '${binding.source}'` : "";
|
|
@@ -35034,6 +35051,9 @@ var unusedLocalRule = createRule({
|
|
|
35034
35051
|
if (binding.isReferenced) continue;
|
|
35035
35052
|
if (SKIP_NAMES.has(binding.name)) continue;
|
|
35036
35053
|
if (binding.name.startsWith("_")) continue;
|
|
35054
|
+
if (binding.scope === "module" && (binding.kind === "const" || binding.kind === "function" || binding.kind === "class" || binding.kind === "type" || binding.kind === "interface" || binding.kind === "enum")) {
|
|
35055
|
+
continue;
|
|
35056
|
+
}
|
|
35037
35057
|
issues.push({
|
|
35038
35058
|
ruleId: "dead/unused-local",
|
|
35039
35059
|
category: "logic",
|
|
@@ -36030,7 +36050,7 @@ var brokenLinkRule = createRule({
|
|
|
36030
36050
|
|
|
36031
36051
|
// src/rules/dup/identical-block.ts
|
|
36032
36052
|
import * as crypto from "crypto";
|
|
36033
|
-
var WINDOW_SIZE =
|
|
36053
|
+
var WINDOW_SIZE = 20;
|
|
36034
36054
|
var MIN_NORMALIZED_LENGTH = 40;
|
|
36035
36055
|
var HASH_PREFIX_LENGTH = 16;
|
|
36036
36056
|
var DEDUP_CACHE = /* @__PURE__ */ new Map();
|
|
@@ -36255,6 +36275,7 @@ var javaArraylistVsLinkedlistRule = createRule({
|
|
|
36255
36275
|
const issues = [];
|
|
36256
36276
|
const source = facts.v2?._source;
|
|
36257
36277
|
if (!source) return issues;
|
|
36278
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36258
36279
|
let m;
|
|
36259
36280
|
NEW_LINKED_LIST_REGEX.lastIndex = 0;
|
|
36260
36281
|
while ((m = NEW_LINKED_LIST_REGEX.exec(source)) !== null) {
|
|
@@ -36289,6 +36310,7 @@ var javaEmptyCatchBlockRule = createRule({
|
|
|
36289
36310
|
const issues = [];
|
|
36290
36311
|
const source = facts.v2?._source;
|
|
36291
36312
|
if (!source) return issues;
|
|
36313
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36292
36314
|
let m;
|
|
36293
36315
|
SINGLE_LINE_EMPTY_CATCH_REGEX.lastIndex = 0;
|
|
36294
36316
|
while ((m = SINGLE_LINE_EMPTY_CATCH_REGEX.exec(source)) !== null) {
|
|
@@ -36325,6 +36347,7 @@ var javaLegacyDateApiRule = createRule({
|
|
|
36325
36347
|
const issues = [];
|
|
36326
36348
|
const source = facts.v2?._source;
|
|
36327
36349
|
if (!source) return issues;
|
|
36350
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36328
36351
|
const flagged = /* @__PURE__ */ new Set();
|
|
36329
36352
|
let m;
|
|
36330
36353
|
LEGACY_IMPORT_REGEX.lastIndex = 0;
|
|
@@ -36391,6 +36414,7 @@ var javaRawTypeOveruseRule = createRule({
|
|
|
36391
36414
|
const issues = [];
|
|
36392
36415
|
const source = facts.v2?._source;
|
|
36393
36416
|
if (!source) return issues;
|
|
36417
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36394
36418
|
let m;
|
|
36395
36419
|
RAW_TYPE_REGEX.lastIndex = 0;
|
|
36396
36420
|
while ((m = RAW_TYPE_REGEX.exec(source)) !== null) {
|
|
@@ -36426,6 +36450,7 @@ var javaStringConcatLoopRule = createRule({
|
|
|
36426
36450
|
const issues = [];
|
|
36427
36451
|
const source = facts.v2?._source;
|
|
36428
36452
|
if (!source) return issues;
|
|
36453
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36429
36454
|
if (!/\b(for|while|do)\b/.test(source)) return issues;
|
|
36430
36455
|
let m;
|
|
36431
36456
|
STRING_CONCAT_REGEX.lastIndex = 0;
|
|
@@ -36462,6 +36487,7 @@ var javaSystemOutPrintlnRule = createRule({
|
|
|
36462
36487
|
const issues = [];
|
|
36463
36488
|
const source = facts.v2?._source;
|
|
36464
36489
|
if (!source) return issues;
|
|
36490
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
36465
36491
|
const matches = [];
|
|
36466
36492
|
let m;
|
|
36467
36493
|
PRINTLN_REGEX.lastIndex = 0;
|
|
@@ -41988,7 +42014,8 @@ var signal_strength_default = {
|
|
|
41988
42014
|
precision: 0.6803,
|
|
41989
42015
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
41990
42016
|
verdict: "USEFUL",
|
|
41991
|
-
|
|
42017
|
+
defaultOff: true,
|
|
42018
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=27874, FP=13097, P=68.0%, FPR=5.20%, lift=13.09. v7 was USEFUL (TP=16673, FP=10112, lift=11.29). v8 was USEFUL (TP=11201, FP=2985). v0.21.0: marked defaultOff because the v0.21.0 self-scan fires 109 times across 47 files \u2014 the heuristic counts `{`, `}`, `(`, `)` as raw characters without skipping string contents, template literals, regex literals, or comments. Files with normal closing-brace tails (the last 4-5 lines of a function definition) consistently fire because the tail has 0 opens and 3-4 closes. The corpus-level 13.09 lift doesn't translate to per-file discrimination. v0.22.0 should re-implement the count with a token-aware state machine (string/comment/regex skip) or switch to a parse-error-based signal.",
|
|
41992
42019
|
aiSpecific: true,
|
|
41993
42020
|
_v7Verdict: "USEFUL",
|
|
41994
42021
|
_v7Lift: 11.29,
|
|
@@ -42091,7 +42118,8 @@ var signal_strength_default = {
|
|
|
42091
42118
|
precision: 0.7495,
|
|
42092
42119
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42093
42120
|
verdict: "USEFUL",
|
|
42094
|
-
|
|
42121
|
+
defaultOff: true,
|
|
42122
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=59390, FP=19849, P=75.0%, FPR=7.87%, lift=9.52. v7 was USEFUL (TP=43498, FP=14983, lift=9.11). v8 was USEFUL (TP=15892, FP=4866). v0.21.0: marked defaultOff because the v0.21.0 self-scan fires 250 times across 250 files (1 per file) \u2014 the Binoculars-style per-segment entropy CV is too coarse a signal at the file level; the entropy floor is hit by most structured TypeScript regardless of whether the author was an LLM. The v8.5 corpus-level lift of 9.52 doesn't translate to per-file discrimination. v0.22.0 should tighten the threshold or re-calibrate against the per-file (not per-corpus) lift.",
|
|
42095
42123
|
aiSpecific: true,
|
|
42096
42124
|
_v7Verdict: "USEFUL",
|
|
42097
42125
|
_v7Lift: 9.11,
|
|
@@ -42159,7 +42187,8 @@ var signal_strength_default = {
|
|
|
42159
42187
|
precision: 0.5583,
|
|
42160
42188
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42161
42189
|
verdict: "USEFUL",
|
|
42162
|
-
|
|
42190
|
+
defaultOff: true,
|
|
42191
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=21567, FP=17061, P=55.8%, FPR=6.77%, lift=8.25. v7 was USEFUL (TP=17466, FP=13438, lift=7.71). v8 was USEFUL (TP=4101, FP=3623). v0.21.2: marked defaultOff because precision 0.5583 < 0.60 floor (the v9.5 methodology in `docs/research/methodology-minimum-sample-size.md` sets the default-on threshold at P >= 0.60). At 55.8% precision, 44% of fires are false positives \u2014 too noisy to gate on. The rule stays available for opt-in via `rules: { 'ai/whitespace-regularity': 'low' }` in slopbrick.config.mjs. The 18 src/ self-scan fires are the typical noise profile.",
|
|
42163
42192
|
aiSpecific: true,
|
|
42164
42193
|
_v7Verdict: "USEFUL",
|
|
42165
42194
|
_v7Lift: 7.71,
|
|
@@ -42507,7 +42536,8 @@ var signal_strength_default = {
|
|
|
42507
42536
|
precision: 0,
|
|
42508
42537
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42509
42538
|
verdict: "DORMANT",
|
|
42510
|
-
|
|
42539
|
+
defaultOff: false,
|
|
42540
|
+
_calibrationNote: "v0.19: new rule, not yet calibrated. v8.5 calibration does not run dup/* rules (the calibration script analyzes each file in isolation, but dup/* rules require cross-file state \u2014 the module-scope dedup cache is per-process, not per-file, so per-file analysis never produces cross-file matches and the rule's recall reads as 0). Scheduled for v0.22 calibration on the v9 corpus with a per-project dedup cache. v0.21.1 self-scan: 207 fires in src/ after WINDOW_SIZE 10\u219220 (was 575) \u2014 69% reduction. The actual precision/recall of these 207 fires is unverified ground truth. Default stays on because the rule's value is real (block-level duplications are a classic AI-fingerprint) and the corpus-measured 0.0 is a methodology artifact, not a real measurement.",
|
|
42511
42541
|
aiSpecific: false,
|
|
42512
42542
|
_v7Verdict: "DORMANT",
|
|
42513
42543
|
_v7Lift: 1,
|
package/dist/index.cjs
CHANGED
|
@@ -36,7 +36,7 @@ var VERSION;
|
|
|
36
36
|
var init_header = __esm({
|
|
37
37
|
"src/types/_header.ts"() {
|
|
38
38
|
"use strict";
|
|
39
|
-
VERSION = "0.21.
|
|
39
|
+
VERSION = "0.21.2";
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -27003,6 +27003,16 @@ var init_segment_surprisal_cv = __esm({
|
|
|
27003
27003
|
category: "ai",
|
|
27004
27004
|
severity: "medium",
|
|
27005
27005
|
aiSpecific: true,
|
|
27006
|
+
// v0.21.0: defaultOff. The rule fires on 250/250 files in the
|
|
27007
|
+
// self-scan (1 per file), which means it's not discriminating
|
|
27008
|
+
// between AI and non-AI text — it fires on every file. The
|
|
27009
|
+
// Binoculars-style per-segment entropy CV is too coarse a
|
|
27010
|
+
// signal at the file level; the entropy floor is hit by most
|
|
27011
|
+
// structured TypeScript regardless of whether the author was
|
|
27012
|
+
// an LLM. Re-enable per-file via
|
|
27013
|
+
// `rules: { 'ai/segment-surprisal-cv': 'medium' }` in
|
|
27014
|
+
// slopbrick.config.mjs once a tighter threshold is calibrated.
|
|
27015
|
+
defaultOff: true,
|
|
27006
27016
|
description: "Per-segment cross-entropy CV is suspiciously low \u2014 Binoculars (Hans 2024): AI text has near-constant per-window entropy",
|
|
27007
27017
|
create(context) {
|
|
27008
27018
|
return context;
|
|
@@ -28295,6 +28305,7 @@ var init_unused_import = __esm({
|
|
|
28295
28305
|
if (binding.kind !== "import-specifier" && binding.kind !== "import-default" && binding.kind !== "import-namespace") {
|
|
28296
28306
|
continue;
|
|
28297
28307
|
}
|
|
28308
|
+
if (binding.isTypeOnly) continue;
|
|
28298
28309
|
if (binding.isReferenced) continue;
|
|
28299
28310
|
if (!binding.name) continue;
|
|
28300
28311
|
const source = binding.source ? ` from '${binding.source}'` : "";
|
|
@@ -28361,6 +28372,9 @@ var init_unused_local = __esm({
|
|
|
28361
28372
|
if (binding.isReferenced) continue;
|
|
28362
28373
|
if (SKIP_NAMES.has(binding.name)) continue;
|
|
28363
28374
|
if (binding.name.startsWith("_")) continue;
|
|
28375
|
+
if (binding.scope === "module" && (binding.kind === "const" || binding.kind === "function" || binding.kind === "class" || binding.kind === "type" || binding.kind === "interface" || binding.kind === "enum")) {
|
|
28376
|
+
continue;
|
|
28377
|
+
}
|
|
28364
28378
|
issues.push({
|
|
28365
28379
|
ruleId: "dead/unused-local",
|
|
28366
28380
|
category: "logic",
|
|
@@ -29596,7 +29610,7 @@ var init_identical_block = __esm({
|
|
|
29596
29610
|
"use strict";
|
|
29597
29611
|
crypto = __toESM(require("crypto"), 1);
|
|
29598
29612
|
init_rule();
|
|
29599
|
-
WINDOW_SIZE =
|
|
29613
|
+
WINDOW_SIZE = 20;
|
|
29600
29614
|
MIN_NORMALIZED_LENGTH = 40;
|
|
29601
29615
|
HASH_PREFIX_LENGTH = 16;
|
|
29602
29616
|
DEDUP_CACHE = /* @__PURE__ */ new Map();
|
|
@@ -29842,6 +29856,7 @@ var init_arraylist_vs_linkedlist = __esm({
|
|
|
29842
29856
|
const issues = [];
|
|
29843
29857
|
const source = facts.v2?._source;
|
|
29844
29858
|
if (!source) return issues;
|
|
29859
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
29845
29860
|
let m;
|
|
29846
29861
|
NEW_LINKED_LIST_REGEX.lastIndex = 0;
|
|
29847
29862
|
while ((m = NEW_LINKED_LIST_REGEX.exec(source)) !== null) {
|
|
@@ -29883,6 +29898,7 @@ var init_empty_catch_block = __esm({
|
|
|
29883
29898
|
const issues = [];
|
|
29884
29899
|
const source = facts.v2?._source;
|
|
29885
29900
|
if (!source) return issues;
|
|
29901
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
29886
29902
|
let m;
|
|
29887
29903
|
SINGLE_LINE_EMPTY_CATCH_REGEX.lastIndex = 0;
|
|
29888
29904
|
while ((m = SINGLE_LINE_EMPTY_CATCH_REGEX.exec(source)) !== null) {
|
|
@@ -29926,6 +29942,7 @@ var init_legacy_date_api = __esm({
|
|
|
29926
29942
|
const issues = [];
|
|
29927
29943
|
const source = facts.v2?._source;
|
|
29928
29944
|
if (!source) return issues;
|
|
29945
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
29929
29946
|
const flagged = /* @__PURE__ */ new Set();
|
|
29930
29947
|
let m;
|
|
29931
29948
|
LEGACY_IMPORT_REGEX.lastIndex = 0;
|
|
@@ -29999,6 +30016,7 @@ var init_raw_type_overuse = __esm({
|
|
|
29999
30016
|
const issues = [];
|
|
30000
30017
|
const source = facts.v2?._source;
|
|
30001
30018
|
if (!source) return issues;
|
|
30019
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
30002
30020
|
let m;
|
|
30003
30021
|
RAW_TYPE_REGEX.lastIndex = 0;
|
|
30004
30022
|
while ((m = RAW_TYPE_REGEX.exec(source)) !== null) {
|
|
@@ -30041,6 +30059,7 @@ var init_string_concat_loop = __esm({
|
|
|
30041
30059
|
const issues = [];
|
|
30042
30060
|
const source = facts.v2?._source;
|
|
30043
30061
|
if (!source) return issues;
|
|
30062
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
30044
30063
|
if (!/\b(for|while|do)\b/.test(source)) return issues;
|
|
30045
30064
|
let m;
|
|
30046
30065
|
STRING_CONCAT_REGEX.lastIndex = 0;
|
|
@@ -30084,6 +30103,7 @@ var init_system_out_println = __esm({
|
|
|
30084
30103
|
const issues = [];
|
|
30085
30104
|
const source = facts.v2?._source;
|
|
30086
30105
|
if (!source) return issues;
|
|
30106
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
30087
30107
|
const matches = [];
|
|
30088
30108
|
let m;
|
|
30089
30109
|
PRINTLN_REGEX.lastIndex = 0;
|
|
@@ -43160,6 +43180,7 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43160
43180
|
if (source) {
|
|
43161
43181
|
const { line, column } = positionFrom(node, vctx.lineOffsets);
|
|
43162
43182
|
const importedNames = [];
|
|
43183
|
+
const isTypeOnly = node.typeOnly === true;
|
|
43163
43184
|
const specifiers = node.specifiers;
|
|
43164
43185
|
if (Array.isArray(specifiers)) {
|
|
43165
43186
|
for (const specifier of specifiers) {
|
|
@@ -43175,7 +43196,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43175
43196
|
line,
|
|
43176
43197
|
column,
|
|
43177
43198
|
source,
|
|
43178
|
-
isReferenced: false
|
|
43199
|
+
isReferenced: false,
|
|
43200
|
+
isTypeOnly
|
|
43179
43201
|
});
|
|
43180
43202
|
}
|
|
43181
43203
|
} else if (specifier.type === "ImportSpecifier") {
|
|
@@ -43190,7 +43212,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43190
43212
|
line,
|
|
43191
43213
|
column,
|
|
43192
43214
|
source,
|
|
43193
|
-
isReferenced: false
|
|
43215
|
+
isReferenced: false,
|
|
43216
|
+
isTypeOnly
|
|
43194
43217
|
});
|
|
43195
43218
|
} else if (isObject(local) && local.type === "Identifier" && typeof local.value === "string") {
|
|
43196
43219
|
const name = local.value;
|
|
@@ -43201,7 +43224,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43201
43224
|
line,
|
|
43202
43225
|
column,
|
|
43203
43226
|
source,
|
|
43204
|
-
isReferenced: false
|
|
43227
|
+
isReferenced: false,
|
|
43228
|
+
isTypeOnly
|
|
43205
43229
|
});
|
|
43206
43230
|
}
|
|
43207
43231
|
}
|
|
@@ -43494,7 +43518,7 @@ function handleJSXOpeningElement(node, _parent, _path, vctx) {
|
|
|
43494
43518
|
}
|
|
43495
43519
|
return false;
|
|
43496
43520
|
}
|
|
43497
|
-
function handleVariableDeclarator(node,
|
|
43521
|
+
function handleVariableDeclarator(node, parent, path, vctx) {
|
|
43498
43522
|
if (!isObject(node)) return false;
|
|
43499
43523
|
const init = node.init;
|
|
43500
43524
|
if (init && vctx.visit) {
|
|
@@ -43509,16 +43533,18 @@ function handleVariableDeclarator(node, _parent, path, vctx) {
|
|
|
43509
43533
|
}
|
|
43510
43534
|
}
|
|
43511
43535
|
if (bindingNames.length > 0) {
|
|
43512
|
-
const
|
|
43513
|
-
const kind = isObject(
|
|
43536
|
+
const declParent = isObject(parent) && parent.type === "ExportNamedDeclaration" ? parent.declaration ?? parent : parent;
|
|
43537
|
+
const kind = isObject(declParent) && declParent.type === "VariableDeclaration" ? String(declParent.kind ?? "var") : "var";
|
|
43514
43538
|
const { line, column } = positionFrom(id, vctx.lineOffsets);
|
|
43539
|
+
const scope = vctx.ctx.stack.length === 0 ? "module" : "function";
|
|
43515
43540
|
for (const bindingName of bindingNames) {
|
|
43516
43541
|
vctx.facts.deadCode.bindings.push({
|
|
43517
43542
|
name: bindingName,
|
|
43518
43543
|
kind,
|
|
43519
43544
|
line,
|
|
43520
43545
|
column,
|
|
43521
|
-
isReferenced: false
|
|
43546
|
+
isReferenced: false,
|
|
43547
|
+
scope
|
|
43522
43548
|
});
|
|
43523
43549
|
}
|
|
43524
43550
|
}
|
|
@@ -47897,7 +47923,8 @@ var init_signal_strength = __esm({
|
|
|
47897
47923
|
precision: 0.6803,
|
|
47898
47924
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
47899
47925
|
verdict: "USEFUL",
|
|
47900
|
-
|
|
47926
|
+
defaultOff: true,
|
|
47927
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=27874, FP=13097, P=68.0%, FPR=5.20%, lift=13.09. v7 was USEFUL (TP=16673, FP=10112, lift=11.29). v8 was USEFUL (TP=11201, FP=2985). v0.21.0: marked defaultOff because the v0.21.0 self-scan fires 109 times across 47 files \u2014 the heuristic counts `{`, `}`, `(`, `)` as raw characters without skipping string contents, template literals, regex literals, or comments. Files with normal closing-brace tails (the last 4-5 lines of a function definition) consistently fire because the tail has 0 opens and 3-4 closes. The corpus-level 13.09 lift doesn't translate to per-file discrimination. v0.22.0 should re-implement the count with a token-aware state machine (string/comment/regex skip) or switch to a parse-error-based signal.",
|
|
47901
47928
|
aiSpecific: true,
|
|
47902
47929
|
_v7Verdict: "USEFUL",
|
|
47903
47930
|
_v7Lift: 11.29,
|
|
@@ -48000,7 +48027,8 @@ var init_signal_strength = __esm({
|
|
|
48000
48027
|
precision: 0.7495,
|
|
48001
48028
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
48002
48029
|
verdict: "USEFUL",
|
|
48003
|
-
|
|
48030
|
+
defaultOff: true,
|
|
48031
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=59390, FP=19849, P=75.0%, FPR=7.87%, lift=9.52. v7 was USEFUL (TP=43498, FP=14983, lift=9.11). v8 was USEFUL (TP=15892, FP=4866). v0.21.0: marked defaultOff because the v0.21.0 self-scan fires 250 times across 250 files (1 per file) \u2014 the Binoculars-style per-segment entropy CV is too coarse a signal at the file level; the entropy floor is hit by most structured TypeScript regardless of whether the author was an LLM. The v8.5 corpus-level lift of 9.52 doesn't translate to per-file discrimination. v0.22.0 should tighten the threshold or re-calibrate against the per-file (not per-corpus) lift.",
|
|
48004
48032
|
aiSpecific: true,
|
|
48005
48033
|
_v7Verdict: "USEFUL",
|
|
48006
48034
|
_v7Lift: 9.11,
|
|
@@ -48068,7 +48096,8 @@ var init_signal_strength = __esm({
|
|
|
48068
48096
|
precision: 0.5583,
|
|
48069
48097
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
48070
48098
|
verdict: "USEFUL",
|
|
48071
|
-
|
|
48099
|
+
defaultOff: true,
|
|
48100
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=21567, FP=17061, P=55.8%, FPR=6.77%, lift=8.25. v7 was USEFUL (TP=17466, FP=13438, lift=7.71). v8 was USEFUL (TP=4101, FP=3623). v0.21.2: marked defaultOff because precision 0.5583 < 0.60 floor (the v9.5 methodology in `docs/research/methodology-minimum-sample-size.md` sets the default-on threshold at P >= 0.60). At 55.8% precision, 44% of fires are false positives \u2014 too noisy to gate on. The rule stays available for opt-in via `rules: { 'ai/whitespace-regularity': 'low' }` in slopbrick.config.mjs. The 18 src/ self-scan fires are the typical noise profile.",
|
|
48072
48101
|
aiSpecific: true,
|
|
48073
48102
|
_v7Verdict: "USEFUL",
|
|
48074
48103
|
_v7Lift: 7.71,
|
|
@@ -48416,7 +48445,8 @@ var init_signal_strength = __esm({
|
|
|
48416
48445
|
precision: 0,
|
|
48417
48446
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
48418
48447
|
verdict: "DORMANT",
|
|
48419
|
-
|
|
48448
|
+
defaultOff: false,
|
|
48449
|
+
_calibrationNote: "v0.19: new rule, not yet calibrated. v8.5 calibration does not run dup/* rules (the calibration script analyzes each file in isolation, but dup/* rules require cross-file state \u2014 the module-scope dedup cache is per-process, not per-file, so per-file analysis never produces cross-file matches and the rule's recall reads as 0). Scheduled for v0.22 calibration on the v9 corpus with a per-project dedup cache. v0.21.1 self-scan: 207 fires in src/ after WINDOW_SIZE 10\u219220 (was 575) \u2014 69% reduction. The actual precision/recall of these 207 fires is unverified ground truth. Default stays on because the rule's value is real (block-level duplications are a classic AI-fingerprint) and the corpus-measured 0.0 is a methodology artifact, not a real measurement.",
|
|
48420
48450
|
aiSpecific: false,
|
|
48421
48451
|
_v7Verdict: "DORMANT",
|
|
48422
48452
|
_v7Lift: 1,
|
|
@@ -57063,7 +57093,6 @@ init_dist2();
|
|
|
57063
57093
|
|
|
57064
57094
|
// src/cli/program.ts
|
|
57065
57095
|
var import_node_path70 = require("path");
|
|
57066
|
-
var import_node_perf_hooks = require("perf_hooks");
|
|
57067
57096
|
var import_commander2 = require("commander");
|
|
57068
57097
|
|
|
57069
57098
|
// src/cli/options.ts
|
|
@@ -62212,7 +62241,7 @@ async function runCli({ start }) {
|
|
|
62212
62241
|
await watchProject(options, cwd, paths);
|
|
62213
62242
|
return;
|
|
62214
62243
|
}
|
|
62215
|
-
const scanStart =
|
|
62244
|
+
const scanStart = performance.now();
|
|
62216
62245
|
const {
|
|
62217
62246
|
report,
|
|
62218
62247
|
scores,
|
|
@@ -62222,8 +62251,8 @@ async function runCli({ start }) {
|
|
|
62222
62251
|
baseline,
|
|
62223
62252
|
machineReadableStdout
|
|
62224
62253
|
} = await runScan(options, paths);
|
|
62225
|
-
const scanElapsed = Math.round(
|
|
62226
|
-
const totalElapsed = Math.round(
|
|
62254
|
+
const scanElapsed = Math.round(performance.now() - scanStart);
|
|
62255
|
+
const totalElapsed = Math.round(performance.now() - start);
|
|
62227
62256
|
if (options.baseline) {
|
|
62228
62257
|
const cwd2 = (0, import_node_path70.resolve)(options.workspace ?? process.cwd());
|
|
62229
62258
|
const configHash = hashConfig(config);
|
package/dist/index.d.cts
CHANGED
|
@@ -316,12 +316,29 @@ type BindingKind = 'import-specifier' | 'import-default' | 'import-namespace' |
|
|
|
316
316
|
* referenced-name set per file (intra-scope shadowing is rare enough
|
|
317
317
|
* in practice that file-level resolution catches ~95% of dead code
|
|
318
318
|
* with negligible false positives — the remaining 5% is mostly
|
|
319
|
-
* intentional re-exports which are handled separately).
|
|
319
|
+
* intentional re-exports which are handled separately).
|
|
320
|
+
*
|
|
321
|
+
* `isTypeOnly` (v0.21.0) is set to true when the binding came from
|
|
322
|
+
* an `import type { X }` declaration. The dead/unused-import rule
|
|
323
|
+
* skips these unconditionally — a type-only import is always
|
|
324
|
+
* "referenced" if the type is used (TypeScript guarantees it), and
|
|
325
|
+
* TypeScript itself elides the import at build time, so the unused-
|
|
326
|
+
* import rule has nothing actionable to report. */
|
|
320
327
|
interface BindingRecord {
|
|
321
328
|
/** The local name as written (`Button` for `import { Button } from ...`,
|
|
322
329
|
* `setFoo` for a parameter, etc.). */
|
|
323
330
|
name: string;
|
|
324
331
|
kind: BindingKind;
|
|
332
|
+
/** v0.21.0: true when this binding came from `import type { X }`. The
|
|
333
|
+
* dead/unused-import rule skips these. */
|
|
334
|
+
isTypeOnly?: boolean;
|
|
335
|
+
/** v0.21.0: lexical scope of the binding. `'module'` for top-level
|
|
336
|
+
* declarations outside any function; `'function'` for bindings
|
|
337
|
+
* inside a function body. The dead/unused-local rule skips
|
|
338
|
+
* `'module'` consts/functions/classes (they are often intentional
|
|
339
|
+
* placeholder exports, type re-exports, or side-effect-ful
|
|
340
|
+
* constructions — not real dead code). */
|
|
341
|
+
scope?: 'module' | 'function';
|
|
325
342
|
/** Source line (1-indexed). */
|
|
326
343
|
line: number;
|
|
327
344
|
/** Source column (0-indexed). */
|
package/dist/index.d.ts
CHANGED
|
@@ -316,12 +316,29 @@ type BindingKind = 'import-specifier' | 'import-default' | 'import-namespace' |
|
|
|
316
316
|
* referenced-name set per file (intra-scope shadowing is rare enough
|
|
317
317
|
* in practice that file-level resolution catches ~95% of dead code
|
|
318
318
|
* with negligible false positives — the remaining 5% is mostly
|
|
319
|
-
* intentional re-exports which are handled separately).
|
|
319
|
+
* intentional re-exports which are handled separately).
|
|
320
|
+
*
|
|
321
|
+
* `isTypeOnly` (v0.21.0) is set to true when the binding came from
|
|
322
|
+
* an `import type { X }` declaration. The dead/unused-import rule
|
|
323
|
+
* skips these unconditionally — a type-only import is always
|
|
324
|
+
* "referenced" if the type is used (TypeScript guarantees it), and
|
|
325
|
+
* TypeScript itself elides the import at build time, so the unused-
|
|
326
|
+
* import rule has nothing actionable to report. */
|
|
320
327
|
interface BindingRecord {
|
|
321
328
|
/** The local name as written (`Button` for `import { Button } from ...`,
|
|
322
329
|
* `setFoo` for a parameter, etc.). */
|
|
323
330
|
name: string;
|
|
324
331
|
kind: BindingKind;
|
|
332
|
+
/** v0.21.0: true when this binding came from `import type { X }`. The
|
|
333
|
+
* dead/unused-import rule skips these. */
|
|
334
|
+
isTypeOnly?: boolean;
|
|
335
|
+
/** v0.21.0: lexical scope of the binding. `'module'` for top-level
|
|
336
|
+
* declarations outside any function; `'function'` for bindings
|
|
337
|
+
* inside a function body. The dead/unused-local rule skips
|
|
338
|
+
* `'module'` consts/functions/classes (they are often intentional
|
|
339
|
+
* placeholder exports, type re-exports, or side-effect-ful
|
|
340
|
+
* constructions — not real dead code). */
|
|
341
|
+
scope?: 'module' | 'function';
|
|
325
342
|
/** Source line (1-indexed). */
|
|
326
343
|
line: number;
|
|
327
344
|
/** Source column (0-indexed). */
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var VERSION;
|
|
|
19
19
|
var init_header = __esm({
|
|
20
20
|
"src/types/_header.ts"() {
|
|
21
21
|
"use strict";
|
|
22
|
-
VERSION = "0.21.
|
|
22
|
+
VERSION = "0.21.2";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -26985,6 +26985,16 @@ var init_segment_surprisal_cv = __esm({
|
|
|
26985
26985
|
category: "ai",
|
|
26986
26986
|
severity: "medium",
|
|
26987
26987
|
aiSpecific: true,
|
|
26988
|
+
// v0.21.0: defaultOff. The rule fires on 250/250 files in the
|
|
26989
|
+
// self-scan (1 per file), which means it's not discriminating
|
|
26990
|
+
// between AI and non-AI text — it fires on every file. The
|
|
26991
|
+
// Binoculars-style per-segment entropy CV is too coarse a
|
|
26992
|
+
// signal at the file level; the entropy floor is hit by most
|
|
26993
|
+
// structured TypeScript regardless of whether the author was
|
|
26994
|
+
// an LLM. Re-enable per-file via
|
|
26995
|
+
// `rules: { 'ai/segment-surprisal-cv': 'medium' }` in
|
|
26996
|
+
// slopbrick.config.mjs once a tighter threshold is calibrated.
|
|
26997
|
+
defaultOff: true,
|
|
26988
26998
|
description: "Per-segment cross-entropy CV is suspiciously low \u2014 Binoculars (Hans 2024): AI text has near-constant per-window entropy",
|
|
26989
26999
|
create(context) {
|
|
26990
27000
|
return context;
|
|
@@ -28277,6 +28287,7 @@ var init_unused_import = __esm({
|
|
|
28277
28287
|
if (binding.kind !== "import-specifier" && binding.kind !== "import-default" && binding.kind !== "import-namespace") {
|
|
28278
28288
|
continue;
|
|
28279
28289
|
}
|
|
28290
|
+
if (binding.isTypeOnly) continue;
|
|
28280
28291
|
if (binding.isReferenced) continue;
|
|
28281
28292
|
if (!binding.name) continue;
|
|
28282
28293
|
const source = binding.source ? ` from '${binding.source}'` : "";
|
|
@@ -28343,6 +28354,9 @@ var init_unused_local = __esm({
|
|
|
28343
28354
|
if (binding.isReferenced) continue;
|
|
28344
28355
|
if (SKIP_NAMES.has(binding.name)) continue;
|
|
28345
28356
|
if (binding.name.startsWith("_")) continue;
|
|
28357
|
+
if (binding.scope === "module" && (binding.kind === "const" || binding.kind === "function" || binding.kind === "class" || binding.kind === "type" || binding.kind === "interface" || binding.kind === "enum")) {
|
|
28358
|
+
continue;
|
|
28359
|
+
}
|
|
28346
28360
|
issues.push({
|
|
28347
28361
|
ruleId: "dead/unused-local",
|
|
28348
28362
|
category: "logic",
|
|
@@ -29578,7 +29592,7 @@ var init_identical_block = __esm({
|
|
|
29578
29592
|
"src/rules/dup/identical-block.ts"() {
|
|
29579
29593
|
"use strict";
|
|
29580
29594
|
init_rule();
|
|
29581
|
-
WINDOW_SIZE =
|
|
29595
|
+
WINDOW_SIZE = 20;
|
|
29582
29596
|
MIN_NORMALIZED_LENGTH = 40;
|
|
29583
29597
|
HASH_PREFIX_LENGTH = 16;
|
|
29584
29598
|
DEDUP_CACHE = /* @__PURE__ */ new Map();
|
|
@@ -29824,6 +29838,7 @@ var init_arraylist_vs_linkedlist = __esm({
|
|
|
29824
29838
|
const issues = [];
|
|
29825
29839
|
const source = facts.v2?._source;
|
|
29826
29840
|
if (!source) return issues;
|
|
29841
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
29827
29842
|
let m;
|
|
29828
29843
|
NEW_LINKED_LIST_REGEX.lastIndex = 0;
|
|
29829
29844
|
while ((m = NEW_LINKED_LIST_REGEX.exec(source)) !== null) {
|
|
@@ -29865,6 +29880,7 @@ var init_empty_catch_block = __esm({
|
|
|
29865
29880
|
const issues = [];
|
|
29866
29881
|
const source = facts.v2?._source;
|
|
29867
29882
|
if (!source) return issues;
|
|
29883
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
29868
29884
|
let m;
|
|
29869
29885
|
SINGLE_LINE_EMPTY_CATCH_REGEX.lastIndex = 0;
|
|
29870
29886
|
while ((m = SINGLE_LINE_EMPTY_CATCH_REGEX.exec(source)) !== null) {
|
|
@@ -29908,6 +29924,7 @@ var init_legacy_date_api = __esm({
|
|
|
29908
29924
|
const issues = [];
|
|
29909
29925
|
const source = facts.v2?._source;
|
|
29910
29926
|
if (!source) return issues;
|
|
29927
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
29911
29928
|
const flagged = /* @__PURE__ */ new Set();
|
|
29912
29929
|
let m;
|
|
29913
29930
|
LEGACY_IMPORT_REGEX.lastIndex = 0;
|
|
@@ -29981,6 +29998,7 @@ var init_raw_type_overuse = __esm({
|
|
|
29981
29998
|
const issues = [];
|
|
29982
29999
|
const source = facts.v2?._source;
|
|
29983
30000
|
if (!source) return issues;
|
|
30001
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
29984
30002
|
let m;
|
|
29985
30003
|
RAW_TYPE_REGEX.lastIndex = 0;
|
|
29986
30004
|
while ((m = RAW_TYPE_REGEX.exec(source)) !== null) {
|
|
@@ -30023,6 +30041,7 @@ var init_string_concat_loop = __esm({
|
|
|
30023
30041
|
const issues = [];
|
|
30024
30042
|
const source = facts.v2?._source;
|
|
30025
30043
|
if (!source) return issues;
|
|
30044
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
30026
30045
|
if (!/\b(for|while|do)\b/.test(source)) return issues;
|
|
30027
30046
|
let m;
|
|
30028
30047
|
STRING_CONCAT_REGEX.lastIndex = 0;
|
|
@@ -30066,6 +30085,7 @@ var init_system_out_println = __esm({
|
|
|
30066
30085
|
const issues = [];
|
|
30067
30086
|
const source = facts.v2?._source;
|
|
30068
30087
|
if (!source) return issues;
|
|
30088
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
30069
30089
|
const matches = [];
|
|
30070
30090
|
let m;
|
|
30071
30091
|
PRINTLN_REGEX.lastIndex = 0;
|
|
@@ -43142,6 +43162,7 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43142
43162
|
if (source) {
|
|
43143
43163
|
const { line, column } = positionFrom(node, vctx.lineOffsets);
|
|
43144
43164
|
const importedNames = [];
|
|
43165
|
+
const isTypeOnly = node.typeOnly === true;
|
|
43145
43166
|
const specifiers = node.specifiers;
|
|
43146
43167
|
if (Array.isArray(specifiers)) {
|
|
43147
43168
|
for (const specifier of specifiers) {
|
|
@@ -43157,7 +43178,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43157
43178
|
line,
|
|
43158
43179
|
column,
|
|
43159
43180
|
source,
|
|
43160
|
-
isReferenced: false
|
|
43181
|
+
isReferenced: false,
|
|
43182
|
+
isTypeOnly
|
|
43161
43183
|
});
|
|
43162
43184
|
}
|
|
43163
43185
|
} else if (specifier.type === "ImportSpecifier") {
|
|
@@ -43172,7 +43194,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43172
43194
|
line,
|
|
43173
43195
|
column,
|
|
43174
43196
|
source,
|
|
43175
|
-
isReferenced: false
|
|
43197
|
+
isReferenced: false,
|
|
43198
|
+
isTypeOnly
|
|
43176
43199
|
});
|
|
43177
43200
|
} else if (isObject(local) && local.type === "Identifier" && typeof local.value === "string") {
|
|
43178
43201
|
const name = local.value;
|
|
@@ -43183,7 +43206,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43183
43206
|
line,
|
|
43184
43207
|
column,
|
|
43185
43208
|
source,
|
|
43186
|
-
isReferenced: false
|
|
43209
|
+
isReferenced: false,
|
|
43210
|
+
isTypeOnly
|
|
43187
43211
|
});
|
|
43188
43212
|
}
|
|
43189
43213
|
}
|
|
@@ -43476,7 +43500,7 @@ function handleJSXOpeningElement(node, _parent, _path, vctx) {
|
|
|
43476
43500
|
}
|
|
43477
43501
|
return false;
|
|
43478
43502
|
}
|
|
43479
|
-
function handleVariableDeclarator(node,
|
|
43503
|
+
function handleVariableDeclarator(node, parent, path, vctx) {
|
|
43480
43504
|
if (!isObject(node)) return false;
|
|
43481
43505
|
const init = node.init;
|
|
43482
43506
|
if (init && vctx.visit) {
|
|
@@ -43491,16 +43515,18 @@ function handleVariableDeclarator(node, _parent, path, vctx) {
|
|
|
43491
43515
|
}
|
|
43492
43516
|
}
|
|
43493
43517
|
if (bindingNames.length > 0) {
|
|
43494
|
-
const
|
|
43495
|
-
const kind = isObject(
|
|
43518
|
+
const declParent = isObject(parent) && parent.type === "ExportNamedDeclaration" ? parent.declaration ?? parent : parent;
|
|
43519
|
+
const kind = isObject(declParent) && declParent.type === "VariableDeclaration" ? String(declParent.kind ?? "var") : "var";
|
|
43496
43520
|
const { line, column } = positionFrom(id, vctx.lineOffsets);
|
|
43521
|
+
const scope = vctx.ctx.stack.length === 0 ? "module" : "function";
|
|
43497
43522
|
for (const bindingName of bindingNames) {
|
|
43498
43523
|
vctx.facts.deadCode.bindings.push({
|
|
43499
43524
|
name: bindingName,
|
|
43500
43525
|
kind,
|
|
43501
43526
|
line,
|
|
43502
43527
|
column,
|
|
43503
|
-
isReferenced: false
|
|
43528
|
+
isReferenced: false,
|
|
43529
|
+
scope
|
|
43504
43530
|
});
|
|
43505
43531
|
}
|
|
43506
43532
|
}
|
|
@@ -47876,7 +47902,8 @@ var init_signal_strength = __esm({
|
|
|
47876
47902
|
precision: 0.6803,
|
|
47877
47903
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
47878
47904
|
verdict: "USEFUL",
|
|
47879
|
-
|
|
47905
|
+
defaultOff: true,
|
|
47906
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=27874, FP=13097, P=68.0%, FPR=5.20%, lift=13.09. v7 was USEFUL (TP=16673, FP=10112, lift=11.29). v8 was USEFUL (TP=11201, FP=2985). v0.21.0: marked defaultOff because the v0.21.0 self-scan fires 109 times across 47 files \u2014 the heuristic counts `{`, `}`, `(`, `)` as raw characters without skipping string contents, template literals, regex literals, or comments. Files with normal closing-brace tails (the last 4-5 lines of a function definition) consistently fire because the tail has 0 opens and 3-4 closes. The corpus-level 13.09 lift doesn't translate to per-file discrimination. v0.22.0 should re-implement the count with a token-aware state machine (string/comment/regex skip) or switch to a parse-error-based signal.",
|
|
47880
47907
|
aiSpecific: true,
|
|
47881
47908
|
_v7Verdict: "USEFUL",
|
|
47882
47909
|
_v7Lift: 11.29,
|
|
@@ -47979,7 +48006,8 @@ var init_signal_strength = __esm({
|
|
|
47979
48006
|
precision: 0.7495,
|
|
47980
48007
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
47981
48008
|
verdict: "USEFUL",
|
|
47982
|
-
|
|
48009
|
+
defaultOff: true,
|
|
48010
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=59390, FP=19849, P=75.0%, FPR=7.87%, lift=9.52. v7 was USEFUL (TP=43498, FP=14983, lift=9.11). v8 was USEFUL (TP=15892, FP=4866). v0.21.0: marked defaultOff because the v0.21.0 self-scan fires 250 times across 250 files (1 per file) \u2014 the Binoculars-style per-segment entropy CV is too coarse a signal at the file level; the entropy floor is hit by most structured TypeScript regardless of whether the author was an LLM. The v8.5 corpus-level lift of 9.52 doesn't translate to per-file discrimination. v0.22.0 should tighten the threshold or re-calibrate against the per-file (not per-corpus) lift.",
|
|
47983
48011
|
aiSpecific: true,
|
|
47984
48012
|
_v7Verdict: "USEFUL",
|
|
47985
48013
|
_v7Lift: 9.11,
|
|
@@ -48047,7 +48075,8 @@ var init_signal_strength = __esm({
|
|
|
48047
48075
|
precision: 0.5583,
|
|
48048
48076
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
48049
48077
|
verdict: "USEFUL",
|
|
48050
|
-
|
|
48078
|
+
defaultOff: true,
|
|
48079
|
+
_calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=21567, FP=17061, P=55.8%, FPR=6.77%, lift=8.25. v7 was USEFUL (TP=17466, FP=13438, lift=7.71). v8 was USEFUL (TP=4101, FP=3623). v0.21.2: marked defaultOff because precision 0.5583 < 0.60 floor (the v9.5 methodology in `docs/research/methodology-minimum-sample-size.md` sets the default-on threshold at P >= 0.60). At 55.8% precision, 44% of fires are false positives \u2014 too noisy to gate on. The rule stays available for opt-in via `rules: { 'ai/whitespace-regularity': 'low' }` in slopbrick.config.mjs. The 18 src/ self-scan fires are the typical noise profile.",
|
|
48051
48080
|
aiSpecific: true,
|
|
48052
48081
|
_v7Verdict: "USEFUL",
|
|
48053
48082
|
_v7Lift: 7.71,
|
|
@@ -48395,7 +48424,8 @@ var init_signal_strength = __esm({
|
|
|
48395
48424
|
precision: 0,
|
|
48396
48425
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
48397
48426
|
verdict: "DORMANT",
|
|
48398
|
-
|
|
48427
|
+
defaultOff: false,
|
|
48428
|
+
_calibrationNote: "v0.19: new rule, not yet calibrated. v8.5 calibration does not run dup/* rules (the calibration script analyzes each file in isolation, but dup/* rules require cross-file state \u2014 the module-scope dedup cache is per-process, not per-file, so per-file analysis never produces cross-file matches and the rule's recall reads as 0). Scheduled for v0.22 calibration on the v9 corpus with a per-project dedup cache. v0.21.1 self-scan: 207 fires in src/ after WINDOW_SIZE 10\u219220 (was 575) \u2014 69% reduction. The actual precision/recall of these 207 fires is unverified ground truth. Default stays on because the rule's value is real (block-level duplications are a classic AI-fingerprint) and the corpus-measured 0.0 is a methodology artifact, not a real measurement.",
|
|
48399
48429
|
aiSpecific: false,
|
|
48400
48430
|
_v7Verdict: "DORMANT",
|
|
48401
48431
|
_v7Lift: 1,
|
|
@@ -55926,10 +55956,7 @@ var init_renderOutput = __esm({
|
|
|
55926
55956
|
});
|
|
55927
55957
|
|
|
55928
55958
|
// src/cli/watch.ts
|
|
55929
|
-
import {
|
|
55930
|
-
watch,
|
|
55931
|
-
statSync as statSync7
|
|
55932
|
-
} from "fs";
|
|
55959
|
+
import { watch, statSync as statSync7 } from "fs";
|
|
55933
55960
|
import { resolve as resolve11 } from "path";
|
|
55934
55961
|
async function watchProject(options, cwd, paths) {
|
|
55935
55962
|
let baselineMtime;
|
|
@@ -56966,7 +56993,6 @@ init_dist2();
|
|
|
56966
56993
|
|
|
56967
56994
|
// src/cli/program.ts
|
|
56968
56995
|
import { resolve as resolve41 } from "path";
|
|
56969
|
-
import { performance } from "perf_hooks";
|
|
56970
56996
|
import { Command } from "commander";
|
|
56971
56997
|
|
|
56972
56998
|
// src/cli/options.ts
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slopbrick",
|
|
3
|
-
"version": "0.21.
|
|
4
|
-
"description": "Discovered, modeled, and governed repository structure. SlopBrick scans source code, classifies it against 95 rules in 15 categories, computes 4 scores (
|
|
3
|
+
"version": "0.21.2",
|
|
4
|
+
"description": "Discovered, modeled, and governed repository structure. SlopBrick scans source code, classifies it against 95+ rules in 15 categories, computes 4 scores (aiSlopScore: lower=cleaner, engineeringHygiene, security, repositoryHealth composite), and persists the structure for AI agents and CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"slopbrick": "bin/slopbrick.js"
|