slopbrick 0.20.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 +18 -14
- package/dist/engine/worker.cjs +108 -27
- package/dist/engine/worker.js +108 -27
- package/dist/index.cjs +324 -108
- package/dist/index.d.cts +47 -12
- package/dist/index.d.ts +47 -12
- package/dist/index.js +322 -109
- 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
|
-
| **`
|
|
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
|
|
|
@@ -108,13 +112,13 @@ For per-rule precision/recall/FPR (auditable), see
|
|
|
108
112
|
$ npx slopbrick scan
|
|
109
113
|
Repo is concerning (25/100). The biggest problem is AI patterns — worst file is src/cli/scan.ts. Run `slopbrick scan --why-failing` for the top 5 rules, or `slopbrick scan --suggest` for fixes.
|
|
110
114
|
|
|
111
|
-
AI
|
|
115
|
+
AI Slop Score: 25 / 100 [HIGH] ↑5 (worse)
|
|
112
116
|
higher = better · measures AI-slop signatures. The same number in .slopbrick/health.json.
|
|
113
117
|
├─ boundary: 10 (40%) — structural integrity
|
|
114
118
|
├─ context: 50 (35%) — props / state / imports
|
|
115
119
|
└─ visual: 5 (25%) — CSS / a11y / layout
|
|
116
120
|
|
|
117
|
-
Engineering Hygiene: 60 / 100 [NEEDS WORK] — higher = better · measures internal consistency. This is a secondary view; the AI
|
|
121
|
+
Engineering Hygiene: 60 / 100 [NEEDS WORK] — higher = better · measures internal consistency. This is a secondary view; the AI Slop Score above is the gate.
|
|
118
122
|
|
|
119
123
|
Other signals (not the gate):
|
|
120
124
|
Code Hygiene 75/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
|
}
|
|
@@ -32882,8 +32888,8 @@ function getCorpusBaselines() {
|
|
|
32882
32888
|
|
|
32883
32889
|
// src/rules/ai/comment-ratio.ts
|
|
32884
32890
|
var MIN_FILE_LINES = 20;
|
|
32885
|
-
var FALLBACK_LOW = 0.
|
|
32886
|
-
var FALLBACK_HIGH = 0.
|
|
32891
|
+
var FALLBACK_LOW = 0.02;
|
|
32892
|
+
var FALLBACK_HIGH = 0.55;
|
|
32887
32893
|
var LINE_COMMENT_RE = /^\s*(?:\/\/|#|--|;|%)/;
|
|
32888
32894
|
var BLOCK_COMMENT_OPEN_RE = /\/\*/;
|
|
32889
32895
|
var aiCommentRatioRule = createRule({
|
|
@@ -33012,9 +33018,9 @@ function std(xs) {
|
|
|
33012
33018
|
|
|
33013
33019
|
// src/rules/ai/compression-profile.ts
|
|
33014
33020
|
var MIN_BYTES = 2e3;
|
|
33015
|
-
var GZIP_RATIO_AI_THRESHOLD = 0.
|
|
33016
|
-
var MEAN_LINE_NCD_HUMAN_MAX = 0.
|
|
33017
|
-
var NCD_CV_AI_MAX = 0.
|
|
33021
|
+
var GZIP_RATIO_AI_THRESHOLD = 0.6;
|
|
33022
|
+
var MEAN_LINE_NCD_HUMAN_MAX = 0.25;
|
|
33023
|
+
var NCD_CV_AI_MAX = 0.4;
|
|
33018
33024
|
var aiCompressionProfileRule = createRule({
|
|
33019
33025
|
id: "ai/compression-profile",
|
|
33020
33026
|
category: "ai",
|
|
@@ -33169,6 +33175,13 @@ var aiDefaultReactStackRule = createRule({
|
|
|
33169
33175
|
category: "ai",
|
|
33170
33176
|
severity: "low",
|
|
33171
33177
|
aiSpecific: true,
|
|
33178
|
+
// v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.001
|
|
33179
|
+
// (fires 1 time per 1,000 files). The "≥3 of default stack"
|
|
33180
|
+
// threshold catches almost nothing on real codebases — the
|
|
33181
|
+
// default stack is now ubiquitous, so the signal is near-zero.
|
|
33182
|
+
// Disable until the threshold is lowered or the rule is split
|
|
33183
|
+
// into per-library detectors.
|
|
33184
|
+
defaultOff: true,
|
|
33172
33185
|
description: "File uses \u22653 of the default React stack (Next.js + TanStack Query + Zustand + shadcn/ui) \u2014 Sascha 2025 + Nam et al. MSR 2026",
|
|
33173
33186
|
create(context) {
|
|
33174
33187
|
return context;
|
|
@@ -33421,6 +33434,10 @@ var aiLibraryReinventionRule = createRule({
|
|
|
33421
33434
|
category: "ai",
|
|
33422
33435
|
severity: "medium",
|
|
33423
33436
|
aiSpecific: true,
|
|
33437
|
+
// v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
|
|
33438
|
+
// (fires never on the corpus). The "≥2 reinvented patterns"
|
|
33439
|
+
// threshold is too strict. Disable until rewritten.
|
|
33440
|
+
defaultOff: true,
|
|
33424
33441
|
description: "File reinvents \u22652 common patterns (date picker, form validation, chart, modal, etc.) without importing the canonical library \u2014 GitClear 2025 + Cui et al. 2025",
|
|
33425
33442
|
create(context) {
|
|
33426
33443
|
return context;
|
|
@@ -33607,6 +33624,12 @@ var aiMarkdownLeakageRule = createRule({
|
|
|
33607
33624
|
category: "ai",
|
|
33608
33625
|
severity: "high",
|
|
33609
33626
|
aiSpecific: true,
|
|
33627
|
+
// v0.20.0 calibration: v8.5 verdict = INVERTED (lift 65,504× but
|
|
33628
|
+
// recall 0.000 — fires never on the corpus, and when it does fire
|
|
33629
|
+
// it's wrong). The rule was on by default, contributing false
|
|
33630
|
+
// positives to the self-scan. Disable until the rule is rewritten
|
|
33631
|
+
// or the v9 corpus shows it works.
|
|
33632
|
+
defaultOff: true,
|
|
33610
33633
|
description: "Stray Markdown fence or bare language name at top of file \u2014 AI tools leak the fenced-block format into code",
|
|
33611
33634
|
create(context) {
|
|
33612
33635
|
return context;
|
|
@@ -33833,6 +33856,16 @@ var aiSegmentSurprisalCvRule = createRule({
|
|
|
33833
33856
|
category: "ai",
|
|
33834
33857
|
severity: "medium",
|
|
33835
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,
|
|
33836
33869
|
description: "Per-segment cross-entropy CV is suspiciously low \u2014 Binoculars (Hans 2024): AI text has near-constant per-window entropy",
|
|
33837
33870
|
create(context) {
|
|
33838
33871
|
return context;
|
|
@@ -34032,6 +34065,11 @@ var aiTextLikeRatioRule = createRule({
|
|
|
34032
34065
|
category: "ai",
|
|
34033
34066
|
severity: "low",
|
|
34034
34067
|
aiSpecific: true,
|
|
34068
|
+
// v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
|
|
34069
|
+
// (fires never on the corpus). The rule is too specific to ever
|
|
34070
|
+
// fire on real code — the prose-detection heuristic is too
|
|
34071
|
+
// conservative. Disable until rewritten.
|
|
34072
|
+
defaultOff: true,
|
|
34035
34073
|
description: "High ratio of prose-like lines embedded in code \u2014 AI tools often leave natural-language explanations in source files (Yotkova 2026)",
|
|
34036
34074
|
create(context) {
|
|
34037
34075
|
return context;
|
|
@@ -34968,6 +35006,7 @@ var unusedImportRule = createRule({
|
|
|
34968
35006
|
if (binding.kind !== "import-specifier" && binding.kind !== "import-default" && binding.kind !== "import-namespace") {
|
|
34969
35007
|
continue;
|
|
34970
35008
|
}
|
|
35009
|
+
if (binding.isTypeOnly) continue;
|
|
34971
35010
|
if (binding.isReferenced) continue;
|
|
34972
35011
|
if (!binding.name) continue;
|
|
34973
35012
|
const source = binding.source ? ` from '${binding.source}'` : "";
|
|
@@ -35041,6 +35080,9 @@ var unusedLocalRule = createRule({
|
|
|
35041
35080
|
if (binding.isReferenced) continue;
|
|
35042
35081
|
if (SKIP_NAMES.has(binding.name)) continue;
|
|
35043
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
|
+
}
|
|
35044
35086
|
issues.push({
|
|
35045
35087
|
ruleId: "dead/unused-local",
|
|
35046
35088
|
category: "logic",
|
|
@@ -35708,7 +35750,7 @@ function collectExports(cwd) {
|
|
|
35708
35750
|
/\bexport\s+default\s+(?:function\s+|class\s+)?([A-Za-z_$][\w$]*)/g,
|
|
35709
35751
|
// v0.18.6: also collect field names from `export interface` and
|
|
35710
35752
|
// `export type` declarations. Without this, fields like
|
|
35711
|
-
// `crossFileDrift`, `
|
|
35753
|
+
// `crossFileDrift`, `aiSlopScore`, `engineeringHygiene` are
|
|
35712
35754
|
// flagged as stale even though they're valid type fields.
|
|
35713
35755
|
/^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*[?:]/gm
|
|
35714
35756
|
]) {
|
|
@@ -36037,7 +36079,7 @@ var brokenLinkRule = createRule({
|
|
|
36037
36079
|
|
|
36038
36080
|
// src/rules/dup/identical-block.ts
|
|
36039
36081
|
var crypto = __toESM(require("crypto"), 1);
|
|
36040
|
-
var WINDOW_SIZE =
|
|
36082
|
+
var WINDOW_SIZE = 20;
|
|
36041
36083
|
var MIN_NORMALIZED_LENGTH = 40;
|
|
36042
36084
|
var HASH_PREFIX_LENGTH = 16;
|
|
36043
36085
|
var DEDUP_CACHE = /* @__PURE__ */ new Map();
|
|
@@ -36499,6 +36541,9 @@ var javaSystemOutPrintlnRule = createRule({
|
|
|
36499
36541
|
var GAP_RE = /\bgap(?:-x|-y)?-(\d+)\b/g;
|
|
36500
36542
|
var gapMonopolyRule = createRule({
|
|
36501
36543
|
id: "layout/gap-monopoly",
|
|
36544
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
36545
|
+
// until rewritten.
|
|
36546
|
+
defaultOff: true,
|
|
36502
36547
|
category: "layout",
|
|
36503
36548
|
severity: "medium",
|
|
36504
36549
|
aiSpecific: false,
|
|
@@ -36595,6 +36640,9 @@ var mathElementUniformityRule = createRule({
|
|
|
36595
36640
|
var GRID_COLS_RE = /\bgrid-cols-(\d+)\b/g;
|
|
36596
36641
|
var mathGridUniformityRule = createRule({
|
|
36597
36642
|
id: "layout/math-grid-uniformity",
|
|
36643
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
36644
|
+
// until rewritten.
|
|
36645
|
+
defaultOff: true,
|
|
36598
36646
|
category: "layout",
|
|
36599
36647
|
severity: "high",
|
|
36600
36648
|
aiSpecific: true,
|
|
@@ -37046,6 +37094,9 @@ function checkInlineValue(value, scale) {
|
|
|
37046
37094
|
}
|
|
37047
37095
|
var spacingGridRule = createRule({
|
|
37048
37096
|
id: "layout/spacing-grid",
|
|
37097
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
37098
|
+
// until rewritten.
|
|
37099
|
+
defaultOff: true,
|
|
37049
37100
|
category: "layout",
|
|
37050
37101
|
severity: "medium",
|
|
37051
37102
|
aiSpecific: false,
|
|
@@ -37155,15 +37206,9 @@ var boundaryViolationRule = createRule({
|
|
|
37155
37206
|
if (context.supportsRsc === false) return [];
|
|
37156
37207
|
if (!facts.v2) return [];
|
|
37157
37208
|
const issues = [];
|
|
37158
|
-
const hookLines = /* @__PURE__ */ new Map();
|
|
37159
|
-
for (const h of facts.v2.logic.hooks) {
|
|
37160
|
-
const arr = hookLines.get(h.name) ?? [];
|
|
37161
|
-
arr.push({ line: h.line, column: h.column });
|
|
37162
|
-
hookLines.set(h.name, arr);
|
|
37163
|
-
}
|
|
37164
37209
|
for (const component of facts.v2.components) {
|
|
37165
37210
|
if (!component.isServerComponent) continue;
|
|
37166
|
-
for (const hook of
|
|
37211
|
+
for (const hook of component.hookCalls) {
|
|
37167
37212
|
if (!context.clientHooks.has(hook.name)) continue;
|
|
37168
37213
|
issues.push({
|
|
37169
37214
|
ruleId: "logic/boundary-violation",
|
|
@@ -37171,8 +37216,8 @@ var boundaryViolationRule = createRule({
|
|
|
37171
37216
|
severity: "high",
|
|
37172
37217
|
aiSpecific: true,
|
|
37173
37218
|
message: `'${hook.name}' only works in interactive client components. This component runs on the server.`,
|
|
37174
|
-
line:
|
|
37175
|
-
column:
|
|
37219
|
+
line: hook.line,
|
|
37220
|
+
column: hook.column,
|
|
37176
37221
|
advice: "Add the 'use client' directive at the top of this file, or move the stateful part to a separate client component.",
|
|
37177
37222
|
fix: {
|
|
37178
37223
|
kind: "insert",
|
|
@@ -39956,7 +40001,12 @@ var WEAK_MATCHERS = /* @__PURE__ */ new Set([
|
|
|
39956
40001
|
"toBeUndefined",
|
|
39957
40002
|
"toBeTruthy",
|
|
39958
40003
|
"toBeFalsy",
|
|
39959
|
-
"toBeNull"
|
|
40004
|
+
"toBeNull",
|
|
40005
|
+
"toBeGreaterThan",
|
|
40006
|
+
"toBeLessThan",
|
|
40007
|
+
"toContain",
|
|
40008
|
+
"toHaveLength",
|
|
40009
|
+
"toBeInstanceOf"
|
|
39960
40010
|
]);
|
|
39961
40011
|
function isTautologicalAssertion(hit) {
|
|
39962
40012
|
if (hit.matcherArg && hit.expectArg && hit.expectArg === hit.matcherArg) return true;
|
|
@@ -40797,6 +40847,10 @@ function rgbToHue([r, g, b]) {
|
|
|
40797
40847
|
}
|
|
40798
40848
|
var mathColorClusterRule = createRule({
|
|
40799
40849
|
id: "visual/math-color-cluster",
|
|
40850
|
+
// v0.20.0 calibration: recall 0.000, fires never. The
|
|
40851
|
+
// "math-color cluster" heuristic is too specific to ever fire
|
|
40852
|
+
// on real code. Disable until rewritten.
|
|
40853
|
+
defaultOff: true,
|
|
40800
40854
|
category: "visual",
|
|
40801
40855
|
severity: "high",
|
|
40802
40856
|
aiSpecific: true,
|
|
@@ -40851,6 +40905,9 @@ var FONT_IMPORT_HINT_RE = /next\/font|@import\s+url|fonts\.googleapis|fonts\.gst
|
|
|
40851
40905
|
var NEXT_FONT_IMPORT_RE = /import\s+.*from\s+['"]next\/font\/(?:google|local)['"]/;
|
|
40852
40906
|
var mathDefaultFontRule = createRule({
|
|
40853
40907
|
id: "visual/math-default-font",
|
|
40908
|
+
// v0.20.0 calibration: recall 0.001, fires 1 per 1,000 files.
|
|
40909
|
+
// Too specific to be useful. Disable until rewritten.
|
|
40910
|
+
defaultOff: true,
|
|
40854
40911
|
category: "visual",
|
|
40855
40912
|
severity: "high",
|
|
40856
40913
|
aiSpecific: true,
|
|
@@ -40917,6 +40974,9 @@ var TEXT_SIZE_MAP = {
|
|
|
40917
40974
|
var TEXT_SIZE_RE = /\btext-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl)\b/g;
|
|
40918
40975
|
var mathFontEntropyRule = createRule({
|
|
40919
40976
|
id: "visual/math-font-entropy",
|
|
40977
|
+
// v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
|
|
40978
|
+
// (verified). Disable until rewritten.
|
|
40979
|
+
defaultOff: true,
|
|
40920
40980
|
category: "visual",
|
|
40921
40981
|
severity: "high",
|
|
40922
40982
|
aiSpecific: true,
|
|
@@ -41039,6 +41099,10 @@ var ROUNDED_MAP = {
|
|
|
41039
41099
|
var ROUNDED_RE = /\brounded(?:-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?\b/g;
|
|
41040
41100
|
var mathRoundedEntropyRule = createRule({
|
|
41041
41101
|
id: "visual/math-rounded-entropy",
|
|
41102
|
+
// v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
|
|
41103
|
+
// (verified). The entropy heuristic is too specific to ever fire
|
|
41104
|
+
// on real code. Disable until rewritten.
|
|
41105
|
+
defaultOff: true,
|
|
41042
41106
|
category: "visual",
|
|
41043
41107
|
severity: "high",
|
|
41044
41108
|
aiSpecific: true,
|
|
@@ -41079,6 +41143,9 @@ var mathRoundedEntropyRule = createRule({
|
|
|
41079
41143
|
var SPACING_PREFIX_RE2 = /\b(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|gap-x|gap-y)-(\d+)\b/g;
|
|
41080
41144
|
var mathSpacingEntropyRule = createRule({
|
|
41081
41145
|
id: "visual/math-spacing-entropy",
|
|
41146
|
+
// v0.20.0 calibration: recall 0.002, fires 0 times on self-scan
|
|
41147
|
+
// (verified). Disable until rewritten.
|
|
41148
|
+
defaultOff: true,
|
|
41082
41149
|
category: "visual",
|
|
41083
41150
|
severity: "medium",
|
|
41084
41151
|
aiSpecific: true,
|
|
@@ -41116,7 +41183,7 @@ var mathSpacingEntropyRule = createRule({
|
|
|
41116
41183
|
|
|
41117
41184
|
// src/rules/visual/naturalness-anomaly.ts
|
|
41118
41185
|
var MIN_LENGTH = 50;
|
|
41119
|
-
var DISTINCT_RATIO_FLOOR = 0.
|
|
41186
|
+
var DISTINCT_RATIO_FLOOR = 0.2;
|
|
41120
41187
|
var naturalnessAnomalyRule = createRule({
|
|
41121
41188
|
id: "visual/naturalness-anomaly",
|
|
41122
41189
|
category: "visual",
|
|
@@ -41161,6 +41228,9 @@ var naturalnessAnomalyRule = createRule({
|
|
|
41161
41228
|
var SCALE_TOLERANCE = 1e-3;
|
|
41162
41229
|
var radiusScaleViolationRule = createRule({
|
|
41163
41230
|
id: "visual/radius-scale-violation",
|
|
41231
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
41232
|
+
// until rewritten.
|
|
41233
|
+
defaultOff: true,
|
|
41164
41234
|
category: "visual",
|
|
41165
41235
|
severity: "medium",
|
|
41166
41236
|
aiSpecific: false,
|
|
@@ -41345,6 +41415,10 @@ var ALT_HANDLER_RE = /^(?:onClick|onPointerDown|onKey(?:Down|Up|Press))$/;
|
|
|
41345
41415
|
var ALT_ROLE_RE = /^(?:button|application|tab|menuitem)$/;
|
|
41346
41416
|
var draggingMovementsRule = createRule({
|
|
41347
41417
|
id: "wcag/dragging-movements",
|
|
41418
|
+
// v0.20.0 calibration: v8.5 = DORMANT but defaultOff: false
|
|
41419
|
+
// was a bug — DORMANT rules should be off by default. The
|
|
41420
|
+
// rule was contributing 0 useful fires. Disable.
|
|
41421
|
+
defaultOff: true,
|
|
41348
41422
|
category: "wcag",
|
|
41349
41423
|
severity: "medium",
|
|
41350
41424
|
aiSpecific: false,
|
|
@@ -41491,6 +41565,11 @@ function scanForMissingAlt(source) {
|
|
|
41491
41565
|
}
|
|
41492
41566
|
var missingAltRule = createRule({
|
|
41493
41567
|
id: "wcag/missing-alt",
|
|
41568
|
+
// v0.20.0 calibration: lift 1.0, recall 0.000. Lift of 1.0
|
|
41569
|
+
// means the rule has zero discriminative power — it fires
|
|
41570
|
+
// equally on positive and negative examples. Dead rule.
|
|
41571
|
+
// Disable until rewritten.
|
|
41572
|
+
defaultOff: true,
|
|
41494
41573
|
category: "wcag",
|
|
41495
41574
|
severity: "medium",
|
|
41496
41575
|
aiSpecific: false,
|
|
@@ -41958,7 +42037,8 @@ var signal_strength_default = {
|
|
|
41958
42037
|
precision: 0.6803,
|
|
41959
42038
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
41960
42039
|
verdict: "USEFUL",
|
|
41961
|
-
|
|
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.",
|
|
41962
42042
|
aiSpecific: true,
|
|
41963
42043
|
_v7Verdict: "USEFUL",
|
|
41964
42044
|
_v7Lift: 11.29,
|
|
@@ -42061,7 +42141,8 @@ var signal_strength_default = {
|
|
|
42061
42141
|
precision: 0.7495,
|
|
42062
42142
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42063
42143
|
verdict: "USEFUL",
|
|
42064
|
-
|
|
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.",
|
|
42065
42146
|
aiSpecific: true,
|
|
42066
42147
|
_v7Verdict: "USEFUL",
|
|
42067
42148
|
_v7Lift: 9.11,
|