slopbrick 0.17.4 → 0.18.1

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.
Files changed (3) hide show
  1. package/dist/index.cjs +332 -302
  2. package/dist/index.js +291 -261
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -45379,7 +45379,7 @@ function formatPretty(report) {
45379
45379
  }
45380
45380
  function formatScoringExplainer(_report) {
45381
45381
  return import_chalk.default.dim(
45382
- "Why two scores? The Slop Index measures AI-slop signatures (lower = better, this is the CI gate). The Repository Coherence measures internal consistency (higher = better, informational). A codebase can be hand-written AND inconsistent (low Slop, low Coherence) or AI-generated AND consistent (high Slop, high Coherence). See docs/scoring-explained.md for the full math."
45382
+ "Four orthogonal scores (all 0-100, higher = better): AI Quality (AI-slop signatures; the CI gate, AI Quality >= 70), Engineering Hygiene (issues per category across arch/logic/layout/component/test), Security (AI-flagged security risks, inverted from risk level), Repository Health (composite: 0.4*AI Quality + 0.3*Engineering Hygiene + 0.2*Security + 0.1*Test Quality). Only AI Quality gates CI; the others are informational. Default-off rules (INVERTED/NOISY/DORMANT) are suppressed from the scores automatically."
45383
45383
  );
45384
45384
  }
