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.
package/dist/index.cjs CHANGED
@@ -36,7 +36,7 @@ var VERSION;
36
36
  var init_header = __esm({
37
37
  "src/types/_header.ts"() {
38
38
  "use strict";
39
- VERSION = "0.18.9";
39
+ VERSION = "0.19.0";
40
40
  }
41
41
  });
42
42
 
@@ -31353,6 +31353,246 @@ var init_broken_link = __esm({
31353
31353
  }
31354
31354
  });
31355
31355
 
31356
+ // src/rules/dup/identical-block.ts
31357
+ function normalizeAndHash(lines) {
31358
+ const normalized = lines.map(
31359
+ (line) => line.replace(/\/\/.*$/, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim()
31360
+ ).filter((line) => line.length > 0).join("\n");
31361
+ if (normalized.length < MIN_NORMALIZED_LENGTH) return void 0;
31362
+ return crypto.createHash("sha1").update(normalized).digest("hex").slice(0, HASH_PREFIX_LENGTH);
31363
+ }
31364
+ var crypto, WINDOW_SIZE, MIN_NORMALIZED_LENGTH, HASH_PREFIX_LENGTH, DEDUP_CACHE, dupIdenticalBlockRule;
31365
+ var init_identical_block = __esm({
31366
+ "src/rules/dup/identical-block.ts"() {
31367
+ "use strict";
31368
+ crypto = __toESM(require("crypto"), 1);
31369
+ init_rule();
31370
+ WINDOW_SIZE = 10;
31371
+ MIN_NORMALIZED_LENGTH = 40;
31372
+ HASH_PREFIX_LENGTH = 16;
31373
+ DEDUP_CACHE = /* @__PURE__ */ new Map();
31374
+ dupIdenticalBlockRule = createRule({
31375
+ id: "dup/identical-block",
31376
+ category: "logic",
31377
+ severity: "medium",
31378
+ aiSpecific: false,
31379
+ description: "Block of >=10 lines is identical across >=2 files (Type-1 clone detector)",
31380
+ create(_context) {
31381
+ return {};
31382
+ },
31383
+ analyze(_context, facts) {
31384
+ const issues = [];
31385
+ const source = facts.v2?._source;
31386
+ if (!source) return issues;
31387
+ const filePath = facts.filePath;
31388
+ const lines = source.split("\n");
31389
+ for (let i = 0; i <= lines.length - WINDOW_SIZE; i++) {
31390
+ const window = lines.slice(i, i + WINDOW_SIZE);
31391
+ const hash = normalizeAndHash(window);
31392
+ if (!hash) continue;
31393
+ const existing = DEDUP_CACHE.get(hash) ?? [];
31394
+ const matches = existing.filter((m) => m.file !== filePath);
31395
+ for (const match of matches) {
31396
+ issues.push({
31397
+ ruleId: "dup/identical-block",
31398
+ category: "logic",
31399
+ severity: "medium",
31400
+ aiSpecific: false,
31401
+ message: `Identical ${WINDOW_SIZE}-line block at line ${i + 1} also appears in ${match.file}:${match.line + 1}`,
31402
+ line: i + 1,
31403
+ column: 0,
31404
+ 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.",
31405
+ extras: {
31406
+ duplicateOf: {
31407
+ file: match.file,
31408
+ line: match.line + 1,
31409
+ hash
31410
+ }
31411
+ }
31412
+ });
31413
+ }
31414
+ existing.push({ file: filePath, line: i });
31415
+ DEDUP_CACHE.set(hash, existing);
31416
+ }
31417
+ return issues;
31418
+ }
31419
+ });
31420
+ }
31421
+ });
31422
+
31423
+ // src/rules/go/error-wrap-without-context.ts
31424
+ var ERR_WRAP_REGEX, GENERIC_PREFIX_REGEX, goErrorWrapWithoutContextRule;
31425
+ var init_error_wrap_without_context = __esm({
31426
+ "src/rules/go/error-wrap-without-context.ts"() {
31427
+ "use strict";
31428
+ init_rule();
31429
+ ERR_WRAP_REGEX = /fmt\.Errorf\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*([^)]+)\)/g;
31430
+ GENERIC_PREFIX_REGEX = /^\s*(?:error|err|failed|wrap(?:ping)?|invalid)\s*(?::\s*)?%w\b/i;
31431
+ goErrorWrapWithoutContextRule = createRule({
31432
+ id: "go/error-wrap-without-context",
31433
+ category: "typo",
31434
+ severity: "low",
31435
+ aiSpecific: true,
31436
+ description: 'fmt.Errorf wrap without operation context \u2014 AI defaults to generic "error: %w"',
31437
+ create(_context) {
31438
+ return {};
31439
+ },
31440
+ analyze(_context, facts) {
31441
+ const issues = [];
31442
+ const source = facts.v2?._source;
31443
+ if (!source) return issues;
31444
+ let match;
31445
+ ERR_WRAP_REGEX.lastIndex = 0;
31446
+ while ((match = ERR_WRAP_REGEX.exec(source)) !== null) {
31447
+ const formatString = match[1];
31448
+ if (!formatString.includes("%w")) continue;
31449
+ if (formatString.length >= 30) continue;
31450
+ if (!GENERIC_PREFIX_REGEX.test(formatString)) continue;
31451
+ const line = source.slice(0, match.index).split("\n").length;
31452
+ issues.push({
31453
+ ruleId: "go/error-wrap-without-context",
31454
+ category: "typo",
31455
+ severity: "low",
31456
+ aiSpecific: true,
31457
+ message: `fmt.Errorf wrap with generic message "${formatString}" \u2014 include the failing operation`,
31458
+ line,
31459
+ column: match[0].indexOf("fmt") + 1,
31460
+ 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'
31461
+ });
31462
+ }
31463
+ return issues;
31464
+ }
31465
+ });
31466
+ }
31467
+ });
31468
+
31469
+ // src/rules/go/nil-slice-vs-empty.ts
31470
+ var NIL_SLICE_DECL_REGEX, EMPTY_SLICE_ASSIGN_REGEX, goNilSliceVsEmptyRule;
31471
+ var init_nil_slice_vs_empty = __esm({
31472
+ "src/rules/go/nil-slice-vs-empty.ts"() {
31473
+ "use strict";
31474
+ init_rule();
31475
+ NIL_SLICE_DECL_REGEX = /^[\t ]*var\s+([A-Za-z_][A-Za-z0-9_]*)\s+\[\][\w.*]+\b/gm;
31476
+ EMPTY_SLICE_ASSIGN_REGEX = /^[\t ]*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:\[\][\w.*]*\{\}|make\(\[\][\w.*]*)/gm;
31477
+ goNilSliceVsEmptyRule = createRule({
31478
+ id: "go/nil-slice-vs-empty",
31479
+ category: "typo",
31480
+ severity: "low",
31481
+ aiSpecific: true,
31482
+ description: "Variable declared `var x []int` but later assigned `x = []int{}` or `make([]int, n)` \u2014 pick one form",
31483
+ create(_context) {
31484
+ return {};
31485
+ },
31486
+ analyze(_context, facts) {
31487
+ const issues = [];
31488
+ const source = facts.v2?._source;
31489
+ if (!source) return issues;
31490
+ const nilDecls = /* @__PURE__ */ new Map();
31491
+ let m;
31492
+ NIL_SLICE_DECL_REGEX.lastIndex = 0;
31493
+ while ((m = NIL_SLICE_DECL_REGEX.exec(source)) !== null) {
31494
+ nilDecls.set(m[1], source.slice(0, m.index).split("\n").length);
31495
+ }
31496
+ if (nilDecls.size === 0) return issues;
31497
+ EMPTY_SLICE_ASSIGN_REGEX.lastIndex = 0;
31498
+ while ((m = EMPTY_SLICE_ASSIGN_REGEX.exec(source)) !== null) {
31499
+ const name = m[1];
31500
+ if (!nilDecls.has(name)) continue;
31501
+ const declLine = nilDecls.get(name);
31502
+ const assignLine = source.slice(0, m.index).split("\n").length;
31503
+ if (assignLine <= declLine) continue;
31504
+ issues.push({
31505
+ ruleId: "go/nil-slice-vs-empty",
31506
+ category: "typo",
31507
+ severity: "low",
31508
+ aiSpecific: true,
31509
+ message: `Variable '${name}' declared as nil slice (line ${declLine}) but assigned an empty slice (line ${assignLine}) \u2014 pick one form`,
31510
+ line: declLine,
31511
+ column: 1,
31512
+ 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."
31513
+ });
31514
+ }
31515
+ return issues;
31516
+ }
31517
+ });
31518
+ }
31519
+ });
31520
+
31521
+ // src/rules/go/struct-tag-inconsistency.ts
31522
+ var JSON_TAG_REGEX, goStructTagInconsistencyRule;
31523
+ var init_struct_tag_inconsistency = __esm({
31524
+ "src/rules/go/struct-tag-inconsistency.ts"() {
31525
+ "use strict";
31526
+ init_rule();
31527
+ JSON_TAG_REGEX = /`json:"([^",]+)(?:,([^"]+))?"`/g;
31528
+ goStructTagInconsistencyRule = createRule({
31529
+ id: "go/struct-tag-inconsistency",
31530
+ category: "typo",
31531
+ severity: "low",
31532
+ aiSpecific: true,
31533
+ description: 'Struct fields mix json:"foo" and json:"foo,omitempty" \u2014 pick one convention per struct',
31534
+ create(_context) {
31535
+ return {};
31536
+ },
31537
+ analyze(_context, facts) {
31538
+ const issues = [];
31539
+ const source = facts.v2?._source;
31540
+ if (!source) return issues;
31541
+ const structRegex = /type\s+[A-Z][A-Za-z0-9_]*\s+struct\s*\{/g;
31542
+ let structMatch;
31543
+ while ((structMatch = structRegex.exec(source)) !== null) {
31544
+ const startIdx = structMatch.index;
31545
+ const openBrace = source.indexOf("{", startIdx);
31546
+ if (openBrace < 0) continue;
31547
+ let depth = 1;
31548
+ let i = openBrace + 1;
31549
+ while (i < source.length && depth > 0) {
31550
+ const ch = source[i];
31551
+ if (ch === "{") depth++;
31552
+ else if (ch === "}") depth--;
31553
+ i++;
31554
+ }
31555
+ const structBody = source.slice(openBrace, i);
31556
+ const structLine = source.slice(0, startIdx).split("\n").length;
31557
+ const styleCount = {};
31558
+ const tagMatches = [];
31559
+ let m;
31560
+ JSON_TAG_REGEX.lastIndex = 0;
31561
+ while ((m = JSON_TAG_REGEX.exec(structBody)) !== null) {
31562
+ const tag = m[1];
31563
+ const options = m[2] ?? "";
31564
+ const style = options ? "with-options" : "no-options";
31565
+ styleCount[style] = (styleCount[style] ?? 0) + 1;
31566
+ tagMatches.push({ tag, style, idx: openBrace + m.index });
31567
+ }
31568
+ const styles = Object.keys(styleCount);
31569
+ if (styles.length < 2 || tagMatches.length < 2) continue;
31570
+ const dominant = styles.reduce(
31571
+ (a, b) => (styleCount[a] ?? 0) >= (styleCount[b] ?? 0) ? a : b
31572
+ );
31573
+ const minority = tagMatches.filter((t) => t.style !== dominant);
31574
+ if (minority.length === 0) continue;
31575
+ for (const m2 of minority) {
31576
+ const line = source.slice(0, m2.idx).split("\n").length;
31577
+ issues.push({
31578
+ ruleId: "go/struct-tag-inconsistency",
31579
+ category: "typo",
31580
+ severity: "low",
31581
+ aiSpecific: true,
31582
+ 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"}`,
31583
+ line,
31584
+ column: 1,
31585
+ 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.'
31586
+ });
31587
+ }
31588
+ if (issues.length > 0) break;
31589
+ }
31590
+ return issues;
31591
+ }
31592
+ });
31593
+ }
31594
+ });
31595
+
31356
31596
  // src/rules/layout/gap-monopoly.ts
31357
31597
  var GAP_RE, gapMonopolyRule;
31358
31598
  var init_gap_monopoly = __esm({
@@ -38641,91 +38881,6 @@ var init_key_prop_missing = __esm({
38641
38881
  }
38642
38882
  });
38643
38883
 
38644
- // src/rules/logic/ks-distribution-shift.ts
38645
- function extractFileFeatures(source) {
38646
- const lines = source.split("\n");
38647
- const lineLengths = lines.map((l) => l.length);
38648
- const identifierLengths = [];
38649
- const idRe = /[A-Za-z_$][A-Za-z0-9_$]*/g;
38650
- let m;
38651
- while ((m = idRe.exec(source)) !== null) {
38652
- identifierLengths.push(m[0].length);
38653
- }
38654
- const commentDensity = lines.map((l) => {
38655
- const trimmed = l.trim();
38656
- if (trimmed.length === 0) return 0;
38657
- const commentChars = (trimmed.match(/^\/\/.*$/)?.[0]?.length ?? 0) + (trimmed.match(/^\s*\/\*.*?\*\/\s*$/)?.at(0)?.length ?? 0);
38658
- return commentChars / trimmed.length;
38659
- });
38660
- return { lineLengths, identifierLengths, commentDensity };
38661
- }
38662
- var MIN_SAMPLES_PER_FEATURE, ksDistributionShiftRule;
38663
- var init_ks_distribution_shift = __esm({
38664
- "src/rules/logic/ks-distribution-shift.ts"() {
38665
- "use strict";
38666
- init_rule();
38667
- init_dist2();
38668
- init_corpus_baselines2();
38669
- MIN_SAMPLES_PER_FEATURE = 20;
38670
- ksDistributionShiftRule = createRule({
38671
- id: "logic/ks-distribution-shift",
38672
- category: "logic",
38673
- severity: "medium",
38674
- aiSpecific: false,
38675
- description: "Multi-feature Kolmogorov\u2013Smirnov distribution-shift vs corpus baseline (Bonferroni-corrected). Peer-reviewed ML distribution-shift detector (arXiv:2510.15996, Oct 2025).",
38676
- create(context) {
38677
- return context;
38678
- },
38679
- analyze(_context, facts) {
38680
- const issues = [];
38681
- if (!facts.v2) return issues;
38682
- const source = facts.v2._source ?? "";
38683
- if (source.length < 200) return issues;
38684
- const features = extractFileFeatures(source);
38685
- const samples = /* @__PURE__ */ new Map([
38686
- ["lineLengths", features.lineLengths],
38687
- ["identifierLengths", features.identifierLengths],
38688
- ["commentDensity", features.commentDensity]
38689
- ]);
38690
- const baselines = getCorpusBaselines();
38691
- const baselinesMap = /* @__PURE__ */ new Map();
38692
- if (baselines) {
38693
- baselinesMap.set("lineLengths", baselines.features.lineLengths.sample);
38694
- baselinesMap.set("identifierLengths", baselines.features.identifierLengths.sample);
38695
- baselinesMap.set("commentDensity", baselines.features.commentDensity.sample);
38696
- } else {
38697
- baselinesMap.set("lineLengths", [20, 25, 30, 32, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]);
38698
- baselinesMap.set("identifierLengths", [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 24, 28]);
38699
- baselinesMap.set("commentDensity", [0, 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5]);
38700
- }
38701
- for (const [name, vals] of samples) {
38702
- if (vals.length < MIN_SAMPLES_PER_FEATURE) samples.delete(name);
38703
- }
38704
- if (samples.size === 0) return issues;
38705
- const result = multiFeatureKsTest(samples, baselinesMap, 0.05);
38706
- if (!result.anySignificant) return issues;
38707
- const shifted = result.significantFeatures.join(", ");
38708
- const details = result.significantFeatures.map((name) => {
38709
- const r = result.perFeature.get(name);
38710
- if (!r) return name;
38711
- return `${name} (D=${r.statistic.toFixed(3)}, p=${r.pValue.toExponential(2)})`;
38712
- }).join("; ");
38713
- issues.push({
38714
- ruleId: "logic/ks-distribution-shift",
38715
- category: "logic",
38716
- severity: "medium",
38717
- aiSpecific: true,
38718
- message: `Distribution shift detected on ${result.significantFeatures.length} of ${result.perFeature.size} features (Bonferroni \u03B1=${result.bonferroniAlpha.toExponential(2)}). Features: ${shifted}. Detail: ${details}.`,
38719
- line: 1,
38720
- column: 1,
38721
- 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."
38722
- });
38723
- return issues;
38724
- }
38725
- });
38726
- }
38727
- });
38728
-
38729
38884
  // src/rules/logic/math-any-density.ts
38730
38885
  var ANY_PER_100_LINES, MIN_ABSOLUTE, COLON, ANY_KEYWORD, ANY_TYPE_RE, MSG_FRAGMENT, RULE_DESCRIPTION, RULE_MESSAGE, RULE_ADVICE, mathAnyDensityRule;
38731
38886
  var init_math_any_density = __esm({
@@ -38795,13 +38950,13 @@ var init_math_any_density = __esm({
38795
38950
  });
38796
38951
 
38797
38952
  // src/rules/logic/math-console-log-storm.ts
38798
- var WINDOW_SIZE, STORM_THRESHOLD, CONSOLE_LOG_RE, mathConsoleLogStormRule;
38953
+ var WINDOW_SIZE2, STORM_THRESHOLD, CONSOLE_LOG_RE, mathConsoleLogStormRule;
38799
38954
  var init_math_console_log_storm = __esm({
38800
38955
  "src/rules/logic/math-console-log-storm.ts"() {
38801
38956
  "use strict";
38802
38957
  init_rule();
38803
38958
  init_utils();
38804
- WINDOW_SIZE = 30;
38959
+ WINDOW_SIZE2 = 30;
38805
38960
  STORM_THRESHOLD = 5;
38806
38961
  CONSOLE_LOG_RE = /\bconsole\.log\s*\(/g;
38807
38962
  mathConsoleLogStormRule = createRule({
@@ -38832,7 +38987,7 @@ var init_math_console_log_storm = __esm({
38832
38987
  let maxEndLine = 0;
38833
38988
  let i = 0;
38834
38989
  for (let j = 0; j < lines.length; j++) {
38835
- while (lines[j] - lines[i] > WINDOW_SIZE) i++;
38990
+ while (lines[j] - lines[i] > WINDOW_SIZE2) i++;
38836
38991
  const count = j - i + 1;
38837
38992
  if (count > maxCount) {
38838
38993
  maxCount = count;
@@ -38846,7 +39001,7 @@ var init_math_console_log_storm = __esm({
38846
39001
  category: "logic",
38847
39002
  severity: "high",
38848
39003
  aiSpecific: true,
38849
- 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.`,
39004
+ 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.`,
38850
39005
  line: firstIdx >= 0 ? lines[firstIdx] : 1,
38851
39006
  column: firstIdx >= 0 ? columns[firstIdx] : 1,
38852
39007
  advice: "Replace debug logs with a proper debugger or logger.debug() \u2014 remove all console.log before shipping."
@@ -41670,6 +41825,251 @@ var init_weak_assertion = __esm({
41670
41825
  }
41671
41826
  });
41672
41827
 
41828
+ // src/rules/ts/enum-vs-as-const.ts
41829
+ var ENUM_DECL_REGEX, tsEnumVsAsConstRule;
41830
+ var init_enum_vs_as_const = __esm({
41831
+ "src/rules/ts/enum-vs-as-const.ts"() {
41832
+ "use strict";
41833
+ init_rule();
41834
+ ENUM_DECL_REGEX = /^[ \t]*(?:export\s+)?(?:const\s+)?enum\s+[A-Z_][A-Za-z0-9_]*\s*\{/gm;
41835
+ tsEnumVsAsConstRule = createRule({
41836
+ id: "ts/enum-vs-as-const",
41837
+ category: "typo",
41838
+ severity: "low",
41839
+ aiSpecific: true,
41840
+ description: "Uses `enum` \u2014 modern TS prefers `as const` objects",
41841
+ create(_context) {
41842
+ return {};
41843
+ },
41844
+ analyze(_context, facts) {
41845
+ const issues = [];
41846
+ const source = facts.v2?._source;
41847
+ if (!source) return issues;
41848
+ let match;
41849
+ ENUM_DECL_REGEX.lastIndex = 0;
41850
+ while ((match = ENUM_DECL_REGEX.exec(source)) !== null) {
41851
+ const line = source.slice(0, match.index).split("\n").length;
41852
+ issues.push({
41853
+ ruleId: "ts/enum-vs-as-const",
41854
+ category: "typo",
41855
+ severity: "low",
41856
+ aiSpecific: true,
41857
+ message: `'enum' is an AI / older-TS pattern \u2014 prefer 'as const' for a frozen object literal`,
41858
+ line,
41859
+ column: match[0].indexOf("enum") + 1,
41860
+ 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.'
41861
+ });
41862
+ }
41863
+ return issues;
41864
+ }
41865
+ });
41866
+ }
41867
+ });
41868
+
41869
+ // src/rules/ts/excessive-type-assertion.ts
41870
+ var DEFAULT_MAX, FN_DECL_REGEX, AS_REGEX, tsExcessiveTypeAssertionRule;
41871
+ var init_excessive_type_assertion = __esm({
41872
+ "src/rules/ts/excessive-type-assertion.ts"() {
41873
+ "use strict";
41874
+ init_rule();
41875
+ DEFAULT_MAX = 3;
41876
+ 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;
41877
+ AS_REGEX = /(?<![\w$])as\s+(?!const)([A-Z][\w$.,<>\[\]|&]*)/g;
41878
+ tsExcessiveTypeAssertionRule = createRule({
41879
+ id: "ts/excessive-type-assertion",
41880
+ category: "typo",
41881
+ severity: "low",
41882
+ aiSpecific: true,
41883
+ description: "Function with >3 `as` type assertions \u2014 likely AI fighting the type system",
41884
+ create(_context) {
41885
+ return { maxAssertionsPerFunction: DEFAULT_MAX };
41886
+ },
41887
+ analyze(context, facts) {
41888
+ const issues = [];
41889
+ const source = facts.v2?._source;
41890
+ if (!source) return issues;
41891
+ let match;
41892
+ FN_DECL_REGEX.lastIndex = 0;
41893
+ while ((match = FN_DECL_REGEX.exec(source)) !== null) {
41894
+ const startIdx = match.index;
41895
+ const openBraceIdx = source.indexOf("{", startIdx);
41896
+ if (openBraceIdx < 0) continue;
41897
+ let depth = 1;
41898
+ let i = openBraceIdx + 1;
41899
+ while (i < source.length && depth > 0) {
41900
+ const ch = source[i];
41901
+ if (ch === "{") depth++;
41902
+ else if (ch === "}") depth--;
41903
+ i++;
41904
+ }
41905
+ const body = source.slice(openBraceIdx, i);
41906
+ const line = source.slice(0, startIdx).split("\n").length;
41907
+ let asCount = 0;
41908
+ const seen = /* @__PURE__ */ new Set();
41909
+ let asMatch;
41910
+ AS_REGEX.lastIndex = 0;
41911
+ while ((asMatch = AS_REGEX.exec(body)) !== null) {
41912
+ const captured = asMatch[1];
41913
+ if (seen.has(captured)) continue;
41914
+ seen.add(captured);
41915
+ asCount++;
41916
+ }
41917
+ if (asCount > context.maxAssertionsPerFunction) {
41918
+ issues.push({
41919
+ ruleId: "ts/excessive-type-assertion",
41920
+ category: "typo",
41921
+ severity: "low",
41922
+ aiSpecific: true,
41923
+ message: `Function has ${asCount} 'as' assertions (max ${context.maxAssertionsPerFunction}) \u2014 likely AI fighting the type system`,
41924
+ line,
41925
+ column: 1,
41926
+ 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."
41927
+ });
41928
+ }
41929
+ }
41930
+ return issues;
41931
+ }
41932
+ });
41933
+ }
41934
+ });
41935
+
41936
+ // src/rules/ts/import-type-misuse.ts
41937
+ var INLINE_TYPE_IMPORT_REGEX, tsImportTypeMisuseRule;
41938
+ var init_import_type_misuse = __esm({
41939
+ "src/rules/ts/import-type-misuse.ts"() {
41940
+ "use strict";
41941
+ init_rule();
41942
+ INLINE_TYPE_IMPORT_REGEX = /^[ \t]*import\s*\{[^}]*\btype\s+[A-Za-z_]/gm;
41943
+ tsImportTypeMisuseRule = createRule({
41944
+ id: "ts/import-type-misuse",
41945
+ category: "typo",
41946
+ severity: "low",
41947
+ aiSpecific: true,
41948
+ description: "Inline `import { type X }` \u2014 prefer `import type { X }` for clarity",
41949
+ create(_context) {
41950
+ return {};
41951
+ },
41952
+ analyze(_context, facts) {
41953
+ const issues = [];
41954
+ const source = facts.v2?._source;
41955
+ if (!source) return issues;
41956
+ let match;
41957
+ INLINE_TYPE_IMPORT_REGEX.lastIndex = 0;
41958
+ while ((match = INLINE_TYPE_IMPORT_REGEX.exec(source)) !== null) {
41959
+ const line = source.slice(0, match.index).split("\n").length;
41960
+ issues.push({
41961
+ ruleId: "ts/import-type-misuse",
41962
+ category: "typo",
41963
+ severity: "low",
41964
+ aiSpecific: true,
41965
+ message: "Inline `type` in a value import \u2014 split into a separate `import type` statement",
41966
+ line,
41967
+ column: match[0].indexOf("type") + 1,
41968
+ 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.'
41969
+ });
41970
+ }
41971
+ return issues;
41972
+ }
41973
+ });
41974
+ }
41975
+ });
41976
+
41977
+ // src/rules/ts/never-vs-unknown.ts
41978
+ var NEVER_RETURN_REGEX, THROW_OR_LOOP_REGEX, tsNeverVsUnknownRule;
41979
+ var init_never_vs_unknown = __esm({
41980
+ "src/rules/ts/never-vs-unknown.ts"() {
41981
+ "use strict";
41982
+ init_rule();
41983
+ NEVER_RETURN_REGEX = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)[^{]*:\s*(?:Promise<)?never\b[^{]*\{/gm;
41984
+ THROW_OR_LOOP_REGEX = /\b(?:throw\b|while\s*\(|for\s*\(|process\.exit\b|System\.exit\b)/;
41985
+ tsNeverVsUnknownRule = createRule({
41986
+ id: "ts/never-vs-unknown",
41987
+ category: "typo",
41988
+ severity: "low",
41989
+ aiSpecific: true,
41990
+ description: "Return type `: never` but body has no throw / loop / exit \u2014 likely AI misuse",
41991
+ create(_context) {
41992
+ return {};
41993
+ },
41994
+ analyze(_context, facts) {
41995
+ const issues = [];
41996
+ const source = facts.v2?._source;
41997
+ if (!source) return issues;
41998
+ let match;
41999
+ NEVER_RETURN_REGEX.lastIndex = 0;
42000
+ while ((match = NEVER_RETURN_REGEX.exec(source)) !== null) {
42001
+ const fnName = match[1];
42002
+ const startIdx = match.index;
42003
+ const line = source.slice(0, startIdx).split("\n").length;
42004
+ const openBraceIdx = source.indexOf("{", startIdx);
42005
+ if (openBraceIdx < 0) continue;
42006
+ let depth = 1;
42007
+ let i = openBraceIdx + 1;
42008
+ while (i < source.length && depth > 0) {
42009
+ const ch = source[i];
42010
+ if (ch === "{") depth++;
42011
+ else if (ch === "}") depth--;
42012
+ i++;
42013
+ }
42014
+ const body = source.slice(openBraceIdx, i);
42015
+ if (THROW_OR_LOOP_REGEX.test(body)) continue;
42016
+ issues.push({
42017
+ ruleId: "ts/never-vs-unknown",
42018
+ category: "typo",
42019
+ severity: "low",
42020
+ aiSpecific: true,
42021
+ message: `Function '${fnName}' returns 'never' but its body has no throw, loop, or exit \u2014 likely AI misuse`,
42022
+ line,
42023
+ column: match[0].indexOf("never") + 1,
42024
+ 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.'
42025
+ });
42026
+ }
42027
+ return issues;
42028
+ }
42029
+ });
42030
+ }
42031
+ });
42032
+
42033
+ // src/rules/ts/optional-chain-overuse.ts
42034
+ var DEFAULT_MIN_CHAIN_LENGTH, tsOptionalChainOveruseRule;
42035
+ var init_optional_chain_overuse = __esm({
42036
+ "src/rules/ts/optional-chain-overuse.ts"() {
42037
+ "use strict";
42038
+ init_rule();
42039
+ DEFAULT_MIN_CHAIN_LENGTH = 5;
42040
+ tsOptionalChainOveruseRule = createRule({
42041
+ id: "ts/optional-chain-overuse",
42042
+ category: "logic",
42043
+ severity: "low",
42044
+ aiSpecific: true,
42045
+ description: "Optional chaining (?.) used 5+ times in a single chain \u2014 AI tends to chain rather than narrow",
42046
+ create(_context) {
42047
+ return { minChainLength: DEFAULT_MIN_CHAIN_LENGTH };
42048
+ },
42049
+ analyze(context, facts) {
42050
+ const issues = [];
42051
+ const expressions = facts.v2.logic?.logicalExpressions;
42052
+ if (!expressions) return issues;
42053
+ for (const expression of expressions) {
42054
+ if (expression.depth >= context.minChainLength && expression.isOptionalChainLike) {
42055
+ issues.push({
42056
+ ruleId: "ts/optional-chain-overuse",
42057
+ category: "logic",
42058
+ severity: "low",
42059
+ aiSpecific: true,
42060
+ message: `Optional chain depth ${expression.depth} \u2014 break with an intermediate variable or guard clause`,
42061
+ line: expression.line,
42062
+ column: expression.column,
42063
+ 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."
42064
+ });
42065
+ }
42066
+ }
42067
+ return issues;
42068
+ }
42069
+ });
42070
+ }
42071
+ });
42072
+
41673
42073
  // src/rules/typo/calc-fontsize.ts
41674
42074
  var FONT_SIZE_RE, calcFontsizeRule;
41675
42075
  var init_calc_fontsize = __esm({
@@ -45811,6 +46211,10 @@ var init_builtins = __esm({
45811
46211
  init_expired_code_example();
45812
46212
  init_stale_function_reference();
45813
46213
  init_stale_package_reference();
46214
+ init_identical_block();
46215
+ init_error_wrap_without_context();
46216
+ init_nil_slice_vs_empty();
46217
+ init_struct_tag_inconsistency();
45814
46218
  init_gap_monopoly();
45815
46219
  init_math_element_uniformity();
45816
46220
  init_math_grid_uniformity();
@@ -45820,7 +46224,6 @@ var init_builtins = __esm({
45820
46224
  init_ghost_defensive();
45821
46225
  init_heaps_deviation();
45822
46226
  init_key_prop_missing();
45823
- init_ks_distribution_shift();
45824
46227
  init_math_any_density();
45825
46228
  init_math_console_log_storm();
45826
46229
  init_math_gini_class_usage();
@@ -45854,6 +46257,11 @@ var init_builtins = __esm({
45854
46257
  init_fake_placeholder();
45855
46258
  init_missing_edge_case();
45856
46259
  init_weak_assertion();
46260
+ init_enum_vs_as_const();
46261
+ init_excessive_type_assertion();
46262
+ init_import_type_misuse();
46263
+ init_never_vs_unknown();
46264
+ init_optional_chain_overuse();
45857
46265
  init_calc_fontsize();
45858
46266
  init_calc_raw_px();
45859
46267
  init_clamp_offscale();
@@ -45916,6 +46324,10 @@ var init_builtins = __esm({
45916
46324
  expiredCodeExampleRule,
45917
46325
  staleFunctionReferenceRule,
45918
46326
  stalePackageReferenceRule,
46327
+ dupIdenticalBlockRule,
46328
+ goErrorWrapWithoutContextRule,
46329
+ goNilSliceVsEmptyRule,
46330
+ goStructTagInconsistencyRule,
45919
46331
  gapMonopolyRule,
45920
46332
  mathElementUniformityRule,
45921
46333
  mathGridUniformityRule,
@@ -45925,7 +46337,6 @@ var init_builtins = __esm({
45925
46337
  ghostDefensiveRule,
45926
46338
  heapsDeviationRule,
45927
46339
  keyPropMissingRule,
45928
- ksDistributionShiftRule,
45929
46340
  mathAnyDensityRule,
45930
46341
  mathConsoleLogStormRule,
45931
46342
  mathGiniClassUsageRule,
@@ -45959,6 +46370,11 @@ var init_builtins = __esm({
45959
46370
  fakePlaceholderRule,
45960
46371
  missingEdgeCaseRule,
45961
46372
  weakAssertionRule,
46373
+ tsEnumVsAsConstRule,
46374
+ tsExcessiveTypeAssertionRule,
46375
+ tsImportTypeMisuseRule,
46376
+ tsNeverVsUnknownRule,
46377
+ tsOptionalChainOveruseRule,
45962
46378
  calcFontsizeRule,
45963
46379
  calcRawPxRule,
45964
46380
  clampOffscaleRule,
@@ -48040,7 +48456,7 @@ var init_signal_strength = __esm({
48040
48456
  precision: 0.9966,
48041
48457
  lastCalibratedAt: "2026-07-01T00:00:00Z",
48042
48458
  verdict: "USEFUL",
48043
- _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).",
48459
+ _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.",
48044
48460
  aiSpecific: true,
48045
48461
  _v7Verdict: "USEFUL",
48046
48462
  _v7Lift: 182622.43,
@@ -48048,7 +48464,8 @@ var init_signal_strength = __esm({
48048
48464
  _v7FpRate: 0,
48049
48465
  _v7Precision: 0.9957,
48050
48466
  _v8Verdict: "USEFUL",
48051
- _v8Lift: 99999
48467
+ _v8Lift: 99999,
48468
+ defaultOff: false
48052
48469
  },
48053
48470
  "ai/errors-near-eof": {
48054
48471
  recall: 0.0948,
@@ -48297,7 +48714,7 @@ var init_signal_strength = __esm({
48297
48714
  precision: 0.9512,
48298
48715
  lastCalibratedAt: "2026-07-01T00:00:00Z",
48299
48716
  verdict: "USEFUL",
48300
- _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).",
48717
+ _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.",
48301
48718
  aiSpecific: true,
48302
48719
  _v7Verdict: "USEFUL",
48303
48720
  _v7Lift: 7099.57,
@@ -48305,7 +48722,8 @@ var init_signal_strength = __esm({
48305
48722
  _v7FpRate: 1e-4,
48306
48723
  _v7Precision: 0.929,
48307
48724
  _v8Verdict: "USEFUL",
48308
- _v8Lift: 99999
48725
+ _v8Lift: 99999,
48726
+ defaultOff: false
48309
48727
  },
48310
48728
  "context/import-path-mismatch": {
48311
48729
  recall: 0.0681,
@@ -48489,7 +48907,7 @@ var init_signal_strength = __esm({
48489
48907
  precision: 0.8834,
48490
48908
  lastCalibratedAt: "2026-07-01T00:00:00Z",
48491
48909
  verdict: "USEFUL",
48492
- _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.",
48910
+ _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.",
48493
48911
  aiSpecific: true,
48494
48912
  _v7Verdict: "DORMANT",
48495
48913
  _v7Lift: 1,
@@ -48497,7 +48915,8 @@ var init_signal_strength = __esm({
48497
48915
  _v7FpRate: 0,
48498
48916
  _v7Precision: 0,
48499
48917
  _v8Verdict: "USEFUL",
48500
- _v8Lift: 32.74
48918
+ _v8Lift: 32.74,
48919
+ defaultOff: false
48501
48920
  },
48502
48921
  "dead/unused-parameter": {
48503
48922
  recall: 8e-4,
@@ -48585,6 +49004,78 @@ var init_signal_strength = __esm({
48585
49004
  _v8Verdict: "OK",
48586
49005
  _v8Lift: 255.9
48587
49006
  },
49007
+ "dup/identical-block": {
49008
+ recall: 0,
49009
+ fpRate: 0,
49010
+ ratio: 1,
49011
+ precision: 0,
49012
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49013
+ verdict: "DORMANT",
49014
+ _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.",
49015
+ aiSpecific: false,
49016
+ _v7Verdict: "DORMANT",
49017
+ _v7Lift: 1,
49018
+ _v7Recall: 0,
49019
+ _v7FpRate: 0,
49020
+ _v7Precision: 0,
49021
+ _v8Verdict: "DORMANT",
49022
+ _v8Lift: 1,
49023
+ defaultOff: true
49024
+ },
49025
+ "go/error-wrap-without-context": {
49026
+ recall: 0,
49027
+ fpRate: 0,
49028
+ ratio: 1,
49029
+ precision: 0,
49030
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49031
+ verdict: "DORMANT",
49032
+ _calibrationNote: "v0.19: new rule (fmt.Errorf wrap with generic message \u2014 needs operation context). Not yet calibrated. Scheduled for v9 calibration.",
49033
+ aiSpecific: true,
49034
+ _v7Verdict: "DORMANT",
49035
+ _v7Lift: 1,
49036
+ _v7Recall: 0,
49037
+ _v7FpRate: 0,
49038
+ _v7Precision: 0,
49039
+ _v8Verdict: "DORMANT",
49040
+ _v8Lift: 1,
49041
+ defaultOff: true
49042
+ },
49043
+ "go/nil-slice-vs-empty": {
49044
+ recall: 0,
49045
+ fpRate: 0,
49046
+ ratio: 1,
49047
+ precision: 0,
49048
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49049
+ verdict: "DORMANT",
49050
+ _calibrationNote: "v0.19: new rule (Variable declared nil slice but assigned empty slice). Not yet calibrated. Scheduled for v9 calibration.",
49051
+ aiSpecific: true,
49052
+ _v7Verdict: "DORMANT",
49053
+ _v7Lift: 1,
49054
+ _v7Recall: 0,
49055
+ _v7FpRate: 0,
49056
+ _v7Precision: 0,
49057
+ _v8Verdict: "DORMANT",
49058
+ _v8Lift: 1,
49059
+ defaultOff: true
49060
+ },
49061
+ "go/struct-tag-inconsistency": {
49062
+ recall: 0,
49063
+ fpRate: 0,
49064
+ ratio: 1,
49065
+ precision: 0,
49066
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49067
+ verdict: "DORMANT",
49068
+ _calibrationNote: "v0.19: new rule (Struct fields mix json tag styles). Not yet calibrated. Scheduled for v9 calibration.",
49069
+ aiSpecific: true,
49070
+ _v7Verdict: "DORMANT",
49071
+ _v7Lift: 1,
49072
+ _v7Recall: 0,
49073
+ _v7FpRate: 0,
49074
+ _v7Precision: 0,
49075
+ _v8Verdict: "DORMANT",
49076
+ _v8Lift: 1,
49077
+ defaultOff: true
49078
+ },
48588
49079
  "layout/forced-layout": {
48589
49080
  recall: 0,
48590
49081
  fpRate: 0,
@@ -48713,7 +49204,7 @@ var init_signal_strength = __esm({
48713
49204
  precision: 0.8889,
48714
49205
  lastCalibratedAt: "2026-07-01T00:00:00Z",
48715
49206
  verdict: "USEFUL",
48716
- _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).",
49207
+ _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.",
48717
49208
  aiSpecific: true,
48718
49209
  _v7Verdict: "USEFUL",
48719
49210
  _v7Lift: 80917.5,
@@ -48721,7 +49212,8 @@ var init_signal_strength = __esm({
48721
49212
  _v7FpRate: 0,
48722
49213
  _v7Precision: 0.8824,
48723
49214
  _v8Verdict: "USEFUL",
48724
- _v8Lift: 99999
49215
+ _v8Lift: 99999,
49216
+ defaultOff: false
48725
49217
  },
48726
49218
  "logic/heaps-deviation": {
48727
49219
  recall: 0.0126,
@@ -48757,24 +49249,6 @@ var init_signal_strength = __esm({
48757
49249
  _v8Verdict: "USEFUL",
48758
49250
  _v8Lift: 1760.69
48759
49251
  },
48760
- "logic/ks-distribution-shift": {
48761
- recall: 0.6889,
48762
- fpRate: 0.4411,
48763
- ratio: 1.46,
48764
- precision: 0.6457,
48765
- lastCalibratedAt: "2026-07-01T00:00:00Z",
48766
- verdict: "NOISY",
48767
- _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).",
48768
- aiSpecific: false,
48769
- _v7Verdict: "USEFUL",
48770
- _v7Lift: 2.09,
48771
- _v7Recall: 0.6431,
48772
- _v7FpRate: 0.34,
48773
- _v7Precision: 0.7096,
48774
- _v8Verdict: "INVERTED",
48775
- _v8Lift: 0.71,
48776
- defaultOff: true
48777
- },
48778
49252
  "logic/math-any-density": {
48779
49253
  recall: 17e-4,
48780
49254
  fpRate: 13e-4,
@@ -49140,7 +49614,7 @@ var init_signal_strength = __esm({
49140
49614
  precision: 1,
49141
49615
  lastCalibratedAt: "2026-07-01T00:00:00Z",
49142
49616
  verdict: "USEFUL",
49143
- _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).",
49617
+ _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.",
49144
49618
  aiSpecific: true,
49145
49619
  _v7Verdict: "USEFUL",
49146
49620
  _v7Lift: 99999,
@@ -49148,7 +49622,8 @@ var init_signal_strength = __esm({
49148
49622
  _v7FpRate: 0,
49149
49623
  _v7Precision: 1,
49150
49624
  _v8Verdict: "DORMANT",
49151
- _v8Lift: 1
49625
+ _v8Lift: 1,
49626
+ defaultOff: false
49152
49627
  },
49153
49628
  "security/hardcoded-secret": {
49154
49629
  recall: 14e-4,
@@ -49338,6 +49813,96 @@ var init_signal_strength = __esm({
49338
49813
  _v8Verdict: "USEFUL",
49339
49814
  _v8Lift: 651.94
49340
49815
  },
49816
+ "ts/enum-vs-as-const": {
49817
+ recall: 0,
49818
+ fpRate: 0,
49819
+ ratio: 1,
49820
+ precision: 0,
49821
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49822
+ verdict: "DORMANT",
49823
+ _calibrationNote: "v0.19: new rule (Uses `enum` \u2014 modern TS prefers `as const`). Not yet calibrated. Scheduled for v9 calibration.",
49824
+ aiSpecific: true,
49825
+ _v7Verdict: "DORMANT",
49826
+ _v7Lift: 1,
49827
+ _v7Recall: 0,
49828
+ _v7FpRate: 0,
49829
+ _v7Precision: 0,
49830
+ _v8Verdict: "DORMANT",
49831
+ _v8Lift: 1,
49832
+ defaultOff: true
49833
+ },
49834
+ "ts/excessive-type-assertion": {
49835
+ recall: 0,
49836
+ fpRate: 0,
49837
+ ratio: 1,
49838
+ precision: 0,
49839
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49840
+ verdict: "DORMANT",
49841
+ _calibrationNote: "v0.19: new rule (Function with >3 `as` assertions \u2014 AI fighting the type system). Not yet calibrated. Scheduled for v9 calibration.",
49842
+ aiSpecific: true,
49843
+ _v7Verdict: "DORMANT",
49844
+ _v7Lift: 1,
49845
+ _v7Recall: 0,
49846
+ _v7FpRate: 0,
49847
+ _v7Precision: 0,
49848
+ _v8Verdict: "DORMANT",
49849
+ _v8Lift: 1,
49850
+ defaultOff: true
49851
+ },
49852
+ "ts/import-type-misuse": {
49853
+ recall: 0,
49854
+ fpRate: 0,
49855
+ ratio: 1,
49856
+ precision: 0,
49857
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49858
+ verdict: "DORMANT",
49859
+ _calibrationNote: "v0.19: new rule (Inline `import { type X }` \u2014 prefer separate `import type`). Not yet calibrated. Scheduled for v9 calibration.",
49860
+ aiSpecific: true,
49861
+ _v7Verdict: "DORMANT",
49862
+ _v7Lift: 1,
49863
+ _v7Recall: 0,
49864
+ _v7FpRate: 0,
49865
+ _v7Precision: 0,
49866
+ _v8Verdict: "DORMANT",
49867
+ _v8Lift: 1,
49868
+ defaultOff: true
49869
+ },
49870
+ "ts/never-vs-unknown": {
49871
+ recall: 0,
49872
+ fpRate: 0,
49873
+ ratio: 1,
49874
+ precision: 0,
49875
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49876
+ verdict: "DORMANT",
49877
+ _calibrationNote: "v0.19: new rule (Return type `never` but body has no throw/loop/exit). Not yet calibrated. Scheduled for v9 calibration.",
49878
+ aiSpecific: true,
49879
+ _v7Verdict: "DORMANT",
49880
+ _v7Lift: 1,
49881
+ _v7Recall: 0,
49882
+ _v7FpRate: 0,
49883
+ _v7Precision: 0,
49884
+ _v8Verdict: "DORMANT",
49885
+ _v8Lift: 1,
49886
+ defaultOff: true
49887
+ },
49888
+ "ts/optional-chain-overuse": {
49889
+ recall: 0,
49890
+ fpRate: 0,
49891
+ ratio: 1,
49892
+ precision: 0,
49893
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49894
+ verdict: "DORMANT",
49895
+ _calibrationNote: "v0.19: new rule (Optional chain depth >= 5 \u2014 AI chains ?. rather than narrowing). Not yet calibrated. Scheduled for v9 calibration.",
49896
+ aiSpecific: true,
49897
+ _v7Verdict: "DORMANT",
49898
+ _v7Lift: 1,
49899
+ _v7Recall: 0,
49900
+ _v7FpRate: 0,
49901
+ _v7Precision: 0,
49902
+ _v8Verdict: "DORMANT",
49903
+ _v8Lift: 1,
49904
+ defaultOff: true
49905
+ },
49341
49906
  "typo/calc-fontsize": {
49342
49907
  recall: 0,
49343
49908
  fpRate: 0,
@@ -49640,7 +50205,7 @@ var init_signal_strength = __esm({
49640
50205
  precision: 0.9774,
49641
50206
  lastCalibratedAt: "2026-07-01T00:00:00Z",
49642
50207
  verdict: "USEFUL",
49643
- _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).",
50208
+ _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.",
49644
50209
  aiSpecific: false,
49645
50210
  _v7Verdict: "USEFUL",
49646
50211
  _v7Lift: 59285.01,
@@ -49648,7 +50213,8 @@ var init_signal_strength = __esm({
49648
50213
  _v7FpRate: 0,
49649
50214
  _v7Precision: 0.9697,
49650
50215
  _v8Verdict: "USEFUL",
49651
- _v8Lift: 99999
50216
+ _v8Lift: 99999,
50217
+ defaultOff: false
49652
50218
  },
49653
50219
  "visual/spacing-scale-violation": {
49654
50220
  recall: 86e-4,
@@ -56730,7 +57296,19 @@ var RULE_HINTS = {
56730
57296
  "rust/unused-pub-fn": "Remove the function or call it. Rust's compiler doesn't warn on `pub fn` references unless `#![warn(dead_code)]` is set at the crate root. AI agents leave these behind during iterative refactors.",
56731
57297
  "rust/unwrap-in-production": "Replace `.unwrap()` / `.expect()` with `?` (early-return on Err), `.map_err(...)` for error conversion, or an explicit `match`. They panic in production; reserve them for `#[cfg(test)]` or `#[test]` scopes.",
56732
57298
  "rust/todo-macro": "Implement the function body or remove the stub. `todo!()` / `unimplemented!()` both expand to `panic!()`, so a hit at runtime crashes the program. Move them into a `#[cfg(test)] mod tests` block to suppress.",
56733
- "rust/stringly-typed": 'Replace the `String` / `&str` parameter with the typed enum that already exists in the file. Stringly-typed APIs lose type information at the boundary \u2014 typos (`"Click"` vs `"click"`) only fail at runtime.'
57299
+ "rust/stringly-typed": 'Replace the `String` / `&str` parameter with the typed enum that already exists in the file. Stringly-typed APIs lose type information at the boundary \u2014 typos (`"Click"` vs `"click"`) only fail at runtime.',
57300
+ // v0.19.0 — TypeScript-specific rules
57301
+ "ts/optional-chain-overuse": "Break long `?.` chains with an intermediate variable or guard clause (`if (!value) return;`). Long chains hide the type-narrowing that should be explicit. AI agents chain `?.` to avoid null checks.",
57302
+ "ts/enum-vs-as-const": "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.",
57303
+ "ts/import-type-misuse": "Use `import type { X } from '...'` instead of `import { type X } from '...'`. The split form is more common in real codebases and makes the type-only intent unambiguous.",
57304
+ "ts/never-vs-unknown": "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.",
57305
+ "ts/excessive-type-assertion": "More than 3 `as` assertions in one 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.",
57306
+ // v0.19.0 — Go-specific rules
57307
+ "go/error-wrap-without-context": 'Use `fmt.Errorf("opening config: %w", err)` not `fmt.Errorf("error: %w", err)`. Real Go errors include the failing operation. See golang.org/wiki/CodeReviewComments#error-strings.',
57308
+ "go/struct-tag-inconsistency": '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).',
57309
+ "go/nil-slice-vs-empty": "Either declare as `var x = []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.",
57310
+ // v0.19.0 — Duplication detector
57311
+ "dup/identical-block": "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."
56734
57312
  };
56735
57313
 
56736
57314
  // src/cli/commands/explain.ts