slopbrick 0.18.9 → 0.19.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.
@@ -5463,76 +5463,6 @@ function tokenizeIdentifiers(source) {
5463
5463
  return tokens;
5464
5464
  }
5465
5465
  var SQRT_2_TIMES_LN_2 = Math.sqrt(2 * Math.LN2);
5466
- function ecdfAt(sortedSamples, x) {
5467
- let lo = 0;
5468
- let hi = sortedSamples.length;
5469
- while (lo < hi) {
5470
- const mid = lo + hi >>> 1;
5471
- if (sortedSamples[mid] <= x) lo = mid + 1;
5472
- else hi = mid;
5473
- }
5474
- return lo / sortedSamples.length;
5475
- }
5476
- function ksStatistic(sampleA, sampleB) {
5477
- if (sampleA.length === 0 || sampleB.length === 0) return 1;
5478
- const sortedA = [...sampleA].sort((a, b) => a - b);
5479
- const sortedB = [...sampleB].sort((a, b) => a - b);
5480
- const allPoints = [...sortedA, ...sortedB].sort((a, b) => a - b);
5481
- let maxDiff = 0;
5482
- for (const x of allPoints) {
5483
- const fa = ecdfAt(sortedA, x);
5484
- const fb = ecdfAt(sortedB, x);
5485
- const diff = Math.abs(fa - fb);
5486
- if (diff > maxDiff) maxDiff = diff;
5487
- }
5488
- return maxDiff;
5489
- }
5490
- function ksPValue(statistic, n, m) {
5491
- if (n === 0 || m === 0) return 1;
5492
- if (statistic < 0) return 1;
5493
- if (statistic > 1) return 0;
5494
- const lambda = Math.sqrt(n * m / (n + m)) * statistic;
5495
- if (lambda > 3.6) return 0;
5496
- let p = 0;
5497
- for (let j = 1; j < 1e3; j++) {
5498
- const term = 2 * Math.pow(-1, j - 1) * Math.exp(-2 * j * j * lambda * lambda);
5499
- p += term;
5500
- if (Math.abs(term) < 1e-15) break;
5501
- }
5502
- return Math.max(0, Math.min(1, p));
5503
- }
5504
- function ksTest(sampleA, sampleB, alpha = 0.05) {
5505
- const statistic = ksStatistic(sampleA, sampleB);
5506
- const pValue = ksPValue(statistic, sampleA.length, sampleB.length);
5507
- return {
5508
- statistic,
5509
- pValue,
5510
- significant: pValue < alpha,
5511
- n: sampleA.length,
5512
- m: sampleB.length
5513
- };
5514
- }
5515
- function multiFeatureKsTest(features, baselines, alpha = 0.05) {
5516
- const featureNames = [...features.keys()];
5517
- const k = featureNames.length;
5518
- const bonferroniAlpha = k > 0 ? alpha / k : alpha;
5519
- const perFeature = /* @__PURE__ */ new Map();
5520
- const significantFeatures = [];
5521
- for (const name of featureNames) {
5522
- const sample = features.get(name);
5523
- const baseline = baselines.get(name);
5524
- if (!sample || !baseline) continue;
5525
- const result = ksTest(sample, baseline, bonferroniAlpha);
5526
- perFeature.set(name, result);
5527
- if (result.significant) significantFeatures.push(name);
5528
- }
5529
- return {
5530
- perFeature,
5531
- bonferroniAlpha,
5532
- anySignificant: significantFeatures.length > 0,
5533
- significantFeatures
5534
- };
5535
- }
5536
5466
 
5537
5467
  // src/engine/visitors/react.ts
5538
5468
  function isObject(node) {
@@ -36262,6 +36192,218 @@ var brokenLinkRule = createRule({
36262
36192
  }
36263
36193
  });
36264
36194
 
