slopbrick 0.21.0 → 0.21.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/README.md +16 -12
- package/dist/engine/worker.cjs +32 -10
- package/dist/engine/worker.js +32 -10
- package/dist/index.cjs +36 -15
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +34 -16
- 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();
|
|
@@ -42017,7 +42037,8 @@ var signal_strength_default = {
|
|
|
42017
42037
|
precision: 0.6803,
|
|
42018
42038
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42019
42039
|
verdict: "USEFUL",
|
|
42020
|
-
|
|
42040
|
+
defaultOff: true,
|
|
42041
|
+
_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
42042
|
aiSpecific: true,
|
|
42022
42043
|
_v7Verdict: "USEFUL",
|
|
42023
42044
|
_v7Lift: 11.29,
|
|
@@ -42120,7 +42141,8 @@ var signal_strength_default = {
|
|
|
42120
42141
|
precision: 0.7495,
|
|
42121
42142
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42122
42143
|
verdict: "USEFUL",
|
|
42123
|
-
|
|
42144
|
+
defaultOff: true,
|
|
42145
|
+
_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
42146
|
aiSpecific: true,
|
|
42125
42147
|
_v7Verdict: "USEFUL",
|
|
42126
42148
|
_v7Lift: 9.11,
|
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();
|
|
@@ -41988,7 +42008,8 @@ var signal_strength_default = {
|
|
|
41988
42008
|
precision: 0.6803,
|
|
41989
42009
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
41990
42010
|
verdict: "USEFUL",
|
|
41991
|
-
|
|
42011
|
+
defaultOff: true,
|
|
42012
|
+
_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
42013
|
aiSpecific: true,
|
|
41993
42014
|
_v7Verdict: "USEFUL",
|
|
41994
42015
|
_v7Lift: 11.29,
|
|
@@ -42091,7 +42112,8 @@ var signal_strength_default = {
|
|
|
42091
42112
|
precision: 0.7495,
|
|
42092
42113
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42093
42114
|
verdict: "USEFUL",
|
|
42094
|
-
|
|
42115
|
+
defaultOff: true,
|
|
42116
|
+
_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
42117
|
aiSpecific: true,
|
|
42096
42118
|
_v7Verdict: "USEFUL",
|
|
42097
42119
|
_v7Lift: 9.11,
|
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.1";
|
|
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();
|
|
@@ -43160,6 +43174,7 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43160
43174
|
if (source) {
|
|
43161
43175
|
const { line, column } = positionFrom(node, vctx.lineOffsets);
|
|
43162
43176
|
const importedNames = [];
|
|
43177
|
+
const isTypeOnly = node.typeOnly === true;
|
|
43163
43178
|
const specifiers = node.specifiers;
|
|
43164
43179
|
if (Array.isArray(specifiers)) {
|
|
43165
43180
|
for (const specifier of specifiers) {
|
|
@@ -43175,7 +43190,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43175
43190
|
line,
|
|
43176
43191
|
column,
|
|
43177
43192
|
source,
|
|
43178
|
-
isReferenced: false
|
|
43193
|
+
isReferenced: false,
|
|
43194
|
+
isTypeOnly
|
|
43179
43195
|
});
|
|
43180
43196
|
}
|
|
43181
43197
|
} else if (specifier.type === "ImportSpecifier") {
|
|
@@ -43190,7 +43206,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43190
43206
|
line,
|
|
43191
43207
|
column,
|
|
43192
43208
|
source,
|
|
43193
|
-
isReferenced: false
|
|
43209
|
+
isReferenced: false,
|
|
43210
|
+
isTypeOnly
|
|
43194
43211
|
});
|
|
43195
43212
|
} else if (isObject(local) && local.type === "Identifier" && typeof local.value === "string") {
|
|
43196
43213
|
const name = local.value;
|
|
@@ -43201,7 +43218,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43201
43218
|
line,
|
|
43202
43219
|
column,
|
|
43203
43220
|
source,
|
|
43204
|
-
isReferenced: false
|
|
43221
|
+
isReferenced: false,
|
|
43222
|
+
isTypeOnly
|
|
43205
43223
|
});
|
|
43206
43224
|
}
|
|
43207
43225
|
}
|
|
@@ -43494,7 +43512,7 @@ function handleJSXOpeningElement(node, _parent, _path, vctx) {
|
|
|
43494
43512
|
}
|
|
43495
43513
|
return false;
|
|
43496
43514
|
}
|
|
43497
|
-
function handleVariableDeclarator(node,
|
|
43515
|
+
function handleVariableDeclarator(node, parent, path, vctx) {
|
|
43498
43516
|
if (!isObject(node)) return false;
|
|
43499
43517
|
const init = node.init;
|
|
43500
43518
|
if (init && vctx.visit) {
|
|
@@ -43509,16 +43527,18 @@ function handleVariableDeclarator(node, _parent, path, vctx) {
|
|
|
43509
43527
|
}
|
|
43510
43528
|
}
|
|
43511
43529
|
if (bindingNames.length > 0) {
|
|
43512
|
-
const
|
|
43513
|
-
const kind = isObject(
|
|
43530
|
+
const declParent = isObject(parent) && parent.type === "ExportNamedDeclaration" ? parent.declaration ?? parent : parent;
|
|
43531
|
+
const kind = isObject(declParent) && declParent.type === "VariableDeclaration" ? String(declParent.kind ?? "var") : "var";
|
|
43514
43532
|
const { line, column } = positionFrom(id, vctx.lineOffsets);
|
|
43533
|
+
const scope = vctx.ctx.stack.length === 0 ? "module" : "function";
|
|
43515
43534
|
for (const bindingName of bindingNames) {
|
|
43516
43535
|
vctx.facts.deadCode.bindings.push({
|
|
43517
43536
|
name: bindingName,
|
|
43518
43537
|
kind,
|
|
43519
43538
|
line,
|
|
43520
43539
|
column,
|
|
43521
|
-
isReferenced: false
|
|
43540
|
+
isReferenced: false,
|
|
43541
|
+
scope
|
|
43522
43542
|
});
|
|
43523
43543
|
}
|
|
43524
43544
|
}
|
|
@@ -47897,7 +47917,8 @@ var init_signal_strength = __esm({
|
|
|
47897
47917
|
precision: 0.6803,
|
|
47898
47918
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
47899
47919
|
verdict: "USEFUL",
|
|
47900
|
-
|
|
47920
|
+
defaultOff: true,
|
|
47921
|
+
_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
47922
|
aiSpecific: true,
|
|
47902
47923
|
_v7Verdict: "USEFUL",
|
|
47903
47924
|
_v7Lift: 11.29,
|
|
@@ -48000,7 +48021,8 @@ var init_signal_strength = __esm({
|
|
|
48000
48021
|
precision: 0.7495,
|
|
48001
48022
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
48002
48023
|
verdict: "USEFUL",
|
|
48003
|
-
|
|
48024
|
+
defaultOff: true,
|
|
48025
|
+
_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
48026
|
aiSpecific: true,
|
|
48005
48027
|
_v7Verdict: "USEFUL",
|
|
48006
48028
|
_v7Lift: 9.11,
|
|
@@ -57063,7 +57085,6 @@ init_dist2();
|
|
|
57063
57085
|
|
|
57064
57086
|
// src/cli/program.ts
|
|
57065
57087
|
var import_node_path70 = require("path");
|
|
57066
|
-
var import_node_perf_hooks = require("perf_hooks");
|
|
57067
57088
|
var import_commander2 = require("commander");
|
|
57068
57089
|
|
|
57069
57090
|
// src/cli/options.ts
|
|
@@ -62212,7 +62233,7 @@ async function runCli({ start }) {
|
|
|
62212
62233
|
await watchProject(options, cwd, paths);
|
|
62213
62234
|
return;
|
|
62214
62235
|
}
|
|
62215
|
-
const scanStart =
|
|
62236
|
+
const scanStart = performance.now();
|
|
62216
62237
|
const {
|
|
62217
62238
|
report,
|
|
62218
62239
|
scores,
|
|
@@ -62222,8 +62243,8 @@ async function runCli({ start }) {
|
|
|
62222
62243
|
baseline,
|
|
62223
62244
|
machineReadableStdout
|
|
62224
62245
|
} = await runScan(options, paths);
|
|
62225
|
-
const scanElapsed = Math.round(
|
|
62226
|
-
const totalElapsed = Math.round(
|
|
62246
|
+
const scanElapsed = Math.round(performance.now() - scanStart);
|
|
62247
|
+
const totalElapsed = Math.round(performance.now() - start);
|
|
62227
62248
|
if (options.baseline) {
|
|
62228
62249
|
const cwd2 = (0, import_node_path70.resolve)(options.workspace ?? process.cwd());
|
|
62229
62250
|
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.1";
|
|
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();
|
|
@@ -43142,6 +43156,7 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43142
43156
|
if (source) {
|
|
43143
43157
|
const { line, column } = positionFrom(node, vctx.lineOffsets);
|
|
43144
43158
|
const importedNames = [];
|
|
43159
|
+
const isTypeOnly = node.typeOnly === true;
|
|
43145
43160
|
const specifiers = node.specifiers;
|
|
43146
43161
|
if (Array.isArray(specifiers)) {
|
|
43147
43162
|
for (const specifier of specifiers) {
|
|
@@ -43157,7 +43172,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43157
43172
|
line,
|
|
43158
43173
|
column,
|
|
43159
43174
|
source,
|
|
43160
|
-
isReferenced: false
|
|
43175
|
+
isReferenced: false,
|
|
43176
|
+
isTypeOnly
|
|
43161
43177
|
});
|
|
43162
43178
|
}
|
|
43163
43179
|
} else if (specifier.type === "ImportSpecifier") {
|
|
@@ -43172,7 +43188,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43172
43188
|
line,
|
|
43173
43189
|
column,
|
|
43174
43190
|
source,
|
|
43175
|
-
isReferenced: false
|
|
43191
|
+
isReferenced: false,
|
|
43192
|
+
isTypeOnly
|
|
43176
43193
|
});
|
|
43177
43194
|
} else if (isObject(local) && local.type === "Identifier" && typeof local.value === "string") {
|
|
43178
43195
|
const name = local.value;
|
|
@@ -43183,7 +43200,8 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
|
|
|
43183
43200
|
line,
|
|
43184
43201
|
column,
|
|
43185
43202
|
source,
|
|
43186
|
-
isReferenced: false
|
|
43203
|
+
isReferenced: false,
|
|
43204
|
+
isTypeOnly
|
|
43187
43205
|
});
|
|
43188
43206
|
}
|
|
43189
43207
|
}
|
|
@@ -43476,7 +43494,7 @@ function handleJSXOpeningElement(node, _parent, _path, vctx) {
|
|
|
43476
43494
|
}
|
|
43477
43495
|
return false;
|
|
43478
43496
|
}
|
|
43479
|
-
function handleVariableDeclarator(node,
|
|
43497
|
+
function handleVariableDeclarator(node, parent, path, vctx) {
|
|
43480
43498
|
if (!isObject(node)) return false;
|
|
43481
43499
|
const init = node.init;
|
|
43482
43500
|
if (init && vctx.visit) {
|
|
@@ -43491,16 +43509,18 @@ function handleVariableDeclarator(node, _parent, path, vctx) {
|
|
|
43491
43509
|
}
|
|
43492
43510
|
}
|
|
43493
43511
|
if (bindingNames.length > 0) {
|
|
43494
|
-
const
|
|
43495
|
-
const kind = isObject(
|
|
43512
|
+
const declParent = isObject(parent) && parent.type === "ExportNamedDeclaration" ? parent.declaration ?? parent : parent;
|
|
43513
|
+
const kind = isObject(declParent) && declParent.type === "VariableDeclaration" ? String(declParent.kind ?? "var") : "var";
|
|
43496
43514
|
const { line, column } = positionFrom(id, vctx.lineOffsets);
|
|
43515
|
+
const scope = vctx.ctx.stack.length === 0 ? "module" : "function";
|
|
43497
43516
|
for (const bindingName of bindingNames) {
|
|
43498
43517
|
vctx.facts.deadCode.bindings.push({
|
|
43499
43518
|
name: bindingName,
|
|
43500
43519
|
kind,
|
|
43501
43520
|
line,
|
|
43502
43521
|
column,
|
|
43503
|
-
isReferenced: false
|
|
43522
|
+
isReferenced: false,
|
|
43523
|
+
scope
|
|
43504
43524
|
});
|
|
43505
43525
|
}
|
|
43506
43526
|
}
|
|
@@ -47876,7 +47896,8 @@ var init_signal_strength = __esm({
|
|
|
47876
47896
|
precision: 0.6803,
|
|
47877
47897
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
47878
47898
|
verdict: "USEFUL",
|
|
47879
|
-
|
|
47899
|
+
defaultOff: true,
|
|
47900
|
+
_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
47901
|
aiSpecific: true,
|
|
47881
47902
|
_v7Verdict: "USEFUL",
|
|
47882
47903
|
_v7Lift: 11.29,
|
|
@@ -47979,7 +48000,8 @@ var init_signal_strength = __esm({
|
|
|
47979
48000
|
precision: 0.7495,
|
|
47980
48001
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
47981
48002
|
verdict: "USEFUL",
|
|
47982
|
-
|
|
48003
|
+
defaultOff: true,
|
|
48004
|
+
_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
48005
|
aiSpecific: true,
|
|
47984
48006
|
_v7Verdict: "USEFUL",
|
|
47985
48007
|
_v7Lift: 9.11,
|
|
@@ -55926,10 +55948,7 @@ var init_renderOutput = __esm({
|
|
|
55926
55948
|
});
|
|
55927
55949
|
|
|
55928
55950
|
// src/cli/watch.ts
|
|
55929
|
-
import {
|
|
55930
|
-
watch,
|
|
55931
|
-
statSync as statSync7
|
|
55932
|
-
} from "fs";
|
|
55951
|
+
import { watch, statSync as statSync7 } from "fs";
|
|
55933
55952
|
import { resolve as resolve11 } from "path";
|
|
55934
55953
|
async function watchProject(options, cwd, paths) {
|
|
55935
55954
|
let baselineMtime;
|
|
@@ -56966,7 +56985,6 @@ init_dist2();
|
|
|
56966
56985
|
|
|
56967
56986
|
// src/cli/program.ts
|
|
56968
56987
|
import { resolve as resolve41 } from "path";
|
|
56969
|
-
import { performance } from "perf_hooks";
|
|
56970
56988
|
import { Command } from "commander";
|
|
56971
56989
|
|
|
56972
56990
|
// 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.1",
|
|
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"
|