45385
45385
  function formatWhyFailingReport(report) {
@@ -46041,11 +46041,11 @@ var init_pool = __esm({
46041
46041
  for (let i = 0; i < this.threadCount; i++) {
46042
46042
  spawnWorker();
46043
46043
  }
46044
- return new Promise((resolve18) => {
46044
+ return new Promise((resolve20) => {
46045
46045
  const check = setInterval(() => {
46046
46046
  if (resolved) {
46047
46047
  clearInterval(check);
46048
- resolve18(results);
46048
+ resolve20(results);
46049
46049
  }
46050
46050
  }, 10);
46051
46051
  });
@@ -49990,29 +49990,29 @@ function renderStructureMarkdown(inventory, constitution) {
49990
49990
  return lines.join("\n");
49991
49991
  }
49992
49992
  async function writeStructureMarkdown(workspaceDir, md) {
49993
- await new Promise((resolve18, reject) => {
49993
+ await new Promise((resolve20, reject) => {
49994
49994
  try {
49995
49995
  const path = (0, import_node_path21.join)(workspaceDir, STRUCTURE_MD_FILE);
49996
49996
  (0, import_node_fs21.mkdirSync)((0, import_node_path21.dirname)(path), { recursive: true });
49997
49997
  (0, import_node_fs21.writeFileSync)(path, md, "utf-8");
49998
- resolve18();
49998
+ resolve20();
49999
49999
  } catch (err) {
50000
50000
  reject(err instanceof Error ? err : new Error(String(err)));
50001
50001
  }
50002
50002
  });
50003
50003
  }
50004
50004
  async function readStructureMarkdown(workspaceDir) {
50005
- return new Promise((resolve18) => {
50005
+ return new Promise((resolve20) => {
50006
50006
  try {
50007
50007
  const path = (0, import_node_path21.join)(workspaceDir, STRUCTURE_MD_FILE);
50008
50008
  if (!(0, import_node_fs21.existsSync)(path)) {
50009
- resolve18(null);
50009
+ resolve20(null);
50010
50010
  return;
50011
50011
  }
50012
50012
  const content = (0, import_node_fs21.readFileSync)(path, "utf-8");
50013
- resolve18(content);
50013
+ resolve20(content);
50014
50014
  } catch {
50015
- resolve18(null);
50015
+ resolve20(null);
50016
50016
  }
50017
50017
  });
50018
50018
  }
@@ -50281,11 +50281,12 @@ async function finalizeReport(input) {
50281
50281
  if (options.noIncrease) {
50282
50282
  const previous = (await readRuns(cwd, fsMemoryIO)).at(-1);
50283
50283
  if (previous) {
50284
- if ((report.aiQuality ?? 0) < previous.slopIndex) {
50284
+ const previousBaseline = previous.slopIndex;
50285
+ if ((report.aiQuality ?? 0) < previousBaseline) {
50285
50286
  noIncreaseFailure = true;
50286
50287
  if (!options.quiet) {
50287
50288
  logger.error(
50288
- `AI Quality went DOWN from ${previous.slopIndex.toFixed(1)} to ${(report.aiQuality ?? 0).toFixed(1)} \u2014 your code got sloppier. See which files changed and fix the new issues.`
50289
+ `AI Quality went DOWN from ${previousBaseline.toFixed(1)} to ${(report.aiQuality ?? 0).toFixed(1)} \u2014 your code got sloppier. (Both values are 0-100, higher = better; the comparison is against the previous run's aiQuality, stored historically in the legacy slopIndex field.) See which files changed and fix the new issues.`
50289
50290
  );
50290
50291
  }
50291
50292
  }
@@ -52560,11 +52561,11 @@ __export(installer_exports, {
52560
52561
  uninstallHook: () => uninstallHook
52561
52562
  });
52562
52563
  function hookPath(gitRoot) {
52563
- const huskyDir = (0, import_node_path37.join)(gitRoot, ".husky");
52564
+ const huskyDir = (0, import_node_path39.join)(gitRoot, ".husky");
52564
52565
  if ((0, import_node_fs35.existsSync)(huskyDir)) {
52565
- return (0, import_node_path37.join)(huskyDir, "pre-commit");
52566
+ return (0, import_node_path39.join)(huskyDir, "pre-commit");
52566
52567
  }
52567
- return (0, import_node_path37.join)(gitRoot, ".git", "hooks", "pre-commit");
52568
+ return (0, import_node_path39.join)(gitRoot, ".git", "hooks", "pre-commit");
52568
52569
  }
52569
52570
  function readHookContent(path) {
52570
52571
  return (0, import_node_fs35.readFileSync)(path, "utf8");
@@ -52629,7 +52630,7 @@ function installHook(gitRoot) {
52629
52630
  exitCode: 0
52630
52631
  };
52631
52632
  }
52632
- (0, import_node_fs35.mkdirSync)((0, import_node_path37.dirname)(path), { recursive: true });
52633
+ (0, import_node_fs35.mkdirSync)((0, import_node_path39.dirname)(path), { recursive: true });
52633
52634
  (0, import_node_fs35.writeFileSync)(path, SENTINEL_BLOCK, { mode: 493 });
52634
52635
  (0, import_node_fs35.chmodSync)(path, 493);
52635
52636
  return {
@@ -52684,12 +52685,12 @@ function uninstallHook(gitRoot) {
52684
52685
  exitCode: 0
52685
52686
  };
52686
52687
  }
52687
- var import_node_fs35, import_node_path37, BEGIN_SENTINEL, END_SENTINEL, SENTINEL_BLOCK;
52688
+ var import_node_fs35, import_node_path39, BEGIN_SENTINEL, END_SENTINEL, SENTINEL_BLOCK;
52688
52689
  var init_installer = __esm({
52689
52690
  "src/cli/installer.ts"() {
52690
52691
  "use strict";
52691
52692
  import_node_fs35 = require("fs");
52692
- import_node_path37 = require("path");
52693
+ import_node_path39 = require("path");
52693
52694
  BEGIN_SENTINEL = "# slopbrick-hook-begin";
52694
52695
  END_SENTINEL = "# slopbrick-hook-end";
52695
52696
  SENTINEL_BLOCK = `${BEGIN_SENTINEL}
@@ -52767,7 +52768,7 @@ async function runScanFile(args, ctx) {
52767
52768
  content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
52768
52769
  };
52769
52770
  }
52770
- function explainRule(args, ctx) {
52771
+ function explainRule2(args, ctx) {
52771
52772
  const ruleId = args.ruleId;
52772
52773
  if (!ruleId) return toolError("Missing required argument: ruleId");
52773
52774
  const rule = ctx.rules.find((r) => r.id === ruleId);
@@ -52878,7 +52879,7 @@ async function runGovernance(args, ctx) {
52878
52879
  function runCheckConstitution(args, ctx) {
52879
52880
  const path = args.path;
52880
52881
  if (!path) return toolError("Missing required argument: path");
52881
- const absPath = (0, import_node_path40.resolve)(ctx.cwd, path);
52882
+ const absPath = (0, import_node_path42.resolve)(ctx.cwd, path);
52882
52883
  let source;
52883
52884
  try {
52884
52885
  source = (0, import_node_fs38.readFileSync)(absPath, "utf-8");
@@ -53041,7 +53042,7 @@ async function handleToolCall(toolName, args, ctx) {
53041
53042
  case "slop_scan_file":
53042
53043
  return runScanFile(args, ctx);
53043
53044
  case "slop_explain_rule":
53044
- return explainRule(args, ctx);
53045
+ return explainRule2(args, ctx);
53045
53046
  case "slop_list_rules":
53046
53047
  return listRules(args, ctx);
53047
53048
  case "slop_suggest":
@@ -53068,12 +53069,12 @@ function canonicalToolNames() {
53068
53069
  function getDeprecation(toolName) {
53069
53070
  return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
53070
53071
  }
53071
- var import_node_fs38, import_node_path40, TOOL_DEFINITIONS;
53072
+ var import_node_fs38, import_node_path42, TOOL_DEFINITIONS;
53072
53073
  var init_tools = __esm({
53073
53074
  "src/mcp/tools.ts"() {
53074
53075
  "use strict";
53075
53076
  import_node_fs38 = require("fs");
53076
- import_node_path40 = require("path");
53077
+ import_node_path42 = require("path");
53077
53078
  init_worker();
53078
53079
  init_patterns();
53079
53080
  init_architecture_score();
@@ -53259,36 +53260,36 @@ function planMigration(workspaceDir) {
53259
53260
  const moves = [];
53260
53261
  const rewrites = [];
53261
53262
  const gitignoreEdits = [];
53262
- const oldDir = (0, import_node_path43.join)(workspaceDir, ".slop-audit");
53263
- const newDir = (0, import_node_path43.join)(workspaceDir, ".slopbrick");
53263
+ const oldDir = (0, import_node_path45.join)(workspaceDir, ".slop-audit");
53264
+ const newDir = (0, import_node_path45.join)(workspaceDir, ".slopbrick");
53264
53265
  if ((0, import_node_fs44.existsSync)(oldDir)) {
53265
53266
  moves.push({ from: oldDir, to: newDir, kind: "dir" });
53266
53267
  rewrites.push({
53267
- path: (0, import_node_path43.join)(newDir, "inventory.json"),
53268
+ path: (0, import_node_path45.join)(newDir, "inventory.json"),
53268
53269
  field: "version",
53269
53270
  from: '"1"',
53270
53271
  to: '"2"'
53271
53272
  });
53272
53273
  rewrites.push({
53273
- path: (0, import_node_path43.join)(newDir, "constitution.json"),
53274
+ path: (0, import_node_path45.join)(newDir, "constitution.json"),
53274
53275
  field: "version",
53275
53276
  from: '"1"',
53276
53277
  to: '"2"'
53277
53278
  });
53278
53279
  }
53279
- const oldCache = (0, import_node_path43.join)(workspaceDir, ".slop-audit-cache.json");
53280
- const newCache = (0, import_node_path43.join)(workspaceDir, ".slopbrick-cache.json");
53280
+ const oldCache = (0, import_node_path45.join)(workspaceDir, ".slop-audit-cache.json");
53281
+ const newCache = (0, import_node_path45.join)(workspaceDir, ".slopbrick-cache.json");
53281
53282
  if ((0, import_node_fs44.existsSync)(oldCache)) {
53282
53283
  moves.push({ from: oldCache, to: newCache, kind: "file" });
53283
53284
  }
53284
53285
  for (const ext of ["mjs", "cjs", "js"]) {
53285
- const oldCfg = (0, import_node_path43.join)(workspaceDir, `slop-audit.config.${ext}`);
53286
- const newCfg = (0, import_node_path43.join)(workspaceDir, `slopbrick.config.${ext}`);
53286
+ const oldCfg = (0, import_node_path45.join)(workspaceDir, `slop-audit.config.${ext}`);
53287
+ const newCfg = (0, import_node_path45.join)(workspaceDir, `slopbrick.config.${ext}`);
53287
53288
  if ((0, import_node_fs44.existsSync)(oldCfg)) {
53288
53289
  moves.push({ from: oldCfg, to: newCfg, kind: "config" });
53289
53290
  }
53290
53291
  }
53291
- const gi = (0, import_node_path43.join)(workspaceDir, ".gitignore");
53292
+ const gi = (0, import_node_path45.join)(workspaceDir, ".gitignore");
53292
53293
  if ((0, import_node_fs44.existsSync)(gi)) {
53293
53294
  const content = (0, import_node_fs44.readFileSync)(gi, "utf-8");
53294
53295
  if (content.includes(".slop-audit/")) {
@@ -53309,7 +53310,7 @@ function planMigration(workspaceDir) {
53309
53310
  return { moves, rewrites, gitignoreEdits };
53310
53311
  }
53311
53312
  function isAlreadyMigrated(workspaceDir) {
53312
- return (0, import_node_fs44.existsSync)((0, import_node_path43.join)(workspaceDir, ".slopbrick")) && !(0, import_node_fs44.existsSync)((0, import_node_path43.join)(workspaceDir, ".slop-audit"));
53313
+ return (0, import_node_fs44.existsSync)((0, import_node_path45.join)(workspaceDir, ".slopbrick")) && !(0, import_node_fs44.existsSync)((0, import_node_path45.join)(workspaceDir, ".slop-audit"));
53313
53314
  }
53314
53315
  function applyMigration(plan, options = {}) {
53315
53316
  if (options.dryRun) return;
@@ -53340,8 +53341,8 @@ function runMigrate(options) {
53340
53341
  };
53341
53342
  }
53342
53343
  const alreadyMigrated = isAlreadyMigrated(workspace);
53343
- const newDir = (0, import_node_path43.join)(workspace, ".slopbrick");
53344
- const oldDir = (0, import_node_path43.join)(workspace, ".slop-audit");
53344
+ const newDir = (0, import_node_path45.join)(workspace, ".slopbrick");
53345
+ const oldDir = (0, import_node_path45.join)(workspace, ".slop-audit");
53345
53346
  if ((0, import_node_fs44.existsSync)(newDir) && (0, import_node_fs44.existsSync)(oldDir) && !force) {
53346
53347
  return {
53347
53348
  ok: false,
@@ -53409,12 +53410,12 @@ function formatMigrate(result) {
53409
53410
  }
53410
53411
  return lines.join("\n");
53411
53412
  }
53412
- var import_node_fs44, import_node_path43;
53413
+ var import_node_fs44, import_node_path45;
53413
53414
  var init_migrate = __esm({
53414
53415
  "src/cli/migrate.ts"() {
53415
53416
  "use strict";
53416
53417
  import_node_fs44 = require("fs");
53417
- import_node_path43 = require("path");
53418
+ import_node_path45 = require("path");
53418
53419
  init_logger();
53419
53420
  }
53420
53421
  });
@@ -53506,7 +53507,7 @@ init_dist2();
53506
53507
 
53507
53508
  // src/cli/program.ts
53508
53509
  var import_node_fs45 = require("fs");
53509
- var import_node_path44 = require("path");
53510
+ var import_node_path46 = require("path");
53510
53511
  var import_node_perf_hooks = require("perf_hooks");
53511
53512
  var import_commander2 = require("commander");
53512
53513
 
@@ -55012,17 +55013,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
55012
55013
  var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
55013
55014
  var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
55014
55015
  function promptText(rl, question, detected) {
55015
- return new Promise((resolve18) => {
55016
+ return new Promise((resolve20) => {
55016
55017
  const lines = [
55017
55018
  `? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
55018
55019
  ];
55019
55020
  rl.question(lines.join("\n") + "\n", (answer) => {
55020
55021
  const trimmed = answer.trim();
55021
55022
  if (trimmed === "") {
55022
- resolve18(void 0);
55023
+ resolve20(void 0);
55023
55024
  return;
55024
55025
  }
55025
- resolve18(trimmed);
55026
+ resolve20(trimmed);
55026
55027
  });
55027
55028
  });
55028
55029
  }
@@ -55030,7 +55031,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
55030
55031
  const defaultIndex = options.indexOf(defaultValue);
55031
55032
  const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
55032
55033
  const safeDefaultValue = options[safeDefaultIndex];
55033
- return new Promise((resolve18) => {
55034
+ return new Promise((resolve20) => {
55034
55035
  function ask() {
55035
55036
  const lines = [
55036
55037
  `? ${question} (detected: ${safeDefaultValue}):`,
@@ -55040,7 +55041,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
55040
55041
  rl.question(lines.join("\n") + "\n", (answer) => {
55041
55042
  const trimmed = answer.trim();
55042
55043
  if (trimmed === "") {
55043
- resolve18(safeDefaultValue);
55044
+ resolve20(safeDefaultValue);
55044
55045
  return;
55045
55046
  }
55046
55047
  const num = parseInt(trimmed, 10);
@@ -55049,7 +55050,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
55049
55050
  ask();
55050
55051
  return;
55051
55052
  }
55052
- resolve18(options[num - 1]);
55053
+ resolve20(options[num - 1]);
55053
55054
  });
55054
55055
  }
55055
55056
  ask();
@@ -55059,7 +55060,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
55059
55060
  const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
55060
55061
  const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
55061
55062
  const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
55062
- return new Promise((resolve18) => {
55063
+ return new Promise((resolve20) => {
55063
55064
  function ask() {
55064
55065
  const lines = [
55065
55066
  `? ${question} (detected: ${defaultDisplay}):`,
@@ -55070,7 +55071,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
55070
55071
  rl.question(lines.join("\n") + "\n", (answer) => {
55071
55072
  const trimmed = answer.trim();
55072
55073
  if (trimmed === "") {
55073
- resolve18(defaultValue.length > 0 ? defaultValue : []);
55074
+ resolve20(defaultValue.length > 0 ? defaultValue : []);
55074
55075
  return;
55075
55076
  }
55076
55077
  const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
@@ -55080,11 +55081,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
55080
55081
  return;
55081
55082
  }
55082
55083
  if (numbers.includes(1)) {
55083
- resolve18([]);
55084
+ resolve20([]);
55084
55085
  return;
55085
55086
  }
55086
55087
  const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
55087
- resolve18(selected);
55088
+ resolve20(selected);
55088
55089
  });
55089
55090
  }
55090
55091
  ask();
@@ -55264,6 +55265,234 @@ async function runDoctor(cwd) {
55264
55265
  return exitCode;
55265
55266
  }
55266
55267
 
55268
+ // src/cli/commands/badge.ts
55269
+ var import_node_path37 = require("path");
55270
+ init_render();
55271
+ init_logger();
55272
+ init_scan();
55273
+ function registerBadge(program) {
55274
+ program.command("badge").description(
55275
+ "print a shields.io slop-index badge. Reads .slopbrick/health.json if present (no re-scan); falls back to a fresh scan."
55276
+ ).action(async (_cmdOptions, command) => {
55277
+ const options = command.optsWithGlobals();
55278
+ const cwd = (0, import_node_path37.resolve)(options.workspace ?? process.cwd());
55279
+ const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
55280
+ const health = loadHealth2(cwd);
55281
+ if (health) {
55282
+ const synthetic = {
55283
+ slopIndex: 100 - health.repositoryHealth
55284
+ };
55285
+ logger.info(formatBadge(synthetic));
55286
+ process.exit(0);
55287
+ }
55288
+ const { report } = await runScan(options);
55289
+ logger.info(formatBadge(report));
55290
+ process.exit(0);
55291
+ });
55292
+ }
55293
+
55294
+ // src/cli/commands/suggest.ts
55295
+ var import_node_path38 = require("path");
55296
+ init_advice();
55297
+ init_unified_diff();
55298
+ init_logger();
55299
+ init_scan();
55300
+ function registerSuggest(program) {
55301
+ program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
55302
+ const options = command.optsWithGlobals();
55303
+ const { report } = await runScan(options);
55304
+ const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
55305
+ logger.info(formatAdvice(report));
55306
+ const diff = formatUnifiedDiff(report, cwd);
55307
+ if (diff) logger.info(diff);
55308
+ process.exit(0);
55309
+ });
55310
+ }
55311
+
55312
+ // src/cli/explain.ts
55313
+ var RULES_BASE_URL2 = "https://github.com/Dystx/slopbrick/blob/main/src/rules";
55314
+ function ruleIdToFilename2(ruleId) {
55315
+ const slash = ruleId.indexOf("/");
55316
+ return slash === -1 ? ruleId : ruleId.slice(slash + 1);
55317
+ }
55318
+ function explainRule(ruleId, rules, ruleHints) {
55319
+ const rule = rules.find((r) => r.id === ruleId);
55320
+ if (!rule) {
55321
+ return { error: "Unknown rule: " + ruleId + ". Run `slopbrick rules` to see all available rules." };
55322
+ }
55323
+ const filename = ruleIdToFilename2(rule.id);
55324
+ return {
55325
+ ruleId: rule.id,
55326
+ category: rule.category,
55327
+ severity: rule.severity,
55328
+ aiSpecific: rule.aiSpecific,
55329
+ pattern: ruleHints[rule.id] ?? "Patterns flagged by " + rule.id + ".",
55330
+ remediation: "See the rule source for the canonical before/after: src/rules/" + rule.category + "/" + filename + ".ts",
55331
+ sourcePath: "src/rules/" + rule.category + "/" + filename + ".ts",
55332
+ helpUri: `${RULES_BASE_URL2}/${rule.category}/${filename}.ts`,
55333
+ suppressionSnippet: 'rules: { "' + rule.id + '": "off" } // or set to a lower severity'
55334
+ };
55335
+ }
55336
+ function formatExplain(result) {
55337
+ if ("error" in result) return result.error;
55338
+ const lines = [];
55339
+ lines.push("Rule: " + result.ruleId);
55340
+ lines.push("Category: " + result.category);
55341
+ lines.push("Severity: " + result.severity);
55342
+ lines.push("AI-specific: " + (result.aiSpecific ? "yes (designed to fire on AI tells)" : "no (cross-cutting quality rule)"));
55343
+ lines.push("Source: " + result.sourcePath);
55344
+ lines.push("Help: " + result.helpUri);
55345
+ lines.push("");
55346
+ lines.push("Pattern:");
55347
+ lines.push(" " + result.pattern);
55348
+ lines.push("");
55349
+ lines.push("Remediation:");
55350
+ lines.push(" " + result.remediation);
55351
+ lines.push("");
55352
+ lines.push("Suppress / configure in slopbrick.config.mjs:");
55353
+ lines.push(" " + result.suppressionSnippet);
55354
+ lines.push("");
55355
+ return lines.join("\n");
55356
+ }
55357
+
55358
+ // src/cli/commands/explain.ts
55359
+ init_logger();
55360
+ init_builtins();
55361
+
55362
+ // src/snippet/data.ts
55363
+ var CATEGORY_DIRECTIVES = {
55364
+ visual: 'Avoid the saturated "vibe purple" Tailwind palette (violet-400-700, indigo-400-700). Prefer emerald, sky, amber, rose for accents. Never use arbitrary color values like bg-[#7c3aed] when a token exists.',
55365
+ logic: "Never use explicit `any`. Use `unknown` and narrow. Always add an AbortSignal to fetches. Handle errors with try/catch, never swallow with empty catch. Use `as const` instead of `as Type` casts.",
55366
+ wcag: 'All form inputs must have an accessible label (visible <label>, aria-label, or aria-labelledby). Decorative images must have empty alt="". Buttons must be <button>, never <div onClick>. Touch targets must be \u2265 24\xD724 CSS px.',
55367
+ security: 'Never store tokens in localStorage or sessionStorage \u2014 use httpOnly Secure SameSite cookies. Never put secrets in NEXT_PUBLIC_* / REACT_APP_* / VITE_* env vars. Validate e.origin in postMessage handlers. Never dangerouslySetInnerHTML with user input. Never use target="_blank" without rel="noopener".',
55368
+ perf: "Always use AbortController with fetch. Use image width/height attributes to prevent CLS. Use <Suspense> around async client components. Don't load all images eagerly.",
55369
+ typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
55370
+ layout: "Don't stack badge-above-h1 hero patterns. Don't build 3-stat banner rows without explicit user request. Don't use emoji inside nav items (use SVG icons). Use the project's spacing scale (4px or 8px grid), never arbitrary values like p-[13px].",
55371
+ component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
55372
+ arch: "For Astro: server-render everything by default; only opt-in to client islands when you need interactivity. Don't put secrets in client-side code.",
55373
+ test: "Use domain-specific fixture data, assert on value shapes not just truthiness, and consolidate repeated setup into helpers. Avoid `expect(x).toBeDefined()` placeholders and textbook fixtures like 'John Doe' or 'test@test.com'."
55374
+ };
55375
+ var RULE_HINTS = {
55376
+ // v0.16.0 hygiene: 35 out-of-scope orphan hints (keys with no matching
55377
+ // rule in src/rules/builtins.ts) were moved out of this map. The
55378
+ // verbatim source text is preserved in
55379
+ // docs/research/backlog-rule-hints.md
55380
+ // so future implementers can paste a hint back when the corresponding
55381
+ // rule ships. The 5 in-scope orphans (`security/eval`,
55382
+ // `security/localstorage-token`, `security/target-blank-no-noopener`,
55383
+ // `wcag/missing-alt`, `typo/placeholder-text`) are kept here for v0.16.0.
55384
+ "security/hardcoded-secret": "Never inline API keys, JWT secrets, or database passwords in source. Load them from env vars and never commit a .env file. Assume any published secret is compromised and rotate it.",
55385
+ "security/exposed-env-var": "NEVER prefix a secret with NEXT_PUBLIC_, VITE_, REACT_APP_, EXPO_PUBLIC_, GATSBY_, or PUBLIC_ \u2014 those vars are inlined into every browser build.",
55386
+ "security/dangerous-cors": "Don't set Access-Control-Allow-Origin: * on production endpoints. Restrict to an explicit allowlist; never combine wildcard origin with credentials: true.",
55387
+ "security/missing-auth-check": "Every server route handler must perform an authentication + authorization check at the top. Reachability of an endpoint by any user (authenticated or not) is a vulnerability, not a feature.",
55388
+ "security/unsafe-html-render": "Sanitize any non-literal value passed to dangerouslySetInnerHTML with DOMPurify. Better: avoid the prop entirely and let React escape via children.",
55389
+ "security/fail-open-auth": "Don't gate auth bypasses on NODE_ENV. Replace dev-env checks with an explicit AUTH_BYPASS flag that's never set in production.",
55390
+ "security/sql-construction": 'Never build SQL with string concatenation or template-literal interpolation. Use parameterized queries: pg client.query("... WHERE id = $1", [id]) or your ORM query builder.',
55391
+ "security/public-admin-route": "Routes under /admin, /internal, /debug, /staff, /manage, /private need an explicit role check on top of standard auth \u2014 auth alone is not enough for privileged paths.",
55392
+ "visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
55393
+ "visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
55394
+ "visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
55395
+ // v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
55396
+ "typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
55397
+ "logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
55398
+ "logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
55399
+ "wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
55400
+ "security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
55401
+ "security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
55402
+ "security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
55403
+ "arch/astro-island-leak": "For Astro: server-render everything by default. Only opt-in to client islands when interactivity is needed.",
55404
+ "component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
55405
+ "component/multiple-components-per-file": "One component per file. Move subcomponents into their own files so the Context Window stays small and boundary tests are easy.",
55406
+ "component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
55407
+ "context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
55408
+ "layout/forced-layout": "Vary structural patterns: some containers as grids, some as horizontal flex, some as blocks. Don't repeat `flex flex-col gap-4` everywhere.",
55409
+ "layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
55410
+ "layout/math-element-uniformity": "Human files have lopsided interactive counts (1 button + 12 inputs). AI tends to balance them. Build forms with many inputs and few buttons.",
55411
+ "layout/math-grid-uniformity": "Vary grid-cols-N (grid-cols-2, grid-cols-3, grid-cols-4, grid-cols-6) across sections instead of repeating grid-cols-3.",
55412
+ "layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
55413
+ "logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
55414
+ "logic/bayesian-conditional": "The Bayesian combiner aggregates multiple weak signals into a calibrated posterior P(AI|fires). Treat any fire above 0.7 as evidence of AI authorship; above 0.9 as strong evidence. (v0.12.0 \u2014 Bento et al. 2024 *Neurocomputing*.)",
55415
+ "logic/heaps-deviation": "Inspect for LLM-style vocabulary patterns: this file's vocabulary grows faster (high Heaps \u03BB) or slower (low \u03BB) than typical source code. Verify authorship if unexpected. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
55416
+ "logic/ks-distribution-shift": "Inspect the shifted features. KS detects both AI anomalies and production-rot anomalies (it is symmetric); combine with Heaps/Zipf for AI-specific signal. (v0.12.0 \u2014 arXiv:2510.15996, Oct 2025.)",
55417
+ "logic/zipf-slope-anomaly": "Inspect for LLM-style frequency distribution: this file's identifier usage is more peaked or flatter than typical human code. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
55418
+ "logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
55419
+ "logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
55420
+ "logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
55421
+ "logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
55422
+ "logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
55423
+ "logic/qwik-hook-leak": "Use Qwik primitives ($state, $effect, useSignal) instead of React hooks (useState, useEffect).",
55424
+ "logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
55425
+ "logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
55426
+ "perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
55427
+ "perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
55428
+ "perf/halstead-anomaly": "Introduce domain-specific identifiers and varied operations. Low vocabulary per line is a strong AI signature (Halstead 1977 \xA73).",
55429
+ "typo/calc-fontsize": "Use a design token (`var(--font-size-lg)`) or `clamp(min, fluid, max)` for responsive typography.",
55430
+ "typo/calc-raw-px": "Replace px values in calc() with rem or em units for scalable layout.",
55431
+ "typo/clamp-offscale": "Anchor clamp() values to standard sizes (12, 14, 16, 18, 20, 24, 30, 36, 48) so they remain on the design grid.",
55432
+ "typo/math-button-label-uniformity": 'Mix button lengths deliberately \u2014 pair a short "Save" with a longer "Mark as complete" \u2014 instead of repeating the same template.',
55433
+ "typo/math-cta-vocabulary": 'Use domain-specific action verbs ("Reserve", "Confirm ride", "Activate card") instead of falling back on the AI-default CTA vocabulary.',
55434
+ "visual/clamp-soup": "Use design-system aliases (`--text-fluid-sm`, `--text-fluid-lg`) with bounded ranges (typically 2\xD7 max).",
55435
+ "visual/generic-centering": "Vary hero layouts: some as grids (`grid place-items-center`), some as blocks, some with different alignment.",
55436
+ "visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
55437
+ "visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
55438
+ "visual/math-font-entropy": "Use a wider range of text sizes (text-xs, text-sm, text-lg, text-xl, text-2xl, text-3xl) for a more deliberate type scale.",
55439
+ "visual/math-gradient-hue-rotation": "Use wider hue spans across gradients (e.g. blue\u2192amber, emerald\u2192indigo) to break the violet-fuchsia monotony.",
55440
+ "visual/math-rounded-entropy": "Use a wider range of border-radius values (sm, md, 2xl, 3xl) instead of repeating the same lg/xl/full pattern.",
55441
+ "visual/math-spacing-entropy": "Mix more spacing values from the design scale (e.g. 3, 5, 7, 10, 14, 20, 28) instead of repeating the same 4/8 pattern.",
55442
+ "visual/naturalness-anomaly": "Use domain-specific identifier names so the identifier stream reflects the actual problem domain. Hindle 2012 \xA74.3: LLM-generated code reuses a narrow band of training-data identifiers, dropping distinct-token ratio below 30%.",
55443
+ "visual/math-color-cluster": "Use at least 3 distinct hue families (e.g. blue + amber + green) instead of clustering every color in the violet/fuchsia band.",
55444
+ "wcag/dragging-movements": "Provide an onClick, onKeyDown, or button role as an alternative to dragging (WCAG 2.1.1).",
55445
+ "wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
55446
+ "wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
55447
+ "wcag/target-size": "Add h-*, w-*, p-*, min-w-*, min-h-*, size-*, or an explicit width/height attribute to bring the target to \u2265 24\xD724 px.",
55448
+ "test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
55449
+ "test/duplicate-setup": "Extract shared `beforeEach` / `setupServer` blocks into a single helper (e.g. `renderWithProviders`) so each describe block calls it instead of repeating setup.",
55450
+ "test/missing-edge-case": "When generating tests, cover the alternate path: `else` branches, `catch` blocks, ternary alternates, and `??` fallbacks. Production branches without tests are a CI smell.",
55451
+ "test/fake-placeholder": "Use domain-specific fixture values (`alice@acme-corp.com`, `Order#48231`) or a factory like @faker-js/faker. Avoid textbook placeholders (`John Doe`, `test@test.com`, `id: 1`).",
55452
+ "product/terminology-drift": "Keep the leading noun consistent across files: `PostList`, `PostDetail`, `PostCard` are one entity, not three. AI agents pick slightly different words each invocation; product copy drifts.",
55453
+ "product/ux-pattern-fragmentation": "Keep the per-category count tight: modal \u22643, toast \u22642, button \u22644, input \u22643, card \u22643. Pick the canonical one and alias the rest. `slopbrick patterns` reports the per-category count.",
55454
+ // v0.13.0 — AI-specific rules (peer-reviewed signals).
55455
+ "ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
55456
+ "ai/comment-ratio": "AI tools either skip comments (reductive models) or over-comment (expansive models). Match the corpus mean \xB1 2\u03C3 (Rahman et al. 2024, Bisztray et al. 2025).",
55457
+ "ai/whitespace-regularity": "Vary inter-token spacing (single spaces mostly, occasional alignment in tables). Uniform runs are an AI tell (Shi et al. DetectCodeGPT 2024).",
55458
+ "ai/text-like-ratio": "Move natural-language explanations to README files or doc comments. Inline prose in source code is hard to maintain (Yotkova 2026).",
55459
+ "ai/errors-near-eof": "Check whether the file was truncated by a token limit. Unbalanced delimiters near EOF suggest the model ran out of output budget (Yotkova 2026).",
55460
+ "ai/any-density": "Replace `any` with `unknown`, `Record<string, unknown>`, or a domain type. The `: any` annotation propagates type-errors and defeats TS safety (Lee, Hassan, Hindle MSR 2026).",
55461
+ "ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
55462
+ "ai/log-rank-histogram": "The token vocabulary is concentrated in the top-1000 most common tokens. Real codebases use more diverse identifiers (Gehrmann 2019 GLTR).",
55463
+ "ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
55464
+ "ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
55465
+ // v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
55466
+ // reclassified post-v7 calibration in v0.14.5d)
55467
+ "ai/tailwind-color-overuse": "If most utility classes are bg-violet-500, text-violet-600, ring-violet-400 \u2014 the project is on the AI-default palette. Audit and replace with the project's design tokens.",
55468
+ "ai/default-react-stack": "Every new file is a Vite + React + Tailwind + Zustand + React Hook Form clone. Verify the project actually needs each piece before adding it.",
55469
+ "ai/library-reinvention": "Re-implementing zustand, react-hook-form, or date-fns inline (custom event emitters, useState reducers, manual date math) is a sign of LLM completion-mode code. Use the libraries the project already depends on.",
55470
+ "ai/state-default-overuse": "Wrapping every component in useState + useEffect for transient UI state is the React tutorial default. Real production code uses refs, uncontrolled inputs, or the project's state lib.",
55471
+ "ai/fetch-default-overuse": "Calling fetch() inline in components instead of going through the project's data-fetching layer (react-query, swr, or your own client) bypasses the cache, error boundary, and abort handling.",
55472
+ "ai/console-debug-storm": "5+ console.log calls in a single file is debug-by-print-statement, the LLM training-data default. Remove before commit; use the project's logger or a real debugger.",
55473
+ // v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
55474
+ "db/missing-fk-index": "Add `CREATE INDEX ON <table> (<fk_column>);` for every foreign key column. Without it, parent deletes do a sequential scan on the child. Use `CREATE INDEX CONCURRENTLY` in production (Squawk `require-concurrent-index-creation`).",
55475
+ "db/duplicate-index": "Drop one of two indexes that cover the same column list \u2014 extra indexes slow writes without read benefit. Postgres does not warn about this; the duplicate will silently sit in production.",
55476
+ "db/missing-not-null": "Add `NOT NULL` (or `PRIMARY KEY`) on required-identifier columns (id, email, created_at, status, uuid, \u2026). Optional identifiers are a common AI-generated SQL smell that produces silent NULL inserts in production.",
55477
+ "db/enum-sprawl": "Enums with more than 12 values are brittle to extend and hard to localize. Move to a lookup table joined by foreign key.",
55478
+ "db/naming-inconsistency": "Standardize on snake_case (Postgres convention) or camelCase, but never mix both in the same schema. Mixed styles break ORM generators and confuse code-reviewers.",
55479
+ "db/sql-concat": 'Never build SQL with template-literal interpolation \u2014 `db.query(`SELECT \u2026 WHERE id = ${id}`)` is a SQL injection vector. Use parameterized queries (`db.query("\u2026 WHERE id = $1", [id])`) or your ORM query builder.',
55480
+ // v0.17.0 — docs/* rules (markdown drift detection)
55481
+ "docs/stale-package-reference": "Update the doc to reference an installed package, or add the package to package.json. Copy-pasted install commands from a previous project are the #1 doc-drift failure mode.",
55482
+ "docs/stale-function-reference": "Rename the doc reference to match a current export, or add a wrapper export. Stale function callouts in tutorials cost readers 10+ minutes of debugging.",
55483
+ "docs/expired-code-example": "Update the example to use a declared dependency, or add the package to package.json. A copy-pasteable example that does not install erodes trust in the whole docs site.",
55484
+ "docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy."
55485
+ };
55486
+
55487
+ // src/cli/commands/explain.ts
55488
+ function registerExplain(program) {
55489
+ program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
55490
+ const result = explainRule(ruleId, builtinRules, RULE_HINTS);
55491
+ logger.info(formatExplain(result));
55492
+ if ("error" in result) process.exit(2);
55493
+ });
55494
+ }
55495
+
55267
55496
  // src/cli/program.ts
55268
55497
  init_config();
55269
55498
  init_git();
@@ -55325,7 +55554,7 @@ function createProvider(config) {
55325
55554
 
55326
55555
  // src/research/generator.ts
55327
55556
  var import_node_fs36 = require("fs");
55328
- var import_node_path38 = require("path");
55557
+ var import_node_path40 = require("path");
55329
55558
 
55330
55559
  // src/research/prompts.ts
55331
55560
  var DEFAULT_PROMPT_TEMPLATES = [
@@ -55388,13 +55617,13 @@ async function generateSamples(options) {
55388
55617
  }
55389
55618
  const samples = [];
55390
55619
  const ext = extForFramework(framework);
55391
- const dir = (0, import_node_path38.join)(outputDir, framework, componentType);
55620
+ const dir = (0, import_node_path40.join)(outputDir, framework, componentType);
55392
55621
  (0, import_node_fs36.mkdirSync)(dir, { recursive: true });
55393
55622
  for (let i = 1; i <= count; i += 1) {
55394
55623
  const raw = await provider.generateSample(renderPrompt(template), { temperature });
55395
55624
  const code = extractCodeFromMarkdown(raw);
55396
55625
  const fileName = `sample-${i}${ext}`;
55397
- const filePath = (0, import_node_path38.join)(dir, fileName);
55626
+ const filePath = (0, import_node_path40.join)(dir, fileName);
55398
55627
  (0, import_node_fs36.writeFileSync)(filePath, code, "utf8");
55399
55628
  const sample = {
55400
55629
  filePath,
@@ -55406,7 +55635,7 @@ async function generateSamples(options) {
55406
55635
  };
55407
55636
  samples.push(sample);
55408
55637
  }
55409
- const metadataPath = (0, import_node_path38.join)(dir, "metadata.json");
55638
+ const metadataPath = (0, import_node_path40.join)(dir, "metadata.json");
55410
55639
  (0, import_node_fs36.writeFileSync)(metadataPath, JSON.stringify(samples, null, 2), "utf8");
55411
55640
  return samples;
55412
55641
  }
@@ -55614,11 +55843,11 @@ function slugify(value) {
55614
55843
  // src/research/calibrator.ts
55615
55844
  var import_node_child_process3 = require("child_process");
55616
55845
  var import_node_fs37 = require("fs");
55617
- var import_node_path39 = require("path");
55846
+ var import_node_path41 = require("path");
55618
55847
  var DEFAULT_POSITIVE = "/Users/cheng/ai-slop-baseline/extracted/positive";
55619
55848
  var DEFAULT_NEGATIVE = "/Users/cheng/ai-slop-baseline/extracted/negative";
55620
55849
  function buildFileList(dir, extensions) {
55621
- const tmpList = (0, import_node_path39.join)("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
55850
+ const tmpList = (0, import_node_path41.join)("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
55622
55851
  const expr = extensions.map((e) => `-name '*.${e}'`).join(" -o ");
55623
55852
  (0, import_node_child_process3.execFileSync)("bash", ["-c", `find ${dir} -maxdepth 8 -type f \\( ${expr} \\) -print0 | xargs -0 realpath > ${tmpList}`]);
55624
55853
  const out = (0, import_node_fs37.readFileSync)(tmpList, "utf8");
@@ -55631,13 +55860,13 @@ function runScan2(fileListPath) {
55631
55860
  const ruleFires = /* @__PURE__ */ new Map();
55632
55861
  const uniqueFilesPerRule = /* @__PURE__ */ new Map();
55633
55862
  let fileCount = 0;
55634
- const tmpOut = (0, import_node_path39.join)("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
55863
+ const tmpOut = (0, import_node_path41.join)("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
55635
55864
  for (let i = 0; i < files.length; i += CHUNK) {
55636
55865
  const chunk = files.slice(i, i + CHUNK);
55637
55866
  try {
55638
55867
  (0, import_node_child_process3.execFileSync)(
55639
55868
  "node",
55640
- [(0, import_node_path39.join)(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
55869
+ [(0, import_node_path41.join)(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
55641
55870
  { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
55642
55871
  );
55643
55872
  } catch {
@@ -55677,8 +55906,8 @@ async function calibrate(cwd, options = {}) {
55677
55906
  const negativeFiles = buildFileList(negativeDir, ["tsx", "ts"]);
55678
55907
  const posSample = options.positiveLimit ? positiveFiles.slice(0, options.positiveLimit) : positiveFiles;
55679
55908
  const negSample = options.negativeLimit ? negativeFiles.slice(0, options.negativeLimit) : negativeFiles;
55680
- const posListPath = (0, import_node_path39.join)("/tmp", `cal-pos-${Date.now()}.txt`);
55681
- const negListPath = (0, import_node_path39.join)("/tmp", `cal-neg-${Date.now()}.txt`);
55909
+ const posListPath = (0, import_node_path41.join)("/tmp", `cal-pos-${Date.now()}.txt`);
55910
+ const negListPath = (0, import_node_path41.join)("/tmp", `cal-neg-${Date.now()}.txt`);
55682
55911
  (0, import_node_fs37.writeFileSync)(posListPath, posSample.join("\n"));
55683
55912
  (0, import_node_fs37.writeFileSync)(negListPath, negSample.join("\n"));
55684
55913
  const builtins = await Promise.resolve().then(() => (init_builtins(), builtins_exports));
@@ -55947,7 +56176,7 @@ function errorResponse(id, code, message, data2) {
55947
56176
  return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
55948
56177
  }
55949
56178
  async function runMcpServer(input, output, cwd) {
55950
- return new Promise((resolve18) => {
56179
+ return new Promise((resolve20) => {
55951
56180
  let buffer = "";
55952
56181
  input.setEncoding("utf-8");
55953
56182
  input.on("data", (chunk) => {
@@ -55985,138 +56214,13 @@ async function runMcpServer(input, output, cwd) {
55985
56214
  nlIdx = buffer.indexOf("\n");
55986
56215
  }
55987
56216
  });
55988
- input.on("end", () => resolve18());
55989
- input.on("close", () => resolve18());
56217
+ input.on("end", () => resolve20());
56218
+ input.on("close", () => resolve20());
55990
56219
  });
55991
56220
  }
55992
56221
 
55993
- // src/snippet/data.ts
55994
- var CATEGORY_DIRECTIVES = {
55995
- visual: 'Avoid the saturated "vibe purple" Tailwind palette (violet-400-700, indigo-400-700). Prefer emerald, sky, amber, rose for accents. Never use arbitrary color values like bg-[#7c3aed] when a token exists.',
55996
- logic: "Never use explicit `any`. Use `unknown` and narrow. Always add an AbortSignal to fetches. Handle errors with try/catch, never swallow with empty catch. Use `as const` instead of `as Type` casts.",
55997
- wcag: 'All form inputs must have an accessible label (visible <label>, aria-label, or aria-labelledby). Decorative images must have empty alt="". Buttons must be <button>, never <div onClick>. Touch targets must be \u2265 24\xD724 CSS px.',
55998
- security: 'Never store tokens in localStorage or sessionStorage \u2014 use httpOnly Secure SameSite cookies. Never put secrets in NEXT_PUBLIC_* / REACT_APP_* / VITE_* env vars. Validate e.origin in postMessage handlers. Never dangerouslySetInnerHTML with user input. Never use target="_blank" without rel="noopener".',
55999
- perf: "Always use AbortController with fetch. Use image width/height attributes to prevent CLS. Use <Suspense> around async client components. Don't load all images eagerly.",
56000
- typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
56001
- layout: "Don't stack badge-above-h1 hero patterns. Don't build 3-stat banner rows without explicit user request. Don't use emoji inside nav items (use SVG icons). Use the project's spacing scale (4px or 8px grid), never arbitrary values like p-[13px].",
56002
- component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
56003
- arch: "For Astro: server-render everything by default; only opt-in to client islands when you need interactivity. Don't put secrets in client-side code.",
56004
- test: "Use domain-specific fixture data, assert on value shapes not just truthiness, and consolidate repeated setup into helpers. Avoid `expect(x).toBeDefined()` placeholders and textbook fixtures like 'John Doe' or 'test@test.com'."
56005
- };
56006
- var RULE_HINTS = {
56007
- // v0.16.0 hygiene: 35 out-of-scope orphan hints (keys with no matching
56008
- // rule in src/rules/builtins.ts) were moved out of this map. The
56009
- // verbatim source text is preserved in
56010
- // docs/research/backlog-rule-hints.md
56011
- // so future implementers can paste a hint back when the corresponding
56012
- // rule ships. The 5 in-scope orphans (`security/eval`,
56013
- // `security/localstorage-token`, `security/target-blank-no-noopener`,
56014
- // `wcag/missing-alt`, `typo/placeholder-text`) are kept here for v0.16.0.
56015
- "security/hardcoded-secret": "Never inline API keys, JWT secrets, or database passwords in source. Load them from env vars and never commit a .env file. Assume any published secret is compromised and rotate it.",
56016
- "security/exposed-env-var": "NEVER prefix a secret with NEXT_PUBLIC_, VITE_, REACT_APP_, EXPO_PUBLIC_, GATSBY_, or PUBLIC_ \u2014 those vars are inlined into every browser build.",
56017
- "security/dangerous-cors": "Don't set Access-Control-Allow-Origin: * on production endpoints. Restrict to an explicit allowlist; never combine wildcard origin with credentials: true.",
56018
- "security/missing-auth-check": "Every server route handler must perform an authentication + authorization check at the top. Reachability of an endpoint by any user (authenticated or not) is a vulnerability, not a feature.",
56019
- "security/unsafe-html-render": "Sanitize any non-literal value passed to dangerouslySetInnerHTML with DOMPurify. Better: avoid the prop entirely and let React escape via children.",
56020
- "security/fail-open-auth": "Don't gate auth bypasses on NODE_ENV. Replace dev-env checks with an explicit AUTH_BYPASS flag that's never set in production.",
56021
- "security/sql-construction": 'Never build SQL with string concatenation or template-literal interpolation. Use parameterized queries: pg client.query("... WHERE id = $1", [id]) or your ORM query builder.',
56022
- "security/public-admin-route": "Routes under /admin, /internal, /debug, /staff, /manage, /private need an explicit role check on top of standard auth \u2014 auth alone is not enough for privileged paths.",
56023
- "visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
56024
- "visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
56025
- "visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
56026
- // v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
56027
- "typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
56028
- "logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
56029
- "logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
56030
- "wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
56031
- "security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
56032
- "security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
56033
- "security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
56034
- "arch/astro-island-leak": "For Astro: server-render everything by default. Only opt-in to client islands when interactivity is needed.",
56035
- "component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
56036
- "component/multiple-components-per-file": "One component per file. Move subcomponents into their own files so the Context Window stays small and boundary tests are easy.",
56037
- "component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
56038
- "context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
56039
- "layout/forced-layout": "Vary structural patterns: some containers as grids, some as horizontal flex, some as blocks. Don't repeat `flex flex-col gap-4` everywhere.",
56040
- "layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
56041
- "layout/math-element-uniformity": "Human files have lopsided interactive counts (1 button + 12 inputs). AI tends to balance them. Build forms with many inputs and few buttons.",
56042
- "layout/math-grid-uniformity": "Vary grid-cols-N (grid-cols-2, grid-cols-3, grid-cols-4, grid-cols-6) across sections instead of repeating grid-cols-3.",
56043
- "layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
56044
- "logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
56045
- "logic/bayesian-conditional": "The Bayesian combiner aggregates multiple weak signals into a calibrated posterior P(AI|fires). Treat any fire above 0.7 as evidence of AI authorship; above 0.9 as strong evidence. (v0.12.0 \u2014 Bento et al. 2024 *Neurocomputing*.)",
56046
- "logic/heaps-deviation": "Inspect for LLM-style vocabulary patterns: this file's vocabulary grows faster (high Heaps \u03BB) or slower (low \u03BB) than typical source code. Verify authorship if unexpected. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
56047
- "logic/ks-distribution-shift": "Inspect the shifted features. KS detects both AI anomalies and production-rot anomalies (it is symmetric); combine with Heaps/Zipf for AI-specific signal. (v0.12.0 \u2014 arXiv:2510.15996, Oct 2025.)",
56048
- "logic/zipf-slope-anomaly": "Inspect for LLM-style frequency distribution: this file's identifier usage is more peaked or flatter than typical human code. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
56049
- "logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
56050
- "logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
56051
- "logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
56052
- "logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
56053
- "logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
56054
- "logic/qwik-hook-leak": "Use Qwik primitives ($state, $effect, useSignal) instead of React hooks (useState, useEffect).",
56055
- "logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
56056
- "logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
56057
- "perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
56058
- "perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
56059
- "perf/halstead-anomaly": "Introduce domain-specific identifiers and varied operations. Low vocabulary per line is a strong AI signature (Halstead 1977 \xA73).",
56060
- "typo/calc-fontsize": "Use a design token (`var(--font-size-lg)`) or `clamp(min, fluid, max)` for responsive typography.",
56061
- "typo/calc-raw-px": "Replace px values in calc() with rem or em units for scalable layout.",
56062
- "typo/clamp-offscale": "Anchor clamp() values to standard sizes (12, 14, 16, 18, 20, 24, 30, 36, 48) so they remain on the design grid.",
56063
- "typo/math-button-label-uniformity": 'Mix button lengths deliberately \u2014 pair a short "Save" with a longer "Mark as complete" \u2014 instead of repeating the same template.',
56064
- "typo/math-cta-vocabulary": 'Use domain-specific action verbs ("Reserve", "Confirm ride", "Activate card") instead of falling back on the AI-default CTA vocabulary.',
56065
- "visual/clamp-soup": "Use design-system aliases (`--text-fluid-sm`, `--text-fluid-lg`) with bounded ranges (typically 2\xD7 max).",
56066
- "visual/generic-centering": "Vary hero layouts: some as grids (`grid place-items-center`), some as blocks, some with different alignment.",
56067
- "visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
56068
- "visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
56069
- "visual/math-font-entropy": "Use a wider range of text sizes (text-xs, text-sm, text-lg, text-xl, text-2xl, text-3xl) for a more deliberate type scale.",
56070
- "visual/math-gradient-hue-rotation": "Use wider hue spans across gradients (e.g. blue\u2192amber, emerald\u2192indigo) to break the violet-fuchsia monotony.",
56071
- "visual/math-rounded-entropy": "Use a wider range of border-radius values (sm, md, 2xl, 3xl) instead of repeating the same lg/xl/full pattern.",
56072
- "visual/math-spacing-entropy": "Mix more spacing values from the design scale (e.g. 3, 5, 7, 10, 14, 20, 28) instead of repeating the same 4/8 pattern.",
56073
- "visual/naturalness-anomaly": "Use domain-specific identifier names so the identifier stream reflects the actual problem domain. Hindle 2012 \xA74.3: LLM-generated code reuses a narrow band of training-data identifiers, dropping distinct-token ratio below 30%.",
56074
- "visual/math-color-cluster": "Use at least 3 distinct hue families (e.g. blue + amber + green) instead of clustering every color in the violet/fuchsia band.",
56075
- "wcag/dragging-movements": "Provide an onClick, onKeyDown, or button role as an alternative to dragging (WCAG 2.1.1).",
56076
- "wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
56077
- "wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
56078
- "wcag/target-size": "Add h-*, w-*, p-*, min-w-*, min-h-*, size-*, or an explicit width/height attribute to bring the target to \u2265 24\xD724 px.",
56079
- "test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
56080
- "test/duplicate-setup": "Extract shared `beforeEach` / `setupServer` blocks into a single helper (e.g. `renderWithProviders`) so each describe block calls it instead of repeating setup.",
56081
- "test/missing-edge-case": "When generating tests, cover the alternate path: `else` branches, `catch` blocks, ternary alternates, and `??` fallbacks. Production branches without tests are a CI smell.",
56082
- "test/fake-placeholder": "Use domain-specific fixture values (`alice@acme-corp.com`, `Order#48231`) or a factory like @faker-js/faker. Avoid textbook placeholders (`John Doe`, `test@test.com`, `id: 1`).",
56083
- "product/terminology-drift": "Keep the leading noun consistent across files: `PostList`, `PostDetail`, `PostCard` are one entity, not three. AI agents pick slightly different words each invocation; product copy drifts.",
56084
- "product/ux-pattern-fragmentation": "Keep the per-category count tight: modal \u22643, toast \u22642, button \u22644, input \u22643, card \u22643. Pick the canonical one and alias the rest. `slopbrick patterns` reports the per-category count.",
56085
- // v0.13.0 — AI-specific rules (peer-reviewed signals).
56086
- "ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
56087
- "ai/comment-ratio": "AI tools either skip comments (reductive models) or over-comment (expansive models). Match the corpus mean \xB1 2\u03C3 (Rahman et al. 2024, Bisztray et al. 2025).",
56088
- "ai/whitespace-regularity": "Vary inter-token spacing (single spaces mostly, occasional alignment in tables). Uniform runs are an AI tell (Shi et al. DetectCodeGPT 2024).",
56089
- "ai/text-like-ratio": "Move natural-language explanations to README files or doc comments. Inline prose in source code is hard to maintain (Yotkova 2026).",
56090
- "ai/errors-near-eof": "Check whether the file was truncated by a token limit. Unbalanced delimiters near EOF suggest the model ran out of output budget (Yotkova 2026).",
56091
- "ai/any-density": "Replace `any` with `unknown`, `Record<string, unknown>`, or a domain type. The `: any` annotation propagates type-errors and defeats TS safety (Lee, Hassan, Hindle MSR 2026).",
56092
- "ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
56093
- "ai/log-rank-histogram": "The token vocabulary is concentrated in the top-1000 most common tokens. Real codebases use more diverse identifiers (Gehrmann 2019 GLTR).",
56094
- "ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
56095
- "ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
56096
- // v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
56097
- // reclassified post-v7 calibration in v0.14.5d)
56098
- "ai/tailwind-color-overuse": "If most utility classes are bg-violet-500, text-violet-600, ring-violet-400 \u2014 the project is on the AI-default palette. Audit and replace with the project's design tokens.",
56099
- "ai/default-react-stack": "Every new file is a Vite + React + Tailwind + Zustand + React Hook Form clone. Verify the project actually needs each piece before adding it.",
56100
- "ai/library-reinvention": "Re-implementing zustand, react-hook-form, or date-fns inline (custom event emitters, useState reducers, manual date math) is a sign of LLM completion-mode code. Use the libraries the project already depends on.",
56101
- "ai/state-default-overuse": "Wrapping every component in useState + useEffect for transient UI state is the React tutorial default. Real production code uses refs, uncontrolled inputs, or the project's state lib.",
56102
- "ai/fetch-default-overuse": "Calling fetch() inline in components instead of going through the project's data-fetching layer (react-query, swr, or your own client) bypasses the cache, error boundary, and abort handling.",
56103
- "ai/console-debug-storm": "5+ console.log calls in a single file is debug-by-print-statement, the LLM training-data default. Remove before commit; use the project's logger or a real debugger.",
56104
- // v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
56105
- "db/missing-fk-index": "Add `CREATE INDEX ON <table> (<fk_column>);` for every foreign key column. Without it, parent deletes do a sequential scan on the child. Use `CREATE INDEX CONCURRENTLY` in production (Squawk `require-concurrent-index-creation`).",
56106
- "db/duplicate-index": "Drop one of two indexes that cover the same column list \u2014 extra indexes slow writes without read benefit. Postgres does not warn about this; the duplicate will silently sit in production.",
56107
- "db/missing-not-null": "Add `NOT NULL` (or `PRIMARY KEY`) on required-identifier columns (id, email, created_at, status, uuid, \u2026). Optional identifiers are a common AI-generated SQL smell that produces silent NULL inserts in production.",
56108
- "db/enum-sprawl": "Enums with more than 12 values are brittle to extend and hard to localize. Move to a lookup table joined by foreign key.",
56109
- "db/naming-inconsistency": "Standardize on snake_case (Postgres convention) or camelCase, but never mix both in the same schema. Mixed styles break ORM generators and confuse code-reviewers.",
56110
- "db/sql-concat": 'Never build SQL with template-literal interpolation \u2014 `db.query(`SELECT \u2026 WHERE id = ${id}`)` is a SQL injection vector. Use parameterized queries (`db.query("\u2026 WHERE id = $1", [id])`) or your ORM query builder.',
56111
- // v0.17.0 — docs/* rules (markdown drift detection)
56112
- "docs/stale-package-reference": "Update the doc to reference an installed package, or add the package to package.json. Copy-pasted install commands from a previous project are the #1 doc-drift failure mode.",
56113
- "docs/stale-function-reference": "Rename the doc reference to match a current export, or add a wrapper export. Stale function callouts in tutorials cost readers 10+ minutes of debugging.",
56114
- "docs/expired-code-example": "Update the example to use a declared dependency, or add the package to package.json. A copy-pasteable example that does not install erodes trust in the whole docs site.",
56115
- "docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy."
56116
- };
56117
-
56118
56222
  // src/snippet/targets.ts
56119
- var import_node_path41 = require("path");
56223
+ var import_node_path43 = require("path");
56120
56224
 
56121
56225
  // src/snippet/render.ts
56122
56226
  function aiSpecificRules(rules) {
@@ -56347,7 +56451,7 @@ var SNIPPET_TARGETS = [
56347
56451
  }
56348
56452
  ];
56349
56453
  function resolveTargetPath(target) {
56350
- return target.isFolder ? (0, import_node_path41.join)(target.path, target.filename) : target.path;
56454
+ return target.isFolder ? (0, import_node_path43.join)(target.path, target.filename) : target.path;
56351
56455
  }
56352
56456
  function renderMatrix() {
56353
56457
  const lines = [];
@@ -56361,52 +56465,6 @@ function renderMatrix() {
56361
56465
  return lines.join("\n");
56362
56466
  }
56363
56467
 
56364
- // src/cli/explain.ts
56365
- var RULES_BASE_URL2 = "https://github.com/Dystx/slopbrick/blob/main/src/rules";
56366
- function ruleIdToFilename2(ruleId) {
56367
- const slash = ruleId.indexOf("/");
56368
- return slash === -1 ? ruleId : ruleId.slice(slash + 1);
56369
- }
56370
- function explainRule2(ruleId, rules, ruleHints) {
56371
- const rule = rules.find((r) => r.id === ruleId);
56372
- if (!rule) {
56373
- return { error: "Unknown rule: " + ruleId + ". Run `slopbrick rules` to see all available rules." };
56374
- }
56375
- const filename = ruleIdToFilename2(rule.id);
56376
- return {
56377
- ruleId: rule.id,
56378
- category: rule.category,
56379
- severity: rule.severity,
56380
- aiSpecific: rule.aiSpecific,
56381
- pattern: ruleHints[rule.id] ?? "Patterns flagged by " + rule.id + ".",
56382
- remediation: "See the rule source for the canonical before/after: src/rules/" + rule.category + "/" + filename + ".ts",
56383
- sourcePath: "src/rules/" + rule.category + "/" + filename + ".ts",
56384
- helpUri: `${RULES_BASE_URL2}/${rule.category}/${filename}.ts`,
56385
- suppressionSnippet: 'rules: { "' + rule.id + '": "off" } // or set to a lower severity'
56386
- };
56387
- }
56388
- function formatExplain(result) {
56389
- if ("error" in result) return result.error;
56390
- const lines = [];
56391
- lines.push("Rule: " + result.ruleId);
56392
- lines.push("Category: " + result.category);
56393
- lines.push("Severity: " + result.severity);
56394
- lines.push("AI-specific: " + (result.aiSpecific ? "yes (designed to fire on AI tells)" : "no (cross-cutting quality rule)"));
56395
- lines.push("Source: " + result.sourcePath);
56396
- lines.push("Help: " + result.helpUri);
56397
- lines.push("");
56398
- lines.push("Pattern:");
56399
- lines.push(" " + result.pattern);
56400
- lines.push("");
56401
- lines.push("Remediation:");
56402
- lines.push(" " + result.remediation);
56403
- lines.push("");
56404
- lines.push("Suppress / configure in slopbrick.config.mjs:");
56405
- lines.push(" " + result.suppressionSnippet);
56406
- lines.push("");
56407
- return lines.join("\n");
56408
- }
56409
-
56410
56468
  // src/cli/program.ts
56411
56469
  init_validation();
56412
56470
  init_signal_strength2();
@@ -56564,13 +56622,12 @@ function formatMarkdown3(report) {
56564
56622
  }
56565
56623
 
56566
56624
  // src/cli/program.ts
56567
- init_advice();
56568
56625
  init_unified_diff();
56569
56626
  init_heatmap();
56570
56627
 
56571
56628
  // src/report/flywheel.ts
56572
56629
  var import_node_fs39 = require("fs");
56573
- var import_node_path42 = require("path");
56630
+ var import_node_path44 = require("path");
56574
56631
  function average(values) {
56575
56632
  if (values.length === 0) return 0;
56576
56633
  return values.reduce((a, b) => a + b, 0) / values.length;
@@ -57100,8 +57157,8 @@ async function runCli({ start }) {
57100
57157
  logger.info(renderMatrix());
57101
57158
  process.exit(0);
57102
57159
  }
57103
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57104
- const configPath = (0, import_node_path44.join)(cwd, "slopbrick.config.mjs");
57160
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57161
+ const configPath = (0, import_node_path46.join)(cwd, "slopbrick.config.mjs");
57105
57162
  const detected = detectStack(cwd);
57106
57163
  const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
57107
57164
  const proposed = serializeConfig(fallbackConfig);
@@ -57157,8 +57214,8 @@ async function runCli({ start }) {
57157
57214
  return Boolean(opts[t.flag]);
57158
57215
  });
57159
57216
  for (const target of targetsToWrite) {
57160
- const snippetPath = (0, import_node_path44.join)(cwd, resolveTargetPath(target));
57161
- (0, import_node_fs45.mkdirSync)((0, import_node_path44.dirname)(snippetPath), { recursive: true });
57217
+ const snippetPath = (0, import_node_path46.join)(cwd, resolveTargetPath(target));
57218
+ (0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(snippetPath), { recursive: true });
57162
57219
  const generated = target.generator(builtinRules);
57163
57220
  if (!target.isFolder && (0, import_node_fs45.existsSync)(snippetPath)) {
57164
57221
  const existing = (0, import_node_fs45.readFileSync)(snippetPath, "utf8");
@@ -57196,7 +57253,7 @@ async function runCli({ start }) {
57196
57253
  });
57197
57254
  program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
57198
57255
  const options = command.optsWithGlobals();
57199
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57256
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57200
57257
  const root = getGitRoot(cwd);
57201
57258
  if (!root) {
57202
57259
  logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
@@ -57210,7 +57267,7 @@ async function runCli({ start }) {
57210
57267
  });
57211
57268
  program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
57212
57269
  const options = command.optsWithGlobals();
57213
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57270
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57214
57271
  const root = getGitRoot(cwd);
57215
57272
  if (!root) {
57216
57273
  logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
@@ -57222,34 +57279,12 @@ async function runCli({ start }) {
57222
57279
  }
57223
57280
  process.exit(result.exitCode);
57224
57281
  });
57225
- program.command("badge").description("print a shields.io slop-index badge. Reads .slopbrick/health.json if present (no re-scan); falls back to a fresh scan.").action(async (_cmdOptions, command) => {
57226
- const options = command.optsWithGlobals();
57227
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57228
- const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
57229
- const health = loadHealth2(cwd);
57230
- if (health) {
57231
- const synthetic = {
57232
- slopIndex: 100 - health.repositoryHealth
57233
- };
57234
- logger.info(formatBadge(synthetic));
57235
- process.exit(0);
57236
- }
57237
- const { report } = await runScan(options);
57238
- logger.info(formatBadge(report));
57239
- process.exit(0);
57240
- });
57241
- program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
57242
- const options = command.optsWithGlobals();
57243
- const { report } = await runScan(options);
57244
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57245
- logger.info(formatAdvice(report));
57246
- const diff = formatUnifiedDiff(report, cwd);
57247
- if (diff) logger.info(diff);
57248
- process.exit(0);
57249
- });
57282
+ registerBadge(program);
57283
+ registerSuggest(program);
57284
+ registerExplain(program);
57250
57285
  program.command("flywheel").description("summarize aggregated scan telemetry").option("--format <pretty|json>", "output format", "pretty").option("--export <path>", "write summary as JSON to <path>").action(async (cmdOptions, command) => {
57251
57286
  const options = command.optsWithGlobals();
57252
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57287
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57253
57288
  const payloads = readTelemetry(cwd);
57254
57289
  if (payloads.length === 0) {
57255
57290
  logger.info("No flywheel telemetry found. Run a scan first.");
@@ -57257,8 +57292,8 @@ async function runCli({ start }) {
57257
57292
  }
57258
57293
  const summary = summarizeTelemetry(payloads);
57259
57294
  if (cmdOptions.export) {
57260
- const exportPath = (0, import_node_path44.resolve)(cmdOptions.export);
57261
- (0, import_node_fs45.mkdirSync)((0, import_node_path44.dirname)(exportPath), { recursive: true });
57295
+ const exportPath = (0, import_node_path46.resolve)(cmdOptions.export);
57296
+ (0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(exportPath), { recursive: true });
57262
57297
  (0, import_node_fs45.writeFileSync)(exportPath, JSON.stringify(summary, null, 2), "utf-8");
57263
57298
  logger.info(`Wrote flywheel summary to ${exportPath}`);
57264
57299
  process.exit(0);
@@ -57282,7 +57317,7 @@ async function runCli({ start }) {
57282
57317
  logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
57283
57318
  process.exit(2);
57284
57319
  }
57285
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57320
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57286
57321
  if (options.trend !== void 0) {
57287
57322
  const runs = await readRuns(cwd, fsMemoryIO);
57288
57323
  if (runs.length === 0) {
@@ -57315,7 +57350,7 @@ async function runCli({ start }) {
57315
57350
  const scanElapsed = Math.round(import_node_perf_hooks.performance.now() - scanStart);
57316
57351
  const totalElapsed = Math.round(import_node_perf_hooks.performance.now() - start);
57317
57352
  if (options.baseline) {
57318
- const cwd2 = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57353
+ const cwd2 = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57319
57354
  const configHash = hashConfig(config);
57320
57355
  const gitHead = await getGitHead(cwd2) ?? "unknown";
57321
57356
  const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
@@ -57409,14 +57444,14 @@ async function runCli({ start }) {
57409
57444
  framework: cmdOptions.framework,
57410
57445
  componentType: cmdOptions.componentType,
57411
57446
  provider,
57412
- outputDir: (0, import_node_path44.resolve)(cmdOptions.outputDir),
57447
+ outputDir: (0, import_node_path46.resolve)(cmdOptions.outputDir),
57413
57448
  temperature: cmdOptions.temperature
57414
57449
  });
57415
57450
  logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
57416
57451
  });
57417
57452
  research.command("analyze").description("analyze generated samples and report coverage").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "analysis output path", ".slopbrick/flywheel/analysis.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").action(async (cmdOptions) => {
57418
57453
  try {
57419
- const metadataPath = (0, import_node_path44.resolve)(cmdOptions.inputDir, "metadata.json");
57454
+ const metadataPath = (0, import_node_path46.resolve)(cmdOptions.inputDir, "metadata.json");
57420
57455
  if (!(0, import_node_fs45.existsSync)(metadataPath)) {
57421
57456
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
57422
57457
  process.exit(2);
@@ -57424,8 +57459,8 @@ async function runCli({ start }) {
57424
57459
  const samples = JSON.parse((0, import_node_fs45.readFileSync)(metadataPath, "utf8"));
57425
57460
  const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
57426
57461
  const analysis = await analyzeSamples(samples, config);
57427
- const outputPath = (0, import_node_path44.resolve)(cmdOptions.output);
57428
- (0, import_node_fs45.mkdirSync)((0, import_node_path44.dirname)(outputPath), { recursive: true });
57462
+ const outputPath = (0, import_node_path46.resolve)(cmdOptions.output);
57463
+ (0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(outputPath), { recursive: true });
57429
57464
  (0, import_node_fs45.writeFileSync)(outputPath, JSON.stringify(analysis, null, 2), "utf8");
57430
57465
  logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
57431
57466
  logger.info(`Wrote analysis to ${outputPath}`);
@@ -57436,7 +57471,7 @@ async function runCli({ start }) {
57436
57471
  });
57437
57472
  research.command("candidates").description("extract patterns from generated samples and emit candidate rules").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "output path", ".slopbrick/flywheel/rule-candidates.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").option("--min-frequency <n>", "minimum cluster frequency", parseCount, 2).option("--include-covered", "include samples already covered by AI-specific rules").action(async (cmdOptions) => {
57438
57473
  try {
57439
- const metadataPath = (0, import_node_path44.resolve)(cmdOptions.inputDir, "metadata.json");
57474
+ const metadataPath = (0, import_node_path46.resolve)(cmdOptions.inputDir, "metadata.json");
57440
57475
  if (!(0, import_node_fs45.existsSync)(metadataPath)) {
57441
57476
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
57442
57477
  process.exit(2);
@@ -57451,8 +57486,8 @@ async function runCli({ start }) {
57451
57486
  const candidates = clustersToCandidates(extraction.clusters, {
57452
57487
  minFrequency: cmdOptions.minFrequency
57453
57488
  });
57454
- const outputPath = (0, import_node_path44.resolve)(cmdOptions.output);
57455
- (0, import_node_fs45.mkdirSync)((0, import_node_path44.dirname)(outputPath), { recursive: true });
57489
+ const outputPath = (0, import_node_path46.resolve)(cmdOptions.output);
57490
+ (0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(outputPath), { recursive: true });
57456
57491
  const payload = {
57457
57492
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
57458
57493
  sampleCount: analysis.summary.total,
@@ -57477,8 +57512,8 @@ async function runCli({ start }) {
57477
57512
  positiveLimit: cmdOptions.positiveLimit,
57478
57513
  negativeLimit: cmdOptions.negativeLimit
57479
57514
  });
57480
- const outputPath = cmdOptions.output ? (0, import_node_path44.resolve)(cwd, cmdOptions.output) : (0, import_node_path44.resolve)(cwd, "corpus", "calibration-empirical.md");
57481
- (0, import_node_fs45.mkdirSync)((0, import_node_path44.dirname)(outputPath), { recursive: true });
57515
+ const outputPath = cmdOptions.output ? (0, import_node_path46.resolve)(cwd, cmdOptions.output) : (0, import_node_path46.resolve)(cwd, "corpus", "calibration-empirical.md");
57516
+ (0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(outputPath), { recursive: true });
57482
57517
  (0, import_node_fs45.writeFileSync)(outputPath, reportToMarkdown(report), "utf8");
57483
57518
  logger.info(
57484
57519
  "Calibrated " + report.rules.length + " rules across " + report.positiveFileCount + " positive + " + report.negativeFileCount + " negative files."
@@ -57517,7 +57552,7 @@ async function runCli({ start }) {
57517
57552
  const options = command.optsWithGlobals();
57518
57553
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
57519
57554
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
57520
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57555
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57521
57556
  const { config } = await runScan({ ...options, workspace: cwd });
57522
57557
  const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
57523
57558
  logger.info(formatDrift(result, { json: format === "json" }));
@@ -57534,7 +57569,7 @@ async function runCli({ start }) {
57534
57569
  async (cmdOptions, command) => {
57535
57570
  try {
57536
57571
  const options = command.optsWithGlobals();
57537
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57572
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57538
57573
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57539
57574
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57540
57575
  const { config } = await runScan({ ...options, workspace: cwd });
@@ -57561,7 +57596,7 @@ async function runCli({ start }) {
57561
57596
  const options = command.optsWithGlobals();
57562
57597
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
57563
57598
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
57564
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57599
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57565
57600
  const { report } = await runScan({ ...options, workspace: cwd });
57566
57601
  const securityIssues = report.issues.filter((i) => i.category === "security");
57567
57602
  const { risk, findings } = computeAiSecurityRisk(securityIssues);
@@ -57612,7 +57647,7 @@ async function runCli({ start }) {
57612
57647
  const options = command.optsWithGlobals();
57613
57648
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
57614
57649
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
57615
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57650
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57616
57651
  const { config } = await runScan({ ...options, workspace: cwd });
57617
57652
  const { result } = await runTestScan(cwd, config, { strict: options.strict });
57618
57653
  logger.info(formatTestReport(result, { json: format === "json" }));
@@ -57631,7 +57666,7 @@ async function runCli({ start }) {
57631
57666
  const options = command.optsWithGlobals();
57632
57667
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
57633
57668
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
57634
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57669
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57635
57670
  const { config } = await runScan({ ...options, workspace: cwd });
57636
57671
  const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
57637
57672
  const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
@@ -57651,7 +57686,7 @@ async function runCli({ start }) {
57651
57686
  const options = command.optsWithGlobals();
57652
57687
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57653
57688
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57654
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57689
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57655
57690
  const { config } = await runScan({ ...options, workspace: cwd });
57656
57691
  const result = await runBusinessLogicScan(cwd, config, {
57657
57692
  maxFiles: cmdOptions.maxFiles
@@ -57673,7 +57708,7 @@ async function runCli({ start }) {
57673
57708
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57674
57709
  const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
57675
57710
  const strict = options.strict ?? cmdOptions.strict ?? false;
57676
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57711
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57677
57712
  const { config } = await runScan({ ...options, workspace: cwd });
57678
57713
  const result = await runMaintenanceCostScan(cwd, config, {
57679
57714
  maxFiles: cmdOptions.maxFiles,
@@ -57696,7 +57731,7 @@ async function runCli({ start }) {
57696
57731
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57697
57732
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57698
57733
  const strict = options.strict ?? cmdOptions.strict ?? false;
57699
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57734
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57700
57735
  const { config } = await runScan({ ...options, workspace: cwd });
57701
57736
  const result = await runDocsScan(cwd, config, {
57702
57737
  maxDocFiles: cmdOptions.maxFiles,
@@ -57724,7 +57759,7 @@ async function runCli({ start }) {
57724
57759
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57725
57760
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57726
57761
  const strict = options.strict ?? cmdOptions.strict ?? false;
57727
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57762
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57728
57763
  const { config } = await runScan({ ...options, workspace: cwd });
57729
57764
  const result = await runDbScan(cwd, config, {
57730
57765
  maxFiles: cmdOptions.maxFiles,
@@ -57751,7 +57786,7 @@ async function runCli({ start }) {
57751
57786
  const options = command.optsWithGlobals();
57752
57787
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57753
57788
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57754
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57789
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57755
57790
  const { config } = await runScan({ ...options, workspace: cwd });
57756
57791
  const result = await runPatternsScan(cwd, config, {
57757
57792
  maxFiles: cmdOptions.maxFiles,
@@ -57782,14 +57817,14 @@ async function runCli({ start }) {
57782
57817
  ...rawGlobals,
57783
57818
  noIncrease: rawGlobals.increase === false
57784
57819
  };
57785
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57820
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57786
57821
  const { watchProject: watchProject2 } = await Promise.resolve().then(() => (init_scan(), scan_exports));
57787
57822
  await scanAction([], options, command);
57788
57823
  await watchProject2(options, cwd, []);
57789
57824
  });
57790
57825
  program.command("lock").description("install a Git pre-commit hook that runs `slopbrick scan --staged` on every commit. The LockBrick prevention loop: block AI-introduced slop from ever reaching the repo.").option("--uninstall", "remove the pre-commit hook instead of installing it").option("--husky", "force-install under .husky/pre-commit (Husky v9). Default auto-detects via .husky/ dir.").option("--workspace <path>", "workspace directory", process.cwd()).action(
57791
57826
  (cmdOptions) => {
57792
- const cwd = (0, import_node_path44.resolve)(cmdOptions.workspace ?? process.cwd());
57827
+ const cwd = (0, import_node_path46.resolve)(cmdOptions.workspace ?? process.cwd());
57793
57828
  const { installHook: installHook2, uninstallHook: uninstallHook2 } = (init_installer(), __toCommonJS(installer_exports));
57794
57829
  if (cmdOptions.uninstall) {
57795
57830
  const result2 = uninstallHook2(cwd);
@@ -57819,7 +57854,7 @@ async function runCli({ start }) {
57819
57854
  // scan only changed files
57820
57855
  format: cmdOptions.format ?? "json"
57821
57856
  };
57822
- const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
57857
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
57823
57858
  await scanAction([], options, command);
57824
57859
  const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
57825
57860
  const health = loadHealth2(cwd);
@@ -57847,7 +57882,7 @@ async function runCli({ start }) {
57847
57882
  );
57848
57883
  program.command("memory").description("show or regenerate .slopbrick/structure.md (the agent-readable repository summary) without re-scanning").option("--show", "print the current .slopbrick/structure.md to stdout (default if no flag is passed)").option("--regenerate", "re-render structure.md from the existing inventory.json + constitution.json (no scan)").option("--workspace <path>", "workspace directory", process.cwd()).action(
57849
57884
  async (cmdOptions) => {
57850
- const cwd = (0, import_node_path44.resolve)(cmdOptions.workspace ?? process.cwd());
57885
+ const cwd = (0, import_node_path46.resolve)(cmdOptions.workspace ?? process.cwd());
57851
57886
  const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
57852
57887
  const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
57853
57888
  if (cmdOptions.regenerate) {
@@ -57880,7 +57915,7 @@ async function runCli({ start }) {
57880
57915
  (cmdOptions, command) => {
57881
57916
  const globals = command.optsWithGlobals();
57882
57917
  const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
57883
- const cwd = (0, import_node_path44.resolve)(cmdOptions.workspace ?? process.cwd());
57918
+ const cwd = (0, import_node_path46.resolve)(cmdOptions.workspace ?? process.cwd());
57884
57919
  const { runMigrate: runMigrate2, formatMigrate: formatMigrate2 } = (init_migrate(), __toCommonJS(migrate_exports));
57885
57920
  const result = runMigrate2({
57886
57921
  workspace: cwd,
@@ -57975,19 +58010,14 @@ async function runCli({ start }) {
57975
58010
  }
57976
58011
  logger.info(lines.join("\n"));
57977
58012
  });
57978
- program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
57979
- const result = explainRule2(ruleId, builtinRules, RULE_HINTS);
57980
- logger.info(formatExplain(result));
57981
- if ("error" in result) process.exit(2);
57982
- });
57983
58013
  program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
57984
- const path = configPath ? (0, import_node_path44.resolve)(configPath) : (0, import_node_path44.resolve)(process.cwd(), "slopbrick.config.mjs");
58014
+ const path = configPath ? (0, import_node_path46.resolve)(configPath) : (0, import_node_path46.resolve)(process.cwd(), "slopbrick.config.mjs");
57985
58015
  if (!(0, import_node_fs45.existsSync)(path)) {
57986
58016
  logger.error(`Error: config file not found: ${path}`);
57987
58017
  process.exit(2);
57988
58018
  }
57989
58019
  try {
57990
- const mod = (0, import_node_path44.extname)(path) === ".cjs" ? require(path) : await import(path);
58020
+ const mod = (0, import_node_path46.extname)(path) === ".cjs" ? require(path) : await import(path);
57991
58021
  const userConfig = mod.default ?? mod;
57992
58022
  const result = validateConfig(userConfig);
57993
58023
  if (result.errors.length === 0) {