slopbrick 0.18.4 → 0.18.7

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.
@@ -6316,6 +6316,78 @@ function extractStateBinding(node, lineOffsets) {
6316
6316
  setterReferenced: false
6317
6317
  };
6318
6318
  }
6319
+ function findUnreachableStatements(ast, source, lineOffsets) {
6320
+ const out = [];
6321
+ function isTerminator(node) {
6322
+ if (node.type === "ReturnStatement") return "return";
6323
+ if (node.type === "ThrowStatement") return "throw";
6324
+ if (node.type === "BreakStatement") return "break";
6325
+ if (node.type === "ContinueStatement") return "continue";
6326
+ return null;
6327
+ }
6328
+ function snippetFor(node, src) {
6329
+ const span = node.span;
6330
+ if (!span || typeof span.start !== "number" || typeof span.end !== "number") {
6331
+ return "<unreachable>";
6332
+ }
6333
+ const text = src.slice(span.start, Math.min(span.end, span.start + 60));
6334
+ return text.replace(/\s+/g, " ").trim() || "<unreachable>";
6335
+ }
6336
+ function lineColumn(offset) {
6337
+ let lo = 0;
6338
+ let hi = lineOffsets.length - 1;
6339
+ while (lo < hi) {
6340
+ const mid = lo + hi + 1 >>> 1;
6341
+ const midVal = lineOffsets[mid];
6342
+ const loVal = lineOffsets[lo];
6343
+ if (midVal === void 0) break;
6344
+ if (midVal <= offset) lo = mid;
6345
+ else hi = mid - 1;
6346
+ void loVal;
6347
+ }
6348
+ const baseOffset = lineOffsets[lo] ?? 0;
6349
+ return { line: lo + 1, column: offset - baseOffset };
6350
+ }
6351
+ function visitBody(body) {
6352
+ if (!Array.isArray(body)) return;
6353
+ let lastTerminator = null;
6354
+ for (const stmt of body) {
6355
+ if (!isObject(stmt)) continue;
6356
+ if (lastTerminator && stmt.type !== "EmptyStatement") {
6357
+ const span = stmt.span;
6358
+ const offset = typeof span?.start === "number" ? span.start : 0;
6359
+ const { line, column } = lineColumn(offset);
6360
+ out.push({
6361
+ terminator: lastTerminator,
6362
+ line,
6363
+ column,
6364
+ snippet: snippetFor(stmt, source)
6365
+ });
6366
+ }
6367
+ const t = isTerminator(stmt);
6368
+ if (t) lastTerminator = t;
6369
+ }
6370
+ }
6371
+ function walk3(node) {
6372
+ if (!isObject(node)) return;
6373
+ if (node.type === "BlockStatement") {
6374
+ visitBody(node.stmts);
6375
+ } else if (node.type === "Module") {
6376
+ visitBody(node.body);
6377
+ }
6378
+ for (const key of Object.keys(node)) {
6379
+ if (key === "parent" || key === "span" || key === "ctxt") continue;
6380
+ const child = node[key];
6381
+ if (Array.isArray(child)) {
6382
+ for (const c of child) walk3(c);
6383
+ } else {
6384
+ walk3(child);
6385
+ }
6386
+ }
6387
+ }
6388
+ walk3(ast);
6389
+ return out;
6390
+ }
6319
6391
 
6320
6392
  // src/engine/visitors/dispatch.ts
6321
6393
  function nearestComponent(vctx) {
@@ -6392,15 +6464,42 @@ function handleImportDeclaration(node, _parent, _path, vctx) {
6392
6464
  if (specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier") {
6393
6465
  const local = specifier.local;
6394
6466
  if (isObject(local) && local.type === "Identifier" && typeof local.value === "string") {
6395
- importedNames.push(local.value);
6467
+ const name = local.value;
6468
+ importedNames.push(name);
6469
+ vctx.facts.deadCode.bindings.push({
6470
+ name,
6471
+ kind: specifier.type === "ImportDefaultSpecifier" ? "import-default" : "import-namespace",
6472
+ line,
6473
+ column,
6474
+ source,
6475
+ isReferenced: false
6476
+ });
6396
6477
  }
6397
6478
  } else if (specifier.type === "ImportSpecifier") {
6398
6479
  const imported = specifier.imported;
6399
6480
  const local = specifier.local;
6400
6481
  if (isObject(imported) && typeof imported.value === "string" && imported.value.length > 0) {
6401
- importedNames.push(imported.value);
6482
+ const name = imported.value;
6483
+ importedNames.push(name);
6484
+ vctx.facts.deadCode.bindings.push({
6485
+ name,
6486
+ kind: "import-specifier",
6487
+ line,
6488
+ column,
6489
+ source,
6490
+ isReferenced: false
6491
+ });
6402
6492
  } else if (isObject(local) && local.type === "Identifier" && typeof local.value === "string") {
6403
- importedNames.push(local.value);
6493
+ const name = local.value;
6494
+ importedNames.push(name);
6495
+ vctx.facts.deadCode.bindings.push({
6496
+ name,
6497
+ kind: "import-specifier",
6498
+ line,
6499
+ column,
6500
+ source,
6501
+ isReferenced: false
6502
+ });
6404
6503
  }
6405
6504
  }
6406
6505
  }
@@ -6490,6 +6589,13 @@ function isBindingSite(node, parent) {
6490
6589
  if (params.some((param) => containsNode(param, node))) return true;
6491
6590
  }
6492
6591
  }
6592
+ if (parent.type === "ImportSpecifier" || parent.type === "ImportDefaultSpecifier" || parent.type === "ImportNamespaceSpecifier") {
6593
+ if (parent.type === "ImportSpecifier") {
6594
+ if (parent.imported === node || parent.local === node) return true;
6595
+ } else {
6596
+ if (parent.local === node) return true;
6597
+ }
6598
+ }
6493
6599
  return false;
6494
6600
  }
