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.
@@ -5435,76 +5435,6 @@ function tokenizeIdentifiers(source) {
5435
5435
  return tokens;
5436
5436
  }
5437
5437
  var SQRT_2_TIMES_LN_2 = Math.sqrt(2 * Math.LN2);
5438
- function ecdfAt(sortedSamples, x) {
5439
- let lo = 0;
5440
- let hi = sortedSamples.length;
5441
- while (lo < hi) {
5442
- const mid = lo + hi >>> 1;
5443
- if (sortedSamples[mid] <= x) lo = mid + 1;
5444
- else hi = mid;
5445
- }
5446
- return lo / sortedSamples.length;
5447
- }
5448
- function ksStatistic(sampleA, sampleB) {
5449
- if (sampleA.length === 0 || sampleB.length === 0) return 1;
5450
- const sortedA = [...sampleA].sort((a, b) => a - b);
5451
- const sortedB = [...sampleB].sort((a, b) => a - b);
5452
- const allPoints = [...sortedA, ...sortedB].sort((a, b) => a - b);
5453
- let maxDiff = 0;
5454
- for (const x of allPoints) {
5455
- const fa = ecdfAt(sortedA, x);
5456
- const fb = ecdfAt(sortedB, x);
5457
- const diff = Math.abs(fa - fb);
5458
- if (diff > maxDiff) maxDiff = diff;
5459
- }
5460
- return maxDiff;
5461
- }
5462
- function ksPValue(statistic, n, m) {
5463
- if (n === 0 || m === 0) return 1;
5464
- if (statistic < 0) return 1;
5465
- if (statistic > 1) return 0;
5466
- const lambda = Math.sqrt(n * m / (n + m)) * statistic;
5467
- if (lambda > 3.6) return 0;
5468
- let p = 0;
5469
- for (let j = 1; j < 1e3; j++) {
5470
- const term = 2 * Math.pow(-1, j - 1) * Math.exp(-2 * j * j * lambda * lambda);
5471
- p += term;
5472
- if (Math.abs(term) < 1e-15) break;
5473
- }
5474
- return Math.max(0, Math.min(1, p));
5475
- }
5476
- function ksTest(sampleA, sampleB, alpha = 0.05) {
5477
- const statistic = ksStatistic(sampleA, sampleB);
5478
- const pValue = ksPValue(statistic, sampleA.length, sampleB.length);
5479
- return {
5480
- statistic,
5481
- pValue,
5482
- significant: pValue < alpha,
5483
- n: sampleA.length,
5484
- m: sampleB.length
5485
- };
5486
- }
5487
- function multiFeatureKsTest(features, baselines, alpha = 0.05) {
5488
- const featureNames = [...features.keys()];
5489
- const k = featureNames.length;
5490
- const bonferroniAlpha = k > 0 ? alpha / k : alpha;
5491
- const perFeature = /* @__PURE__ */ new Map();
5492
- const significantFeatures = [];
5493
- for (const name of featureNames) {
5494
- const sample = features.get(name);
5495
- const baseline = baselines.get(name);
5496
- if (!sample || !baseline) continue;
5497
- const result = ksTest(sample, baseline, bonferroniAlpha);
5498
- perFeature.set(name, result);
5499
- if (result.significant) significantFeatures.push(name);
5500
- }
5501
- return {
5502
- perFeature,
5503
- bonferroniAlpha,
5504
- anySignificant: significantFeatures.length > 0,
5505
- significantFeatures
5506
- };
5507
- }
5508
5438
 
5509
5439
  // src/engine/visitors/react.ts
5510
5440
  function isObject(node) {
@@ -36234,6 +36164,218 @@ var brokenLinkRule = createRule({
36234
36164
  }
36235
36165
  });
36236
36166
 
