slopbrick 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/engine/worker.cjs +76 -17
- package/dist/engine/worker.js +76 -17
- package/dist/index.cjs +289 -94
- package/dist/index.d.cts +29 -11
- package/dist/index.d.ts +29 -11
- package/dist/index.js +289 -94
- package/package.json +1 -1
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.20.0 (current). See the [CHANGELOG](./CHANGELOG.md) for
|
|
50
50
|
the full release notes.
|
|
51
51
|
|
|
52
52
|
---
|
|
@@ -76,7 +76,7 @@ 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.20.0+)
|
|
80
80
|
|
|
81
81
|
> **v0.15.0 introduced the 4-score model; v0.16.0 + v0.17.0 completed it.**
|
|
82
82
|
> The single `Slop Index` is replaced by **4 independent scores**
|
|
@@ -87,7 +87,7 @@ For every other config question, see [`EXAMPLES.md`](./EXAMPLES.md).
|
|
|
87
87
|
|
|
88
88
|
| Score | What it measures | CI gate? |
|
|
89
89
|
|-------|------------------|----------|
|
|
90
|
-
| **`
|
|
90
|
+
| **`aiSlopScore`** | AI-slop signatures (16 `ai/*` rules). Raw amount of slop (0=clean, 100=saturated). | **Yes** (≤ `meanSlop: 30` passes) |
|
|
91
91
|
| **`engineeringHygiene`** | Average of 6 category scores: arch, logic, layout, visual, component, test | No (informational) |
|
|
92
92
|
| **`security`** | AI Security Risk band: low=100, medium=67, high=33, critical=0 | No (informational) |
|
|
93
93
|
| **`repositoryHealth`** (composite) | Weighted: 0.4×aiQ + 0.3×eng + 0.2×sec + 0.1×test | No (informational) |
|
|
@@ -108,13 +108,13 @@ For per-rule precision/recall/FPR (auditable), see
|
|
|
108
108
|
$ npx slopbrick scan
|
|
109
109
|
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
110
|
|
|
111
|
-
AI
|
|
111
|
+
AI Slop Score: 25 / 100 [HIGH] ↑5 (worse)
|
|
112
112
|
higher = better · measures AI-slop signatures. The same number in .slopbrick/health.json.
|
|
113
113
|
├─ boundary: 10 (40%) — structural integrity
|
|
114
114
|
├─ context: 50 (35%) — props / state / imports
|
|
115
115
|
└─ visual: 5 (25%) — CSS / a11y / layout
|
|
116
116
|
|
|
117
|
-
Engineering Hygiene: 60 / 100 [NEEDS WORK] — higher = better · measures internal consistency. This is a secondary view; the AI
|
|
117
|
+
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
118
|
|
|
119
119
|
Other signals (not the gate):
|
|
120
120
|
Code Hygiene 75/100
|
package/dist/engine/worker.cjs
CHANGED
|
@@ -32882,8 +32882,8 @@ function getCorpusBaselines() {
|
|
|
32882
32882
|
|
|
32883
32883
|
// src/rules/ai/comment-ratio.ts
|
|
32884
32884
|
var MIN_FILE_LINES = 20;
|
|
32885
|
-
var FALLBACK_LOW = 0.
|
|
32886
|
-
var FALLBACK_HIGH = 0.
|
|
32885
|
+
var FALLBACK_LOW = 0.02;
|
|
32886
|
+
var FALLBACK_HIGH = 0.55;
|
|
32887
32887
|
var LINE_COMMENT_RE = /^\s*(?:\/\/|#|--|;|%)/;
|
|
32888
32888
|
var BLOCK_COMMENT_OPEN_RE = /\/\*/;
|
|
32889
32889
|
var aiCommentRatioRule = createRule({
|
|
@@ -33012,9 +33012,9 @@ function std(xs) {
|
|
|
33012
33012
|
|
|
33013
33013
|
// src/rules/ai/compression-profile.ts
|
|
33014
33014
|
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.
|
|
33015
|
+
var GZIP_RATIO_AI_THRESHOLD = 0.6;
|
|
33016
|
+
var MEAN_LINE_NCD_HUMAN_MAX = 0.25;
|
|
33017
|
+
var NCD_CV_AI_MAX = 0.4;
|
|
33018
33018
|
var aiCompressionProfileRule = createRule({
|
|
33019
33019
|
id: "ai/compression-profile",
|
|
33020
33020
|
category: "ai",
|
|
@@ -33169,6 +33169,13 @@ var aiDefaultReactStackRule = createRule({
|
|
|
33169
33169
|
category: "ai",
|
|
33170
33170
|
severity: "low",
|
|
33171
33171
|
aiSpecific: true,
|
|
33172
|
+
// v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.001
|
|
33173
|
+
// (fires 1 time per 1,000 files). The "≥3 of default stack"
|
|
33174
|
+
// threshold catches almost nothing on real codebases — the
|
|
33175
|
+
// default stack is now ubiquitous, so the signal is near-zero.
|
|
33176
|
+
// Disable until the threshold is lowered or the rule is split
|
|
33177
|
+
// into per-library detectors.
|
|
33178
|
+
defaultOff: true,
|
|
33172
33179
|
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
33180
|
create(context) {
|
|
33174
33181
|
return context;
|
|
@@ -33421,6 +33428,10 @@ var aiLibraryReinventionRule = createRule({
|
|
|
33421
33428
|
category: "ai",
|
|
33422
33429
|
severity: "medium",
|
|
33423
33430
|
aiSpecific: true,
|
|
33431
|
+
// v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
|
|
33432
|
+
// (fires never on the corpus). The "≥2 reinvented patterns"
|
|
33433
|
+
// threshold is too strict. Disable until rewritten.
|
|
33434
|
+
defaultOff: true,
|
|
33424
33435
|
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
33436
|
create(context) {
|
|
33426
33437
|
return context;
|
|
@@ -33607,6 +33618,12 @@ var aiMarkdownLeakageRule = createRule({
|
|
|
33607
33618
|
category: "ai",
|
|
33608
33619
|
severity: "high",
|
|
33609
33620
|
aiSpecific: true,
|
|
33621
|
+
// v0.20.0 calibration: v8.5 verdict = INVERTED (lift 65,504× but
|
|
33622
|
+
// recall 0.000 — fires never on the corpus, and when it does fire
|
|
33623
|
+
// it's wrong). The rule was on by default, contributing false
|
|
33624
|
+
// positives to the self-scan. Disable until the rule is rewritten
|
|
33625
|
+
// or the v9 corpus shows it works.
|
|
33626
|
+
defaultOff: true,
|
|
33610
33627
|
description: "Stray Markdown fence or bare language name at top of file \u2014 AI tools leak the fenced-block format into code",
|
|
33611
33628
|
create(context) {
|
|
33612
33629
|
return context;
|
|
@@ -34032,6 +34049,11 @@ var aiTextLikeRatioRule = createRule({
|
|
|
34032
34049
|
category: "ai",
|
|
34033
34050
|
severity: "low",
|
|
34034
34051
|
aiSpecific: true,
|
|
34052
|
+
// v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
|
|
34053
|
+
// (fires never on the corpus). The rule is too specific to ever
|
|
34054
|
+
// fire on real code — the prose-detection heuristic is too
|
|
34055
|
+
// conservative. Disable until rewritten.
|
|
34056
|
+
defaultOff: true,
|
|
34035
34057
|
description: "High ratio of prose-like lines embedded in code \u2014 AI tools often leave natural-language explanations in source files (Yotkova 2026)",
|
|
34036
34058
|
create(context) {
|
|
34037
34059
|
return context;
|
|
@@ -35708,7 +35730,7 @@ function collectExports(cwd) {
|
|
|
35708
35730
|
/\bexport\s+default\s+(?:function\s+|class\s+)?([A-Za-z_$][\w$]*)/g,
|
|
35709
35731
|
// v0.18.6: also collect field names from `export interface` and
|
|
35710
35732
|
// `export type` declarations. Without this, fields like
|
|
35711
|
-
// `crossFileDrift`, `
|
|
35733
|
+
// `crossFileDrift`, `aiSlopScore`, `engineeringHygiene` are
|
|
35712
35734
|
// flagged as stale even though they're valid type fields.
|
|
35713
35735
|
/^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*[?:]/gm
|
|
35714
35736
|
]) {
|
|
@@ -36499,6 +36521,9 @@ var javaSystemOutPrintlnRule = createRule({
|
|
|
36499
36521
|
var GAP_RE = /\bgap(?:-x|-y)?-(\d+)\b/g;
|
|
36500
36522
|
var gapMonopolyRule = createRule({
|
|
36501
36523
|
id: "layout/gap-monopoly",
|
|
36524
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
36525
|
+
// until rewritten.
|
|
36526
|
+
defaultOff: true,
|
|
36502
36527
|
category: "layout",
|
|
36503
36528
|
severity: "medium",
|
|
36504
36529
|
aiSpecific: false,
|
|
@@ -36595,6 +36620,9 @@ var mathElementUniformityRule = createRule({
|
|
|
36595
36620
|
var GRID_COLS_RE = /\bgrid-cols-(\d+)\b/g;
|
|
36596
36621
|
var mathGridUniformityRule = createRule({
|
|
36597
36622
|
id: "layout/math-grid-uniformity",
|
|
36623
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
36624
|
+
// until rewritten.
|
|
36625
|
+
defaultOff: true,
|
|
36598
36626
|
category: "layout",
|
|
36599
36627
|
severity: "high",
|
|
36600
36628
|
aiSpecific: true,
|
|
@@ -37046,6 +37074,9 @@ function checkInlineValue(value, scale) {
|
|
|
37046
37074
|
}
|
|
37047
37075
|
var spacingGridRule = createRule({
|
|
37048
37076
|
id: "layout/spacing-grid",
|
|
37077
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
37078
|
+
// until rewritten.
|
|
37079
|
+
defaultOff: true,
|
|
37049
37080
|
category: "layout",
|
|
37050
37081
|
severity: "medium",
|
|
37051
37082
|
aiSpecific: false,
|
|
@@ -37155,15 +37186,9 @@ var boundaryViolationRule = createRule({
|
|
|
37155
37186
|
if (context.supportsRsc === false) return [];
|
|
37156
37187
|
if (!facts.v2) return [];
|
|
37157
37188
|
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
37189
|
for (const component of facts.v2.components) {
|
|
37165
37190
|
if (!component.isServerComponent) continue;
|
|
37166
|
-
for (const hook of
|
|
37191
|
+
for (const hook of component.hookCalls) {
|
|
37167
37192
|
if (!context.clientHooks.has(hook.name)) continue;
|
|
37168
37193
|
issues.push({
|
|
37169
37194
|
ruleId: "logic/boundary-violation",
|
|
@@ -37171,8 +37196,8 @@ var boundaryViolationRule = createRule({
|
|
|
37171
37196
|
severity: "high",
|
|
37172
37197
|
aiSpecific: true,
|
|
37173
37198
|
message: `'${hook.name}' only works in interactive client components. This component runs on the server.`,
|
|
37174
|
-
line:
|
|
37175
|
-
column:
|
|
37199
|
+
line: hook.line,
|
|
37200
|
+
column: hook.column,
|
|
37176
37201
|
advice: "Add the 'use client' directive at the top of this file, or move the stateful part to a separate client component.",
|
|
37177
37202
|
fix: {
|
|
37178
37203
|
kind: "insert",
|
|
@@ -39956,7 +39981,12 @@ var WEAK_MATCHERS = /* @__PURE__ */ new Set([
|
|
|
39956
39981
|
"toBeUndefined",
|
|
39957
39982
|
"toBeTruthy",
|
|
39958
39983
|
"toBeFalsy",
|
|
39959
|
-
"toBeNull"
|
|
39984
|
+
"toBeNull",
|
|
39985
|
+
"toBeGreaterThan",
|
|
39986
|
+
"toBeLessThan",
|
|
39987
|
+
"toContain",
|
|
39988
|
+
"toHaveLength",
|
|
39989
|
+
"toBeInstanceOf"
|
|
39960
39990
|
]);
|
|
39961
39991
|
function isTautologicalAssertion(hit) {
|
|
39962
39992
|
if (hit.matcherArg && hit.expectArg && hit.expectArg === hit.matcherArg) return true;
|
|
@@ -40797,6 +40827,10 @@ function rgbToHue([r, g, b]) {
|
|
|
40797
40827
|
}
|
|
40798
40828
|
var mathColorClusterRule = createRule({
|
|
40799
40829
|
id: "visual/math-color-cluster",
|
|
40830
|
+
// v0.20.0 calibration: recall 0.000, fires never. The
|
|
40831
|
+
// "math-color cluster" heuristic is too specific to ever fire
|
|
40832
|
+
// on real code. Disable until rewritten.
|
|
40833
|
+
defaultOff: true,
|
|
40800
40834
|
category: "visual",
|
|
40801
40835
|
severity: "high",
|
|
40802
40836
|
aiSpecific: true,
|
|
@@ -40851,6 +40885,9 @@ var FONT_IMPORT_HINT_RE = /next\/font|@import\s+url|fonts\.googleapis|fonts\.gst
|
|
|
40851
40885
|
var NEXT_FONT_IMPORT_RE = /import\s+.*from\s+['"]next\/font\/(?:google|local)['"]/;
|
|
40852
40886
|
var mathDefaultFontRule = createRule({
|
|
40853
40887
|
id: "visual/math-default-font",
|
|
40888
|
+
// v0.20.0 calibration: recall 0.001, fires 1 per 1,000 files.
|
|
40889
|
+
// Too specific to be useful. Disable until rewritten.
|
|
40890
|
+
defaultOff: true,
|
|
40854
40891
|
category: "visual",
|
|
40855
40892
|
severity: "high",
|
|
40856
40893
|
aiSpecific: true,
|
|
@@ -40917,6 +40954,9 @@ var TEXT_SIZE_MAP = {
|
|
|
40917
40954
|
var TEXT_SIZE_RE = /\btext-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl)\b/g;
|
|
40918
40955
|
var mathFontEntropyRule = createRule({
|
|
40919
40956
|
id: "visual/math-font-entropy",
|
|
40957
|
+
// v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
|
|
40958
|
+
// (verified). Disable until rewritten.
|
|
40959
|
+
defaultOff: true,
|
|
40920
40960
|
category: "visual",
|
|
40921
40961
|
severity: "high",
|
|
40922
40962
|
aiSpecific: true,
|
|
@@ -41039,6 +41079,10 @@ var ROUNDED_MAP = {
|
|
|
41039
41079
|
var ROUNDED_RE = /\brounded(?:-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?\b/g;
|
|
41040
41080
|
var mathRoundedEntropyRule = createRule({
|
|
41041
41081
|
id: "visual/math-rounded-entropy",
|
|
41082
|
+
// v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
|
|
41083
|
+
// (verified). The entropy heuristic is too specific to ever fire
|
|
41084
|
+
// on real code. Disable until rewritten.
|
|
41085
|
+
defaultOff: true,
|
|
41042
41086
|
category: "visual",
|
|
41043
41087
|
severity: "high",
|
|
41044
41088
|
aiSpecific: true,
|
|
@@ -41079,6 +41123,9 @@ var mathRoundedEntropyRule = createRule({
|
|
|
41079
41123
|
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
41124
|
var mathSpacingEntropyRule = createRule({
|
|
41081
41125
|
id: "visual/math-spacing-entropy",
|
|
41126
|
+
// v0.20.0 calibration: recall 0.002, fires 0 times on self-scan
|
|
41127
|
+
// (verified). Disable until rewritten.
|
|
41128
|
+
defaultOff: true,
|
|
41082
41129
|
category: "visual",
|
|
41083
41130
|
severity: "medium",
|
|
41084
41131
|
aiSpecific: true,
|
|
@@ -41116,7 +41163,7 @@ var mathSpacingEntropyRule = createRule({
|
|
|
41116
41163
|
|
|
41117
41164
|
// src/rules/visual/naturalness-anomaly.ts
|
|
41118
41165
|
var MIN_LENGTH = 50;
|
|
41119
|
-
var DISTINCT_RATIO_FLOOR = 0.
|
|
41166
|
+
var DISTINCT_RATIO_FLOOR = 0.2;
|
|
41120
41167
|
var naturalnessAnomalyRule = createRule({
|
|
41121
41168
|
id: "visual/naturalness-anomaly",
|
|
41122
41169
|
category: "visual",
|
|
@@ -41161,6 +41208,9 @@ var naturalnessAnomalyRule = createRule({
|
|
|
41161
41208
|
var SCALE_TOLERANCE = 1e-3;
|
|
41162
41209
|
var radiusScaleViolationRule = createRule({
|
|
41163
41210
|
id: "visual/radius-scale-violation",
|
|
41211
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
41212
|
+
// until rewritten.
|
|
41213
|
+
defaultOff: true,
|
|
41164
41214
|
category: "visual",
|
|
41165
41215
|
severity: "medium",
|
|
41166
41216
|
aiSpecific: false,
|
|
@@ -41345,6 +41395,10 @@ var ALT_HANDLER_RE = /^(?:onClick|onPointerDown|onKey(?:Down|Up|Press))$/;
|
|
|
41345
41395
|
var ALT_ROLE_RE = /^(?:button|application|tab|menuitem)$/;
|
|
41346
41396
|
var draggingMovementsRule = createRule({
|
|
41347
41397
|
id: "wcag/dragging-movements",
|
|
41398
|
+
// v0.20.0 calibration: v8.5 = DORMANT but defaultOff: false
|
|
41399
|
+
// was a bug — DORMANT rules should be off by default. The
|
|
41400
|
+
// rule was contributing 0 useful fires. Disable.
|
|
41401
|
+
defaultOff: true,
|
|
41348
41402
|
category: "wcag",
|
|
41349
41403
|
severity: "medium",
|
|
41350
41404
|
aiSpecific: false,
|
|
@@ -41491,6 +41545,11 @@ function scanForMissingAlt(source) {
|
|
|
41491
41545
|
}
|
|
41492
41546
|
var missingAltRule = createRule({
|
|
41493
41547
|
id: "wcag/missing-alt",
|
|
41548
|
+
// v0.20.0 calibration: lift 1.0, recall 0.000. Lift of 1.0
|
|
41549
|
+
// means the rule has zero discriminative power — it fires
|
|
41550
|
+
// equally on positive and negative examples. Dead rule.
|
|
41551
|
+
// Disable until rewritten.
|
|
41552
|
+
defaultOff: true,
|
|
41494
41553
|
category: "wcag",
|
|
41495
41554
|
severity: "medium",
|
|
41496
41555
|
aiSpecific: false,
|
package/dist/engine/worker.js
CHANGED
|
@@ -32853,8 +32853,8 @@ function getCorpusBaselines() {
|
|
|
32853
32853
|
|
|
32854
32854
|
// src/rules/ai/comment-ratio.ts
|
|
32855
32855
|
var MIN_FILE_LINES = 20;
|
|
32856
|
-
var FALLBACK_LOW = 0.
|
|
32857
|
-
var FALLBACK_HIGH = 0.
|
|
32856
|
+
var FALLBACK_LOW = 0.02;
|
|
32857
|
+
var FALLBACK_HIGH = 0.55;
|
|
32858
32858
|
var LINE_COMMENT_RE = /^\s*(?:\/\/|#|--|;|%)/;
|
|
32859
32859
|
var BLOCK_COMMENT_OPEN_RE = /\/\*/;
|
|
32860
32860
|
var aiCommentRatioRule = createRule({
|
|
@@ -32983,9 +32983,9 @@ function std(xs) {
|
|
|
32983
32983
|
|
|
32984
32984
|
// src/rules/ai/compression-profile.ts
|
|
32985
32985
|
var MIN_BYTES = 2e3;
|
|
32986
|
-
var GZIP_RATIO_AI_THRESHOLD = 0.
|
|
32987
|
-
var MEAN_LINE_NCD_HUMAN_MAX = 0.
|
|
32988
|
-
var NCD_CV_AI_MAX = 0.
|
|
32986
|
+
var GZIP_RATIO_AI_THRESHOLD = 0.6;
|
|
32987
|
+
var MEAN_LINE_NCD_HUMAN_MAX = 0.25;
|
|
32988
|
+
var NCD_CV_AI_MAX = 0.4;
|
|
32989
32989
|
var aiCompressionProfileRule = createRule({
|
|
32990
32990
|
id: "ai/compression-profile",
|
|
32991
32991
|
category: "ai",
|
|
@@ -33140,6 +33140,13 @@ var aiDefaultReactStackRule = createRule({
|
|
|
33140
33140
|
category: "ai",
|
|
33141
33141
|
severity: "low",
|
|
33142
33142
|
aiSpecific: true,
|
|
33143
|
+
// v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.001
|
|
33144
|
+
// (fires 1 time per 1,000 files). The "≥3 of default stack"
|
|
33145
|
+
// threshold catches almost nothing on real codebases — the
|
|
33146
|
+
// default stack is now ubiquitous, so the signal is near-zero.
|
|
33147
|
+
// Disable until the threshold is lowered or the rule is split
|
|
33148
|
+
// into per-library detectors.
|
|
33149
|
+
defaultOff: true,
|
|
33143
33150
|
description: "File uses \u22653 of the default React stack (Next.js + TanStack Query + Zustand + shadcn/ui) \u2014 Sascha 2025 + Nam et al. MSR 2026",
|
|
33144
33151
|
create(context) {
|
|
33145
33152
|
return context;
|
|
@@ -33392,6 +33399,10 @@ var aiLibraryReinventionRule = createRule({
|
|
|
33392
33399
|
category: "ai",
|
|
33393
33400
|
severity: "medium",
|
|
33394
33401
|
aiSpecific: true,
|
|
33402
|
+
// v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
|
|
33403
|
+
// (fires never on the corpus). The "≥2 reinvented patterns"
|
|
33404
|
+
// threshold is too strict. Disable until rewritten.
|
|
33405
|
+
defaultOff: true,
|
|
33395
33406
|
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",
|
|
33396
33407
|
create(context) {
|
|
33397
33408
|
return context;
|
|
@@ -33578,6 +33589,12 @@ var aiMarkdownLeakageRule = createRule({
|
|
|
33578
33589
|
category: "ai",
|
|
33579
33590
|
severity: "high",
|
|
33580
33591
|
aiSpecific: true,
|
|
33592
|
+
// v0.20.0 calibration: v8.5 verdict = INVERTED (lift 65,504× but
|
|
33593
|
+
// recall 0.000 — fires never on the corpus, and when it does fire
|
|
33594
|
+
// it's wrong). The rule was on by default, contributing false
|
|
33595
|
+
// positives to the self-scan. Disable until the rule is rewritten
|
|
33596
|
+
// or the v9 corpus shows it works.
|
|
33597
|
+
defaultOff: true,
|
|
33581
33598
|
description: "Stray Markdown fence or bare language name at top of file \u2014 AI tools leak the fenced-block format into code",
|
|
33582
33599
|
create(context) {
|
|
33583
33600
|
return context;
|
|
@@ -34003,6 +34020,11 @@ var aiTextLikeRatioRule = createRule({
|
|
|
34003
34020
|
category: "ai",
|
|
34004
34021
|
severity: "low",
|
|
34005
34022
|
aiSpecific: true,
|
|
34023
|
+
// v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
|
|
34024
|
+
// (fires never on the corpus). The rule is too specific to ever
|
|
34025
|
+
// fire on real code — the prose-detection heuristic is too
|
|
34026
|
+
// conservative. Disable until rewritten.
|
|
34027
|
+
defaultOff: true,
|
|
34006
34028
|
description: "High ratio of prose-like lines embedded in code \u2014 AI tools often leave natural-language explanations in source files (Yotkova 2026)",
|
|
34007
34029
|
create(context) {
|
|
34008
34030
|
return context;
|
|
@@ -35679,7 +35701,7 @@ function collectExports(cwd) {
|
|
|
35679
35701
|
/\bexport\s+default\s+(?:function\s+|class\s+)?([A-Za-z_$][\w$]*)/g,
|
|
35680
35702
|
// v0.18.6: also collect field names from `export interface` and
|
|
35681
35703
|
// `export type` declarations. Without this, fields like
|
|
35682
|
-
// `crossFileDrift`, `
|
|
35704
|
+
// `crossFileDrift`, `aiSlopScore`, `engineeringHygiene` are
|
|
35683
35705
|
// flagged as stale even though they're valid type fields.
|
|
35684
35706
|
/^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*[?:]/gm
|
|
35685
35707
|
]) {
|
|
@@ -36470,6 +36492,9 @@ var javaSystemOutPrintlnRule = createRule({
|
|
|
36470
36492
|
var GAP_RE = /\bgap(?:-x|-y)?-(\d+)\b/g;
|
|
36471
36493
|
var gapMonopolyRule = createRule({
|
|
36472
36494
|
id: "layout/gap-monopoly",
|
|
36495
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
36496
|
+
// until rewritten.
|
|
36497
|
+
defaultOff: true,
|
|
36473
36498
|
category: "layout",
|
|
36474
36499
|
severity: "medium",
|
|
36475
36500
|
aiSpecific: false,
|
|
@@ -36566,6 +36591,9 @@ var mathElementUniformityRule = createRule({
|
|
|
36566
36591
|
var GRID_COLS_RE = /\bgrid-cols-(\d+)\b/g;
|
|
36567
36592
|
var mathGridUniformityRule = createRule({
|
|
36568
36593
|
id: "layout/math-grid-uniformity",
|
|
36594
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
36595
|
+
// until rewritten.
|
|
36596
|
+
defaultOff: true,
|
|
36569
36597
|
category: "layout",
|
|
36570
36598
|
severity: "high",
|
|
36571
36599
|
aiSpecific: true,
|
|
@@ -37017,6 +37045,9 @@ function checkInlineValue(value, scale) {
|
|
|
37017
37045
|
}
|
|
37018
37046
|
var spacingGridRule = createRule({
|
|
37019
37047
|
id: "layout/spacing-grid",
|
|
37048
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
37049
|
+
// until rewritten.
|
|
37050
|
+
defaultOff: true,
|
|
37020
37051
|
category: "layout",
|
|
37021
37052
|
severity: "medium",
|
|
37022
37053
|
aiSpecific: false,
|
|
@@ -37126,15 +37157,9 @@ var boundaryViolationRule = createRule({
|
|
|
37126
37157
|
if (context.supportsRsc === false) return [];
|
|
37127
37158
|
if (!facts.v2) return [];
|
|
37128
37159
|
const issues = [];
|
|
37129
|
-
const hookLines = /* @__PURE__ */ new Map();
|
|
37130
|
-
for (const h of facts.v2.logic.hooks) {
|
|
37131
|
-
const arr = hookLines.get(h.name) ?? [];
|
|
37132
|
-
arr.push({ line: h.line, column: h.column });
|
|
37133
|
-
hookLines.set(h.name, arr);
|
|
37134
|
-
}
|
|
37135
37160
|
for (const component of facts.v2.components) {
|
|
37136
37161
|
if (!component.isServerComponent) continue;
|
|
37137
|
-
for (const hook of
|
|
37162
|
+
for (const hook of component.hookCalls) {
|
|
37138
37163
|
if (!context.clientHooks.has(hook.name)) continue;
|
|
37139
37164
|
issues.push({
|
|
37140
37165
|
ruleId: "logic/boundary-violation",
|
|
@@ -37142,8 +37167,8 @@ var boundaryViolationRule = createRule({
|
|
|
37142
37167
|
severity: "high",
|
|
37143
37168
|
aiSpecific: true,
|
|
37144
37169
|
message: `'${hook.name}' only works in interactive client components. This component runs on the server.`,
|
|
37145
|
-
line:
|
|
37146
|
-
column:
|
|
37170
|
+
line: hook.line,
|
|
37171
|
+
column: hook.column,
|
|
37147
37172
|
advice: "Add the 'use client' directive at the top of this file, or move the stateful part to a separate client component.",
|
|
37148
37173
|
fix: {
|
|
37149
37174
|
kind: "insert",
|
|
@@ -39927,7 +39952,12 @@ var WEAK_MATCHERS = /* @__PURE__ */ new Set([
|
|
|
39927
39952
|
"toBeUndefined",
|
|
39928
39953
|
"toBeTruthy",
|
|
39929
39954
|
"toBeFalsy",
|
|
39930
|
-
"toBeNull"
|
|
39955
|
+
"toBeNull",
|
|
39956
|
+
"toBeGreaterThan",
|
|
39957
|
+
"toBeLessThan",
|
|
39958
|
+
"toContain",
|
|
39959
|
+
"toHaveLength",
|
|
39960
|
+
"toBeInstanceOf"
|
|
39931
39961
|
]);
|
|
39932
39962
|
function isTautologicalAssertion(hit) {
|
|
39933
39963
|
if (hit.matcherArg && hit.expectArg && hit.expectArg === hit.matcherArg) return true;
|
|
@@ -40768,6 +40798,10 @@ function rgbToHue([r, g, b]) {
|
|
|
40768
40798
|
}
|
|
40769
40799
|
var mathColorClusterRule = createRule({
|
|
40770
40800
|
id: "visual/math-color-cluster",
|
|
40801
|
+
// v0.20.0 calibration: recall 0.000, fires never. The
|
|
40802
|
+
// "math-color cluster" heuristic is too specific to ever fire
|
|
40803
|
+
// on real code. Disable until rewritten.
|
|
40804
|
+
defaultOff: true,
|
|
40771
40805
|
category: "visual",
|
|
40772
40806
|
severity: "high",
|
|
40773
40807
|
aiSpecific: true,
|
|
@@ -40822,6 +40856,9 @@ var FONT_IMPORT_HINT_RE = /next\/font|@import\s+url|fonts\.googleapis|fonts\.gst
|
|
|
40822
40856
|
var NEXT_FONT_IMPORT_RE = /import\s+.*from\s+['"]next\/font\/(?:google|local)['"]/;
|
|
40823
40857
|
var mathDefaultFontRule = createRule({
|
|
40824
40858
|
id: "visual/math-default-font",
|
|
40859
|
+
// v0.20.0 calibration: recall 0.001, fires 1 per 1,000 files.
|
|
40860
|
+
// Too specific to be useful. Disable until rewritten.
|
|
40861
|
+
defaultOff: true,
|
|
40825
40862
|
category: "visual",
|
|
40826
40863
|
severity: "high",
|
|
40827
40864
|
aiSpecific: true,
|
|
@@ -40888,6 +40925,9 @@ var TEXT_SIZE_MAP = {
|
|
|
40888
40925
|
var TEXT_SIZE_RE = /\btext-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl)\b/g;
|
|
40889
40926
|
var mathFontEntropyRule = createRule({
|
|
40890
40927
|
id: "visual/math-font-entropy",
|
|
40928
|
+
// v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
|
|
40929
|
+
// (verified). Disable until rewritten.
|
|
40930
|
+
defaultOff: true,
|
|
40891
40931
|
category: "visual",
|
|
40892
40932
|
severity: "high",
|
|
40893
40933
|
aiSpecific: true,
|
|
@@ -41010,6 +41050,10 @@ var ROUNDED_MAP = {
|
|
|
41010
41050
|
var ROUNDED_RE = /\brounded(?:-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?\b/g;
|
|
41011
41051
|
var mathRoundedEntropyRule = createRule({
|
|
41012
41052
|
id: "visual/math-rounded-entropy",
|
|
41053
|
+
// v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
|
|
41054
|
+
// (verified). The entropy heuristic is too specific to ever fire
|
|
41055
|
+
// on real code. Disable until rewritten.
|
|
41056
|
+
defaultOff: true,
|
|
41013
41057
|
category: "visual",
|
|
41014
41058
|
severity: "high",
|
|
41015
41059
|
aiSpecific: true,
|
|
@@ -41050,6 +41094,9 @@ var mathRoundedEntropyRule = createRule({
|
|
|
41050
41094
|
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;
|
|
41051
41095
|
var mathSpacingEntropyRule = createRule({
|
|
41052
41096
|
id: "visual/math-spacing-entropy",
|
|
41097
|
+
// v0.20.0 calibration: recall 0.002, fires 0 times on self-scan
|
|
41098
|
+
// (verified). Disable until rewritten.
|
|
41099
|
+
defaultOff: true,
|
|
41053
41100
|
category: "visual",
|
|
41054
41101
|
severity: "medium",
|
|
41055
41102
|
aiSpecific: true,
|
|
@@ -41087,7 +41134,7 @@ var mathSpacingEntropyRule = createRule({
|
|
|
41087
41134
|
|
|
41088
41135
|
// src/rules/visual/naturalness-anomaly.ts
|
|
41089
41136
|
var MIN_LENGTH = 50;
|
|
41090
|
-
var DISTINCT_RATIO_FLOOR = 0.
|
|
41137
|
+
var DISTINCT_RATIO_FLOOR = 0.2;
|
|
41091
41138
|
var naturalnessAnomalyRule = createRule({
|
|
41092
41139
|
id: "visual/naturalness-anomaly",
|
|
41093
41140
|
category: "visual",
|
|
@@ -41132,6 +41179,9 @@ var naturalnessAnomalyRule = createRule({
|
|
|
41132
41179
|
var SCALE_TOLERANCE = 1e-3;
|
|
41133
41180
|
var radiusScaleViolationRule = createRule({
|
|
41134
41181
|
id: "visual/radius-scale-violation",
|
|
41182
|
+
// v0.20.0 calibration: recall 0.000, fires never. Disable
|
|
41183
|
+
// until rewritten.
|
|
41184
|
+
defaultOff: true,
|
|
41135
41185
|
category: "visual",
|
|
41136
41186
|
severity: "medium",
|
|
41137
41187
|
aiSpecific: false,
|
|
@@ -41316,6 +41366,10 @@ var ALT_HANDLER_RE = /^(?:onClick|onPointerDown|onKey(?:Down|Up|Press))$/;
|
|
|
41316
41366
|
var ALT_ROLE_RE = /^(?:button|application|tab|menuitem)$/;
|
|
41317
41367
|
var draggingMovementsRule = createRule({
|
|
41318
41368
|
id: "wcag/dragging-movements",
|
|
41369
|
+
// v0.20.0 calibration: v8.5 = DORMANT but defaultOff: false
|
|
41370
|
+
// was a bug — DORMANT rules should be off by default. The
|
|
41371
|
+
// rule was contributing 0 useful fires. Disable.
|
|
41372
|
+
defaultOff: true,
|
|
41319
41373
|
category: "wcag",
|
|
41320
41374
|
severity: "medium",
|
|
41321
41375
|
aiSpecific: false,
|
|
@@ -41462,6 +41516,11 @@ function scanForMissingAlt(source) {
|
|
|
41462
41516
|
}
|
|
41463
41517
|
var missingAltRule = createRule({
|
|
41464
41518
|
id: "wcag/missing-alt",
|
|
41519
|
+
// v0.20.0 calibration: lift 1.0, recall 0.000. Lift of 1.0
|
|
41520
|
+
// means the rule has zero discriminative power — it fires
|
|
41521
|
+
// equally on positive and negative examples. Dead rule.
|
|
41522
|
+
// Disable until rewritten.
|
|
41523
|
+
defaultOff: true,
|
|
41465
41524
|
category: "wcag",
|
|
41466
41525
|
severity: "medium",
|
|
41467
41526
|
aiSpecific: false,
|