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 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.17.0 (current). See the [CHANGELOG](./CHANGELOG.md) for
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.17.0+)
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
- | **`aiQuality`** | AI-slop signatures (16 `ai/*` rules). INVERTED from legacy Slop Index. | **Yes** ( 70 passes) |
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 Quality: 25 / 100 [CONCERNING] ↑5 (worse)
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 Quality above is the gate.
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
@@ -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.05;
32886
- var FALLBACK_HIGH = 0.45;
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.5;
33016
- var MEAN_LINE_NCD_HUMAN_MAX = 0.3;
33017
- var NCD_CV_AI_MAX = 0.5;
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`, `aiQuality`, `engineeringHygiene` are
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 facts.v2.logic.hooks) {
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: component.line,
37175
- column: component.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.3;
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,
@@ -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.05;
32857
- var FALLBACK_HIGH = 0.45;
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.5;
32987
- var MEAN_LINE_NCD_HUMAN_MAX = 0.3;
32988
- var NCD_CV_AI_MAX = 0.5;
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`, `aiQuality`, `engineeringHygiene` are
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 facts.v2.logic.hooks) {
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: component.line,
37146
- column: component.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.3;
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,