36167
+ // src/rules/dup/identical-block.ts
36168
+ import * as crypto from "crypto";
36169
+ var WINDOW_SIZE = 10;
36170
+ var MIN_NORMALIZED_LENGTH = 40;
36171
+ var HASH_PREFIX_LENGTH = 16;
36172
+ var DEDUP_CACHE = /* @__PURE__ */ new Map();
36173
+ function normalizeAndHash(lines) {
36174
+ const normalized = lines.map(
36175
+ (line) => line.replace(/\/\/.*$/, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim()
36176
+ ).filter((line) => line.length > 0).join("\n");
36177
+ if (normalized.length < MIN_NORMALIZED_LENGTH) return void 0;
36178
+ return crypto.createHash("sha1").update(normalized).digest("hex").slice(0, HASH_PREFIX_LENGTH);
36179
+ }
36180
+ var dupIdenticalBlockRule = createRule({
36181
+ id: "dup/identical-block",
36182
+ category: "logic",
36183
+ severity: "medium",
36184
+ aiSpecific: false,
36185
+ description: "Block of >=10 lines is identical across >=2 files (Type-1 clone detector)",
36186
+ create(_context) {
36187
+ return {};
36188
+ },
36189
+ analyze(_context, facts) {
36190
+ const issues = [];
36191
+ const source = facts.v2?._source;
36192
+ if (!source) return issues;
36193
+ const filePath = facts.filePath;
36194
+ const lines = source.split("\n");
36195
+ for (let i = 0; i <= lines.length - WINDOW_SIZE; i++) {
36196
+ const window = lines.slice(i, i + WINDOW_SIZE);
36197
+ const hash = normalizeAndHash(window);
36198
+ if (!hash) continue;
36199
+ const existing = DEDUP_CACHE.get(hash) ?? [];
36200
+ const matches = existing.filter((m) => m.file !== filePath);
36201
+ for (const match of matches) {
36202
+ issues.push({
36203
+ ruleId: "dup/identical-block",
36204
+ category: "logic",
36205
+ severity: "medium",
36206
+ aiSpecific: false,
36207
+ message: `Identical ${WINDOW_SIZE}-line block at line ${i + 1} also appears in ${match.file}:${match.line + 1}`,
36208
+ line: i + 1,
36209
+ column: 0,
36210
+ 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.",
36211
+ extras: {
36212
+ duplicateOf: {
36213
+ file: match.file,
36214
+ line: match.line + 1,
36215
+ hash
36216
+ }
36217
+ }
36218
+ });
36219
+ }
36220
+ existing.push({ file: filePath, line: i });
36221
+ DEDUP_CACHE.set(hash, existing);
36222
+ }
36223
+ return issues;
36224
+ }
36225
+ });
36226
+
36227
+ // src/rules/go/error-wrap-without-context.ts
36228
+ var ERR_WRAP_REGEX = /fmt\.Errorf\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*([^)]+)\)/g;
36229
+ var GENERIC_PREFIX_REGEX = /^\s*(?:error|err|failed|wrap(?:ping)?|invalid)\s*(?::\s*)?%w\b/i;
36230
+ var goErrorWrapWithoutContextRule = createRule({
36231
+ id: "go/error-wrap-without-context",
36232
+ category: "typo",
36233
+ severity: "low",
36234
+ aiSpecific: true,
36235
+ description: 'fmt.Errorf wrap without operation context \u2014 AI defaults to generic "error: %w"',
36236
+ create(_context) {
36237
+ return {};
36238
+ },
36239
+ analyze(_context, facts) {
36240
+ const issues = [];
36241
+ const source = facts.v2?._source;
36242
+ if (!source) return issues;
36243
+ let match;
36244
+ ERR_WRAP_REGEX.lastIndex = 0;
36245
+ while ((match = ERR_WRAP_REGEX.exec(source)) !== null) {
36246
+ const formatString = match[1];
36247
+ if (!formatString.includes("%w")) continue;
36248
+ if (formatString.length >= 30) continue;
36249
+ if (!GENERIC_PREFIX_REGEX.test(formatString)) continue;
36250
+ const line = source.slice(0, match.index).split("\n").length;
36251
+ issues.push({
36252
+ ruleId: "go/error-wrap-without-context",
36253
+ category: "typo",
36254
+ severity: "low",
36255
+ aiSpecific: true,
36256
+ message: `fmt.Errorf wrap with generic message "${formatString}" \u2014 include the failing operation`,
36257
+ line,
36258
+ column: match[0].indexOf("fmt") + 1,
36259
+ 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'
36260
+ });
36261
+ }
36262
+ return issues;
36263
+ }
36264
+ });
36265
+
36266
+ // src/rules/go/nil-slice-vs-empty.ts
36267
+ var NIL_SLICE_DECL_REGEX = /^[\t ]*var\s+([A-Za-z_][A-Za-z0-9_]*)\s+\[\][\w.*]+\b/gm;
36268
+ var EMPTY_SLICE_ASSIGN_REGEX = /^[\t ]*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:\[\][\w.*]*\{\}|make\(\[\][\w.*]*)/gm;
36269
+ var goNilSliceVsEmptyRule = createRule({
36270
+ id: "go/nil-slice-vs-empty",
36271
+ category: "typo",
36272
+ severity: "low",
36273
+ aiSpecific: true,
36274
+ description: "Variable declared `var x []int` but later assigned `x = []int{}` or `make([]int, n)` \u2014 pick one form",
36275
+ create(_context) {
36276
+ return {};
36277
+ },
36278
+ analyze(_context, facts) {
36279
+ const issues = [];
36280
+ const source = facts.v2?._source;
36281
+ if (!source) return issues;
36282
+ const nilDecls = /* @__PURE__ */ new Map();
36283
+ let m;
36284
+ NIL_SLICE_DECL_REGEX.lastIndex = 0;
36285
+ while ((m = NIL_SLICE_DECL_REGEX.exec(source)) !== null) {
36286
+ nilDecls.set(m[1], source.slice(0, m.index).split("\n").length);
36287
+ }
36288
+ if (nilDecls.size === 0) return issues;
36289
+ EMPTY_SLICE_ASSIGN_REGEX.lastIndex = 0;
36290
+ while ((m = EMPTY_SLICE_ASSIGN_REGEX.exec(source)) !== null) {
36291
+ const name = m[1];
36292
+ if (!nilDecls.has(name)) continue;
36293
+ const declLine = nilDecls.get(name);
36294
+ const assignLine = source.slice(0, m.index).split("\n").length;
36295
+ if (assignLine <= declLine) continue;
36296
+ issues.push({
36297
+ ruleId: "go/nil-slice-vs-empty",
36298
+ category: "typo",
36299
+ severity: "low",
36300
+ aiSpecific: true,
36301
+ message: `Variable '${name}' declared as nil slice (line ${declLine}) but assigned an empty slice (line ${assignLine}) \u2014 pick one form`,
36302
+ line: declLine,
36303
+ column: 1,
36304
+ 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."
36305
+ });
36306
+ }
36307
+ return issues;
36308
+ }
36309
+ });
36310
+
36311
+ // src/rules/go/struct-tag-inconsistency.ts
36312
+ var JSON_TAG_REGEX = /`json:"([^",]+)(?:,([^"]+))?"`/g;
36313
+ var goStructTagInconsistencyRule = createRule({
36314
+ id: "go/struct-tag-inconsistency",
36315
+ category: "typo",
36316
+ severity: "low",
36317
+ aiSpecific: true,
36318
+ description: 'Struct fields mix json:"foo" and json:"foo,omitempty" \u2014 pick one convention per struct',
36319
+ create(_context) {
36320
+ return {};
36321
+ },
36322
+ analyze(_context, facts) {
36323
+ const issues = [];
36324
+ const source = facts.v2?._source;
36325
+ if (!source) return issues;
36326
+ const structRegex = /type\s+[A-Z][A-Za-z0-9_]*\s+struct\s*\{/g;
36327
+ let structMatch;
36328
+ while ((structMatch = structRegex.exec(source)) !== null) {
36329
+ const startIdx = structMatch.index;
36330
+ const openBrace = source.indexOf("{", startIdx);
36331
+ if (openBrace < 0) continue;
36332
+ let depth = 1;
36333
+ let i = openBrace + 1;
36334
+ while (i < source.length && depth > 0) {
36335
+ const ch = source[i];
36336
+ if (ch === "{") depth++;
36337
+ else if (ch === "}") depth--;
36338
+ i++;
36339
+ }
36340
+ const structBody = source.slice(openBrace, i);
36341
+ const structLine = source.slice(0, startIdx).split("\n").length;
36342
+ const styleCount = {};
36343
+ const tagMatches = [];
36344
+ let m;
36345
+ JSON_TAG_REGEX.lastIndex = 0;
36346
+ while ((m = JSON_TAG_REGEX.exec(structBody)) !== null) {
36347
+ const tag = m[1];
36348
+ const options = m[2] ?? "";
36349
+ const style = options ? "with-options" : "no-options";
36350
+ styleCount[style] = (styleCount[style] ?? 0) + 1;
36351
+ tagMatches.push({ tag, style, idx: openBrace + m.index });
36352
+ }
36353
+ const styles = Object.keys(styleCount);
36354
+ if (styles.length < 2 || tagMatches.length < 2) continue;
36355
+ const dominant = styles.reduce(
36356
+ (a, b) => (styleCount[a] ?? 0) >= (styleCount[b] ?? 0) ? a : b
36357
+ );
36358
+ const minority = tagMatches.filter((t) => t.style !== dominant);
36359
+ if (minority.length === 0) continue;
36360
+ for (const m2 of minority) {
36361
+ const line = source.slice(0, m2.idx).split("\n").length;
36362
+ issues.push({
36363
+ ruleId: "go/struct-tag-inconsistency",
36364
+ category: "typo",
36365
+ severity: "low",
36366
+ aiSpecific: true,
36367
+ 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"}`,
36368
+ line,
36369
+ column: 1,
36370
+ 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.'
36371
+ });
36372
+ }
36373
+ if (issues.length > 0) break;
36374
+ }
36375
+ return issues;
36376
+ }
36377
+ });
36378
+
36237
36379
  // src/rules/layout/gap-monopoly.ts
36238
36380
  var GAP_RE = /\bgap(?:-x|-y)?-(\d+)\b/g;
36239
36381
  var gapMonopolyRule = createRule({
@@ -37021,82 +37163,6 @@ var keyPropMissingRule = createRule({
37021
37163
  }
37022
37164
  });
37023
37165
 
37024
- // src/rules/logic/ks-distribution-shift.ts
37025
- var MIN_SAMPLES_PER_FEATURE = 20;
37026
- function extractFileFeatures(source) {
37027
- const lines = source.split("\n");
37028
- const lineLengths = lines.map((l) => l.length);
37029
- const identifierLengths = [];
37030
- const idRe = /[A-Za-z_$][A-Za-z0-9_$]*/g;
37031
- let m;
37032
- while ((m = idRe.exec(source)) !== null) {
37033
- identifierLengths.push(m[0].length);
37034
- }
37035
- const commentDensity = lines.map((l) => {
37036
- const trimmed = l.trim();
37037
- if (trimmed.length === 0) return 0;
37038
- const commentChars = (trimmed.match(/^\/\/.*$/)?.[0]?.length ?? 0) + (trimmed.match(/^\s*\/\*.*?\*\/\s*$/)?.at(0)?.length ?? 0);
37039
- return commentChars / trimmed.length;
37040
- });
37041
- return { lineLengths, identifierLengths, commentDensity };
37042
- }
37043
- var ksDistributionShiftRule = createRule({
37044
- id: "logic/ks-distribution-shift",
37045
- category: "logic",
37046
- severity: "medium",
37047
- aiSpecific: false,
37048
- description: "Multi-feature Kolmogorov\u2013Smirnov distribution-shift vs corpus baseline (Bonferroni-corrected). Peer-reviewed ML distribution-shift detector (arXiv:2510.15996, Oct 2025).",
37049
- create(context) {
37050
- return context;
37051
- },
37052
- analyze(_context, facts) {
37053
- const issues = [];
37054
- if (!facts.v2) return issues;
37055
- const source = facts.v2._source ?? "";
37056
- if (source.length < 200) return issues;
37057
- const features = extractFileFeatures(source);
37058
- const samples = /* @__PURE__ */ new Map([
37059
- ["lineLengths", features.lineLengths],
37060
- ["identifierLengths", features.identifierLengths],
37061
- ["commentDensity", features.commentDensity]
37062
- ]);
37063
- const baselines = getCorpusBaselines();
37064
- const baselinesMap = /* @__PURE__ */ new Map();
37065
- if (baselines) {
37066
- baselinesMap.set("lineLengths", baselines.features.lineLengths.sample);
37067
- baselinesMap.set("identifierLengths", baselines.features.identifierLengths.sample);
37068
- baselinesMap.set("commentDensity", baselines.features.commentDensity.sample);
37069
- } else {
37070
- baselinesMap.set("lineLengths", [20, 25, 30, 32, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]);
37071
- baselinesMap.set("identifierLengths", [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 24, 28]);
37072
- baselinesMap.set("commentDensity", [0, 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5]);
37073
- }
37074
- for (const [name, vals] of samples) {
37075
- if (vals.length < MIN_SAMPLES_PER_FEATURE) samples.delete(name);
37076
- }
37077
- if (samples.size === 0) return issues;
37078
- const result = multiFeatureKsTest(samples, baselinesMap, 0.05);
37079
- if (!result.anySignificant) return issues;
37080
- const shifted = result.significantFeatures.join(", ");
37081
- const details = result.significantFeatures.map((name) => {
37082
- const r = result.perFeature.get(name);
37083
- if (!r) return name;
37084
- return `${name} (D=${r.statistic.toFixed(3)}, p=${r.pValue.toExponential(2)})`;
37085
- }).join("; ");
37086
- issues.push({
37087
- ruleId: "logic/ks-distribution-shift",
37088
- category: "logic",
37089
- severity: "medium",
37090
- aiSpecific: true,
37091
- message: `Distribution shift detected on ${result.significantFeatures.length} of ${result.perFeature.size} features (Bonferroni \u03B1=${result.bonferroniAlpha.toExponential(2)}). Features: ${shifted}. Detail: ${details}.`,
37092
- line: 1,
37093
- column: 1,
37094
- 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."
37095
- });
37096
- return issues;
37097
- }
37098
- });
37099
-
37100
37166
  // src/rules/logic/math-any-density.ts
37101
37167
  var ANY_PER_100_LINES = 5;
37102
37168
  var MIN_ABSOLUTE = 6;
@@ -37158,7 +37224,7 @@ var mathAnyDensityRule = createRule({
37158
37224
  });
37159
37225
 
37160
37226
  // src/rules/logic/math-console-log-storm.ts
37161
- var WINDOW_SIZE = 30;
37227
+ var WINDOW_SIZE2 = 30;
37162
37228
  var STORM_THRESHOLD = 5;
37163
37229
  var CONSOLE_LOG_RE = /\bconsole\.log\s*\(/g;
37164
37230
  var mathConsoleLogStormRule = createRule({
@@ -37189,7 +37255,7 @@ var mathConsoleLogStormRule = createRule({
37189
37255
  let maxEndLine = 0;
37190
37256
  let i = 0;
37191
37257
  for (let j = 0; j < lines.length; j++) {
37192
- while (lines[j] - lines[i] > WINDOW_SIZE) i++;
37258
+ while (lines[j] - lines[i] > WINDOW_SIZE2) i++;
37193
37259
  const count = j - i + 1;
37194
37260
  if (count > maxCount) {
37195
37261
  maxCount = count;
@@ -37203,7 +37269,7 @@ var mathConsoleLogStormRule = createRule({
37203
37269
  category: "logic",
37204
37270
  severity: "high",
37205
37271
  aiSpecific: true,
37206
- 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.`,
37272
+ 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.`,
37207
37273
  line: firstIdx >= 0 ? lines[firstIdx] : 1,
37208
37274
  column: firstIdx >= 0 ? columns[firstIdx] : 1,
37209
37275
  advice: "Replace debug logs with a proper debugger or logger.debug() \u2014 remove all console.log before shipping."
@@ -39007,7 +39073,7 @@ var unsafeHtmlRenderRule = createRule({
39007
39073
  });
39008
39074
 
39009
39075
  // src/rules/test/utils.ts
39010
- import { createHash as createHash2 } from "crypto";
39076
+ import { createHash as createHash3 } from "crypto";
39011
39077
  function isTestFile(filePath) {
39012
39078
  const normalized = filePath.replace(/\\/g, "/");
39013
39079
  if (/(^|\/)__tests__\//.test(normalized)) return true;
@@ -39167,7 +39233,7 @@ function extractSetupBlocks(source, minLines = 3) {
39167
39233
  }).trim();
39168
39234
  const lineCount = rawBody.split("\n").length;
39169
39235
  if (lineCount < minLines) continue;
39170
- const bodyHash = createHash2("sha1").update(normalizedBody).digest("hex");
39236
+ const bodyHash = createHash3("sha1").update(normalizedBody).digest("hex");
39171
39237
  const { line, column } = lineColumnOf(source, m.index);
39172
39238
  hits.push({
39173
39239
  kind,
@@ -39777,6 +39843,216 @@ function isTautologicalAssertion(hit) {
39777
39843
  return false;
39778
39844
  }
39779
39845
 
39846
+ // src/rules/ts/enum-vs-as-const.ts
39847
+ var ENUM_DECL_REGEX = /^[ \t]*(?:export\s+)?(?:const\s+)?enum\s+[A-Z_][A-Za-z0-9_]*\s*\{/gm;
39848
+ var tsEnumVsAsConstRule = createRule({
39849
+ id: "ts/enum-vs-as-const",
39850
+ category: "typo",
39851
+ severity: "low",
39852
+ aiSpecific: true,
39853
+ description: "Uses `enum` \u2014 modern TS prefers `as const` objects",
39854
+ create(_context) {
39855
+ return {};
39856
+ },
39857
+ analyze(_context, facts) {
39858
+ const issues = [];
39859
+ const source = facts.v2?._source;
39860
+ if (!source) return issues;
39861
+ let match;
39862
+ ENUM_DECL_REGEX.lastIndex = 0;
39863
+ while ((match = ENUM_DECL_REGEX.exec(source)) !== null) {
39864
+ const line = source.slice(0, match.index).split("\n").length;
39865
+ issues.push({
39866
+ ruleId: "ts/enum-vs-as-const",
39867
+ category: "typo",
39868
+ severity: "low",
39869
+ aiSpecific: true,
39870
+ message: `'enum' is an AI / older-TS pattern \u2014 prefer 'as const' for a frozen object literal`,
39871
+ line,
39872
+ column: match[0].indexOf("enum") + 1,
39873
+ 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.'
39874
+ });
39875
+ }
39876
+ return issues;
39877
+ }
39878
+ });
39879
+
39880
+ // src/rules/ts/excessive-type-assertion.ts
39881
+ var DEFAULT_MAX = 3;
39882
+ 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;
39883
+ var AS_REGEX = /(?<![\w$])as\s+(?!const)([A-Z][\w$.,<>\[\]|&]*)/g;
39884
+ var tsExcessiveTypeAssertionRule = createRule({
39885
+ id: "ts/excessive-type-assertion",
39886
+ category: "typo",
39887
+ severity: "low",
39888
+ aiSpecific: true,
39889
+ description: "Function with >3 `as` type assertions \u2014 likely AI fighting the type system",
39890
+ create(_context) {
39891
+ return { maxAssertionsPerFunction: DEFAULT_MAX };
39892
+ },
39893
+ analyze(context, facts) {
39894
+ const issues = [];
39895
+ const source = facts.v2?._source;
39896
+ if (!source) return issues;
39897
+ let match;
39898
+ FN_DECL_REGEX.lastIndex = 0;
39899
+ while ((match = FN_DECL_REGEX.exec(source)) !== null) {
39900
+ const startIdx = match.index;
39901
+ const openBraceIdx = source.indexOf("{", startIdx);
39902
+ if (openBraceIdx < 0) continue;
39903
+ let depth = 1;
39904
+ let i = openBraceIdx + 1;
39905
+ while (i < source.length && depth > 0) {
39906
+ const ch = source[i];
39907
+ if (ch === "{") depth++;
39908
+ else if (ch === "}") depth--;
39909
+ i++;
39910
+ }
39911
+ const body = source.slice(openBraceIdx, i);
39912
+ const line = source.slice(0, startIdx).split("\n").length;
39913
+ let asCount = 0;
39914
+ const seen = /* @__PURE__ */ new Set();
39915
+ let asMatch;
39916
+ AS_REGEX.lastIndex = 0;
39917
+ while ((asMatch = AS_REGEX.exec(body)) !== null) {
39918
+ const captured = asMatch[1];
39919
+ if (seen.has(captured)) continue;
39920
+ seen.add(captured);
39921
+ asCount++;
39922
+ }
39923
+ if (asCount > context.maxAssertionsPerFunction) {
39924
+ issues.push({
39925
+ ruleId: "ts/excessive-type-assertion",
39926
+ category: "typo",
39927
+ severity: "low",
39928
+ aiSpecific: true,
39929
+ message: `Function has ${asCount} 'as' assertions (max ${context.maxAssertionsPerFunction}) \u2014 likely AI fighting the type system`,
39930
+ line,
39931
+ column: 1,
39932
+ 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."
39933
+ });
39934
+ }
39935
+ }
39936
+ return issues;
39937
+ }
39938
+ });
39939
+
39940
+ // src/rules/ts/import-type-misuse.ts
39941
+ var INLINE_TYPE_IMPORT_REGEX = /^[ \t]*import\s*\{[^}]*\btype\s+[A-Za-z_]/gm;
39942
+ var tsImportTypeMisuseRule = createRule({
39943
+ id: "ts/import-type-misuse",
39944
+ category: "typo",
39945
+ severity: "low",
39946
+ aiSpecific: true,
39947
+ description: "Inline `import { type X }` \u2014 prefer `import type { X }` for clarity",
39948
+ create(_context) {
39949
+ return {};
39950
+ },
39951
+ analyze(_context, facts) {
39952
+ const issues = [];
39953
+ const source = facts.v2?._source;
39954
+ if (!source) return issues;
39955
+ let match;
39956
+ INLINE_TYPE_IMPORT_REGEX.lastIndex = 0;
39957
+ while ((match = INLINE_TYPE_IMPORT_REGEX.exec(source)) !== null) {
39958
+ const line = source.slice(0, match.index).split("\n").length;
39959
+ issues.push({
39960
+ ruleId: "ts/import-type-misuse",
39961
+ category: "typo",
39962
+ severity: "low",
39963
+ aiSpecific: true,
39964
+ message: "Inline `type` in a value import \u2014 split into a separate `import type` statement",
39965
+ line,
39966
+ column: match[0].indexOf("type") + 1,
39967
+ 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.'
39968
+ });
39969
+ }
39970
+ return issues;
39971
+ }
39972
+ });
39973
+
39974
+ // src/rules/ts/never-vs-unknown.ts
39975
+ var NEVER_RETURN_REGEX = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)[^{]*:\s*(?:Promise<)?never\b[^{]*\{/gm;
39976
+ var THROW_OR_LOOP_REGEX = /\b(?:throw\b|while\s*\(|for\s*\(|process\.exit\b|System\.exit\b)/;
39977
+ var tsNeverVsUnknownRule = createRule({
39978
+ id: "ts/never-vs-unknown",
39979
+ category: "typo",
39980
+ severity: "low",
39981
+ aiSpecific: true,
39982
+ description: "Return type `: never` but body has no throw / loop / exit \u2014 likely AI misuse",
39983
+ create(_context) {
39984
+ return {};
39985
+ },
39986
+ analyze(_context, facts) {
39987
+ const issues = [];
39988
+ const source = facts.v2?._source;
39989
+ if (!source) return issues;
39990
+ let match;
39991
+ NEVER_RETURN_REGEX.lastIndex = 0;
39992
+ while ((match = NEVER_RETURN_REGEX.exec(source)) !== null) {
39993
+ const fnName = match[1];
39994
+ const startIdx = match.index;
39995
+ const line = source.slice(0, startIdx).split("\n").length;
39996
+ const openBraceIdx = source.indexOf("{", startIdx);
39997
+ if (openBraceIdx < 0) continue;
39998
+ let depth = 1;
39999
+ let i = openBraceIdx + 1;
40000
+ while (i < source.length && depth > 0) {
40001
+ const ch = source[i];
40002
+ if (ch === "{") depth++;
40003
+ else if (ch === "}") depth--;
40004
+ i++;
40005
+ }
40006
+ const body = source.slice(openBraceIdx, i);
40007
+ if (THROW_OR_LOOP_REGEX.test(body)) continue;
40008
+ issues.push({
40009
+ ruleId: "ts/never-vs-unknown",
40010
+ category: "typo",
40011
+ severity: "low",
40012
+ aiSpecific: true,
40013
+ message: `Function '${fnName}' returns 'never' but its body has no throw, loop, or exit \u2014 likely AI misuse`,
40014
+ line,
40015
+ column: match[0].indexOf("never") + 1,
40016
+ 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.'
40017
+ });
40018
+ }
40019
+ return issues;
40020
+ }
40021
+ });
40022
+
40023
+ // src/rules/ts/optional-chain-overuse.ts
40024
+ var DEFAULT_MIN_CHAIN_LENGTH = 5;
40025
+ var tsOptionalChainOveruseRule = createRule({
40026
+ id: "ts/optional-chain-overuse",
40027
+ category: "logic",
40028
+ severity: "low",
40029
+ aiSpecific: true,
40030
+ description: "Optional chaining (?.) used 5+ times in a single chain \u2014 AI tends to chain rather than narrow",
40031
+ create(_context) {
40032
+ return { minChainLength: DEFAULT_MIN_CHAIN_LENGTH };
40033
+ },
40034
+ analyze(context, facts) {
40035
+ const issues = [];
40036
+ const expressions = facts.v2.logic?.logicalExpressions;
40037
+ if (!expressions) return issues;
40038
+ for (const expression of expressions) {
40039
+ if (expression.depth >= context.minChainLength && expression.isOptionalChainLike) {
40040
+ issues.push({
40041
+ ruleId: "ts/optional-chain-overuse",
40042
+ category: "logic",
40043
+ severity: "low",
40044
+ aiSpecific: true,
40045
+ message: `Optional chain depth ${expression.depth} \u2014 break with an intermediate variable or guard clause`,
40046
+ line: expression.line,
40047
+ column: expression.column,
40048
+ 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."
40049
+ });
40050
+ }
40051
+ }
40052
+ return issues;
40053
+ }
40054
+ });
40055
+
39780
40056
  // src/rules/typo/calc-fontsize.ts
39781
40057
  var FONT_SIZE_RE = /\bfont-size\s*:\s*[^;]*\bcalc\s*\(/i;
39782
40058
  var calcFontsizeRule = createRule({
@@ -41317,6 +41593,10 @@ var builtinRules = [
41317
41593
  expiredCodeExampleRule,
41318
41594
  staleFunctionReferenceRule,
41319
41595
  stalePackageReferenceRule,
41596
+ dupIdenticalBlockRule,
41597
+ goErrorWrapWithoutContextRule,
41598
+ goNilSliceVsEmptyRule,
41599
+ goStructTagInconsistencyRule,
41320
41600
  gapMonopolyRule,
41321
41601
  mathElementUniformityRule,
41322
41602
  mathGridUniformityRule,
@@ -41326,7 +41606,6 @@ var builtinRules = [
41326
41606
  ghostDefensiveRule,
41327
41607
  heapsDeviationRule,
41328
41608
  keyPropMissingRule,
41329
- ksDistributionShiftRule,
41330
41609
  mathAnyDensityRule,
41331
41610
  mathConsoleLogStormRule,
41332
41611
  mathGiniClassUsageRule,
@@ -41360,6 +41639,11 @@ var builtinRules = [
41360
41639
  fakePlaceholderRule,
41361
41640
  missingEdgeCaseRule,
41362
41641
  weakAssertionRule,
41642
+ tsEnumVsAsConstRule,
41643
+ tsExcessiveTypeAssertionRule,
41644
+ tsImportTypeMisuseRule,
41645
+ tsNeverVsUnknownRule,
41646
+ tsOptionalChainOveruseRule,
41363
41647
  calcFontsizeRule,
41364
41648
  calcRawPxRule,
41365
41649
  clampOffscaleRule,
@@ -41530,7 +41814,7 @@ var signal_strength_default = {
41530
41814
  precision: 0.9966,
41531
41815
  lastCalibratedAt: "2026-07-01T00:00:00Z",
41532
41816
  verdict: "USEFUL",
41533
- _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).",
41817
+ _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.",
41534
41818
  aiSpecific: true,
41535
41819
  _v7Verdict: "USEFUL",
41536
41820
  _v7Lift: 182622.43,
@@ -41538,7 +41822,8 @@ var signal_strength_default = {
41538
41822
  _v7FpRate: 0,
41539
41823
  _v7Precision: 0.9957,
41540
41824
  _v8Verdict: "USEFUL",
41541
- _v8Lift: 99999
41825
+ _v8Lift: 99999,
41826
+ defaultOff: false
41542
41827
  },
41543
41828
  "ai/errors-near-eof": {
41544
41829
  recall: 0.0948,
@@ -41787,7 +42072,7 @@ var signal_strength_default = {
41787
42072
  precision: 0.9512,
41788
42073
  lastCalibratedAt: "2026-07-01T00:00:00Z",
41789
42074
  verdict: "USEFUL",
41790
- _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).",
42075
+ _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.",
41791
42076
  aiSpecific: true,
41792
42077
  _v7Verdict: "USEFUL",
41793
42078
  _v7Lift: 7099.57,
@@ -41795,7 +42080,8 @@ var signal_strength_default = {
41795
42080
  _v7FpRate: 1e-4,
41796
42081
  _v7Precision: 0.929,
41797
42082
  _v8Verdict: "USEFUL",
41798
- _v8Lift: 99999
42083
+ _v8Lift: 99999,
42084
+ defaultOff: false
41799
42085
  },
41800
42086
  "context/import-path-mismatch": {
41801
42087
  recall: 0.0681,
@@ -41979,7 +42265,7 @@ var signal_strength_default = {
41979
42265
  precision: 0.8834,
41980
42266
  lastCalibratedAt: "2026-07-01T00:00:00Z",
41981
42267
  verdict: "USEFUL",
41982
- _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.",
42268
+ _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.",
41983
42269
  aiSpecific: true,
41984
42270
  _v7Verdict: "DORMANT",
41985
42271
  _v7Lift: 1,
@@ -41987,7 +42273,8 @@ var signal_strength_default = {
41987
42273
  _v7FpRate: 0,
41988
42274
  _v7Precision: 0,
41989
42275
  _v8Verdict: "USEFUL",
41990
- _v8Lift: 32.74
42276
+ _v8Lift: 32.74,
42277
+ defaultOff: false
41991
42278
  },
41992
42279
  "dead/unused-parameter": {
41993
42280
  recall: 8e-4,
@@ -42075,6 +42362,78 @@ var signal_strength_default = {
42075
42362
  _v8Verdict: "OK",
42076
42363
  _v8Lift: 255.9
42077
42364
  },
42365
+ "dup/identical-block": {
42366
+ recall: 0,
42367
+ fpRate: 0,
42368
+ ratio: 1,
42369
+ precision: 0,
42370
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
42371
+ verdict: "DORMANT",
42372
+ _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.",
42373
+ aiSpecific: false,
42374
+ _v7Verdict: "DORMANT",
42375
+ _v7Lift: 1,
42376
+ _v7Recall: 0,
42377
+ _v7FpRate: 0,
42378
+ _v7Precision: 0,
42379
+ _v8Verdict: "DORMANT",
42380
+ _v8Lift: 1,
42381
+ defaultOff: true
42382
+ },
42383
+ "go/error-wrap-without-context": {
42384
+ recall: 0,
42385
+ fpRate: 0,
42386
+ ratio: 1,
42387
+ precision: 0,
42388
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
42389
+ verdict: "DORMANT",
42390
+ _calibrationNote: "v0.19: new rule (fmt.Errorf wrap with generic message \u2014 needs operation context). Not yet calibrated. Scheduled for v9 calibration.",
42391
+ aiSpecific: true,
42392
+ _v7Verdict: "DORMANT",
42393
+ _v7Lift: 1,
42394
+ _v7Recall: 0,
42395
+ _v7FpRate: 0,
42396
+ _v7Precision: 0,
42397
+ _v8Verdict: "DORMANT",
42398
+ _v8Lift: 1,
42399
+ defaultOff: true
42400
+ },
42401
+ "go/nil-slice-vs-empty": {
42402
+ recall: 0,
42403
+ fpRate: 0,
42404
+ ratio: 1,
42405
+ precision: 0,
42406
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
42407
+ verdict: "DORMANT",
42408
+ _calibrationNote: "v0.19: new rule (Variable declared nil slice but assigned empty slice). Not yet calibrated. Scheduled for v9 calibration.",
42409
+ aiSpecific: true,
42410
+ _v7Verdict: "DORMANT",
42411
+ _v7Lift: 1,
42412
+ _v7Recall: 0,
42413
+ _v7FpRate: 0,
42414
+ _v7Precision: 0,
42415
+ _v8Verdict: "DORMANT",
42416
+ _v8Lift: 1,
42417
+ defaultOff: true
42418
+ },
42419
+ "go/struct-tag-inconsistency": {
42420
+ recall: 0,
42421
+ fpRate: 0,
42422
+ ratio: 1,
42423
+ precision: 0,
42424
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
42425
+ verdict: "DORMANT",
42426
+ _calibrationNote: "v0.19: new rule (Struct fields mix json tag styles). Not yet calibrated. Scheduled for v9 calibration.",
42427
+ aiSpecific: true,
42428
+ _v7Verdict: "DORMANT",
42429
+ _v7Lift: 1,
42430
+ _v7Recall: 0,
42431
+ _v7FpRate: 0,
42432
+ _v7Precision: 0,
42433
+ _v8Verdict: "DORMANT",
42434
+ _v8Lift: 1,
42435
+ defaultOff: true
42436
+ },
42078
42437
  "layout/forced-layout": {
42079
42438
  recall: 0,
42080
42439
  fpRate: 0,
@@ -42203,7 +42562,7 @@ var signal_strength_default = {
42203
42562
  precision: 0.8889,
42204
42563
  lastCalibratedAt: "2026-07-01T00:00:00Z",
42205
42564
  verdict: "USEFUL",
42206
- _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).",
42565
+ _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.",
42207
42566
  aiSpecific: true,
42208
42567
  _v7Verdict: "USEFUL",
42209
42568
  _v7Lift: 80917.5,
@@ -42211,7 +42570,8 @@ var signal_strength_default = {
42211
42570
  _v7FpRate: 0,
42212
42571
  _v7Precision: 0.8824,
42213
42572
  _v8Verdict: "USEFUL",
42214
- _v8Lift: 99999
42573
+ _v8Lift: 99999,
42574
+ defaultOff: false
42215
42575
  },
42216
42576
  "logic/heaps-deviation": {
42217
42577
  recall: 0.0126,
@@ -42247,24 +42607,6 @@ var signal_strength_default = {
42247
42607
  _v8Verdict: "USEFUL",
42248
42608
  _v8Lift: 1760.69
42249
42609
  },
42250
- "logic/ks-distribution-shift": {
42251
- recall: 0.6889,
42252
- fpRate: 0.4411,
42253
- ratio: 1.46,
42254
- precision: 0.6457,
42255
- lastCalibratedAt: "2026-07-01T00:00:00Z",
42256
- verdict: "NOISY",
42257
- _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).",
42258
- aiSpecific: false,
42259
- _v7Verdict: "USEFUL",
42260
- _v7Lift: 2.09,
42261
- _v7Recall: 0.6431,
42262
- _v7FpRate: 0.34,
42263
- _v7Precision: 0.7096,
42264
- _v8Verdict: "INVERTED",
42265
- _v8Lift: 0.71,
42266
- defaultOff: true
42267
- },
42268
42610
  "logic/math-any-density": {
42269
42611
  recall: 17e-4,
42270
42612
  fpRate: 13e-4,
@@ -42630,7 +42972,7 @@ var signal_strength_default = {
42630
42972
  precision: 1,
42631
42973
  lastCalibratedAt: "2026-07-01T00:00:00Z",
42632
42974
  verdict: "USEFUL",
42633
- _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).",
42975
+ _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.",
42634
42976
  aiSpecific: true,
42635
42977
  _v7Verdict: "USEFUL",
42636
42978
  _v7Lift: 99999,
@@ -42638,7 +42980,8 @@ var signal_strength_default = {
42638
42980
  _v7FpRate: 0,
42639
42981
  _v7Precision: 1,
42640
42982
  _v8Verdict: "DORMANT",
42641
- _v8Lift: 1
42983
+ _v8Lift: 1,
42984
+ defaultOff: false
42642
42985
  },
42643
42986
  "security/hardcoded-secret": {
42644
42987
  recall: 14e-4,
@@ -42828,6 +43171,96 @@ var signal_strength_default = {
42828
43171
  _v8Verdict: "USEFUL",
42829
43172
  _v8Lift: 651.94
42830
43173
  },
43174
+ "ts/enum-vs-as-const": {
43175
+ recall: 0,
43176
+ fpRate: 0,
43177
+ ratio: 1,
43178
+ precision: 0,
43179
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43180
+ verdict: "DORMANT",
43181
+ _calibrationNote: "v0.19: new rule (Uses `enum` \u2014 modern TS prefers `as const`). Not yet calibrated. Scheduled for v9 calibration.",
43182
+ aiSpecific: true,
43183
+ _v7Verdict: "DORMANT",
43184
+ _v7Lift: 1,
43185
+ _v7Recall: 0,
43186
+ _v7FpRate: 0,
43187
+ _v7Precision: 0,
43188
+ _v8Verdict: "DORMANT",
43189
+ _v8Lift: 1,
43190
+ defaultOff: true
43191
+ },
43192
+ "ts/excessive-type-assertion": {
43193
+ recall: 0,
43194
+ fpRate: 0,
43195
+ ratio: 1,
43196
+ precision: 0,
43197
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43198
+ verdict: "DORMANT",
43199
+ _calibrationNote: "v0.19: new rule (Function with >3 `as` assertions \u2014 AI fighting the type system). Not yet calibrated. Scheduled for v9 calibration.",
43200
+ aiSpecific: true,
43201
+ _v7Verdict: "DORMANT",
43202
+ _v7Lift: 1,
43203
+ _v7Recall: 0,
43204
+ _v7FpRate: 0,
43205
+ _v7Precision: 0,
43206
+ _v8Verdict: "DORMANT",
43207
+ _v8Lift: 1,
43208
+ defaultOff: true
43209
+ },
43210
+ "ts/import-type-misuse": {
43211
+ recall: 0,
43212
+ fpRate: 0,
43213
+ ratio: 1,
43214
+ precision: 0,
43215
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43216
+ verdict: "DORMANT",
43217
+ _calibrationNote: "v0.19: new rule (Inline `import { type X }` \u2014 prefer separate `import type`). Not yet calibrated. Scheduled for v9 calibration.",
43218
+ aiSpecific: true,
43219
+ _v7Verdict: "DORMANT",
43220
+ _v7Lift: 1,
43221
+ _v7Recall: 0,
43222
+ _v7FpRate: 0,
43223
+ _v7Precision: 0,
43224
+ _v8Verdict: "DORMANT",
43225
+ _v8Lift: 1,
43226
+ defaultOff: true
43227
+ },
43228
+ "ts/never-vs-unknown": {
43229
+ recall: 0,
43230
+ fpRate: 0,
43231
+ ratio: 1,
43232
+ precision: 0,
43233
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43234
+ verdict: "DORMANT",
43235
+ _calibrationNote: "v0.19: new rule (Return type `never` but body has no throw/loop/exit). Not yet calibrated. Scheduled for v9 calibration.",
43236
+ aiSpecific: true,
43237
+ _v7Verdict: "DORMANT",
43238
+ _v7Lift: 1,
43239
+ _v7Recall: 0,
43240
+ _v7FpRate: 0,
43241
+ _v7Precision: 0,
43242
+ _v8Verdict: "DORMANT",
43243
+ _v8Lift: 1,
43244
+ defaultOff: true
43245
+ },
43246
+ "ts/optional-chain-overuse": {
43247
+ recall: 0,
43248
+ fpRate: 0,
43249
+ ratio: 1,
43250
+ precision: 0,
43251
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
43252
+ verdict: "DORMANT",
43253
+ _calibrationNote: "v0.19: new rule (Optional chain depth >= 5 \u2014 AI chains ?. rather than narrowing). Not yet calibrated. Scheduled for v9 calibration.",
43254
+ aiSpecific: true,
43255
+ _v7Verdict: "DORMANT",
43256
+ _v7Lift: 1,
43257
+ _v7Recall: 0,
43258
+ _v7FpRate: 0,
43259
+ _v7Precision: 0,
43260
+ _v8Verdict: "DORMANT",
43261
+ _v8Lift: 1,
43262
+ defaultOff: true
43263
+ },
42831
43264
  "typo/calc-fontsize": {
42832
43265
  recall: 0,
42833
43266
  fpRate: 0,
@@ -43130,7 +43563,7 @@ var signal_strength_default = {
43130
43563
  precision: 0.9774,
43131
43564
  lastCalibratedAt: "2026-07-01T00:00:00Z",
43132
43565
  verdict: "USEFUL",
43133
- _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).",
43566
+ _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.",
43134
43567
  aiSpecific: false,
43135
43568
  _v7Verdict: "USEFUL",
43136
43569
  _v7Lift: 59285.01,
@@ -43138,7 +43571,8 @@ var signal_strength_default = {
43138
43571
  _v7FpRate: 0,
43139
43572
  _v7Precision: 0.9697,
43140
43573
  _v8Verdict: "USEFUL",
43141
- _v8Lift: 99999
43574
+ _v8Lift: 99999,
43575
+ defaultOff: false
43142
43576
  },
43143
43577
  "visual/spacing-scale-violation": {
43144
43578
  recall: 86e-4,