36195
+ // src/rules/dup/identical-block.ts
36196
+ var crypto = __toESM(require("crypto"), 1);
36197
+ var WINDOW_SIZE = 10;
36198
+ var MIN_NORMALIZED_LENGTH = 40;
36199
+ var HASH_PREFIX_LENGTH = 16;
36200
+ var DEDUP_CACHE = /* @__PURE__ */ new Map();
36201
+ function normalizeAndHash(lines) {
36202
+ const normalized = lines.map(
36203
+ (line) => line.replace(/\/\/.*$/, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim()
36204
+ ).filter((line) => line.length > 0).join("\n");
36205
+ if (normalized.length < MIN_NORMALIZED_LENGTH) return void 0;
36206
+ return crypto.createHash("sha1").update(normalized).digest("hex").slice(0, HASH_PREFIX_LENGTH);
36207
+ }
36208
+ var dupIdenticalBlockRule = createRule({
36209
+ id: "dup/identical-block",
36210
+ category: "logic",
36211
+ severity: "medium",
36212
+ aiSpecific: false,
36213
+ description: "Block of >=10 lines is identical across >=2 files (Type-1 clone detector)",
36214
+ create(_context) {
36215
+ return {};
36216
+ },
36217
+ analyze(_context, facts) {
36218
+ const issues = [];
36219
+ const source = facts.v2?._source;
36220
+ if (!source) return issues;
36221
+ const filePath = facts.filePath;
36222
+ const lines = source.split("\n");
36223
+ for (let i = 0; i <= lines.length - WINDOW_SIZE; i++) {
36224
+ const window = lines.slice(i, i + WINDOW_SIZE);
36225
+ const hash = normalizeAndHash(window);
36226
+ if (!hash) continue;
36227
+ const existing = DEDUP_CACHE.get(hash) ?? [];
36228
+ const matches = existing.filter((m) => m.file !== filePath);
36229
+ for (const match of matches) {
36230
+ issues.push({
36231
+ ruleId: "dup/identical-block",
36232
+ category: "logic",
36233
+ severity: "medium",
36234
+ aiSpecific: false,
36235
+ message: `Identical ${WINDOW_SIZE}-line block at line ${i + 1} also appears in ${match.file}:${match.line + 1}`,
36236
+ line: i + 1,
36237
+ column: 0,
36238
+ advice: "Refactor to a shared helper. This is a Type-1 clone (byte-for-byte identical after normalization). Common in AI-generated code that copy-pastes from training data.",
36239
+ extras: {
36240
+ duplicateOf: {
36241
+ file: match.file,
36242
+ line: match.line + 1,
36243
+ hash
36244
+ }
36245
+ }
36246
+ });
36247
+ }
36248
+ existing.push({ file: filePath, line: i });
36249
+ DEDUP_CACHE.set(hash, existing);
36250
+ }
36251
+ return issues;
36252
+ }
36253
+ });
36254
+
36255
+ // src/rules/go/error-wrap-without-context.ts
36256
+ var ERR_WRAP_REGEX = /fmt\.Errorf\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*([^)]+)\)/g;
36257
+ var GENERIC_PREFIX_REGEX = /^\s*(?:error|err|failed|wrap(?:ping)?|invalid)\s*(?::\s*)?%w\b/i;
36258
+ var goErrorWrapWithoutContextRule = createRule({
36259
+ id: "go/error-wrap-without-context",
36260
+ category: "typo",
36261
+ severity: "low",
36262
+ aiSpecific: true,
36263
+ description: 'fmt.Errorf wrap without operation context \u2014 AI defaults to generic "error: %w"',
36264
+ create(_context) {
36265
+ return {};
36266
+ },
36267
+ analyze(_context, facts) {
36268
+ const issues = [];
36269
+ const source = facts.v2?._source;
36270
+ if (!source) return issues;
36271
+ let match;
36272
+ ERR_WRAP_REGEX.lastIndex = 0;
36273
+ while ((match = ERR_WRAP_REGEX.exec(source)) !== null) {
36274
+ const formatString = match[1];
36275
+ if (!formatString.includes("%w")) continue;
36276
+ if (formatString.length >= 30) continue;
36277
+ if (!GENERIC_PREFIX_REGEX.test(formatString)) continue;
36278
+ const line = source.slice(0, match.index).split("\n").length;
36279
+ issues.push({
36280
+ ruleId: "go/error-wrap-without-context",
36281
+ category: "typo",
36282
+ severity: "low",
36283
+ aiSpecific: true,
36284
+ message: `fmt.Errorf wrap with generic message "${formatString}" \u2014 include the failing operation`,
36285
+ line,
36286
+ column: match[0].indexOf("fmt") + 1,
36287
+ advice: 'Real Go errors include the failing operation: `fmt.Errorf("opening config: %w", err)`. Generic messages ("error: %w", "failed: %w") tell the reader nothing about what failed. Reference: go/error-wrap-without-context v0.19. See: https://github.com/golang/go/wiki/CodeReviewComments#error-strings'
36288
+ });
36289
+ }
36290
+ return issues;
36291
+ }
36292
+ });
36293
+
36294
+ // src/rules/go/nil-slice-vs-empty.ts
36295
+ var NIL_SLICE_DECL_REGEX = /^[\t ]*var\s+([A-Za-z_][A-Za-z0-9_]*)\s+\[\][\w.*]+\b/gm;
36296
+ var EMPTY_SLICE_ASSIGN_REGEX = /^[\t ]*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:\[\][\w.*]*\{\}|make\(\[\][\w.*]*)/gm;
36297
+ var goNilSliceVsEmptyRule = createRule({
36298
+ id: "go/nil-slice-vs-empty",
36299
+ category: "typo",
36300
+ severity: "low",
36301
+ aiSpecific: true,
36302
+ description: "Variable declared `var x []int` but later assigned `x = []int{}` or `make([]int, n)` \u2014 pick one form",
36303
+ create(_context) {
36304
+ return {};
36305
+ },
36306
+ analyze(_context, facts) {
36307
+ const issues = [];
36308
+ const source = facts.v2?._source;
36309
+ if (!source) return issues;
36310
+ const nilDecls = /* @__PURE__ */ new Map();
36311
+ let m;
36312
+ NIL_SLICE_DECL_REGEX.lastIndex = 0;
36313
+ while ((m = NIL_SLICE_DECL_REGEX.exec(source)) !== null) {
36314
+ nilDecls.set(m[1], source.slice(0, m.index).split("\n").length);
36315
+ }
36316
+ if (nilDecls.size === 0) return issues;
36317
+ EMPTY_SLICE_ASSIGN_REGEX.lastIndex = 0;
36318
+ while ((m = EMPTY_SLICE_ASSIGN_REGEX.exec(source)) !== null) {
36319
+ const name = m[1];
36320
+ if (!nilDecls.has(name)) continue;
36321
+ const declLine = nilDecls.get(name);
36322
+ const assignLine = source.slice(0, m.index).split("\n").length;
36323
+ if (assignLine <= declLine) continue;
36324
+ issues.push({
36325
+ ruleId: "go/nil-slice-vs-empty",
36326
+ category: "typo",
36327
+ severity: "low",
36328
+ aiSpecific: true,
36329
+ message: `Variable '${name}' declared as nil slice (line ${declLine}) but assigned an empty slice (line ${assignLine}) \u2014 pick one form`,
36330
+ line: declLine,
36331
+ column: 1,
36332
+ advice: "Either declare as `var " + name + " = []int{}` or assign with `make([]int, 0)`. The nil/empty inconsistency is an AI signal \u2014 real code picks one form and sticks with it. Reference: go/nil-slice-vs-empty v0.19."
36333
+ });
36334
+ }
36335
+ return issues;
36336
+ }
36337
+ });
36338
+
36339
+ // src/rules/go/struct-tag-inconsistency.ts
36340
+ var JSON_TAG_REGEX = /`json:"([^",]+)(?:,([^"]+))?"`/g;
36341
+ var goStructTagInconsistencyRule = createRule({
36342
+ id: "go/struct-tag-inconsistency",
36343
+ category: "typo",
36344
+ severity: "low",
36345
+ aiSpecific: true,
36346
+ description: 'Struct fields mix json:"foo" and json:"foo,omitempty" \u2014 pick one convention per struct',
36347
+ create(_context) {
36348
+ return {};
36349
+ },
36350
+ analyze(_context, facts) {
36351
+ const issues = [];
36352
+ const source = facts.v2?._source;
36353
+ if (!source) return issues;
36354
+ const structRegex = /type\s+[A-Z][A-Za-z0-9_]*\s+struct\s*\{/g;
36355
+ let structMatch;
36356
+ while ((structMatch = structRegex.exec(source)) !== null) {
36357
+ const startIdx = structMatch.index;
36358
+ const openBrace = source.indexOf("{", startIdx);
36359
+ if (openBrace < 0) continue;
36360
+ let depth = 1;
36361
+ let i = openBrace + 1;
36362
+ while (i < source.length && depth > 0) {
36363
+ const ch = source[i];
36364
+ if (ch === "{") depth++;
36365
+ else if (ch === "}") depth--;
36366
+ i++;
36367
+ }
36368
+ const structBody = source.slice(openBrace, i);
36369
+ const structLine = source.slice(0, startIdx).split("\n").length;
36370
+ const styleCount = {};
36371
+ const tagMatches = [];
36372
+ let m;
36373
+ JSON_TAG_REGEX.lastIndex = 0;
36374
+ while ((m = JSON_TAG_REGEX.exec(structBody)) !== null) {
36375
+ const tag = m[1];
36376
+ const options = m[2] ?? "";
36377
+ const style = options ? "with-options" : "no-options";
36378
+ styleCount[style] = (styleCount[style] ?? 0) + 1;
36379
+ tagMatches.push({ tag, style, idx: openBrace + m.index });
36380
+ }
36381
+ const styles = Object.keys(styleCount);
36382
+ if (styles.length < 2 || tagMatches.length < 2) continue;
36383
+ const dominant = styles.reduce(
36384
+ (a, b) => (styleCount[a] ?? 0) >= (styleCount[b] ?? 0) ? a : b
36385
+ );
36386
+ const minority = tagMatches.filter((t) => t.style !== dominant);
36387
+ if (minority.length === 0) continue;
36388
+ for (const m2 of minority) {
36389
+ const line = source.slice(0, m2.idx).split("\n").length;
36390
+ issues.push({
36391
+ ruleId: "go/struct-tag-inconsistency",
36392
+ category: "typo",
36393
+ severity: "low",
36394
+ aiSpecific: true,
36395
+ message: `Struct mixes json tag styles \u2014 this field uses "json:"${m2.tag}${m2.style === "with-options" ? ",..." : ""}"" but the dominant style is ${dominant === "with-options" ? "with options (e.g. omitempty)" : "no options"}`,
36396
+ line,
36397
+ column: 1,
36398
+ advice: 'Pick one tag style per struct. If most fields are `json:"foo"`, this field should be too. Real Go code maintains consistency within a struct (or within a package). Reference: go/struct-tag-inconsistency v0.19.'
36399
+ });
36400
+ }
36401
+ if (issues.length > 0) break;
36402
+ }
36403
+ return issues;
36404
+ }
36405
+ });
36406
+
36265
36407
  // src/rules/layout/gap-monopoly.ts
36266
36408
  var GAP_RE = /\bgap(?:-x|-y)?-(\d+)\b/g;
36267
36409
  var gapMonopolyRule = createRule({
@@ -37049,82 +37191,6 @@ var keyPropMissingRule = createRule({
37049
37191
  }
37050
37192
  });
37051
37193
 
37052
- // src/rules/logic/ks-distribution-shift.ts
37053
- var MIN_SAMPLES_PER_FEATURE = 20;
37054
- function extractFileFeatures(source) {
37055
- const lines = source.split("\n");
37056
- const lineLengths = lines.map((l) => l.length);
37057
- const identifierLengths = [];
37058
- const idRe = /[A-Za-z_$][A-Za-z0-9_$]*/g;
37059
- let m;
37060
- while ((m = idRe.exec(source)) !== null) {
37061
- identifierLengths.push(m[0].length);
37062
- }
37063
- const commentDensity = lines.map((l) => {
37064
- const trimmed = l.trim();
37065
- if (trimmed.length === 0) return 0;
37066
- const commentChars = (trimmed.match(/^\/\/.*$/)?.[0]?.length ?? 0) + (trimmed.match(/^\s*\/\*.*?\*\/\s*$/)?.at(0)?.length ?? 0);
37067
- return commentChars / trimmed.length;
37068
- });
37069
- return { lineLengths, identifierLengths, commentDensity };
37070
- }
37071
- var ksDistributionShiftRule = createRule({
37072
- id: "logic/ks-distribution-shift",
37073
- category: "logic",
37074
- severity: "medium",
37075
- aiSpecific: false,
37076
- description: "Multi-feature Kolmogorov\u2013Smirnov distribution-shift vs corpus baseline (Bonferroni-corrected). Peer-reviewed ML distribution-shift detector (arXiv:2510.15996, Oct 2025).",
37077
- create(context) {
37078
- return context;
37079
- },
37080
- analyze(_context, facts) {
37081
- const issues = [];
37082
- if (!facts.v2) return issues;
37083
- const source = facts.v2._source ?? "";
37084
- if (source.length < 200) return issues;
37085
- const features = extractFileFeatures(source);
37086
- const samples = /* @__PURE__ */ new Map([
37087
- ["lineLengths", features.lineLengths],
37088
- ["identifierLengths", features.identifierLengths],
37089
- ["commentDensity", features.commentDensity]
37090
- ]);
37091
- const baselines = getCorpusBaselines();
37092
- const baselinesMap = /* @__PURE__ */ new Map();
37093
- if (baselines) {
37094
- baselinesMap.set("lineLengths", baselines.features.lineLengths.sample);
37095
- baselinesMap.set("identifierLengths", baselines.features.identifierLengths.sample);
37096
- baselinesMap.set("commentDensity", baselines.features.commentDensity.sample);
37097
- } else {
37098
- baselinesMap.set("lineLengths", [20, 25, 30, 32, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]);
37099
- baselinesMap.set("identifierLengths", [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 24, 28]);
37100
- baselinesMap.set("commentDensity", [0, 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5]);
37101
- }
37102
- for (const [name, vals] of samples) {
37103
- if (vals.length < MIN_SAMPLES_PER_FEATURE) samples.delete(name);
37104
- }
37105
- if (samples.size === 0) return issues;
37106
- const result = multiFeatureKsTest(samples, baselinesMap, 0.05);
37107
- if (!result.anySignificant) return issues;
37108
- const shifted = result.significantFeatures.join(", ");
37109
- const details = result.significantFeatures.map((name) => {
37110
- const r = result.perFeature.get(name);
37111
- if (!r) return name;
37112
- return `${name} (D=${r.statistic.toFixed(3)}, p=${r.pValue.toExponential(2)})`;
37113
- }).join("; ");
37114
- issues.push({
37115
- ruleId: "logic/ks-distribution-shift",
37116
- category: "logic",
37117
- severity: "medium",
37118
- aiSpecific: true,
37119
- message: `Distribution shift detected on ${result.significantFeatures.length} of ${result.perFeature.size} features (Bonferroni \u03B1=${result.bonferroniAlpha.toExponential(2)}). Features: ${shifted}. Detail: ${details}.`,
37120
- line: 1,
37121
- column: 1,
37122
- advice: "Inspect the shifted features. KS detects both AI anomalies and production-rot anomalies (it is symmetric); combine with Heaps/Zipf for AI-specific signal."
37123
- });
37124
- return issues;
37125
- }
37126
- });
37127
-
37128
37194
  // src/rules/logic/math-any-density.ts
37129
37195
  var ANY_PER_100_LINES = 5;
37130
37196
  var MIN_ABSOLUTE = 6;
@@ -37186,7 +37252,7 @@ var mathAnyDensityRule = createRule({
37186
37252
  });
37187
37253
 
37188
37254
  // src/rules/logic/math-console-log-storm.ts
37189
- var WINDOW_SIZE = 30;
37255
+ var WINDOW_SIZE2 = 30;
37190
37256
  var STORM_THRESHOLD = 5;
37191
37257
  var CONSOLE_LOG_RE = /\bconsole\.log\s*\(/g;
37192
37258
  var mathConsoleLogStormRule = createRule({
@@ -37217,7 +37283,7 @@ var mathConsoleLogStormRule = createRule({
37217
37283
  let maxEndLine = 0;
37218
37284
  let i = 0;
37219
37285
  for (let j = 0; j < lines.length; j++) {
37220
- while (lines[j] - lines[i] > WINDOW_SIZE) i++;
37286
+ while (lines[j] - lines[i] > WINDOW_SIZE2) i++;
37221
37287
  const count = j - i + 1;
37222
37288
  if (count > maxCount) {
37223
37289
  maxCount = count;
@@ -37231,7 +37297,7 @@ var mathConsoleLogStormRule = createRule({
37231
37297
  category: "logic",
37232
37298
  severity: "high",
37233
37299
  aiSpecific: true,
37234
- message: `${maxCount} console.log calls clustered in a ${WINDOW_SIZE}-line window ending at line ${maxEndLine}. AI debug-sprays logs in a single function; humans use one strategic log.`,
37300
+ message: `${maxCount} console.log calls clustered in a ${WINDOW_SIZE2}-line window ending at line ${maxEndLine}. AI debug-sprays logs in a single function; humans use one strategic log.`,
37235
37301
  line: firstIdx >= 0 ? lines[firstIdx] : 1,
37236
37302
  column: firstIdx >= 0 ? columns[firstIdx] : 1,
37237
37303
  advice: "Replace debug logs with a proper debugger or logger.debug() \u2014 remove all console.log before shipping."
@@ -39805,6 +39871,216 @@ function isTautologicalAssertion(hit) {
39805
39871
  return false;
39806
39872
  }
39807
39873
 
39874
+ // src/rules/ts/enum-vs-as-const.ts
39875
+ var ENUM_DECL_REGEX = /^[ \t]*(?:export\s+)?(?:const\s+)?enum\s+[A-Z_][A-Za-z0-9_]*\s*\{/gm;
39876
+ var tsEnumVsAsConstRule = createRule({
39877
+ id: "ts/enum-vs-as-const",
39878
+ category: "typo",
39879
+ severity: "low",
39880
+ aiSpecific: true,
39881
+ description: "Uses `enum` \u2014 modern TS prefers `as const` objects",
39882
+ create(_context) {
39883
+ return {};
39884
+ },
39885
+ analyze(_context, facts) {
39886
+ const issues = [];
39887
+ const source = facts.v2?._source;
39888
+ if (!source) return issues;
39889
+ let match;
39890
+ ENUM_DECL_REGEX.lastIndex = 0;
39891
+ while ((match = ENUM_DECL_REGEX.exec(source)) !== null) {
39892
+ const line = source.slice(0, match.index).split("\n").length;
39893
+ issues.push({
39894
+ ruleId: "ts/enum-vs-as-const",
39895
+ category: "typo",
39896
+ severity: "low",
39897
+ aiSpecific: true,
39898
+ message: `'enum' is an AI / older-TS pattern \u2014 prefer 'as const' for a frozen object literal`,
39899
+ line,
39900
+ column: match[0].indexOf("enum") + 1,
39901
+ advice: 'Replace `enum Foo { A, B }` with `const Foo = { A: "A", B: "B" } as const` (or `const Foo = ["A", "B"] as const`). Modern TS style guides (Google, TS-eslint) prefer `as const` because enums have surprising runtime semantics. Reference: ts/enum-vs-as-const v0.19.'
39902
+ });
39903
+ }
39904
+ return issues;
39905
+ }
39906
+ });
39907
+
39908
+ // src/rules/ts/excessive-type-assertion.ts
39909
+ var DEFAULT_MAX = 3;
39910
+ var FN_DECL_REGEX = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+[A-Za-z_$][\w$]*\s*\([^)]*\)\s*[^{]*\{|^\s*(?:export\s+)?(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>\s*\{|^\s*(?:public|private|protected|static|async|abstract|readonly|\s)*\s*[A-Za-z_$][\w$]*\s*\([^)]*\)\s*:\s*[^{]*\{/gm;
39911
+ var AS_REGEX = /(?<![\w$])as\s+(?!const)([A-Z][\w$.,<>\[\]|&]*)/g;
39912
+ var tsExcessiveTypeAssertionRule = createRule({
39913
+ id: "ts/excessive-type-assertion",
39914
+ category: "typo",
39915
+ severity: "low",
39916
+ aiSpecific: true,
39917
+ description: "Function with >3 `as` type assertions \u2014 likely AI fighting the type system",
39918
+ create(_context) {
39919
+ return { maxAssertionsPerFunction: DEFAULT_MAX };
39920
+ },
39921
+ analyze(context, facts) {
39922
+ const issues = [];
39923
+ const source = facts.v2?._source;
39924
+ if (!source) return issues;
39925
+ let match;
39926
+ FN_DECL_REGEX.lastIndex = 0;
39927
+ while ((match = FN_DECL_REGEX.exec(source)) !== null) {
39928
+ const startIdx = match.index;
39929
+ const openBraceIdx = source.indexOf("{", startIdx);
39930
+ if (openBraceIdx < 0) continue;
39931
+ let depth = 1;
39932
+ let i = openBraceIdx + 1;
39933
+ while (i < source.length && depth > 0) {
39934
+ const ch = source[i];
39935
+ if (ch === "{") depth++;
39936
+ else if (ch === "}") depth--;
39937
+ i++;
39938
+ }
39939
+ const body = source.slice(openBraceIdx, i);
39940
+ const line = source.slice(0, startIdx).split("\n").length;
39941
+ let asCount = 0;
39942
+ const seen = /* @__PURE__ */ new Set();
39943
+ let asMatch;
39944
+ AS_REGEX.lastIndex = 0;
39945
+ while ((asMatch = AS_REGEX.exec(body)) !== null) {
39946
+ const captured = asMatch[1];
39947
+ if (seen.has(captured)) continue;
39948
+ seen.add(captured);
39949
+ asCount++;
39950
+ }
39951
+ if (asCount > context.maxAssertionsPerFunction) {
39952
+ issues.push({
39953
+ ruleId: "ts/excessive-type-assertion",
39954
+ category: "typo",
39955
+ severity: "low",
39956
+ aiSpecific: true,
39957
+ message: `Function has ${asCount} 'as' assertions (max ${context.maxAssertionsPerFunction}) \u2014 likely AI fighting the type system`,
39958
+ line,
39959
+ column: 1,
39960
+ advice: "More than 3 `as` assertions in a function is a strong signal that the type is wrong, not the code. Fix the type definition (or use a type guard) instead of bypassing the type system. Reference: ts/excessive-type-assertion v0.19."
39961
+ });
39962
+ }
39963
+ }
39964
+ return issues;
39965
+ }
39966
+ });
39967
+
39968
+ // src/rules/ts/import-type-misuse.ts
39969
+ var INLINE_TYPE_IMPORT_REGEX = /^[ \t]*import\s*\{[^}]*\btype\s+[A-Za-z_]/gm;
39970
+ var tsImportTypeMisuseRule = createRule({
39971
+ id: "ts/import-type-misuse",
39972
+ category: "typo",
39973
+ severity: "low",
39974
+ aiSpecific: true,
39975
+ description: "Inline `import { type X }` \u2014 prefer `import type { X }` for clarity",
39976
+ create(_context) {
39977
+ return {};
39978
+ },
39979
+ analyze(_context, facts) {
39980
+ const issues = [];
39981
+ const source = facts.v2?._source;
39982
+ if (!source) return issues;
39983
+ let match;
39984
+ INLINE_TYPE_IMPORT_REGEX.lastIndex = 0;
39985
+ while ((match = INLINE_TYPE_IMPORT_REGEX.exec(source)) !== null) {
39986
+ const line = source.slice(0, match.index).split("\n").length;
39987
+ issues.push({
39988
+ ruleId: "ts/import-type-misuse",
39989
+ category: "typo",
39990
+ severity: "low",
39991
+ aiSpecific: true,
39992
+ message: "Inline `type` in a value import \u2014 split into a separate `import type` statement",
39993
+ line,
39994
+ column: match[0].indexOf("type") + 1,
39995
+ advice: 'Use `import type { X } from "..."` instead of `import { type X } from "..."`. The inline form is valid but the split form is more common in real codebases and makes the type-only intent unambiguous. Reference: ts/import-type-misuse v0.19.'
39996
+ });
39997
+ }
39998
+ return issues;
39999
+ }
40000
+ });
40001
+
40002
+ // src/rules/ts/never-vs-unknown.ts
40003
+ var NEVER_RETURN_REGEX = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)[^{]*:\s*(?:Promise<)?never\b[^{]*\{/gm;
40004
+ var THROW_OR_LOOP_REGEX = /\b(?:throw\b|while\s*\(|for\s*\(|process\.exit\b|System\.exit\b)/;
40005
+ var tsNeverVsUnknownRule = createRule({
40006
+ id: "ts/never-vs-unknown",
40007
+ category: "typo",
40008
+ severity: "low",
40009
+ aiSpecific: true,
40010
+ description: "Return type `: never` but body has no throw / loop / exit \u2014 likely AI misuse",
40011
+ create(_context) {
40012
+ return {};
40013
+ },
40014
+ analyze(_context, facts) {
40015
+ const issues = [];
40016
+ const source = facts.v2?._source;
40017
+ if (!source) return issues;
40018
+ let match;
40019
+ NEVER_RETURN_REGEX.lastIndex = 0;
40020
+ while ((match = NEVER_RETURN_REGEX.exec(source)) !== null) {
40021
+ const fnName = match[1];
40022
+ const startIdx = match.index;
40023
+ const line = source.slice(0, startIdx).split("\n").length;
40024
+ const openBraceIdx = source.indexOf("{", startIdx);
40025
+ if (openBraceIdx < 0) continue;
40026
+ let depth = 1;
40027
+ let i = openBraceIdx + 1;
40028
+ while (i < source.length && depth > 0) {
40029
+ const ch = source[i];
40030
+ if (ch === "{") depth++;
40031
+ else if (ch === "}") depth--;
40032
+ i++;
40033
+ }
40034
+ const body = source.slice(openBraceIdx, i);
40035
+ if (THROW_OR_LOOP_REGEX.test(body)) continue;
40036
+ issues.push({
40037
+ ruleId: "ts/never-vs-unknown",
40038
+ category: "typo",
40039
+ severity: "low",
40040
+ aiSpecific: true,
40041
+ message: `Function '${fnName}' returns 'never' but its body has no throw, loop, or exit \u2014 likely AI misuse`,
40042
+ line,
40043
+ column: match[0].indexOf("never") + 1,
40044
+ advice: 'The `never` return type means "this function never returns". Reserve it for functions that always throw, always loop, or always exit. For "impossible" branches, use a concrete type (`void`, `Error`, `unknown`) and an exhaustive check. Reference: ts/never-vs-unknown v0.19.'
40045
+ });
40046
+ }
40047
+ return issues;
40048
+ }
40049
+ });
40050
+
40051
+ // src/rules/ts/optional-chain-overuse.ts
40052
+ var DEFAULT_MIN_CHAIN_LENGTH = 5;
40053
+ var tsOptionalChainOveruseRule = createRule({
40054
+ id: "ts/optional-chain-overuse",
40055
+ category: "logic",
40056
+ severity: "low",
40057
+ aiSpecific: true,
40058
+ description: "Optional chaining (?.) used 5+ times in a single chain \u2014 AI tends to chain rather than narrow",
40059
+ create(_context) {
40060
+ return { minChainLength: DEFAULT_MIN_CHAIN_LENGTH };
40061
+ },
40062
+ analyze(context, facts) {
40063
+ const issues = [];
40064
+ const expressions = facts.v2.logic?.logicalExpressions;
40065
+ if (!expressions) return issues;
40066
+ for (const expression of expressions) {
40067
+ if (expression.depth >= context.minChainLength && expression.isOptionalChainLike) {
40068
+ issues.push({
40069
+ ruleId: "ts/optional-chain-overuse",
40070
+ category: "logic",
40071
+ severity: "low",
40072
+ aiSpecific: true,
40073
+ message: `Optional chain depth ${expression.depth} \u2014 break with an intermediate variable or guard clause`,
40074
+ line: expression.line,
40075
+ column: expression.column,
40076
+ advice: "Long optional chains are an AI pattern. Use a guard clause (`if (!value) return`) or intermediate variables to make the narrowing explicit. Reference: ts/optional-chain-overuse v0.19."
40077
+ });
40078
+ }
40079
+ }
40080
+ return issues;
40081
+ }
40082
+ });
40083
+
39808
40084
  // src/rules/typo/calc-fontsize.ts
39809
40085
  var FONT_SIZE_RE = /\bfont-size\s*:\s*[^;]*\bcalc\s*\(/i;
39810
40086
  var calcFontsizeRule = createRule({
@@ -41345,6 +41621,10 @@ var builtinRules = [
41345
41621
  expiredCodeExampleRule,
41346
41622
  staleFunctionReferenceRule,
41347
41623
  stalePackageReferenceRule,
41624
+ dupIdenticalBlockRule,
41625
+ goErrorWrapWithoutContextRule,
41626
+ goNilSliceVsEmptyRule,
41627
+ goStructTagInconsistencyRule,
41348
41628
  gapMonopolyRule,
41349
41629
  mathElementUniformityRule,
41350
41630
  mathGridUniformityRule,
@@ -41354,7 +41634,6 @@ var builtinRules = [
41354
41634
  ghostDefensiveRule,
41355
41635
  heapsDeviationRule,
41356
41636
  keyPropMissingRule,
41357
- ksDistributionShiftRule,
41358
41637
  mathAnyDensityRule,
41359
41638
  mathConsoleLogStormRule,
41360
41639
  mathGiniClassUsageRule,
@@ -41388,6 +41667,11 @@ var builtinRules = [
41388
41667
  fakePlaceholderRule,
41389
41668
  missingEdgeCaseRule,
41390
41669
  weakAssertionRule,
41670
+ tsEnumVsAsConstRule,
41671
+ tsExcessiveTypeAssertionRule,
41672
+ tsImportTypeMisuseRule,
41673
+ tsNeverVsUnknownRule,
41674
+ tsOptionalChainOveruseRule,
41391
41675
  calcFontsizeRule,
41392
41676
  calcRawPxRule,
41393
41677
  clampOffscaleRule,
@@ -41558,7 +41842,7 @@ var signal_strength_default = {
41558
41842
  precision: 0.9966,
41559
41843
  lastCalibratedAt: "2026-07-01T00:00:00Z",
41560
41844
  verdict: "USEFUL",
41561
- _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=294, FP=1, P=99.7%, FPR=0.00%, lift=251225.49. v7 was USEFUL (TP=231, FP=1, lift=182622.43). v8 was USEFUL (TP=63, FP=0).",
41845
+ _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=294, FP=1, P=99.7%, FPR=0.00%, lift=251225.49. v7 was USEFUL (TP=231, FP=1, lift=182622.43). v8 was USEFUL (TP=63, FP=0). v0.19 default-on (explicit defaultOff: false): P 99.7% / 251k lift \u2014 core AI fingerprint.",
41562
41846
  aiSpecific: true,
41563
41847
  _v7Verdict: "USEFUL",
41564
41848
  _v7Lift: 182622.43,
@@ -41566,7 +41850,8 @@ var signal_strength_default = {
41566
41850
  _v7FpRate: 0,
41567
41851
  _v7Precision: 0.9957,
41568
41852
  _v8Verdict: "USEFUL",
41569
- _v8Lift: 99999
41853
+ _v8Lift: 99999,
41854
+ defaultOff: false
41570
41855
  },
41571
41856
  "ai/errors-near-eof": {
41572
41857
  recall: 0.0948,
@@ -41815,7 +42100,7 @@ var signal_strength_default = {
41815
42100
  precision: 0.9512,
41816
42101
  lastCalibratedAt: "2026-07-01T00:00:00Z",
41817
42102
  verdict: "USEFUL",
41818
- _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=468, FP=24, P=95.1%, FPR=0.01%, lift=9990.98. v7 was USEFUL (TP=314, FP=24, lift=7099.57). v8 was USEFUL (TP=154, FP=0).",
42103
+ _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=468, FP=24, P=95.1%, FPR=0.01%, lift=9990.98. v7 was USEFUL (TP=314, FP=24, lift=7099.57). v8 was USEFUL (TP=154, FP=0). v0.19 default-on (explicit defaultOff: false): P 95.1% / 9990x lift \u2014 UI bug.",
41819
42104
  aiSpecific: true,
41820
42105
  _v7Verdict: "USEFUL",
41821
42106
  _v7Lift: 7099.57,
@@ -41823,7 +42108,8 @@ var signal_strength_default = {
41823
42108
  _v7FpRate: 1e-4,
41824
42109
  _v7Precision: 0.929,
41825
42110
  _v8Verdict: "USEFUL",
41826
- _v8Lift: 99999
42111
+ _v8Lift: 99999,
42112
+ defaultOff: false
41827
42113
  },
41828
42114
  "context/import-path-mismatch": {
41829
42115
  recall: 0.0681,
@@ -42007,7 +42293,7 @@ var signal_strength_default = {
42007
42293
  precision: 0.8834,
42008
42294
  lastCalibratedAt: "2026-07-01T00:00:00Z",
42009
42295
  verdict: "USEFUL",
42010
- _calibrationNote: "v8.5 (v0.18.9): v85 verdict=USEFUL, v7 was DORMANT, v8 was USEFUL. v0.18.8 v8a first measurement (1000 files): see docs/research/v0.18.8-dead-rules-measurement.md.",
42296
+ _calibrationNote: "v8.5 (v0.18.9): v85 verdict=USEFUL, v7 was DORMANT, v8 was USEFUL. v0.18.8 v8a first measurement (1000 files): see docs/research/v0.18.8-dead-rules-measurement.md. v0.19 default-on (explicit defaultOff: false): P 88.3% / FPR 0.74% / 120x lift \u2014 code hygiene.",
42011
42297
  aiSpecific: true,
42012
42298
  _v7Verdict: "DORMANT",
42013
42299
  _v7Lift: 1,
@@ -42015,7 +42301,8 @@ var signal_strength_default = {
42015
42301
  _v7FpRate: 0,
42016
42302
  _v7Precision: 0,
42017
42303
  _v8Verdict: "USEFUL",
42018
- _v8Lift: 32.74
42304
+ _v8Lift: 32.74,
42305
+ defaultOff: false
42019
42306
  },
42020
42307
  "dead/unused-parameter": {
42021
42308
  recall: 8e-4,
@@ -42103,6 +42390,78 @@ var signal_strength_default = {
42103
42390
  _v8Verdict: "OK",
42104
42391
  _v8Lift: 255.9
42105
42392
  },
42393
+ "dup/identical-block": {
42394
+ recall: 0,
42395
+ fpRate: 0,
42396
+ ratio: 1,
42397
+ precision: 0,
42398
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
42399
+ verdict: "DORMANT",
42400
+ _calibrationNote: "v0.19: new rule, not yet calibrated. v8.5 calibration does not run dup/* rules. Scheduled for v0.20 calibration on near-dup corpus.",
42401
+ aiSpecific: false,
42402
+ _v7Verdict: "DORMANT",
42403
+ _v7Lift: 1,
42404
+ _v7Recall: 0,
42405
+ _v7FpRate: 0,
42406
+ _v7Precision: 0,
42407
+ _v8Verdict: "DORMANT",
42408
+ _v8Lift: 1,
42409
+ defaultOff: true
42410
+ },
42411
+ "go/error-wrap-without-context": {
42412
+ recall: 0,
42413
+ fpRate: 0,
42414
+ ratio: 1,
42415
+ precision: 0,
42416
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
42417
+ verdict: "DORMANT",
42418
+ _calibrationNote: "v0.19: new rule (fmt.Errorf wrap with generic message \u2014 needs operation context). Not yet calibrated. Scheduled for v9 calibration.",
42419
+ aiSpecific: true,
42420
+ _v7Verdict: "DORMANT",
42421
+ _v7Lift: 1,
42422
+ _v7Recall: 0,
42423
+ _v7FpRate: 0,
42424
+ _v7Precision: 0,
42425
+ _v8Verdict: "DORMANT",
42426
+ _v8Lift: 1,
42427
+ defaultOff: true
42428
+ },
42429
+ "go/nil-slice-vs-empty": {
42430
+ recall: 0,
42431
+ fpRate: 0,
42432
+ ratio: 1,
42433
+ precision: 0,
42434
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
42435
+ verdict: "DORMANT",
42436
+ _calibrationNote: "v0.19: new rule (Variable declared nil slice but assigned empty slice). Not yet calibrated. Scheduled for v9 calibration.",
42437
+ aiSpecific: true,
42438
+ _v7Verdict: "DORMANT",
42439
+ _v7Lift: 1,
42440
+ _v7Recall: 0,
42441
+ _v7FpRate: 0,
42442
+ _v7Precision: 0,
42443
+ _v8Verdict: "DORMANT",
42444
+ _v8Lift: 1,
42445
+ defaultOff: true
42446
+ },
42447
+ "go/struct-tag-inconsistency": {
42448
+ recall: 0,
42449
+ fpRate: 0,
42450
+ ratio: 1,
42451
+ precision: 0,
42452
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
42453
+ verdict: "DORMANT",
42454
+ _calibrationNote: "v0.19: new rule (Struct fields mix json tag styles). Not yet calibrated. Scheduled for v9 calibration.",
42455
+ aiSpecific: true,
42456
+ _v7Verdict: "DORMANT",
42457
+ _v7Lift: 1,
42458
+ _v7Recall: 0,
42459
+ _v7FpRate: 0,
42460
+ _v7Precision: 0,
42461
+ _v8Verdict: "DORMANT",
42462
+ _v8Lift: 1,
42463
+ defaultOff: true
42464
+ },
42106
42465
  "layout/forced-layout": {
42107
42466
  recall: 0,
42108
42467
  fpRate: 0,
@@ -42231,7 +42590,7 @@ var signal_strength_default = {
42231
42590
  precision: 0.8889,
42232
42591
  lastCalibratedAt: "2026-07-01T00:00:00Z",
42233
42592
  verdict: "USEFUL",
42234
- _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=16, FP=2, P=88.9%, FPR=0.00%, lift=112035.56. v7 was USEFUL (TP=15, FP=2, lift=80917.50). v8 was USEFUL (TP=1, FP=0).",
42593
+ _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=16, FP=2, P=88.9%, FPR=0.00%, lift=112035.56. v7 was USEFUL (TP=15, FP=2, lift=80917.50). v8 was USEFUL (TP=1, FP=0). v0.19 default-on (explicit defaultOff: false): P 88.9% / 112k lift \u2014 code smell.",
42235
42594
  aiSpecific: true,
42236
42595
  _v7Verdict: "USEFUL",
42237
42596
  _v7Lift: 80917.5,
@@ -42239,7 +42598,8 @@ var signal_strength_default = {
42239
42598
  _v7FpRate: 0,
42240
42599
  _v7Precision: 0.8824,
42241
42600
  _v8Verdict: "USEFUL",
42242
- _v8Lift: 99999
42601
+ _v8Lift: 99999,
42602
+ defaultOff: false
42243
42603
  },
42244
42604
  "logic/heaps-deviation": {
42245
42605
  recall: 0.0126,
@@ -42275,24 +42635,6 @@ var signal_strength_default = {
42275
42635
  _v8Verdict: "USEFUL",
42276
42636
  _v8Lift: 1760.69
42277
42637
  },
42278
- "logic/ks-distribution-shift": {
42279
- recall: 0.6889,
42280
- fpRate: 0.4411,
42281
- ratio: 1.46,
42282
- precision: 0.6457,
42283
- lastCalibratedAt: "2026-07-01T00:00:00Z",
42284
- verdict: "NOISY",
42285
- _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=202658, FP=111193, P=64.6%, FPR=44.11%, lift=1.46. v7 was USEFUL (TP=152391, FP=62358, lift=2.09). v8 was INVERTED (TP=50267, FP=48835).",
42286
- aiSpecific: false,
42287
- _v7Verdict: "USEFUL",
42288
- _v7Lift: 2.09,
42289
- _v7Recall: 0.6431,
42290
- _v7FpRate: 0.34,
42291
- _v7Precision: 0.7096,
42292
- _v8Verdict: "INVERTED",
42293
- _v8Lift: 0.71,
42294
- defaultOff: true
42295
- },
42296
42638
  "logic/math-any-density": {
42297
42639
  recall: 17e-4,
42298
42640
  fpRate: 13e-4,
@@ -42658,7 +43000,7 @@ var signal_strength_default = {
42658
43000
  precision: 1,
42659
43001
  lastCalibratedAt: "2026-07-01T00:00:00Z",
42660
43002
  verdict: "USEFUL",
42661
- _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=1, FP=0, P=100.0%, FPR=0.00%, lift=inf. v7 was USEFUL (TP=1, FP=0, lift=inf). v8 was DORMANT (TP=0, FP=0).",
43003
+ _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=1, FP=0, P=100.0%, FPR=0.00%, lift=inf. v7 was USEFUL (TP=1, FP=0, lift=inf). v8 was DORMANT (TP=0, FP=0). v0.19 default-on (explicit defaultOff: false): P 100% / inf lift \u2014 security must always be on.",
42662
43004
  aiSpecific: true,
42663
43005
  _v7Verdict: "USEFUL",
42664
43006
  _v7Lift: 99999,
@@ -42666,7 +43008,8 @@ var signal_strength_default = {
42666
43008
  _v7FpRate: 0,
42667
43009
  _v7Precision: 1,
42668
43010
  _v8Verdict: "DORMANT",
42669
- _v8Lift: 1
43011
+ _v8Lift: 1,
43012
+ defaultOff: false
42670
43013
  },
42671
43014
  "security/hardcoded-secret": {
42672
43015
  recall: 14e-4,
@@ -42856,6 +43199,96 @@ var signal_strength_default = {
42856
43199
  _v8Verdict: "USEFUL",
42857
43200
  _v8Lift: 651.94
42858
43201
  },
43202
+ "ts/enum-vs-as-const": {
43203
+ recall: 0,
43204
+ fpRate: 0,
43205
+ ratio: 1,
43206
+ precision: 0,
43207
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43208
+ verdict: "DORMANT",
43209
+ _calibrationNote: "v0.19: new rule (Uses `enum` \u2014 modern TS prefers `as const`). Not yet calibrated. Scheduled for v9 calibration.",
43210
+ aiSpecific: true,
43211
+ _v7Verdict: "DORMANT",
43212
+ _v7Lift: 1,
43213
+ _v7Recall: 0,
43214
+ _v7FpRate: 0,
43215
+ _v7Precision: 0,
43216
+ _v8Verdict: "DORMANT",
43217
+ _v8Lift: 1,
43218
+ defaultOff: true
43219
+ },
43220
+ "ts/excessive-type-assertion": {
43221
+ recall: 0,
43222
+ fpRate: 0,
43223
+ ratio: 1,
43224
+ precision: 0,
43225
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43226
+ verdict: "DORMANT",
43227
+ _calibrationNote: "v0.19: new rule (Function with >3 `as` assertions \u2014 AI fighting the type system). Not yet calibrated. Scheduled for v9 calibration.",
43228
+ aiSpecific: true,
43229
+ _v7Verdict: "DORMANT",
43230
+ _v7Lift: 1,
43231
+ _v7Recall: 0,
43232
+ _v7FpRate: 0,
43233
+ _v7Precision: 0,
43234
+ _v8Verdict: "DORMANT",
43235
+ _v8Lift: 1,
43236
+ defaultOff: true
43237
+ },
43238
+ "ts/import-type-misuse": {
43239
+ recall: 0,
43240
+ fpRate: 0,
43241
+ ratio: 1,
43242
+ precision: 0,
43243
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43244
+ verdict: "DORMANT",
43245
+ _calibrationNote: "v0.19: new rule (Inline `import { type X }` \u2014 prefer separate `import type`). Not yet calibrated. Scheduled for v9 calibration.",
43246
+ aiSpecific: true,
43247
+ _v7Verdict: "DORMANT",
43248
+ _v7Lift: 1,
43249
+ _v7Recall: 0,
43250
+ _v7FpRate: 0,
43251
+ _v7Precision: 0,
43252
+ _v8Verdict: "DORMANT",
43253
+ _v8Lift: 1,
43254
+ defaultOff: true
43255
+ },
43256
+ "ts/never-vs-unknown": {
43257
+ recall: 0,
43258
+ fpRate: 0,
43259
+ ratio: 1,
43260
+ precision: 0,
43261
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43262
+ verdict: "DORMANT",
43263
+ _calibrationNote: "v0.19: new rule (Return type `never` but body has no throw/loop/exit). Not yet calibrated. Scheduled for v9 calibration.",
43264
+ aiSpecific: true,
43265
+ _v7Verdict: "DORMANT",
43266
+ _v7Lift: 1,
43267
+ _v7Recall: 0,
43268
+ _v7FpRate: 0,
43269
+ _v7Precision: 0,
43270
+ _v8Verdict: "DORMANT",
43271
+ _v8Lift: 1,
43272
+ defaultOff: true
43273
+ },
43274
+ "ts/optional-chain-overuse": {
43275
+ recall: 0,
43276
+ fpRate: 0,
43277
+ ratio: 1,
43278
+ precision: 0,
43279
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43280
+ verdict: "DORMANT",
43281
+ _calibrationNote: "v0.19: new rule (Optional chain depth >= 5 \u2014 AI chains ?. rather than narrowing). Not yet calibrated. Scheduled for v9 calibration.",
43282
+ aiSpecific: true,
43283
+ _v7Verdict: "DORMANT",
43284
+ _v7Lift: 1,
43285
+ _v7Recall: 0,
43286
+ _v7FpRate: 0,
43287
+ _v7Precision: 0,
43288
+ _v8Verdict: "DORMANT",
43289
+ _v8Lift: 1,
43290
+ defaultOff: true
43291
+ },
42859
43292
  "typo/calc-fontsize": {
42860
43293
  recall: 0,
42861
43294
  fpRate: 0,
@@ -43158,7 +43591,7 @@ var signal_strength_default = {
43158
43591
  precision: 0.9774,
43159
43592
  lastCalibratedAt: "2026-07-01T00:00:00Z",
43160
43593
  verdict: "USEFUL",
43161
- _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=130, FP=3, P=97.7%, FPR=0.00%, lift=82131.33. v7 was USEFUL (TP=96, FP=3, lift=59285.01). v8 was USEFUL (TP=34, FP=0).",
43594
+ _calibrationNote: "v8.5 calibration (v0.18.9, 2026-07-01): v7+v8 combined corpus (252080 neg + 294178 pos). v8.5 TP=130, FP=3, P=97.7%, FPR=0.00%, lift=82131.33. v7 was USEFUL (TP=96, FP=3, lift=59285.01). v8 was USEFUL (TP=34, FP=0). v0.19 default-on (explicit defaultOff: false): P 97.7% / 82k lift \u2014 UI consistency.",
43162
43595
  aiSpecific: false,
43163
43596
  _v7Verdict: "USEFUL",
43164
43597
  _v7Lift: 59285.01,
@@ -43166,7 +43599,8 @@ var signal_strength_default = {
43166
43599
  _v7FpRate: 0,
43167
43600
  _v7Precision: 0.9697,
43168
43601
  _v8Verdict: "USEFUL",
43169
- _v8Lift: 99999
43602
+ _v8Lift: 99999,
43603
+ defaultOff: false
43170
43604
  },
43171
43605
  "visual/spacing-scale-violation": {
43172
43606
  recall: 86e-4,