6495
6601
  function isNonComputedMemberProperty(node, parent) {
@@ -6556,6 +6662,9 @@ function handleIdentifier(node, parent, path, vctx) {
6556
6662
  if (typeof node.value === "string" && !isBindingSite(node, parent) && !isNonComputedMemberProperty(node, parent)) {
6557
6663
  markStateReference(node.value, vctx);
6558
6664
  trackPropUsage(node, parent, path, vctx);
6665
+ vctx.facts.referencedNames.add(node.value);
6666
+ const top = vctx.ctx.stack[vctx.ctx.stack.length - 1];
6667
+ if (top) top.references.add(node.value);
6559
6668
  }
6560
6669
  return false;
6561
6670
  }
@@ -6696,6 +6805,20 @@ function handleVariableDeclarator(node, _parent, path, vctx) {
6696
6805
  frame.bindings.add(bindingName);
6697
6806
  }
6698
6807
  }
6808
+ if (bindingNames.length > 0) {
6809
+ const parent = node.parent;
6810
+ const kind = isObject(parent) && parent.type === "VariableDeclaration" ? String(parent.kind ?? "var") : "var";
6811
+ const { line, column } = positionFrom(id, vctx.lineOffsets);
6812
+ for (const bindingName of bindingNames) {
6813
+ vctx.facts.deadCode.bindings.push({
6814
+ name: bindingName,
6815
+ kind,
6816
+ line,
6817
+ column,
6818
+ isReferenced: false
6819
+ });
6820
+ }
6821
+ }
6699
6822
  if (isUseStateDeclarator(node)) {
6700
6823
  const binding = extractStateBinding(node, vctx.lineOffsets);
6701
6824
  if (binding) {
@@ -6715,6 +6838,36 @@ function handleVariableDeclarator(node, _parent, path, vctx) {
6715
6838
  }
6716
6839
  return true;
6717
6840
  }
6841
+ function handleIfStatement(node, _parent, _path, vctx) {
6842
+ if (!isObject(node)) return false;
6843
+ const test = node.test;
6844
+ if (!isObject(test)) return false;
6845
+ if (test.type === "BooleanLiteral" && typeof test.value === "boolean") {
6846
+ const { line, column } = positionFrom(test, vctx.lineOffsets);
6847
+ vctx.facts.deadCode.constantConditions.push({
6848
+ kind: test.value ? "if-true" : "if-false",
6849
+ condition: String(test.value),
6850
+ line,
6851
+ column
6852
+ });
6853
+ }
6854
+ return false;
6855
+ }
6856
+ function handleWhileStatement(node, _parent, _path, vctx) {
6857
+ if (!isObject(node)) return false;
6858
+ const test = node.test;
6859
+ if (!isObject(test)) return false;
6860
+ if (test.type === "BooleanLiteral" && typeof test.value === "boolean") {
6861
+ const { line, column } = positionFrom(test, vctx.lineOffsets);
6862
+ vctx.facts.deadCode.constantConditions.push({
6863
+ kind: test.value ? "while-true" : "while-false",
6864
+ condition: String(test.value),
6865
+ line,
6866
+ column
6867
+ });
6868
+ }
6869
+ return false;
6870
+ }
6718
6871
  var HANDLERS = {
6719
6872
  ExpressionStatement: handleExpressionStatement,
6720
6873
  ImportDeclaration: handleImportDeclaration,
@@ -6724,7 +6877,9 @@ var HANDLERS = {
6724
6877
  MemberExpression: handleMemberExpression,
6725
6878
  JSXAttribute: handleJSXAttribute,
6726
6879
  JSXOpeningElement: handleJSXOpeningElement,
6727
- VariableDeclarator: handleVariableDeclarator
6880
+ VariableDeclarator: handleVariableDeclarator,
6881
+ IfStatement: handleIfStatement,
6882
+ WhileStatement: handleWhileStatement
6728
6883
  };
6729
6884
  function dispatchNode(node, parent, path, vctx) {
6730
6885
  if (!isObject(node)) return false;
@@ -6889,6 +7044,17 @@ function buildV2Facts(facts, source, ext, framework, config, templateClassNames
6889
7044
  },
6890
7045
  logic: buildLogicBlock(facts),
6891
7046
  designTokens: scanDesignTokens(facts.staticClassNames),
7047
+ // dead-code detector. Copy the internal accumulator
7048
+ // into the v2 shape, marking each binding as referenced
7049
+ // iff the file-level referenced-name set contains its name.
7050
+ deadCode: {
7051
+ bindings: facts.deadCode.bindings.map((b) => ({
7052
+ ...b,
7053
+ isReferenced: facts.referencedNames.has(b.name)
7054
+ })),
7055
+ constantConditions: facts.deadCode.constantConditions,
7056
+ unreachableStatements: facts.deadCode.unreachableStatements
7057
+ },
6892
7058
  componentSizes: facts.componentSizes.map((cs) => ({
6893
7059
  name: cs.name,
6894
7060
  lineCount: cs.lineCount,
@@ -6956,7 +7122,17 @@ function extractFacts(filePath, ast, source, supportsRsc = true, framework = "re
6956
7122
  componentSizes: [],
6957
7123
  astroComponents: [],
6958
7124
  fetchCalls: [],
6959
- optimisticUpdates: []
7125
+ optimisticUpdates: [],
7126
+ // dead-code detector. The visitor's identifier walk + import/
7127
+ // branch/return handlers populate these. The v2 builder at the
7128
+ // bottom of extractFacts() reads them and produces
7129
+ // `facts.v2.deadCode`.
7130
+ deadCode: {
7131
+ bindings: [],
7132
+ constantConditions: [],
7133
+ unreachableStatements: []
7134
+ },
7135
+ referencedNames: /* @__PURE__ */ new Set()
6960
7136
  };
6961
7137
  const ctx = {
6962
7138
  stack: [],
@@ -7043,6 +7219,16 @@ function extractFacts(filePath, ast, source, supportsRsc = true, framework = "re
7043
7219
  propBindingSet.add(bindingName);
7044
7220
  }
7045
7221
  }
7222
+ const { line: pLine, column: pCol } = positionFrom(param, lineOffsets);
7223
+ for (const bindingName of collectBindingNames2(param)) {
7224
+ facts.deadCode.bindings.push({
7225
+ name: bindingName,
7226
+ kind: "parameter",
7227
+ line: pLine,
7228
+ column: pCol,
7229
+ isReferenced: false
7230
+ });
7231
+ }
7046
7232
  }
7047
7233
  }
7048
7234
  ctx.stack.push({
@@ -7058,6 +7244,12 @@ function extractFacts(filePath, ast, source, supportsRsc = true, framework = "re
7058
7244
  propUsages: [],
7059
7245
  isComponent,
7060
7246
  bindings,
7247
+ // dead-code detector: per-frame referenced-name set.
7248
+ // Identifiers encountered inside the frame are added to this
7249
+ // set; the deadCode builder unions it with parent frames at
7250
+ // pop time so a binding is considered used if any reachable
7251
+ // scope references it.
7252
+ references: /* @__PURE__ */ new Set(),
7061
7253
  propBindingSet,
7062
7254
  propUsageSet: /* @__PURE__ */ new Set(),
7063
7255
  node
@@ -7153,6 +7345,11 @@ function extractFacts(filePath, ast, source, supportsRsc = true, framework = "re
7153
7345
  mergeTemplateClassNames(filePath, source, facts, templateClassNames);
7154
7346
  const { ext } = splitFilePath(filePath);
7155
7347
  facts._source = source;
7348
+ facts.deadCode.unreachableStatements = findUnreachableStatements(
7349
+ ast,
7350
+ source,
7351
+ lineOffsets
7352
+ );
7156
7353
  const v2 = buildV2Facts(facts, source, ext, framework, config, templateClassNames);
7157
7354
  return envelopeScanFacts(filePath, v2);
7158
7355
  }
@@ -34287,6 +34484,170 @@ var sqlConcatRule = createRule({
34287
34484
  }
34288
34485
  });
34289
34486
 
34487
+ // src/rules/dead/dead-branch.ts
34488
+ var deadBranchRule = createRule({
34489
+ id: "dead/dead-branch",
34490
+ category: "logic",
34491
+ severity: "medium",
34492
+ aiSpecific: true,
34493
+ description: "Literal boolean condition makes one branch statically dead",
34494
+ create(_context) {
34495
+ return {};
34496
+ },
34497
+ analyze(_context, facts) {
34498
+ const issues = [];
34499
+ if (!facts.v2) return issues;
34500
+ for (const cond of facts.v2.deadCode.constantConditions) {
34501
+ const isWhileTrue = cond.kind === "while-true";
34502
+ issues.push({
34503
+ ruleId: "dead/dead-branch",
34504
+ category: "logic",
34505
+ severity: isWhileTrue ? "low" : "medium",
34506
+ aiSpecific: true,
34507
+ message: isWhileTrue ? `Infinite loop with literal condition (${cond.kind})` : `Dead branch: condition is always ${cond.condition}`,
34508
+ line: cond.line,
34509
+ column: cond.column,
34510
+ advice: isWhileTrue ? `If this is an intentional infinite loop (event loop, hot loop with explicit \`break\`), add a \`// slopbrick-disable\` comment. Otherwise, replace the literal with a real condition.` : `Replace the literal with a real condition, or remove the dead branch entirely. This is the AI-iteration signature: the model toggled a feature flag to a constant or left a wrapper from a previous refactor.`
34511
+ });
34512
+ }
34513
+ return issues;
34514
+ }
34515
+ });
34516
+
34517
+ // src/rules/dead/unreachable.ts
34518
+ var unreachableRule = createRule({
34519
+ id: "dead/unreachable",
34520
+ category: "logic",
34521
+ severity: "high",
34522
+ aiSpecific: true,
34523
+ description: "Statement is unreachable after an unconditional return/throw/break/continue",
34524
+ create(_context) {
34525
+ return {};
34526
+ },
34527
+ analyze(_context, facts) {
34528
+ const issues = [];
34529
+ if (!facts.v2) return issues;
34530
+ for (const u of facts.v2.deadCode.unreachableStatements) {
34531
+ if (u.snippet === "<unreachable>") continue;
34532
+ issues.push({
34533
+ ruleId: "dead/unreachable",
34534
+ category: "logic",
34535
+ severity: "high",
34536
+ aiSpecific: true,
34537
+ message: `Unreachable after ${u.terminator}: ${u.snippet}`,
34538
+ line: u.line,
34539
+ column: u.column,
34540
+ advice: `Remove this statement \u2014 code after a ${u.terminator} is unreachable. This is the AI-iteration signature: the model added an early ${u.terminator} for a new error path, then forgot the rest of the function body was still sitting below it.`
34541
+ });
34542
+ }
34543
+ return issues;
34544
+ }
34545
+ });
34546
+
34547
+ // src/rules/dead/unused-import.ts
34548
+ var unusedImportRule = createRule({
34549
+ id: "dead/unused-import",
34550
+ category: "logic",
34551
+ severity: "low",
34552
+ aiSpecific: true,
34553
+ description: "ES module import is never referenced in the file",
34554
+ create(_context) {
34555
+ return {};
34556
+ },
34557
+ analyze(_context, facts) {
34558
+ const issues = [];
34559
+ if (!facts.v2) return issues;
34560
+ for (const binding of facts.v2.deadCode.bindings) {
34561
+ if (binding.kind !== "import-specifier" && binding.kind !== "import-default" && binding.kind !== "import-namespace") {
34562
+ continue;
34563
+ }
34564
+ if (binding.isReferenced) continue;
34565
+ if (!binding.name) continue;
34566
+ const source = binding.source ? ` from '${binding.source}'` : "";
34567
+ issues.push({
34568
+ ruleId: "dead/unused-import",
34569
+ category: "logic",
34570
+ severity: "low",
34571
+ aiSpecific: true,
34572
+ message: `Unused import: '${binding.name}'${source}`,
34573
+ line: binding.line,
34574
+ column: binding.column,
34575
+ advice: `Remove the import or use '${binding.name}' somewhere in the file. This is the most common AI-iteration rot \u2014 the model added the import when it introduced a feature, then rewrote the function without cleaning up.`
34576
+ });
34577
+ }
34578
+ return issues;
34579
+ }
34580
+ });
34581
+
34582
+ // src/rules/dead/unused-local.ts
34583
+ var SKIP_NAMES = /* @__PURE__ */ new Set(["React", "_"]);
34584
+ var unusedLocalRule = createRule({
34585
+ id: "dead/unused-local",
34586
+ category: "logic",
34587
+ severity: "low",
34588
+ aiSpecific: true,
34589
+ description: "Variable is declared but never read after declaration",
34590
+ create(_context) {
34591
+ return {};
34592
+ },
34593
+ analyze(_context, facts) {
34594
+ const issues = [];
34595
+ if (!facts.v2) return issues;
34596
+ for (const binding of facts.v2.deadCode.bindings) {
34597
+ if (binding.kind !== "var" && binding.kind !== "let" && binding.kind !== "const" && binding.kind !== "function" && binding.kind !== "class" && binding.kind !== "type" && binding.kind !== "interface" && binding.kind !== "enum") {
34598
+ continue;
34599
+ }
34600
+ if (binding.isReferenced) continue;
34601
+ if (SKIP_NAMES.has(binding.name)) continue;
34602
+ if (binding.name.startsWith("_")) continue;
34603
+ issues.push({
34604
+ ruleId: "dead/unused-local",
34605
+ category: "logic",
34606
+ severity: "low",
34607
+ aiSpecific: true,
34608
+ message: `Unused ${binding.kind}: '${binding.name}'`,
34609
+ line: binding.line,
34610
+ column: binding.column,
34611
+ advice: `Remove the declaration or use '${binding.name}' somewhere in the file. This is the second-most-common AI-iteration signature \u2014 the model declared the binding when it introduced a feature, then rewrote the function without cleaning up.`
34612
+ });
34613
+ }
34614
+ return issues;
34615
+ }
34616
+ });
34617
+
34618
+ // src/rules/dead/unused-parameter.ts
34619
+ var unusedParameterRule = createRule({
34620
+ id: "dead/unused-parameter",
34621
+ category: "logic",
34622
+ severity: "low",
34623
+ aiSpecific: true,
34624
+ description: "Function parameter is declared but never read",
34625
+ create(_context) {
34626
+ return {};
34627
+ },
34628
+ analyze(_context, facts) {
34629
+ const issues = [];
34630
+ if (!facts.v2) return issues;
34631
+ for (const binding of facts.v2.deadCode.bindings) {
34632
+ if (binding.kind !== "parameter") continue;
34633
+ if (binding.isReferenced) continue;
34634
+ if (binding.name.startsWith("_")) continue;
34635
+ if (binding.name === "props") continue;
34636
+ issues.push({
34637
+ ruleId: "dead/unused-parameter",
34638
+ category: "logic",
34639
+ severity: "low",
34640
+ aiSpecific: true,
34641
+ message: `Unused parameter: '${binding.name}'`,
34642
+ line: binding.line,
34643
+ column: binding.column,
34644
+ advice: `Remove the parameter (and update every call site) or use '${binding.name}' in the function body. This is the AI-iteration signature: the model added the parameter when it introduced a feature, then rewrote the function without removing parameters the new code does not need.`
34645
+ });
34646
+ }
34647
+ return issues;
34648
+ }
34649
+ });
34650
+
34290
34651
  // src/rules/docs/broken-link.ts
34291
34652
  import { existsSync as existsSync4 } from "fs";
34292
34653
  import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
@@ -34377,6 +34738,9 @@ var expiredCodeExampleRule = createRule({
34377
34738
  const issues = [];
34378
34739
  const source = facts.v2?._source;
34379
34740
  if (!source) return issues;
34741
+ const packages = declaredPackages(context.cwd);
34742
+ const packageName = context.packageName;
34743
+ if (packageName) packages.add(packageName);
34380
34744
  const blocks = extractFencedCodeBlocks(source);
34381
34745
  for (const block of blocks) {
34382
34746
  if (!CODE_LANGS.has(block.lang)) continue;
@@ -34384,7 +34748,7 @@ var expiredCodeExampleRule = createRule({
34384
34748
  const imports = extractImports(block.body);
34385
34749
  for (const imp of imports) {
34386
34750
  const pkgName = stripSubpath(imp);
34387
- if (context.packages.has(pkgName)) continue;
34751
+ if (packages.has(pkgName)) continue;
34388
34752
  issues.push({
34389
34753
  ruleId: "docs/expired-code-example",
34390
34754
  category: "docs",
@@ -34405,6 +34769,7 @@ var expiredCodeExampleRule = createRule({
34405
34769
  import { readFileSync as readFileSync4, readdirSync, existsSync as existsSync2 } from "fs";
34406
34770
  import { join as join5, extname as extname3 } from "path";
34407
34771
  var RESERVED = /* @__PURE__ */ new Set([
34772
+ // JS reserved words
34408
34773
  "true",
34409
34774
  "false",
34410
34775
  "null",
@@ -34504,7 +34869,472 @@ var RESERVED = /* @__PURE__ */ new Set([
34504
34869
  "next",
34505
34870
  "vue",
34506
34871
  "angular",
34507
- "svelte"
34872
+ "svelte",
34873
+ // Framework / runtime names
34874
+ "html",
34875
+ "astro",
34876
+ "python",
34877
+ "jvm",
34878
+ "kotlin",
34879
+ "swift",
34880
+ "dart",
34881
+ "ruby",
34882
+ "rust",
34883
+ "cpp",
34884
+ "go",
34885
+ "java",
34886
+ "php",
34887
+ "php-html",
34888
+ "csharp",
34889
+ "typescript",
34890
+ "javascript",
34891
+ "jsx",
34892
+ "tsx",
34893
+ "mjs",
34894
+ "cjs",
34895
+ "esnext",
34896
+ "es6",
34897
+ "es2022",
34898
+ "es2023",
34899
+ "esm",
34900
+ "cjs",
34901
+ "umd",
34902
+ "amd",
34903
+ "commonjs",
34904
+ "require",
34905
+ "module",
34906
+ "exports",
34907
+ "define",
34908
+ "global",
34909
+ "window",
34910
+ "document",
34911
+ "process",
34912
+ "console",
34913
+ "buffer",
34914
+ "stream",
34915
+ "fetch",
34916
+ "axios",
34917
+ "express",
34918
+ "fastify",
34919
+ "koa",
34920
+ "hapi",
34921
+ "nextjs",
34922
+ "nuxt",
34923
+ "remix",
34924
+ "gatsby",
34925
+ "sveltekit",
34926
+ "solid",
34927
+ "preact",
34928
+ "qwik",
34929
+ "lit",
34930
+ "stencil",
34931
+ "marko",
34932
+ "alpine",
34933
+ "stimulus",
34934
+ "turbo",
34935
+ "hotwire",
34936
+ // Models / providers
34937
+ "gpt",
34938
+ "claude",
34939
+ "gpt-3",
34940
+ "gpt-3.5",
34941
+ "gpt-4",
34942
+ "gpt-oss",
34943
+ "haiku",
34944
+ "sonnet",
34945
+ "opus",
34946
+ "aider",
34947
+ "tabby",
34948
+ "copilot",
34949
+ "cursor",
34950
+ "windsurf",
34951
+ "devin",
34952
+ "claude-code",
34953
+ // LLM-detection lingo
34954
+ "heuristic",
34955
+ "heuristics",
34956
+ "calibrate",
34957
+ "calibration",
34958
+ "calibrator",
34959
+ "corpus",
34960
+ "baseline",
34961
+ "baselines",
34962
+ "corpus-baselines",
34963
+ "lift",
34964
+ "recall",
34965
+ "precision",
34966
+ "fpRate",
34967
+ "ratio",
34968
+ "verdict",
34969
+ "USEFUL",
34970
+ "NOISY",
34971
+ "INVERTED",
34972
+ "HYGIENE",
34973
+ "DORMANT",
34974
+ "OK",
34975
+ "aiSpecific",
34976
+ "defaultOff",
34977
+ // Common slop-audit verbs/nouns
34978
+ "commit",
34979
+ "push",
34980
+ "reset",
34981
+ "rebase",
34982
+ "merge",
34983
+ "cherry-pick",
34984
+ "revert",
34985
+ "scan",
34986
+ "parse",
34987
+ "build",
34988
+ "test",
34989
+ "lint",
34990
+ "format",
34991
+ "check",
34992
+ "audit",
34993
+ "fix",
34994
+ "patch",
34995
+ "diff",
34996
+ "pr",
34997
+ "ci",
34998
+ "cd",
34999
+ "gh",
35000
+ "npm",
35001
+ "npx",
35002
+ "pnpm",
35003
+ "yaml",
35004
+ "json",
35005
+ "toml",
35006
+ "csv",
35007
+ "md",
35008
+ "mdx",
35009
+ "sh",
35010
+ "bash",
35011
+ "zsh",
35012
+ "fish",
35013
+ "ascii",
35014
+ "utf8",
35015
+ "utf-8",
35016
+ "base64",
35017
+ "hex",
35018
+ "binary",
35019
+ "text",
35020
+ // Common design / ui terms
35021
+ "flex",
35022
+ "grid",
35023
+ "auto",
35024
+ "min",
35025
+ "max",
35026
+ "fill",
35027
+ "stretch",
35028
+ "wrap",
35029
+ "nowrap",
35030
+ "inline",
35031
+ "block",
35032
+ "hidden",
35033
+ "visible",
35034
+ "static",
35035
+ "fixed",
35036
+ "absolute",
35037
+ "relative",
35038
+ "sticky",
35039
+ "pointer",
35040
+ "cursor",
35041
+ "focus",
35042
+ "hover",
35043
+ "active",
35044
+ "disabled",
35045
+ "readonly",
35046
+ "primary",
35047
+ "secondary",
35048
+ "tertiary",
35049
+ "success",
35050
+ "warning",
35051
+ "danger",
35052
+ "info",
35053
+ "muted",
35054
+ "sm",
35055
+ "md",
35056
+ "lg",
35057
+ "xl",
35058
+ "xxl",
35059
+ "xs",
35060
+ "2xl",
35061
+ "3xl",
35062
+ "4xl",
35063
+ // Math / types
35064
+ "array",
35065
+ "map",
35066
+ "set",
35067
+ "weakmap",
35068
+ "weakset",
35069
+ "object",
35070
+ "string",
35071
+ "number",
35072
+ "boolean",
35073
+ "bigint",
35074
+ "symbol",
35075
+ "null",
35076
+ "undefined",
35077
+ "any",
35078
+ "unknown",
35079
+ "never",
35080
+ "void",
35081
+ "readonly",
35082
+ "private",
35083
+ "public",
35084
+ "protected",
35085
+ "static",
35086
+ "abstract",
35087
+ "async",
35088
+ "generator",
35089
+ "iterator",
35090
+ "iterable",
35091
+ "promise",
35092
+ "observable",
35093
+ // Auth / domain
35094
+ "admin",
35095
+ "user",
35096
+ "guest",
35097
+ "anonymous",
35098
+ "authenticated",
35099
+ "unauthenticated",
35100
+ "jwt",
35101
+ "oauth",
35102
+ "oidc",
35103
+ "saml",
35104
+ "csrf",
35105
+ "xss",
35106
+ "sql",
35107
+ "nosql",
35108
+ "orm",
35109
+ "prisma",
35110
+ "drizzle",
35111
+ "sequelize",
35112
+ "mongoose",
35113
+ "redis",
35114
+ "postgres",
35115
+ "mysql",
35116
+ "sqlite",
35117
+ "kafka",
35118
+ "rabbitmq",
35119
+ "graphql",
35120
+ "rest",
35121
+ "grpc",
35122
+ "websocket",
35123
+ // slop-audit specific
35124
+ "slopbrick",
35125
+ "usebrick",
35126
+ "deadcode",
35127
+ "unused",
35128
+ "orphan",
35129
+ "zombie",
35130
+ "blocker",
35131
+ "warning",
35132
+ "info",
35133
+ "error",
35134
+ "verbose",
35135
+ "debug",
35136
+ "silly",
35137
+ "p50",
35138
+ "p90",
35139
+ "p95",
35140
+ "p99",
35141
+ "min",
35142
+ "max",
35143
+ "avg",
35144
+ "mean",
35145
+ "median",
35146
+ "ratchet",
35147
+ "tier",
35148
+ "composite",
35149
+ "fitness",
35150
+ "fpr",
35151
+ "tpr",
35152
+ "roc",
35153
+ "should",
35154
+ "could",
35155
+ "would",
35156
+ "might",
35157
+ "must",
35158
+ "shall",
35159
+ "may",
35160
+ "can",
35161
+ "todo",
35162
+ "fixme",
35163
+ "xxx",
35164
+ "hack",
35165
+ "note",
35166
+ "warning",
35167
+ "attention",
35168
+ "h1",
35169
+ "h2",
35170
+ "h3",
35171
+ "h4",
35172
+ "h5",
35173
+ "h6",
35174
+ "strong",
35175
+ "em",
35176
+ "b",
35177
+ "i",
35178
+ "u",
35179
+ "true",
35180
+ "false",
35181
+ "yes",
35182
+ "no",
35183
+ "on",
35184
+ "off",
35185
+ "enable",
35186
+ "disable",
35187
+ "ltr",
35188
+ "rtl",
35189
+ "auto",
35190
+ "start",
35191
+ "end",
35192
+ "center",
35193
+ "baseline",
35194
+ "stretch",
35195
+ "rounded",
35196
+ "sharp",
35197
+ "outline",
35198
+ "ghost",
35199
+ "link",
35200
+ "filled",
35201
+ "row",
35202
+ "col",
35203
+ "gap",
35204
+ "pad",
35205
+ "margin",
35206
+ "padding",
35207
+ "border",
35208
+ "shadow",
35209
+ "transparent",
35210
+ "currentcolor",
35211
+ "inherit",
35212
+ "initial",
35213
+ "unset",
35214
+ "revert",
35215
+ "hover",
35216
+ "focus",
35217
+ "active",
35218
+ "disabled",
35219
+ "checked",
35220
+ "indeterminate",
35221
+ "open",
35222
+ "close",
35223
+ "expanded",
35224
+ "collapsed",
35225
+ "selected",
35226
+ "pressed",
35227
+ // Web/CSS
35228
+ "div",
35229
+ "span",
35230
+ "p",
35231
+ "a",
35232
+ "img",
35233
+ "ul",
35234
+ "ol",
35235
+ "li",
35236
+ "table",
35237
+ "tr",
35238
+ "td",
35239
+ "th",
35240
+ "thead",
35241
+ "tbody",
35242
+ "tfoot",
35243
+ "caption",
35244
+ "figure",
35245
+ "figcaption",
35246
+ "main",
35247
+ "section",
35248
+ "article",
35249
+ "aside",
35250
+ "header",
35251
+ "footer",
35252
+ "nav",
35253
+ "form",
35254
+ "input",
35255
+ "button",
35256
+ "select",
35257
+ "option",
35258
+ "textarea",
35259
+ "label",
35260
+ "fieldset",
35261
+ "legend",
35262
+ "details",
35263
+ "summary",
35264
+ "dialog",
35265
+ "menu",
35266
+ "menuitem",
35267
+ "template",
35268
+ "slot",
35269
+ "picture",
35270
+ "source",
35271
+ "track",
35272
+ "video",
35273
+ "audio",
35274
+ "canvas",
35275
+ "svg",
35276
+ "iframe",
35277
+ "embed",
35278
+ "object",
35279
+ "portal",
35280
+ // Common business terms
35281
+ "api",
35282
+ "cli",
35283
+ "ui",
35284
+ "ux",
35285
+ "sdk",
35286
+ "ide",
35287
+ "cli",
35288
+ "docs",
35289
+ "doc",
35290
+ "blog",
35291
+ "post",
35292
+ "page",
35293
+ "view",
35294
+ "tab",
35295
+ "panel",
35296
+ "card",
35297
+ "list",
35298
+ "grid",
35299
+ "form",
35300
+ "modal",
35301
+ "menu",
35302
+ "button",
35303
+ "icon",
35304
+ "avatar",
35305
+ "badge",
35306
+ "chip",
35307
+ "tooltip",
35308
+ "popover",
35309
+ "dropdown",
35310
+ "banner",
35311
+ "alert",
35312
+ "toast",
35313
+ "notification",
35314
+ "drawer",
35315
+ "sidebar",
35316
+ "navbar",
35317
+ "header",
35318
+ "footer",
35319
+ "hero",
35320
+ "cta",
35321
+ "cta-primary",
35322
+ "cta-secondary",
35323
+ "pricing",
35324
+ "price",
35325
+ "cost",
35326
+ "rate",
35327
+ "percent",
35328
+ "pct",
35329
+ "count",
35330
+ "total",
35331
+ "small",
35332
+ "medium",
35333
+ "large",
35334
+ "xl",
35335
+ "xxl",
35336
+ "tiny",
35337
+ "huge"
34508
35338
  ]);
34509
35339
  var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
34510
35340
  var SOURCE_ROOTS = ["src", "lib", "app", "components"];
@@ -34542,7 +35372,12 @@ function collectExports(cwd) {
34542
35372
  /\bexport\s+class\s+([A-Za-z_$][\w$]*)/g,
34543
35373
  /\bexport\s+interface\s+([A-Za-z_$][\w$]*)/g,
34544
35374
  /\bexport\s+type\s+([A-Za-z_$][\w$]*)/g,
34545
- /\bexport\s+default\s+(?:function\s+|class\s+)?([A-Za-z_$][\w$]*)/g
35375
+ /\bexport\s+default\s+(?:function\s+|class\s+)?([A-Za-z_$][\w$]*)/g,
35376
+ // v0.18.6: also collect field names from `export interface` and
35377
+ // `export type` declarations. Without this, fields like
35378
+ // `crossFileDrift`, `aiQuality`, `engineeringHygiene` are
35379
+ // flagged as stale even though they're valid type fields.
35380
+ /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*[?:]/gm
34546
35381
  ]) {
34547
35382
  let m;
34548
35383
  while ((m = re.exec(source)) !== null) {
@@ -34553,6 +35388,21 @@ function collectExports(cwd) {
34553
35388
  }
34554
35389
  return out;
34555
35390
  }
35391
+ function looksLikeProseLabel(inside) {
35392
+ const trimmed = inside.trim();
35393
+ if (trimmed.length === 0) return false;
35394
+ if (trimmed.startsWith("`") || trimmed.startsWith("/")) return true;
35395
+ if (trimmed.includes("`")) return true;
35396
+ if (/\d+\s+[a-z]/i.test(trimmed)) return true;
35397
+ const parts = trimmed.split(",").map((s) => s.trim());
35398
+ if (parts.length >= 3) {
35399
+ const allNumeric = parts.every((p) => /^\d+\.?\d*$/.test(p));
35400
+ if (!allNumeric) return true;
35401
+ }
35402
+ if (trimmed.length > 40 && !trimmed.includes(",")) return true;
35403
+ if (trimmed.includes("\u2014") || trimmed.includes("\u2013")) return true;
35404
+ return false;
35405
+ }
34556
35406
  var staleFunctionReferenceRule = createRule({
34557
35407
  id: "docs/stale-function-reference",
34558
35408
  category: "docs",
@@ -34572,8 +35422,39 @@ var staleFunctionReferenceRule = createRule({
34572
35422
  if (text.length < 3) continue;
34573
35423
  if (RESERVED.has(text.toLowerCase())) continue;
34574
35424
  if (context.exports.has(text)) continue;
34575
- const end = Math.min(source.length, span.index + text.length + 50);
34576
- if (!/\(/.test(source.slice(span.index, end))) continue;
35425
+ const lineEnd = source.indexOf("\n", span.index);
35426
+ const restOfLine = source.slice(
35427
+ span.index,
35428
+ lineEnd === -1 ? source.length : lineEnd
35429
+ );
35430
+ const closeTick = restOfLine.indexOf("`", 1);
35431
+ if (closeTick === -1) continue;
35432
+ const afterTick = restOfLine.slice(closeTick + 1);
35433
+ const directCall = /^\s*\(/.test(afterTick);
35434
+ let identifierRepeats = false;
35435
+ if (!directCall) {
35436
+ const afterSpan = restOfLine.slice(closeTick + 1);
35437
+ const needle = text + "(";
35438
+ identifierRepeats = afterSpan.indexOf(needle) !== -1;
35439
+ }
35440
+ if (!directCall && !identifierRepeats) continue;
35441
+ const beforeTickIdx = span.index - 1;
35442
+ const beforeChar = beforeTickIdx >= 0 ? source[beforeTickIdx] : "";
35443
+ if (beforeChar === "." || beforeChar === "|") continue;
35444
+ const parenStart = restOfLine.indexOf("(", closeTick);
35445
+ const parenEnd = restOfLine.indexOf(")", parenStart);
35446
+ if (parenStart !== -1 && parenEnd !== -1) {
35447
+ const inside = restOfLine.slice(parenStart + 1, parenEnd);
35448
+ const trimmed = inside.trim();
35449
+ if (looksLikeProseLabel(inside)) continue;
35450
+ const looksLikeTypeAnnotation = !inside.includes(":") && (/\b(string|number|boolean|null|undefined|object|array|required|optional|categorical|direct|n\/a|\bmapped\b|0[\-–][0-9]+|v[0-9]|higher is better|lower is better|added in|deprecated|pr-[0-9])\b/i.test(
35451
+ inside
35452
+ ) || // Short single-word label (≤ 24 chars, no `,`,
35453
+ // doesn't look like a function arg). Real function
35454
+ // calls are usually longer or contain commas.
35455
+ trimmed.length > 0 && trimmed.length <= 24 && !trimmed.includes(",") && /[a-zA-Z]/.test(trimmed));
35456
+ if (looksLikeTypeAnnotation) continue;
35457
+ }
34577
35458
  issues.push({
34578
35459
  ruleId: "docs/stale-function-reference",
34579
35460
  category: "docs",
@@ -34645,7 +35526,33 @@ var ENGLISH_WORD_DENYLIST = /* @__PURE__ */ new Set([
34645
35526
  "jsx",
34646
35527
  "ok",
34647
35528
  "no",
34648
- "yes"
35529
+ "yes",
35530
+ // v0.18.6: common English adjectives / adverbs that frequently
35531
+ // appear in backticked prose but are not package names.
35532
+ "aspirational",
35533
+ "concrete",
35534
+ "abstract",
35535
+ "inline",
35536
+ "exposed",
35537
+ "deprecated",
35538
+ "experimental",
35539
+ "stable",
35540
+ "beta",
35541
+ "alpha",
35542
+ "wip",
35543
+ "draft",
35544
+ "final",
35545
+ "shim",
35546
+ "polyfill",
35547
+ "stub",
35548
+ "mock",
35549
+ "fake",
35550
+ "real",
35551
+ "false",
35552
+ "true",
35553
+ "optional",
35554
+ "required",
35555
+ "default"
34649
35556
  ]);
34650
35557
  var stalePackageReferenceRule = createRule({
34651
35558
  id: "docs/stale-package-reference",
@@ -34807,7 +35714,9 @@ var brokenLinkRule = createRule({
34807
35714
  if (target.startsWith("#")) continue;
34808
35715
  if (target.startsWith("//")) continue;
34809
35716
  if (target.startsWith("/")) continue;
34810
- const resolved = join7(docDir, target);
35717
+ const filePart = target.split("#")[0] ?? target;
35718
+ if (filePart === "") continue;
35719
+ const resolved = join7(docDir, filePart);
34811
35720
  if (existsSync4(resolved)) continue;
34812
35721
  issues.push({
34813
35722
  ruleId: "docs/broken-link",
@@ -39579,6 +40488,11 @@ var builtinRules = [
39579
40488
  missingNotNullRule,
39580
40489
  namingInconsistencyRule,
39581
40490
  sqlConcatRule,
40491
+ deadBranchRule,
40492
+ unreachableRule,
40493
+ unusedImportRule,
40494
+ unusedLocalRule,
40495
+ unusedParameterRule,
39582
40496
  brokenLinkRule,
39583
40497
  expiredCodeExampleRule,
39584
40498
  staleFunctionReferenceRule,
@@ -40586,6 +41500,61 @@ var signal_strength_default = {
40586
41500
  _calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: OWASP Foundation (2021), *OWASP Top 10 \u2014 A03:2021 Injection*, https://owasp.org/Top10/A03_2021-Injection/; OWASP Foundation (2017), *SQL Injection Prevention Cheat Sheet*. (Template-literal SQL with ${...} interpolation is the #1 SQL injection vector in AI-generated TypeScript code.)",
40587
41501
  aiSpecific: true
40588
41502
  },
41503
+ "dead/unused-import": {
41504
+ recall: 0,
41505
+ fpRate: 0,
41506
+ ratio: 0,
41507
+ precision: 0,
41508
+ lastCalibratedAt: "2026-06-30T00:00:00Z",
41509
+ verdict: "DORMANT",
41510
+ defaultOff: true,
41511
+ _calibrationNote: "v0.18.5 ship \u2014 not in v7 per-rule table. Default-off until v8 calibration data lands. The first of 5 planned `dead/*` rules (this one + dead/unused-local + dead/unused-parameter + dead/dead-branch + dead/unreachable). The pattern is the canonical AI-iteration rot: the model adds an import when introducing a feature, then rewrites the function later without cleaning up. Most real-world tsconfig.json files have `noUnusedLocals: false`, so tsc never fires.",
41512
+ aiSpecific: true
41513
+ },
41514
+ "dead/unused-local": {
41515
+ recall: 0,
41516
+ fpRate: 0,
41517
+ ratio: 0,
41518
+ precision: 0,
41519
+ lastCalibratedAt: "2026-06-30T00:00:00Z",
41520
+ verdict: "DORMANT",
41521
+ defaultOff: true,
41522
+ _calibrationNote: "v0.18.5b ship \u2014 not in v7 per-rule table. Default-off until v8 calibration data lands. The second of 5 planned `dead/*` rules. Muchnick 1997 Ch. 13 'liveness analysis' (textbook compiler optimization).",
41523
+ aiSpecific: true
41524
+ },
41525
+ "dead/unused-parameter": {
41526
+ recall: 0,
41527
+ fpRate: 0,
41528
+ ratio: 0,
41529
+ precision: 0,
41530
+ lastCalibratedAt: "2026-06-30T00:00:00Z",
41531
+ verdict: "DORMANT",
41532
+ defaultOff: true,
41533
+ _calibrationNote: "v0.18.5b ship \u2014 not in v7 per-rule table. Default-off until v8 calibration data lands. The third of 5 planned `dead/*` rules. AI agents add parameters when introducing features, then rewrite the function without removing parameters the new code does not need.",
41534
+ aiSpecific: true
41535
+ },
41536
+ "dead/dead-branch": {
41537
+ recall: 0,
41538
+ fpRate: 0,
41539
+ ratio: 0,
41540
+ precision: 0,
41541
+ lastCalibratedAt: "2026-06-30T00:00:00Z",
41542
+ verdict: "DORMANT",
41543
+ defaultOff: true,
41544
+ _calibrationNote: "v0.18.5b ship \u2014 not in v7 per-rule table. Default-off until v8 calibration data lands. The fourth of 5 planned `dead/*` rules. AI-iteration signature: feature flag toggled to a constant, or wrapper from a previous refactor.",
41545
+ aiSpecific: true
41546
+ },
41547
+ "dead/unreachable": {
41548
+ recall: 0,
41549
+ fpRate: 0,
41550
+ ratio: 0,
41551
+ precision: 0,
41552
+ lastCalibratedAt: "2026-06-30T00:00:00Z",
41553
+ verdict: "DORMANT",
41554
+ defaultOff: true,
41555
+ _calibrationNote: "v0.18.5b ship \u2014 not in v7 per-rule table. Default-off until v8 calibration data lands. The fifth of 5 planned `dead/*` rules. AI-iteration signature: model added an early return for a new error path, then forgot the rest of the function body was still sitting below it.",
41556
+ aiSpecific: true
41557
+ },
40589
41558
  "docs/stale-package-reference": {
40590
41559
  recall: 0,
40591
41560
  fpRate: 0,