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.js CHANGED
@@ -19,7 +19,7 @@ var VERSION;
19
19
  var init_header = __esm({
20
20
  "src/types/_header.ts"() {
21
21
  "use strict";
22
- VERSION = "0.18.9";
22
+ VERSION = "0.19.0";
23
23
  }
24
24
  });
25
25
 
@@ -31335,6 +31335,246 @@ var init_broken_link = __esm({
31335
31335
  }
31336
31336
  });
31337
31337
 
31338
+ // src/rules/dup/identical-block.ts
31339
+ import * as crypto from "crypto";
31340
+ function normalizeAndHash(lines) {
31341
+ const normalized = lines.map(
31342
+ (line) => line.replace(/\/\/.*$/, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim()
31343
+ ).filter((line) => line.length > 0).join("\n");
31344
+ if (normalized.length < MIN_NORMALIZED_LENGTH) return void 0;
31345
+ return crypto.createHash("sha1").update(normalized).digest("hex").slice(0, HASH_PREFIX_LENGTH);
31346
+ }
31347
+ var WINDOW_SIZE, MIN_NORMALIZED_LENGTH, HASH_PREFIX_LENGTH, DEDUP_CACHE, dupIdenticalBlockRule;
31348
+ var init_identical_block = __esm({
31349
+ "src/rules/dup/identical-block.ts"() {
31350
+ "use strict";
31351
+ init_rule();
31352
+ WINDOW_SIZE = 10;
31353
+ MIN_NORMALIZED_LENGTH = 40;
31354
+ HASH_PREFIX_LENGTH = 16;
31355
+ DEDUP_CACHE = /* @__PURE__ */ new Map();
31356
+ dupIdenticalBlockRule = createRule({
31357
+ id: "dup/identical-block",
31358
+ category: "logic",
31359
+ severity: "medium",
31360
+ aiSpecific: false,
31361
+ description: "Block of >=10 lines is identical across >=2 files (Type-1 clone detector)",
31362
+ create(_context) {
31363
+ return {};
31364
+ },
31365
+ analyze(_context, facts) {
31366
+ const issues = [];
31367
+ const source = facts.v2?._source;
31368
+ if (!source) return issues;
31369
+ const filePath = facts.filePath;
31370
+ const lines = source.split("\n");
31371
+ for (let i = 0; i <= lines.length - WINDOW_SIZE; i++) {
31372
+ const window = lines.slice(i, i + WINDOW_SIZE);
31373
+ const hash = normalizeAndHash(window);
31374
+ if (!hash) continue;
31375
+ const existing = DEDUP_CACHE.get(hash) ?? [];
31376
+ const matches = existing.filter((m) => m.file !== filePath);
31377
+ for (const match of matches) {
31378
+ issues.push({
31379
+ ruleId: "dup/identical-block",
31380
+ category: "logic",
31381
+ severity: "medium",
31382
+ aiSpecific: false,
31383
+ message: `Identical ${WINDOW_SIZE}-line block at line ${i + 1} also appears in ${match.file}:${match.line + 1}`,
31384
+ line: i + 1,
31385
+ column: 0,
31386
+ 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.",
31387
+ extras: {
31388
+ duplicateOf: {
31389
+ file: match.file,
31390
+ line: match.line + 1,
31391
+ hash
31392
+ }
31393
+ }
31394
+ });
31395
+ }
31396
+ existing.push({ file: filePath, line: i });
31397
+ DEDUP_CACHE.set(hash, existing);
31398
+ }
31399
+ return issues;
31400
+ }
31401
+ });
31402
+ }
31403
+ });
31404
+
31405
+ // src/rules/go/error-wrap-without-context.ts
31406
+ var ERR_WRAP_REGEX, GENERIC_PREFIX_REGEX, goErrorWrapWithoutContextRule;
31407
+ var init_error_wrap_without_context = __esm({
31408
+ "src/rules/go/error-wrap-without-context.ts"() {
31409
+ "use strict";
31410
+ init_rule();
31411
+ ERR_WRAP_REGEX = /fmt\.Errorf\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*([^)]+)\)/g;
31412
+ GENERIC_PREFIX_REGEX = /^\s*(?:error|err|failed|wrap(?:ping)?|invalid)\s*(?::\s*)?%w\b/i;
31413
+ goErrorWrapWithoutContextRule = createRule({
31414
+ id: "go/error-wrap-without-context",
31415
+ category: "typo",
31416
+ severity: "low",
31417
+ aiSpecific: true,
31418
+ description: 'fmt.Errorf wrap without operation context \u2014 AI defaults to generic "error: %w"',
31419
+ create(_context) {
31420
+ return {};
31421
+ },
31422
+ analyze(_context, facts) {
31423
+ const issues = [];
31424
+ const source = facts.v2?._source;
31425
+ if (!source) return issues;
31426
+ let match;
31427
+ ERR_WRAP_REGEX.lastIndex = 0;
31428
+ while ((match = ERR_WRAP_REGEX.exec(source)) !== null) {
31429
+ const formatString = match[1];
31430
+ if (!formatString.includes("%w")) continue;
31431
+ if (formatString.length >= 30) continue;
31432
+ if (!GENERIC_PREFIX_REGEX.test(formatString)) continue;
31433
+ const line = source.slice(0, match.index).split("\n").length;
31434
+ issues.push({
31435
+ ruleId: "go/error-wrap-without-context",
31436
+ category: "typo",
31437
+ severity: "low",
31438
+ aiSpecific: true,
31439
+ message: `fmt.Errorf wrap with generic message "${formatString}" \u2014 include the failing operation`,
31440
+ line,
31441
+ column: match[0].indexOf("fmt") + 1,
31442
+ 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'
31443
+ });
31444
+ }
31445
+ return issues;
31446
+ }
31447
+ });
31448
+ }
31449
+ });
31450
+
31451
+ // src/rules/go/nil-slice-vs-empty.ts
31452
+ var NIL_SLICE_DECL_REGEX, EMPTY_SLICE_ASSIGN_REGEX, goNilSliceVsEmptyRule;
31453
+ var init_nil_slice_vs_empty = __esm({
31454
+ "src/rules/go/nil-slice-vs-empty.ts"() {
31455
+ "use strict";
31456
+ init_rule();
31457
+ NIL_SLICE_DECL_REGEX = /^[\t ]*var\s+([A-Za-z_][A-Za-z0-9_]*)\s+\[\][\w.*]+\b/gm;
31458
+ EMPTY_SLICE_ASSIGN_REGEX = /^[\t ]*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:\[\][\w.*]*\{\}|make\(\[\][\w.*]*)/gm;
31459
+ goNilSliceVsEmptyRule = createRule({
31460
+ id: "go/nil-slice-vs-empty",
31461
+ category: "typo",
31462
+ severity: "low",
31463
+ aiSpecific: true,
31464
+ description: "Variable declared `var x []int` but later assigned `x = []int{}` or `make([]int, n)` \u2014 pick one form",
31465
+ create(_context) {
31466
+ return {};
31467
+ },
31468
+ analyze(_context, facts) {
31469
+ const issues = [];
31470
+ const source = facts.v2?._source;
31471
+ if (!source) return issues;
31472
+ const nilDecls = /* @__PURE__ */ new Map();
31473
+ let m;
31474
+ NIL_SLICE_DECL_REGEX.lastIndex = 0;
31475
+ while ((m = NIL_SLICE_DECL_REGEX.exec(source)) !== null) {
31476
+ nilDecls.set(m[1], source.slice(0, m.index).split("\n").length);
31477
+ }
31478
+ if (nilDecls.size === 0) return issues;
31479
+ EMPTY_SLICE_ASSIGN_REGEX.lastIndex = 0;
31480
+ while ((m = EMPTY_SLICE_ASSIGN_REGEX.exec(source)) !== null) {
31481
+ const name = m[1];
31482
+ if (!nilDecls.has(name)) continue;
31483
+ const declLine = nilDecls.get(name);
31484
+ const assignLine = source.slice(0, m.index).split("\n").length;
31485
+ if (assignLine <= declLine) continue;
31486
+ issues.push({
31487
+ ruleId: "go/nil-slice-vs-empty",
31488
+ category: "typo",
31489
+ severity: "low",
31490
+ aiSpecific: true,
31491
+ message: `Variable '${name}' declared as nil slice (line ${declLine}) but assigned an empty slice (line ${assignLine}) \u2014 pick one form`,
31492
+ line: declLine,
31493
+ column: 1,
31494
+ 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."
31495
+ });
31496
+ }
31497
+ return issues;
31498
+ }
31499
+ });
31500
+ }
31501
+ });
31502
+
31503
+ // src/rules/go/struct-tag-inconsistency.ts
31504
+ var JSON_TAG_REGEX, goStructTagInconsistencyRule;
31505
+ var init_struct_tag_inconsistency = __esm({
31506
+ "src/rules/go/struct-tag-inconsistency.ts"() {
31507
+ "use strict";
31508
+ init_rule();
31509
+ JSON_TAG_REGEX = /`json:"([^",]+)(?:,([^"]+))?"`/g;
31510
+ goStructTagInconsistencyRule = createRule({
31511
+ id: "go/struct-tag-inconsistency",
31512
+ category: "typo",
31513
+ severity: "low",
31514
+ aiSpecific: true,
31515
+ description: 'Struct fields mix json:"foo" and json:"foo,omitempty" \u2014 pick one convention per struct',
31516
+ create(_context) {
31517
+ return {};
31518
+ },
31519
+ analyze(_context, facts) {
31520
+ const issues = [];
31521
+ const source = facts.v2?._source;
31522
+ if (!source) return issues;
31523
+ const structRegex = /type\s+[A-Z][A-Za-z0-9_]*\s+struct\s*\{/g;
31524
+ let structMatch;
31525
+ while ((structMatch = structRegex.exec(source)) !== null) {
31526
+ const startIdx = structMatch.index;
31527
+ const openBrace = source.indexOf("{", startIdx);
31528
+ if (openBrace < 0) continue;
31529
+ let depth = 1;
31530
+ let i = openBrace + 1;
31531
+ while (i < source.length && depth > 0) {
31532
+ const ch = source[i];
31533
+ if (ch === "{") depth++;
31534
+ else if (ch === "}") depth--;
31535
+ i++;
31536
+ }
31537
+ const structBody = source.slice(openBrace, i);
31538
+ const structLine = source.slice(0, startIdx).split("\n").length;
31539
+ const styleCount = {};
31540
+ const tagMatches = [];
31541
+ let m;
31542
+ JSON_TAG_REGEX.lastIndex = 0;
31543
+ while ((m = JSON_TAG_REGEX.exec(structBody)) !== null) {
31544
+ const tag = m[1];
31545
+ const options = m[2] ?? "";
31546
+ const style = options ? "with-options" : "no-options";
31547
+ styleCount[style] = (styleCount[style] ?? 0) + 1;
31548
+ tagMatches.push({ tag, style, idx: openBrace + m.index });
31549
+ }
31550
+ const styles = Object.keys(styleCount);
31551
+ if (styles.length < 2 || tagMatches.length < 2) continue;
31552
+ const dominant = styles.reduce(
31553
+ (a, b) => (styleCount[a] ?? 0) >= (styleCount[b] ?? 0) ? a : b
31554
+ );
31555
+ const minority = tagMatches.filter((t) => t.style !== dominant);
31556
+ if (minority.length === 0) continue;
31557
+ for (const m2 of minority) {
31558
+ const line = source.slice(0, m2.idx).split("\n").length;
31559
+ issues.push({
31560
+ ruleId: "go/struct-tag-inconsistency",
31561
+ category: "typo",
31562
+ severity: "low",
31563
+ aiSpecific: true,
31564
+ 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"}`,
31565
+ line,
31566
+ column: 1,
31567
+ 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.'
31568
+ });
31569
+ }
31570
+ if (issues.length > 0) break;
31571
+ }
31572
+ return issues;
31573
+ }
31574
+ });
31575
+ }
31576
+ });
31577
+
31338
31578
  // src/rules/layout/gap-monopoly.ts
31339
31579
  var GAP_RE, gapMonopolyRule;
31340
31580
  var init_gap_monopoly = __esm({
@@ -36225,11 +36465,11 @@ __export(dist_exports2, {
36225
36465
  tokenizeIdentifiers: () => tokenizeIdentifiers
36226
36466
  });
36227
36467
  import { readFile, writeFile, mkdir, access } from "fs/promises";
36228
- import { createHash } from "crypto";
36468
+ import { createHash as createHash2 } from "crypto";
36229
36469
  import { join as join10, dirname as dirname6 } from "path";
36230
36470
  import { parseSync } from "@swc/core";
36231
36471
  import { join as join22 } from "path";
36232
- import { createHash as createHash2 } from "crypto";
36472
+ import { createHash as createHash22 } from "crypto";
36233
36473
  import { createHash as createHash3 } from "crypto";
36234
36474
  import { readFile as readFile2 } from "fs/promises";
36235
36475
  import { relative as relative3 } from "path";
@@ -36483,7 +36723,7 @@ function legacyCacheRoot() {
36483
36723
  return join10(process.cwd(), ".slopbrick", "cache", "ast");
36484
36724
  }
36485
36725
  function hashContent(content) {
36486
- return createHash("md5").update(content, "utf-8").digest("hex");
36726
+ return createHash2("md5").update(content, "utf-8").digest("hex");
36487
36727
  }
36488
36728
  function cachePathWithRoot(content, root) {
36489
36729
  return join10(root, `${hashContent(content)}.json`);
@@ -36681,7 +36921,7 @@ function fingerprintFor(component) {
36681
36921
  const hooks = component.hookCalls.map((h) => h.name).sort();
36682
36922
  const props = component.props.map((p) => p.name).sort();
36683
36923
  const payload = `${component.name}|${hooks.join(",")}|${props.join(",")}`;
36684
- return createHash2("sha256").update(payload).digest("hex").slice(0, 16);
36924
+ return createHash22("sha256").update(payload).digest("hex").slice(0, 16);
36685
36925
  }
36686
36926
  function buildComponentFingerprints(results) {
36687
36927
  const byName = /* @__PURE__ */ new Map();
@@ -38623,91 +38863,6 @@ var init_key_prop_missing = __esm({
38623
38863
  }
38624
38864
  });
38625
38865
 
38626
- // src/rules/logic/ks-distribution-shift.ts
38627
- function extractFileFeatures(source) {
38628
- const lines = source.split("\n");
38629
- const lineLengths = lines.map((l) => l.length);
38630
- const identifierLengths = [];
38631
- const idRe = /[A-Za-z_$][A-Za-z0-9_$]*/g;
38632
- let m;
38633
- while ((m = idRe.exec(source)) !== null) {
38634
- identifierLengths.push(m[0].length);
38635
- }
38636
- const commentDensity = lines.map((l) => {
38637
- const trimmed = l.trim();
38638
- if (trimmed.length === 0) return 0;
38639
- const commentChars = (trimmed.match(/^\/\/.*$/)?.[0]?.length ?? 0) + (trimmed.match(/^\s*\/\*.*?\*\/\s*$/)?.at(0)?.length ?? 0);
38640
- return commentChars / trimmed.length;
38641
- });
38642
- return { lineLengths, identifierLengths, commentDensity };
38643
- }
38644
- var MIN_SAMPLES_PER_FEATURE, ksDistributionShiftRule;
38645
- var init_ks_distribution_shift = __esm({
38646
- "src/rules/logic/ks-distribution-shift.ts"() {
38647
- "use strict";
38648
- init_rule();
38649
- init_dist2();
38650
- init_corpus_baselines2();
38651
- MIN_SAMPLES_PER_FEATURE = 20;
38652
- ksDistributionShiftRule = createRule({
38653
- id: "logic/ks-distribution-shift",
38654
- category: "logic",
38655
- severity: "medium",
38656
- aiSpecific: false,
38657
- description: "Multi-feature Kolmogorov\u2013Smirnov distribution-shift vs corpus baseline (Bonferroni-corrected). Peer-reviewed ML distribution-shift detector (arXiv:2510.15996, Oct 2025).",
38658
- create(context) {
38659
- return context;
38660
- },
38661
- analyze(_context, facts) {
38662
- const issues = [];
38663
- if (!facts.v2) return issues;
38664
- const source = facts.v2._source ?? "";
38665
- if (source.length < 200) return issues;
38666
- const features = extractFileFeatures(source);
38667
- const samples = /* @__PURE__ */ new Map([
38668
- ["lineLengths", features.lineLengths],
38669
- ["identifierLengths", features.identifierLengths],
38670
- ["commentDensity", features.commentDensity]
38671
- ]);
38672
- const baselines = getCorpusBaselines();
38673
- const baselinesMap = /* @__PURE__ */ new Map();
38674
- if (baselines) {
38675
- baselinesMap.set("lineLengths", baselines.features.lineLengths.sample);
38676
- baselinesMap.set("identifierLengths", baselines.features.identifierLengths.sample);
38677
- baselinesMap.set("commentDensity", baselines.features.commentDensity.sample);
38678
- } else {
38679
- baselinesMap.set("lineLengths", [20, 25, 30, 32, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]);
38680
- baselinesMap.set("identifierLengths", [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 24, 28]);
38681
- baselinesMap.set("commentDensity", [0, 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5]);
38682
- }
38683
- for (const [name, vals] of samples) {
38684
- if (vals.length < MIN_SAMPLES_PER_FEATURE) samples.delete(name);
38685
- }
38686
- if (samples.size === 0) return issues;
38687
- const result = multiFeatureKsTest(samples, baselinesMap, 0.05);
38688
- if (!result.anySignificant) return issues;
38689
- const shifted = result.significantFeatures.join(", ");
38690
- const details = result.significantFeatures.map((name) => {
38691
- const r = result.perFeature.get(name);
38692
- if (!r) return name;
38693
- return `${name} (D=${r.statistic.toFixed(3)}, p=${r.pValue.toExponential(2)})`;
38694
- }).join("; ");
38695
- issues.push({
38696
- ruleId: "logic/ks-distribution-shift",
38697
- category: "logic",
38698
- severity: "medium",
38699
- aiSpecific: true,
38700
- message: `Distribution shift detected on ${result.significantFeatures.length} of ${result.perFeature.size} features (Bonferroni \u03B1=${result.bonferroniAlpha.toExponential(2)}). Features: ${shifted}. Detail: ${details}.`,
38701
- line: 1,
38702
- column: 1,
38703
- 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."
38704
- });
38705
- return issues;
38706
- }
38707
- });
38708
- }
38709
- });
38710
-
38711
38866
  // src/rules/logic/math-any-density.ts
38712
38867
  var ANY_PER_100_LINES, MIN_ABSOLUTE, COLON, ANY_KEYWORD, ANY_TYPE_RE, MSG_FRAGMENT, RULE_DESCRIPTION, RULE_MESSAGE, RULE_ADVICE, mathAnyDensityRule;
38713
38868
  var init_math_any_density = __esm({
@@ -38777,13 +38932,13 @@ var init_math_any_density = __esm({
38777
38932
  });
38778
38933
 
38779
38934
  // src/rules/logic/math-console-log-storm.ts
38780
- var WINDOW_SIZE, STORM_THRESHOLD, CONSOLE_LOG_RE, mathConsoleLogStormRule;
38935
+ var WINDOW_SIZE2, STORM_THRESHOLD, CONSOLE_LOG_RE, mathConsoleLogStormRule;
38781
38936
  var init_math_console_log_storm = __esm({
38782
38937
  "src/rules/logic/math-console-log-storm.ts"() {
38783
38938
  "use strict";
38784
38939
  init_rule();
38785
38940
  init_utils();
38786
- WINDOW_SIZE = 30;
38941
+ WINDOW_SIZE2 = 30;
38787
38942
  STORM_THRESHOLD = 5;
38788
38943
  CONSOLE_LOG_RE = /\bconsole\.log\s*\(/g;
38789
38944
  mathConsoleLogStormRule = createRule({
@@ -38814,7 +38969,7 @@ var init_math_console_log_storm = __esm({
38814
38969
  let maxEndLine = 0;
38815
38970
  let i = 0;
38816
38971
  for (let j = 0; j < lines.length; j++) {
38817
- while (lines[j] - lines[i] > WINDOW_SIZE) i++;
38972
+ while (lines[j] - lines[i] > WINDOW_SIZE2) i++;
38818
38973
  const count = j - i + 1;
38819
38974
  if (count > maxCount) {
38820
38975
  maxCount = count;
@@ -38828,7 +38983,7 @@ var init_math_console_log_storm = __esm({
38828
38983
  category: "logic",
38829
38984
  severity: "high",
38830
38985
  aiSpecific: true,
38831
- 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.`,
38986
+ 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.`,
38832
38987
  line: firstIdx >= 0 ? lines[firstIdx] : 1,
38833
38988
  column: firstIdx >= 0 ? columns[firstIdx] : 1,
38834
38989
  advice: "Replace debug logs with a proper debugger or logger.debug() \u2014 remove all console.log before shipping."
@@ -41652,6 +41807,251 @@ var init_weak_assertion = __esm({
41652
41807
  }
41653
41808
  });
41654
41809
 
41810
+ // src/rules/ts/enum-vs-as-const.ts
41811
+ var ENUM_DECL_REGEX, tsEnumVsAsConstRule;
41812
+ var init_enum_vs_as_const = __esm({
41813
+ "src/rules/ts/enum-vs-as-const.ts"() {
41814
+ "use strict";
41815
+ init_rule();
41816
+ ENUM_DECL_REGEX = /^[ \t]*(?:export\s+)?(?:const\s+)?enum\s+[A-Z_][A-Za-z0-9_]*\s*\{/gm;
41817
+ tsEnumVsAsConstRule = createRule({
41818
+ id: "ts/enum-vs-as-const",
41819
+ category: "typo",
41820
+ severity: "low",
41821
+ aiSpecific: true,
41822
+ description: "Uses `enum` \u2014 modern TS prefers `as const` objects",
41823
+ create(_context) {
41824
+ return {};
41825
+ },
41826
+ analyze(_context, facts) {
41827
+ const issues = [];
41828
+ const source = facts.v2?._source;
41829
+ if (!source) return issues;
41830
+ let match;
41831
+ ENUM_DECL_REGEX.lastIndex = 0;
41832
+ while ((match = ENUM_DECL_REGEX.exec(source)) !== null) {
41833
+ const line = source.slice(0, match.index).split("\n").length;
41834
+ issues.push({
41835
+ ruleId: "ts/enum-vs-as-const",
41836
+ category: "typo",
41837
+ severity: "low",
41838
+ aiSpecific: true,
41839
+ message: `'enum' is an AI / older-TS pattern \u2014 prefer 'as const' for a frozen object literal`,
41840
+ line,
41841
+ column: match[0].indexOf("enum") + 1,
41842
+ 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.'
41843
+ });
41844
+ }
41845
+ return issues;
41846
+ }
41847
+ });
41848
+ }
41849
+ });
41850
+
41851
+ // src/rules/ts/excessive-type-assertion.ts
41852
+ var DEFAULT_MAX, FN_DECL_REGEX, AS_REGEX, tsExcessiveTypeAssertionRule;
41853
+ var init_excessive_type_assertion = __esm({
41854
+ "src/rules/ts/excessive-type-assertion.ts"() {
41855
+ "use strict";
41856
+ init_rule();
41857
+ DEFAULT_MAX = 3;
41858
+ 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;
41859
+ AS_REGEX = /(?<![\w$])as\s+(?!const)([A-Z][\w$.,<>\[\]|&]*)/g;
41860
+ tsExcessiveTypeAssertionRule = createRule({
41861
+ id: "ts/excessive-type-assertion",
41862
+ category: "typo",
41863
+ severity: "low",
41864
+ aiSpecific: true,
41865
+ description: "Function with >3 `as` type assertions \u2014 likely AI fighting the type system",
41866
+ create(_context) {
41867
+ return { maxAssertionsPerFunction: DEFAULT_MAX };
41868
+ },
41869
+ analyze(context, facts) {
41870
+ const issues = [];
41871
+ const source = facts.v2?._source;
41872
+ if (!source) return issues;
41873
+ let match;
41874
+ FN_DECL_REGEX.lastIndex = 0;
41875
+ while ((match = FN_DECL_REGEX.exec(source)) !== null) {
41876
+ const startIdx = match.index;
41877
+ const openBraceIdx = source.indexOf("{", startIdx);
41878
+ if (openBraceIdx < 0) continue;
41879
+ let depth = 1;
41880
+ let i = openBraceIdx + 1;
41881
+ while (i < source.length && depth > 0) {
41882
+ const ch = source[i];
41883
+ if (ch === "{") depth++;
41884
+ else if (ch === "}") depth--;
41885
+ i++;
41886
+ }
41887
+ const body = source.slice(openBraceIdx, i);
41888
+ const line = source.slice(0, startIdx).split("\n").length;
41889
+ let asCount = 0;
41890
+ const seen = /* @__PURE__ */ new Set();
41891
+ let asMatch;
41892
+ AS_REGEX.lastIndex = 0;
41893
+ while ((asMatch = AS_REGEX.exec(body)) !== null) {
41894
+ const captured = asMatch[1];
41895
+ if (seen.has(captured)) continue;
41896
+ seen.add(captured);
41897
+ asCount++;
41898
+ }
41899
+ if (asCount > context.maxAssertionsPerFunction) {
41900
+ issues.push({
41901
+ ruleId: "ts/excessive-type-assertion",
41902
+ category: "typo",
41903
+ severity: "low",
41904
+ aiSpecific: true,
41905
+ message: `Function has ${asCount} 'as' assertions (max ${context.maxAssertionsPerFunction}) \u2014 likely AI fighting the type system`,
41906
+ line,
41907
+ column: 1,
41908
+ 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."
41909
+ });
41910
+ }
41911
+ }
41912
+ return issues;
41913
+ }
41914
+ });
41915
+ }
41916
+ });
41917
+
41918
+ // src/rules/ts/import-type-misuse.ts
41919
+ var INLINE_TYPE_IMPORT_REGEX, tsImportTypeMisuseRule;
41920
+ var init_import_type_misuse = __esm({
41921
+ "src/rules/ts/import-type-misuse.ts"() {
41922
+ "use strict";
41923
+ init_rule();
41924
+ INLINE_TYPE_IMPORT_REGEX = /^[ \t]*import\s*\{[^}]*\btype\s+[A-Za-z_]/gm;
41925
+ tsImportTypeMisuseRule = createRule({
41926
+ id: "ts/import-type-misuse",
41927
+ category: "typo",
41928
+ severity: "low",
41929
+ aiSpecific: true,
41930
+ description: "Inline `import { type X }` \u2014 prefer `import type { X }` for clarity",
41931
+ create(_context) {
41932
+ return {};
41933
+ },
41934
+ analyze(_context, facts) {
41935
+ const issues = [];
41936
+ const source = facts.v2?._source;
41937
+ if (!source) return issues;
41938
+ let match;
41939
+ INLINE_TYPE_IMPORT_REGEX.lastIndex = 0;
41940
+ while ((match = INLINE_TYPE_IMPORT_REGEX.exec(source)) !== null) {
41941
+ const line = source.slice(0, match.index).split("\n").length;
41942
+ issues.push({
41943
+ ruleId: "ts/import-type-misuse",
41944
+ category: "typo",
41945
+ severity: "low",
41946
+ aiSpecific: true,
41947
+ message: "Inline `type` in a value import \u2014 split into a separate `import type` statement",
41948
+ line,
41949
+ column: match[0].indexOf("type") + 1,
41950
+ 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.'
41951
+ });
41952
+ }
41953
+ return issues;
41954
+ }
41955
+ });
41956
+ }
41957
+ });
41958
+
41959
+ // src/rules/ts/never-vs-unknown.ts
41960
+ var NEVER_RETURN_REGEX, THROW_OR_LOOP_REGEX, tsNeverVsUnknownRule;
41961
+ var init_never_vs_unknown = __esm({
41962
+ "src/rules/ts/never-vs-unknown.ts"() {
41963
+ "use strict";
41964
+ init_rule();
41965
+ NEVER_RETURN_REGEX = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)[^{]*:\s*(?:Promise<)?never\b[^{]*\{/gm;
41966
+ THROW_OR_LOOP_REGEX = /\b(?:throw\b|while\s*\(|for\s*\(|process\.exit\b|System\.exit\b)/;
41967
+ tsNeverVsUnknownRule = createRule({
41968
+ id: "ts/never-vs-unknown",
41969
+ category: "typo",
41970
+ severity: "low",
41971
+ aiSpecific: true,
41972
+ description: "Return type `: never` but body has no throw / loop / exit \u2014 likely AI misuse",
41973
+ create(_context) {
41974
+ return {};
41975
+ },
41976
+ analyze(_context, facts) {
41977
+ const issues = [];
41978
+ const source = facts.v2?._source;
41979
+ if (!source) return issues;
41980
+ let match;
41981
+ NEVER_RETURN_REGEX.lastIndex = 0;
41982
+ while ((match = NEVER_RETURN_REGEX.exec(source)) !== null) {
41983
+ const fnName = match[1];
41984
+ const startIdx = match.index;
41985
+ const line = source.slice(0, startIdx).split("\n").length;
41986
+ const openBraceIdx = source.indexOf("{", startIdx);
41987
+ if (openBraceIdx < 0) continue;
41988
+ let depth = 1;
41989
+ let i = openBraceIdx + 1;
41990
+ while (i < source.length && depth > 0) {
41991
+ const ch = source[i];
41992
+ if (ch === "{") depth++;
41993
+ else if (ch === "}") depth--;
41994
+ i++;
41995
+ }
41996
+ const body = source.slice(openBraceIdx, i);
41997
+ if (THROW_OR_LOOP_REGEX.test(body)) continue;
41998
+ issues.push({
41999
+ ruleId: "ts/never-vs-unknown",
42000
+ category: "typo",
42001
+ severity: "low",
42002
+ aiSpecific: true,
42003
+ message: `Function '${fnName}' returns 'never' but its body has no throw, loop, or exit \u2014 likely AI misuse`,
42004
+ line,
42005
+ column: match[0].indexOf("never") + 1,
42006
+ 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.'
42007
+ });
42008
+ }
42009
+ return issues;
42010
+ }
42011
+ });
42012
+ }
42013
+ });
42014
+
42015
+ // src/rules/ts/optional-chain-overuse.ts
42016
+ var DEFAULT_MIN_CHAIN_LENGTH, tsOptionalChainOveruseRule;
42017
+ var init_optional_chain_overuse = __esm({
42018
+ "src/rules/ts/optional-chain-overuse.ts"() {
42019
+ "use strict";
42020
+ init_rule();
42021
+ DEFAULT_MIN_CHAIN_LENGTH = 5;
42022
+ tsOptionalChainOveruseRule = createRule({
42023
+ id: "ts/optional-chain-overuse",
42024
+ category: "logic",
42025
+ severity: "low",
42026
+ aiSpecific: true,
42027
+ description: "Optional chaining (?.) used 5+ times in a single chain \u2014 AI tends to chain rather than narrow",
42028
+ create(_context) {
42029
+ return { minChainLength: DEFAULT_MIN_CHAIN_LENGTH };
42030
+ },
42031
+ analyze(context, facts) {
42032
+ const issues = [];
42033
+ const expressions = facts.v2.logic?.logicalExpressions;
42034
+ if (!expressions) return issues;
42035
+ for (const expression of expressions) {
42036
+ if (expression.depth >= context.minChainLength && expression.isOptionalChainLike) {
42037
+ issues.push({
42038
+ ruleId: "ts/optional-chain-overuse",
42039
+ category: "logic",
42040
+ severity: "low",
42041
+ aiSpecific: true,
42042
+ message: `Optional chain depth ${expression.depth} \u2014 break with an intermediate variable or guard clause`,
42043
+ line: expression.line,
42044
+ column: expression.column,
42045
+ 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."
42046
+ });
42047
+ }
42048
+ }
42049
+ return issues;
42050
+ }
42051
+ });
42052
+ }
42053
+ });
42054
+
41655
42055
  // src/rules/typo/calc-fontsize.ts
41656
42056
  var FONT_SIZE_RE, calcFontsizeRule;
41657
42057
  var init_calc_fontsize = __esm({
@@ -45793,6 +46193,10 @@ var init_builtins = __esm({
45793
46193
  init_expired_code_example();
45794
46194
  init_stale_function_reference();
45795
46195
  init_stale_package_reference();
46196
+ init_identical_block();
46197
+ init_error_wrap_without_context();
46198
+ init_nil_slice_vs_empty();
46199
+ init_struct_tag_inconsistency();
45796
46200
  init_gap_monopoly();
45797
46201
  init_math_element_uniformity();
45798
46202
  init_math_grid_uniformity();
@@ -45802,7 +46206,6 @@ var init_builtins = __esm({
45802
46206
  init_ghost_defensive();
45803
46207
  init_heaps_deviation();
45804
46208
  init_key_prop_missing();
45805
- init_ks_distribution_shift();
45806
46209
  init_math_any_density();
45807
46210
  init_math_console_log_storm();
45808
46211
  init_math_gini_class_usage();
@@ -45836,6 +46239,11 @@ var init_builtins = __esm({
45836
46239
  init_fake_placeholder();
45837
46240
  init_missing_edge_case();
45838
46241
  init_weak_assertion();
46242
+ init_enum_vs_as_const();
46243
+ init_excessive_type_assertion();
46244
+ init_import_type_misuse();
46245
+ init_never_vs_unknown();
46246
+ init_optional_chain_overuse();
45839
46247
  init_calc_fontsize();
45840
46248
  init_calc_raw_px();
45841
46249
  init_clamp_offscale();
@@ -45898,6 +46306,10 @@ var init_builtins = __esm({
45898
46306
  expiredCodeExampleRule,
45899
46307
  staleFunctionReferenceRule,
45900
46308
  stalePackageReferenceRule,
46309
+ dupIdenticalBlockRule,
46310
+ goErrorWrapWithoutContextRule,
46311
+ goNilSliceVsEmptyRule,
46312
+ goStructTagInconsistencyRule,
45901
46313
  gapMonopolyRule,
45902
46314
  mathElementUniformityRule,
45903
46315
  mathGridUniformityRule,
@@ -45907,7 +46319,6 @@ var init_builtins = __esm({
45907
46319
  ghostDefensiveRule,
45908
46320
  heapsDeviationRule,
45909
46321
  keyPropMissingRule,
45910
- ksDistributionShiftRule,
45911
46322
  mathAnyDensityRule,
45912
46323
  mathConsoleLogStormRule,
45913
46324
  mathGiniClassUsageRule,
@@ -45941,6 +46352,11 @@ var init_builtins = __esm({
45941
46352
  fakePlaceholderRule,
45942
46353
  missingEdgeCaseRule,
45943
46354
  weakAssertionRule,
46355
+ tsEnumVsAsConstRule,
46356
+ tsExcessiveTypeAssertionRule,
46357
+ tsImportTypeMisuseRule,
46358
+ tsNeverVsUnknownRule,
46359
+ tsOptionalChainOveruseRule,
45944
46360
  calcFontsizeRule,
45945
46361
  calcRawPxRule,
45946
46362
  clampOffscaleRule,
@@ -48019,7 +48435,7 @@ var init_signal_strength = __esm({
48019
48435
  precision: 0.9966,
48020
48436
  lastCalibratedAt: "2026-07-01T00:00:00Z",
48021
48437
  verdict: "USEFUL",
48022
- _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).",
48438
+ _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.",
48023
48439
  aiSpecific: true,
48024
48440
  _v7Verdict: "USEFUL",
48025
48441
  _v7Lift: 182622.43,
@@ -48027,7 +48443,8 @@ var init_signal_strength = __esm({
48027
48443
  _v7FpRate: 0,
48028
48444
  _v7Precision: 0.9957,
48029
48445
  _v8Verdict: "USEFUL",
48030
- _v8Lift: 99999
48446
+ _v8Lift: 99999,
48447
+ defaultOff: false
48031
48448
  },
48032
48449
  "ai/errors-near-eof": {
48033
48450
  recall: 0.0948,
@@ -48276,7 +48693,7 @@ var init_signal_strength = __esm({
48276
48693
  precision: 0.9512,
48277
48694
  lastCalibratedAt: "2026-07-01T00:00:00Z",
48278
48695
  verdict: "USEFUL",
48279
- _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).",
48696
+ _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.",
48280
48697
  aiSpecific: true,
48281
48698
  _v7Verdict: "USEFUL",
48282
48699
  _v7Lift: 7099.57,
@@ -48284,7 +48701,8 @@ var init_signal_strength = __esm({
48284
48701
  _v7FpRate: 1e-4,
48285
48702
  _v7Precision: 0.929,
48286
48703
  _v8Verdict: "USEFUL",
48287
- _v8Lift: 99999
48704
+ _v8Lift: 99999,
48705
+ defaultOff: false
48288
48706
  },
48289
48707
  "context/import-path-mismatch": {
48290
48708
  recall: 0.0681,
@@ -48468,7 +48886,7 @@ var init_signal_strength = __esm({
48468
48886
  precision: 0.8834,
48469
48887
  lastCalibratedAt: "2026-07-01T00:00:00Z",
48470
48888
  verdict: "USEFUL",
48471
- _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.",
48889
+ _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.",
48472
48890
  aiSpecific: true,
48473
48891
  _v7Verdict: "DORMANT",
48474
48892
  _v7Lift: 1,
@@ -48476,7 +48894,8 @@ var init_signal_strength = __esm({
48476
48894
  _v7FpRate: 0,
48477
48895
  _v7Precision: 0,
48478
48896
  _v8Verdict: "USEFUL",
48479
- _v8Lift: 32.74
48897
+ _v8Lift: 32.74,
48898
+ defaultOff: false
48480
48899
  },
48481
48900
  "dead/unused-parameter": {
48482
48901
  recall: 8e-4,
@@ -48564,6 +48983,78 @@ var init_signal_strength = __esm({
48564
48983
  _v8Verdict: "OK",
48565
48984
  _v8Lift: 255.9
48566
48985
  },
48986
+ "dup/identical-block": {
48987
+ recall: 0,
48988
+ fpRate: 0,
48989
+ ratio: 1,
48990
+ precision: 0,
48991
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
48992
+ verdict: "DORMANT",
48993
+ _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.",
48994
+ aiSpecific: false,
48995
+ _v7Verdict: "DORMANT",
48996
+ _v7Lift: 1,
48997
+ _v7Recall: 0,
48998
+ _v7FpRate: 0,
48999
+ _v7Precision: 0,
49000
+ _v8Verdict: "DORMANT",
49001
+ _v8Lift: 1,
49002
+ defaultOff: true
49003
+ },
49004
+ "go/error-wrap-without-context": {
49005
+ recall: 0,
49006
+ fpRate: 0,
49007
+ ratio: 1,
49008
+ precision: 0,
49009
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49010
+ verdict: "DORMANT",
49011
+ _calibrationNote: "v0.19: new rule (fmt.Errorf wrap with generic message \u2014 needs operation context). Not yet calibrated. Scheduled for v9 calibration.",
49012
+ aiSpecific: true,
49013
+ _v7Verdict: "DORMANT",
49014
+ _v7Lift: 1,
49015
+ _v7Recall: 0,
49016
+ _v7FpRate: 0,
49017
+ _v7Precision: 0,
49018
+ _v8Verdict: "DORMANT",
49019
+ _v8Lift: 1,
49020
+ defaultOff: true
49021
+ },
49022
+ "go/nil-slice-vs-empty": {
49023
+ recall: 0,
49024
+ fpRate: 0,
49025
+ ratio: 1,
49026
+ precision: 0,
49027
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49028
+ verdict: "DORMANT",
49029
+ _calibrationNote: "v0.19: new rule (Variable declared nil slice but assigned empty slice). Not yet calibrated. Scheduled for v9 calibration.",
49030
+ aiSpecific: true,
49031
+ _v7Verdict: "DORMANT",
49032
+ _v7Lift: 1,
49033
+ _v7Recall: 0,
49034
+ _v7FpRate: 0,
49035
+ _v7Precision: 0,
49036
+ _v8Verdict: "DORMANT",
49037
+ _v8Lift: 1,
49038
+ defaultOff: true
49039
+ },
49040
+ "go/struct-tag-inconsistency": {
49041
+ recall: 0,
49042
+ fpRate: 0,
49043
+ ratio: 1,
49044
+ precision: 0,
49045
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49046
+ verdict: "DORMANT",
49047
+ _calibrationNote: "v0.19: new rule (Struct fields mix json tag styles). Not yet calibrated. Scheduled for v9 calibration.",
49048
+ aiSpecific: true,
49049
+ _v7Verdict: "DORMANT",
49050
+ _v7Lift: 1,
49051
+ _v7Recall: 0,
49052
+ _v7FpRate: 0,
49053
+ _v7Precision: 0,
49054
+ _v8Verdict: "DORMANT",
49055
+ _v8Lift: 1,
49056
+ defaultOff: true
49057
+ },
48567
49058
  "layout/forced-layout": {
48568
49059
  recall: 0,
48569
49060
  fpRate: 0,
@@ -48692,7 +49183,7 @@ var init_signal_strength = __esm({
48692
49183
  precision: 0.8889,
48693
49184
  lastCalibratedAt: "2026-07-01T00:00:00Z",
48694
49185
  verdict: "USEFUL",
48695
- _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).",
49186
+ _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.",
48696
49187
  aiSpecific: true,
48697
49188
  _v7Verdict: "USEFUL",
48698
49189
  _v7Lift: 80917.5,
@@ -48700,7 +49191,8 @@ var init_signal_strength = __esm({
48700
49191
  _v7FpRate: 0,
48701
49192
  _v7Precision: 0.8824,
48702
49193
  _v8Verdict: "USEFUL",
48703
- _v8Lift: 99999
49194
+ _v8Lift: 99999,
49195
+ defaultOff: false
48704
49196
  },
48705
49197
  "logic/heaps-deviation": {
48706
49198
  recall: 0.0126,
@@ -48736,24 +49228,6 @@ var init_signal_strength = __esm({
48736
49228
  _v8Verdict: "USEFUL",
48737
49229
  _v8Lift: 1760.69
48738
49230
  },
48739
- "logic/ks-distribution-shift": {
48740
- recall: 0.6889,
48741
- fpRate: 0.4411,
48742
- ratio: 1.46,
48743
- precision: 0.6457,
48744
- lastCalibratedAt: "2026-07-01T00:00:00Z",
48745
- verdict: "NOISY",
48746
- _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).",
48747
- aiSpecific: false,
48748
- _v7Verdict: "USEFUL",
48749
- _v7Lift: 2.09,
48750
- _v7Recall: 0.6431,
48751
- _v7FpRate: 0.34,
48752
- _v7Precision: 0.7096,
48753
- _v8Verdict: "INVERTED",
48754
- _v8Lift: 0.71,
48755
- defaultOff: true
48756
- },
48757
49231
  "logic/math-any-density": {
48758
49232
  recall: 17e-4,
48759
49233
  fpRate: 13e-4,
@@ -49119,7 +49593,7 @@ var init_signal_strength = __esm({
49119
49593
  precision: 1,
49120
49594
  lastCalibratedAt: "2026-07-01T00:00:00Z",
49121
49595
  verdict: "USEFUL",
49122
- _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).",
49596
+ _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.",
49123
49597
  aiSpecific: true,
49124
49598
  _v7Verdict: "USEFUL",
49125
49599
  _v7Lift: 99999,
@@ -49127,7 +49601,8 @@ var init_signal_strength = __esm({
49127
49601
  _v7FpRate: 0,
49128
49602
  _v7Precision: 1,
49129
49603
  _v8Verdict: "DORMANT",
49130
- _v8Lift: 1
49604
+ _v8Lift: 1,
49605
+ defaultOff: false
49131
49606
  },
49132
49607
  "security/hardcoded-secret": {
49133
49608
  recall: 14e-4,
@@ -49317,6 +49792,96 @@ var init_signal_strength = __esm({
49317
49792
  _v8Verdict: "USEFUL",
49318
49793
  _v8Lift: 651.94
49319
49794
  },
49795
+ "ts/enum-vs-as-const": {
49796
+ recall: 0,
49797
+ fpRate: 0,
49798
+ ratio: 1,
49799
+ precision: 0,
49800
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49801
+ verdict: "DORMANT",
49802
+ _calibrationNote: "v0.19: new rule (Uses `enum` \u2014 modern TS prefers `as const`). Not yet calibrated. Scheduled for v9 calibration.",
49803
+ aiSpecific: true,
49804
+ _v7Verdict: "DORMANT",
49805
+ _v7Lift: 1,
49806
+ _v7Recall: 0,
49807
+ _v7FpRate: 0,
49808
+ _v7Precision: 0,
49809
+ _v8Verdict: "DORMANT",
49810
+ _v8Lift: 1,
49811
+ defaultOff: true
49812
+ },
49813
+ "ts/excessive-type-assertion": {
49814
+ recall: 0,
49815
+ fpRate: 0,
49816
+ ratio: 1,
49817
+ precision: 0,
49818
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49819
+ verdict: "DORMANT",
49820
+ _calibrationNote: "v0.19: new rule (Function with >3 `as` assertions \u2014 AI fighting the type system). Not yet calibrated. Scheduled for v9 calibration.",
49821
+ aiSpecific: true,
49822
+ _v7Verdict: "DORMANT",
49823
+ _v7Lift: 1,
49824
+ _v7Recall: 0,
49825
+ _v7FpRate: 0,
49826
+ _v7Precision: 0,
49827
+ _v8Verdict: "DORMANT",
49828
+ _v8Lift: 1,
49829
+ defaultOff: true
49830
+ },
49831
+ "ts/import-type-misuse": {
49832
+ recall: 0,
49833
+ fpRate: 0,
49834
+ ratio: 1,
49835
+ precision: 0,
49836
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49837
+ verdict: "DORMANT",
49838
+ _calibrationNote: "v0.19: new rule (Inline `import { type X }` \u2014 prefer separate `import type`). Not yet calibrated. Scheduled for v9 calibration.",
49839
+ aiSpecific: true,
49840
+ _v7Verdict: "DORMANT",
49841
+ _v7Lift: 1,
49842
+ _v7Recall: 0,
49843
+ _v7FpRate: 0,
49844
+ _v7Precision: 0,
49845
+ _v8Verdict: "DORMANT",
49846
+ _v8Lift: 1,
49847
+ defaultOff: true
49848
+ },
49849
+ "ts/never-vs-unknown": {
49850
+ recall: 0,
49851
+ fpRate: 0,
49852
+ ratio: 1,
49853
+ precision: 0,
49854
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49855
+ verdict: "DORMANT",
49856
+ _calibrationNote: "v0.19: new rule (Return type `never` but body has no throw/loop/exit). Not yet calibrated. Scheduled for v9 calibration.",
49857
+ aiSpecific: true,
49858
+ _v7Verdict: "DORMANT",
49859
+ _v7Lift: 1,
49860
+ _v7Recall: 0,
49861
+ _v7FpRate: 0,
49862
+ _v7Precision: 0,
49863
+ _v8Verdict: "DORMANT",
49864
+ _v8Lift: 1,
49865
+ defaultOff: true
49866
+ },
49867
+ "ts/optional-chain-overuse": {
49868
+ recall: 0,
49869
+ fpRate: 0,
49870
+ ratio: 1,
49871
+ precision: 0,
49872
+ lastCalibratedAt: "2026-07-01T00:00:00Z",
49873
+ verdict: "DORMANT",
49874
+ _calibrationNote: "v0.19: new rule (Optional chain depth >= 5 \u2014 AI chains ?. rather than narrowing). Not yet calibrated. Scheduled for v9 calibration.",
49875
+ aiSpecific: true,
49876
+ _v7Verdict: "DORMANT",
49877
+ _v7Lift: 1,
49878
+ _v7Recall: 0,
49879
+ _v7FpRate: 0,
49880
+ _v7Precision: 0,
49881
+ _v8Verdict: "DORMANT",
49882
+ _v8Lift: 1,
49883
+ defaultOff: true
49884
+ },
49320
49885
  "typo/calc-fontsize": {
49321
49886
  recall: 0,
49322
49887
  fpRate: 0,
@@ -49619,7 +50184,7 @@ var init_signal_strength = __esm({
49619
50184
  precision: 0.9774,
49620
50185
  lastCalibratedAt: "2026-07-01T00:00:00Z",
49621
50186
  verdict: "USEFUL",
49622
- _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).",
50187
+ _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.",
49623
50188
  aiSpecific: false,
49624
50189
  _v7Verdict: "USEFUL",
49625
50190
  _v7Lift: 59285.01,
@@ -49627,7 +50192,8 @@ var init_signal_strength = __esm({
49627
50192
  _v7FpRate: 0,
49628
50193
  _v7Precision: 0.9697,
49629
50194
  _v8Verdict: "USEFUL",
49630
- _v8Lift: 99999
50195
+ _v8Lift: 99999,
50196
+ defaultOff: false
49631
50197
  },
49632
50198
  "visual/spacing-scale-violation": {
49633
50199
  recall: 86e-4,
@@ -56633,7 +57199,19 @@ var RULE_HINTS = {
56633
57199
  "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.",
56634
57200
  "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.",
56635
57201
  "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.",
56636
- "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.'
57202
+ "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.',
57203
+ // v0.19.0 — TypeScript-specific rules
57204
+ "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.",
57205
+ "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.",
57206
+ "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.",
57207
+ "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.",
57208
+ "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.",
57209
+ // v0.19.0 — Go-specific rules
57210
+ "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.',
57211
+ "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).',
57212
+ "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.",
57213
+ // v0.19.0 — Duplication detector
57214
+ "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."
56637
57215
  };
56638
57216
 
56639
57217
  // src/cli/commands/explain.ts