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.js CHANGED
@@ -45372,7 +45372,7 @@ function formatPretty(report) {
45372
45372
  }
45373
45373
  function formatScoringExplainer(_report) {
45374
45374
  return chalk.dim(
45375
- "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."
45375
+ "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."
45376
45376
  );
45377
45377
  }
45378
45378
  function formatWhyFailingReport(report) {
@@ -46031,11 +46031,11 @@ var init_pool = __esm({
46031
46031
  for (let i = 0; i < this.threadCount; i++) {
46032
46032
  spawnWorker();
46033
46033
  }
46034
- return new Promise((resolve18) => {
46034
+ return new Promise((resolve20) => {
46035
46035
  const check = setInterval(() => {
46036
46036
  if (resolved) {
46037
46037
  clearInterval(check);
46038
- resolve18(results);
46038
+ resolve20(results);
46039
46039
  }
46040
46040
  }, 10);
46041
46041
  });
@@ -49989,29 +49989,29 @@ function renderStructureMarkdown(inventory, constitution) {
49989
49989
  return lines.join("\n");
49990
49990
  }
49991
49991
  async function writeStructureMarkdown(workspaceDir, md) {
49992
- await new Promise((resolve18, reject) => {
49992
+ await new Promise((resolve20, reject) => {
49993
49993
  try {
49994
49994
  const path = join17(workspaceDir, STRUCTURE_MD_FILE);
49995
49995
  mkdirSync6(dirname12(path), { recursive: true });
49996
49996
  writeFileSync6(path, md, "utf-8");
49997
- resolve18();
49997
+ resolve20();
49998
49998
  } catch (err) {
49999
49999
  reject(err instanceof Error ? err : new Error(String(err)));
50000
50000
  }
50001
50001
  });
50002
50002
  }
50003
50003
  async function readStructureMarkdown(workspaceDir) {
50004
- return new Promise((resolve18) => {
50004
+ return new Promise((resolve20) => {
50005
50005
  try {
50006
50006
  const path = join17(workspaceDir, STRUCTURE_MD_FILE);
50007
50007
  if (!existsSync17(path)) {
50008
- resolve18(null);
50008
+ resolve20(null);
50009
50009
  return;
50010
50010
  }
50011
50011
  const content = readFileSync20(path, "utf-8");
50012
- resolve18(content);
50012
+ resolve20(content);
50013
50013
  } catch {
50014
- resolve18(null);
50014
+ resolve20(null);
50015
50015
  }
50016
50016
  });
50017
50017
  }
@@ -50277,11 +50277,12 @@ async function finalizeReport(input) {
50277
50277
  if (options.noIncrease) {
50278
50278
  const previous = (await readRuns(cwd, fsMemoryIO)).at(-1);
50279
50279
  if (previous) {
50280
- if ((report.aiQuality ?? 0) < previous.slopIndex) {
50280
+ const previousBaseline = previous.slopIndex;
50281
+ if ((report.aiQuality ?? 0) < previousBaseline) {
50281
50282
  noIncreaseFailure = true;
50282
50283
  if (!options.quiet) {
50283
50284
  logger.error(
50284
- `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.`
50285
+ `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.`
50285
50286
  );
50286
50287
  }
50287
50288
  }
@@ -52740,7 +52741,7 @@ __export(tools_exports, {
52740
52741
  handleToolCall: () => handleToolCall
52741
52742
  });
52742
52743
  import { readFileSync as readFileSync32 } from "fs";
52743
- import { resolve as resolve16 } from "path";
52744
+ import { resolve as resolve18 } from "path";
52744
52745
  function toolError(message) {
52745
52746
  return {
52746
52747
  content: [{ type: "text", text: JSON.stringify({ error: message }) }],
@@ -52769,7 +52770,7 @@ async function runScanFile(args, ctx) {
52769
52770
  content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
52770
52771
  };
52771
52772
  }
52772
- function explainRule(args, ctx) {
52773
+ function explainRule2(args, ctx) {
52773
52774
  const ruleId = args.ruleId;
52774
52775
  if (!ruleId) return toolError("Missing required argument: ruleId");
52775
52776
  const rule = ctx.rules.find((r) => r.id === ruleId);
@@ -52880,7 +52881,7 @@ async function runGovernance(args, ctx) {
52880
52881
  function runCheckConstitution(args, ctx) {
52881
52882
  const path = args.path;
52882
52883
  if (!path) return toolError("Missing required argument: path");
52883
- const absPath = resolve16(ctx.cwd, path);
52884
+ const absPath = resolve18(ctx.cwd, path);
52884
52885
  let source;
52885
52886
  try {
52886
52887
  source = readFileSync32(absPath, "utf-8");
@@ -53043,7 +53044,7 @@ async function handleToolCall(toolName, args, ctx) {
53043
53044
  case "slop_scan_file":
53044
53045
  return runScanFile(args, ctx);
53045
53046
  case "slop_explain_rule":
53046
- return explainRule(args, ctx);
53047
+ return explainRule2(args, ctx);
53047
53048
  case "slop_list_rules":
53048
53049
  return listRules(args, ctx);
53049
53050
  case "slop_suggest":
@@ -53425,7 +53426,7 @@ init_dist2();
53425
53426
 
53426
53427
  // src/cli/program.ts
53427
53428
  import { existsSync as existsSync28, writeFileSync as writeFileSync19, readFileSync as readFileSync38, mkdirSync as mkdirSync13 } from "fs";
53428
- import { resolve as resolve17, join as join28, dirname as dirname17, extname as extname9 } from "path";
53429
+ import { resolve as resolve19, join as join28, dirname as dirname17, extname as extname9 } from "path";
53429
53430
  import { performance } from "perf_hooks";
53430
53431
  import { Command } from "commander";
53431
53432
 
@@ -54931,17 +54932,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
54931
54932
  var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
54932
54933
  var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
54933
54934
  function promptText(rl, question, detected) {
54934
- return new Promise((resolve18) => {
54935
+ return new Promise((resolve20) => {
54935
54936
  const lines = [
54936
54937
  `? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
54937
54938
  ];
54938
54939
  rl.question(lines.join("\n") + "\n", (answer) => {
54939
54940
  const trimmed = answer.trim();
54940
54941
  if (trimmed === "") {
54941
- resolve18(void 0);
54942
+ resolve20(void 0);
54942
54943
  return;
54943
54944
  }
54944
- resolve18(trimmed);
54945
+ resolve20(trimmed);
54945
54946
  });
54946
54947
  });
54947
54948
  }
@@ -54949,7 +54950,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
54949
54950
  const defaultIndex = options.indexOf(defaultValue);
54950
54951
  const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
54951
54952
  const safeDefaultValue = options[safeDefaultIndex];
54952
- return new Promise((resolve18) => {
54953
+ return new Promise((resolve20) => {
54953
54954
  function ask() {
54954
54955
  const lines = [
54955
54956
  `? ${question} (detected: ${safeDefaultValue}):`,
@@ -54959,7 +54960,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
54959
54960
  rl.question(lines.join("\n") + "\n", (answer) => {
54960
54961
  const trimmed = answer.trim();
54961
54962
  if (trimmed === "") {
54962
- resolve18(safeDefaultValue);
54963
+ resolve20(safeDefaultValue);
54963
54964
  return;
54964
54965
  }
54965
54966
  const num = parseInt(trimmed, 10);
@@ -54968,7 +54969,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
54968
54969
  ask();
54969
54970
  return;
54970
54971
  }
54971
- resolve18(options[num - 1]);
54972
+ resolve20(options[num - 1]);
54972
54973
  });
54973
54974
  }
54974
54975
  ask();
@@ -54978,7 +54979,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
54978
54979
  const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
54979
54980
  const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
54980
54981
  const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
54981
- return new Promise((resolve18) => {
54982
+ return new Promise((resolve20) => {
54982
54983
  function ask() {
54983
54984
  const lines = [
54984
54985
  `? ${question} (detected: ${defaultDisplay}):`,
@@ -54989,7 +54990,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
54989
54990
  rl.question(lines.join("\n") + "\n", (answer) => {
54990
54991
  const trimmed = answer.trim();
54991
54992
  if (trimmed === "") {
54992
- resolve18(defaultValue.length > 0 ? defaultValue : []);
54993
+ resolve20(defaultValue.length > 0 ? defaultValue : []);
54993
54994
  return;
54994
54995
  }
54995
54996
  const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
@@ -54999,11 +55000,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
54999
55000
  return;
55000
55001
  }
55001
55002
  if (numbers.includes(1)) {
55002
- resolve18([]);
55003
+ resolve20([]);
55003
55004
  return;
55004
55005
  }
55005
55006
  const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
55006
- resolve18(selected);
55007
+ resolve20(selected);
55007
55008
  });
55008
55009
  }
55009
55010
  ask();
@@ -55183,6 +55184,234 @@ async function runDoctor(cwd) {
55183
55184
  return exitCode;
55184
55185
  }
55185
55186
 
55187
+ // src/cli/commands/badge.ts
55188
+ init_render();
55189
+ init_logger();
55190
+ init_scan();
55191
+ import { resolve as resolve15 } from "path";
55192
+ function registerBadge(program) {
55193
+ program.command("badge").description(
55194
+ "print a shields.io slop-index badge. Reads .slopbrick/health.json if present (no re-scan); falls back to a fresh scan."
55195
+ ).action(async (_cmdOptions, command) => {
55196
+ const options = command.optsWithGlobals();
55197
+ const cwd = resolve15(options.workspace ?? process.cwd());
55198
+ const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
55199
+ const health = loadHealth2(cwd);
55200
+ if (health) {
55201
+ const synthetic = {
55202
+ slopIndex: 100 - health.repositoryHealth
55203
+ };
55204
+ logger.info(formatBadge(synthetic));
55205
+ process.exit(0);
55206
+ }
55207
+ const { report } = await runScan(options);
55208
+ logger.info(formatBadge(report));
55209
+ process.exit(0);
55210
+ });
55211
+ }
55212
+
55213
+ // src/cli/commands/suggest.ts
55214
+ init_advice();
55215
+ init_unified_diff();
55216
+ init_logger();
55217
+ init_scan();
55218
+ import { resolve as resolve16 } from "path";
55219
+ function registerSuggest(program) {
55220
+ program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
55221
+ const options = command.optsWithGlobals();
55222
+ const { report } = await runScan(options);
55223
+ const cwd = resolve16(options.workspace ?? process.cwd());
55224
+ logger.info(formatAdvice(report));
55225
+ const diff = formatUnifiedDiff(report, cwd);
55226
+ if (diff) logger.info(diff);
55227
+ process.exit(0);
55228
+ });
55229
+ }
55230
+
55231
+ // src/cli/explain.ts
55232
+ var RULES_BASE_URL2 = "https://github.com/Dystx/slopbrick/blob/main/src/rules";
55233
+ function ruleIdToFilename2(ruleId) {
55234
+ const slash = ruleId.indexOf("/");
55235
+ return slash === -1 ? ruleId : ruleId.slice(slash + 1);
55236
+ }
55237
+ function explainRule(ruleId, rules, ruleHints) {
55238
+ const rule = rules.find((r) => r.id === ruleId);
55239
+ if (!rule) {
55240
+ return { error: "Unknown rule: " + ruleId + ". Run `slopbrick rules` to see all available rules." };
55241
+ }
55242
+ const filename = ruleIdToFilename2(rule.id);
55243
+ return {
55244
+ ruleId: rule.id,
55245
+ category: rule.category,
55246
+ severity: rule.severity,
55247
+ aiSpecific: rule.aiSpecific,
55248
+ pattern: ruleHints[rule.id] ?? "Patterns flagged by " + rule.id + ".",
55249
+ remediation: "See the rule source for the canonical before/after: src/rules/" + rule.category + "/" + filename + ".ts",
55250
+ sourcePath: "src/rules/" + rule.category + "/" + filename + ".ts",
55251
+ helpUri: `${RULES_BASE_URL2}/${rule.category}/${filename}.ts`,
55252
+ suppressionSnippet: 'rules: { "' + rule.id + '": "off" } // or set to a lower severity'
55253
+ };
55254
+ }
55255
+ function formatExplain(result) {
55256
+ if ("error" in result) return result.error;
55257
+ const lines = [];
55258
+ lines.push("Rule: " + result.ruleId);
55259
+ lines.push("Category: " + result.category);
55260
+ lines.push("Severity: " + result.severity);
55261
+ lines.push("AI-specific: " + (result.aiSpecific ? "yes (designed to fire on AI tells)" : "no (cross-cutting quality rule)"));
55262
+ lines.push("Source: " + result.sourcePath);
55263
+ lines.push("Help: " + result.helpUri);
55264
+ lines.push("");
55265
+ lines.push("Pattern:");
55266
+ lines.push(" " + result.pattern);
55267
+ lines.push("");
55268
+ lines.push("Remediation:");
55269
+ lines.push(" " + result.remediation);
55270
+ lines.push("");
55271
+ lines.push("Suppress / configure in slopbrick.config.mjs:");
55272
+ lines.push(" " + result.suppressionSnippet);
55273
+ lines.push("");
55274
+ return lines.join("\n");
55275
+ }
55276
+
55277
+ // src/cli/commands/explain.ts
55278
+ init_logger();
55279
+ init_builtins();
55280
+
55281
+ // src/snippet/data.ts
55282
+ var CATEGORY_DIRECTIVES = {
55283
+ 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.',
55284
+ 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.",
55285
+ 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.',
55286
+ 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".',
55287
+ 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.",
55288
+ typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
55289
+ 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].",
55290
+ component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
55291
+ 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.",
55292
+ 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'."
55293
+ };
55294
+ var RULE_HINTS = {
55295
+ // v0.16.0 hygiene: 35 out-of-scope orphan hints (keys with no matching
55296
+ // rule in src/rules/builtins.ts) were moved out of this map. The
55297
+ // verbatim source text is preserved in
55298
+ // docs/research/backlog-rule-hints.md
55299
+ // so future implementers can paste a hint back when the corresponding
55300
+ // rule ships. The 5 in-scope orphans (`security/eval`,
55301
+ // `security/localstorage-token`, `security/target-blank-no-noopener`,
55302
+ // `wcag/missing-alt`, `typo/placeholder-text`) are kept here for v0.16.0.
55303
+ "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.",
55304
+ "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.",
55305
+ "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.",
55306
+ "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.",
55307
+ "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.",
55308
+ "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.",
55309
+ "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.',
55310
+ "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.",
55311
+ "visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
55312
+ "visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
55313
+ "visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
55314
+ // v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
55315
+ "typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
55316
+ "logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
55317
+ "logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
55318
+ "wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
55319
+ "security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
55320
+ "security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
55321
+ "security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
55322
+ "arch/astro-island-leak": "For Astro: server-render everything by default. Only opt-in to client islands when interactivity is needed.",
55323
+ "component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
55324
+ "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.",
55325
+ "component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
55326
+ "context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
55327
+ "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.",
55328
+ "layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
55329
+ "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.",
55330
+ "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.",
55331
+ "layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
55332
+ "logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
55333
+ "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*.)",
55334
+ "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.)",
55335
+ "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.)",
55336
+ "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.)",
55337
+ "logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
55338
+ "logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
55339
+ "logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
55340
+ "logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
55341
+ "logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
55342
+ "logic/qwik-hook-leak": "Use Qwik primitives ($state, $effect, useSignal) instead of React hooks (useState, useEffect).",
55343
+ "logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
55344
+ "logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
55345
+ "perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
55346
+ "perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
55347
+ "perf/halstead-anomaly": "Introduce domain-specific identifiers and varied operations. Low vocabulary per line is a strong AI signature (Halstead 1977 \xA73).",
55348
+ "typo/calc-fontsize": "Use a design token (`var(--font-size-lg)`) or `clamp(min, fluid, max)` for responsive typography.",
55349
+ "typo/calc-raw-px": "Replace px values in calc() with rem or em units for scalable layout.",
55350
+ "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.",
55351
+ "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.',
55352
+ "typo/math-cta-vocabulary": 'Use domain-specific action verbs ("Reserve", "Confirm ride", "Activate card") instead of falling back on the AI-default CTA vocabulary.',
55353
+ "visual/clamp-soup": "Use design-system aliases (`--text-fluid-sm`, `--text-fluid-lg`) with bounded ranges (typically 2\xD7 max).",
55354
+ "visual/generic-centering": "Vary hero layouts: some as grids (`grid place-items-center`), some as blocks, some with different alignment.",
55355
+ "visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
55356
+ "visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
55357
+ "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.",
55358
+ "visual/math-gradient-hue-rotation": "Use wider hue spans across gradients (e.g. blue\u2192amber, emerald\u2192indigo) to break the violet-fuchsia monotony.",
55359
+ "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.",
55360
+ "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.",
55361
+ "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%.",
55362
+ "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.",
55363
+ "wcag/dragging-movements": "Provide an onClick, onKeyDown, or button role as an alternative to dragging (WCAG 2.1.1).",
55364
+ "wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
55365
+ "wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
55366
+ "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.",
55367
+ "test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
55368
+ "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.",
55369
+ "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.",
55370
+ "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`).",
55371
+ "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.",
55372
+ "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.",
55373
+ // v0.13.0 — AI-specific rules (peer-reviewed signals).
55374
+ "ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
55375
+ "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).",
55376
+ "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).",
55377
+ "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).",
55378
+ "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).",
55379
+ "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).",
55380
+ "ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
55381
+ "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).",
55382
+ "ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
55383
+ "ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
55384
+ // v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
55385
+ // reclassified post-v7 calibration in v0.14.5d)
55386
+ "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.",
55387
+ "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.",
55388
+ "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.",
55389
+ "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.",
55390
+ "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.",
55391
+ "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.",
55392
+ // v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
55393
+ "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`).",
55394
+ "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.",
55395
+ "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.",
55396
+ "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.",
55397
+ "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.",
55398
+ "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.',
55399
+ // v0.17.0 — docs/* rules (markdown drift detection)
55400
+ "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.",
55401
+ "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.",
55402
+ "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.",
55403
+ "docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy."
55404
+ };
55405
+
55406
+ // src/cli/commands/explain.ts
55407
+ function registerExplain(program) {
55408
+ program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
55409
+ const result = explainRule(ruleId, builtinRules, RULE_HINTS);
55410
+ logger.info(formatExplain(result));
55411
+ if ("error" in result) process.exit(2);
55412
+ });
55413
+ }
55414
+
55186
55415
  // src/cli/program.ts
55187
55416
  init_config();
55188
55417
  init_git();
@@ -55533,7 +55762,7 @@ function slugify(value) {
55533
55762
  // src/research/calibrator.ts
55534
55763
  import { execFileSync as execFileSync2 } from "child_process";
55535
55764
  import { existsSync as existsSync24, readFileSync as readFileSync31, writeFileSync as writeFileSync14, mkdirSync as mkdirSync12 } from "fs";
55536
- import { join as join24, resolve as resolve15 } from "path";
55765
+ import { join as join24, resolve as resolve17 } from "path";
55537
55766
  var DEFAULT_POSITIVE = "/Users/cheng/ai-slop-baseline/extracted/positive";
55538
55767
  var DEFAULT_NEGATIVE = "/Users/cheng/ai-slop-baseline/extracted/negative";
55539
55768
  function buildFileList(dir, extensions) {
@@ -55866,7 +56095,7 @@ function errorResponse(id, code, message, data2) {
55866
56095
  return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
55867
56096
  }
55868
56097
  async function runMcpServer(input, output, cwd) {
55869
- return new Promise((resolve18) => {
56098
+ return new Promise((resolve20) => {
55870
56099
  let buffer = "";
55871
56100
  input.setEncoding("utf-8");
55872
56101
  input.on("data", (chunk) => {
@@ -55904,136 +56133,11 @@ async function runMcpServer(input, output, cwd) {
55904
56133
  nlIdx = buffer.indexOf("\n");
55905
56134
  }
55906
56135
  });
55907
- input.on("end", () => resolve18());
55908
- input.on("close", () => resolve18());
56136
+ input.on("end", () => resolve20());
56137
+ input.on("close", () => resolve20());
55909
56138
  });
55910
56139
  }
55911
56140
 
55912
- // src/snippet/data.ts
55913
- var CATEGORY_DIRECTIVES = {
55914
- 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.',
55915
- 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.",
55916
- 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.',
55917
- 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".',
55918
- 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.",
55919
- typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
55920
- 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].",
55921
- component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
55922
- 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.",
55923
- 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'."
55924
- };
55925
- var RULE_HINTS = {
55926
- // v0.16.0 hygiene: 35 out-of-scope orphan hints (keys with no matching
55927
- // rule in src/rules/builtins.ts) were moved out of this map. The
55928
- // verbatim source text is preserved in
55929
- // docs/research/backlog-rule-hints.md
55930
- // so future implementers can paste a hint back when the corresponding
55931
- // rule ships. The 5 in-scope orphans (`security/eval`,
55932
- // `security/localstorage-token`, `security/target-blank-no-noopener`,
55933
- // `wcag/missing-alt`, `typo/placeholder-text`) are kept here for v0.16.0.
55934
- "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.",
55935
- "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.",
55936
- "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.",
55937
- "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.",
55938
- "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.",
55939
- "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.",
55940
- "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.',
55941
- "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.",
55942
- "visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
55943
- "visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
55944
- "visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
55945
- // v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
55946
- "typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
55947
- "logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
55948
- "logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
55949
- "wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
55950
- "security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
55951
- "security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
55952
- "security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
55953
- "arch/astro-island-leak": "For Astro: server-render everything by default. Only opt-in to client islands when interactivity is needed.",
55954
- "component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
55955
- "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.",
55956
- "component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
55957
- "context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
55958
- "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.",
55959
- "layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
55960
- "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.",
55961
- "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.",
55962
- "layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
55963
- "logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
55964
- "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*.)",
55965
- "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.)",
55966
- "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.)",
55967
- "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.)",
55968
- "logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
55969
- "logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
55970
- "logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
55971
- "logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
55972
- "logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
55973
- "logic/qwik-hook-leak": "Use Qwik primitives ($state, $effect, useSignal) instead of React hooks (useState, useEffect).",
55974
- "logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
55975
- "logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
55976
- "perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
55977
- "perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
55978
- "perf/halstead-anomaly": "Introduce domain-specific identifiers and varied operations. Low vocabulary per line is a strong AI signature (Halstead 1977 \xA73).",
55979
- "typo/calc-fontsize": "Use a design token (`var(--font-size-lg)`) or `clamp(min, fluid, max)` for responsive typography.",
55980
- "typo/calc-raw-px": "Replace px values in calc() with rem or em units for scalable layout.",
55981
- "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.",
55982
- "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.',
55983
- "typo/math-cta-vocabulary": 'Use domain-specific action verbs ("Reserve", "Confirm ride", "Activate card") instead of falling back on the AI-default CTA vocabulary.',
55984
- "visual/clamp-soup": "Use design-system aliases (`--text-fluid-sm`, `--text-fluid-lg`) with bounded ranges (typically 2\xD7 max).",
55985
- "visual/generic-centering": "Vary hero layouts: some as grids (`grid place-items-center`), some as blocks, some with different alignment.",
55986
- "visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
55987
- "visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
55988
- "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.",
55989
- "visual/math-gradient-hue-rotation": "Use wider hue spans across gradients (e.g. blue\u2192amber, emerald\u2192indigo) to break the violet-fuchsia monotony.",
55990
- "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.",
55991
- "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.",
55992
- "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%.",
55993
- "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.",
55994
- "wcag/dragging-movements": "Provide an onClick, onKeyDown, or button role as an alternative to dragging (WCAG 2.1.1).",
55995
- "wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
55996
- "wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
55997
- "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.",
55998
- "test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
55999
- "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.",
56000
- "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.",
56001
- "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`).",
56002
- "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.",
56003
- "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.",
56004
- // v0.13.0 — AI-specific rules (peer-reviewed signals).
56005
- "ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
56006
- "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).",
56007
- "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).",
56008
- "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).",
56009
- "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).",
56010
- "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).",
56011
- "ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
56012
- "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).",
56013
- "ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
56014
- "ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
56015
- // v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
56016
- // reclassified post-v7 calibration in v0.14.5d)
56017
- "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.",
56018
- "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.",
56019
- "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.",
56020
- "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.",
56021
- "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.",
56022
- "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.",
56023
- // v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
56024
- "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`).",
56025
- "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.",
56026
- "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.",
56027
- "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.",
56028
- "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.",
56029
- "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.',
56030
- // v0.17.0 — docs/* rules (markdown drift detection)
56031
- "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.",
56032
- "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.",
56033
- "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.",
56034
- "docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy."
56035
- };
56036
-
56037
56141
  // src/snippet/targets.ts
56038
56142
  import { join as join25 } from "path";
56039
56143
 
@@ -56280,52 +56384,6 @@ function renderMatrix() {
56280
56384
  return lines.join("\n");
56281
56385
  }
56282
56386
 
56283
- // src/cli/explain.ts
56284
- var RULES_BASE_URL2 = "https://github.com/Dystx/slopbrick/blob/main/src/rules";
56285
- function ruleIdToFilename2(ruleId) {
56286
- const slash = ruleId.indexOf("/");
56287
- return slash === -1 ? ruleId : ruleId.slice(slash + 1);
56288
- }
56289
- function explainRule2(ruleId, rules, ruleHints) {
56290
- const rule = rules.find((r) => r.id === ruleId);
56291
- if (!rule) {
56292
- return { error: "Unknown rule: " + ruleId + ". Run `slopbrick rules` to see all available rules." };
56293
- }
56294
- const filename = ruleIdToFilename2(rule.id);
56295
- return {
56296
- ruleId: rule.id,
56297
- category: rule.category,
56298
- severity: rule.severity,
56299
- aiSpecific: rule.aiSpecific,
56300
- pattern: ruleHints[rule.id] ?? "Patterns flagged by " + rule.id + ".",
56301
- remediation: "See the rule source for the canonical before/after: src/rules/" + rule.category + "/" + filename + ".ts",
56302
- sourcePath: "src/rules/" + rule.category + "/" + filename + ".ts",
56303
- helpUri: `${RULES_BASE_URL2}/${rule.category}/${filename}.ts`,
56304
- suppressionSnippet: 'rules: { "' + rule.id + '": "off" } // or set to a lower severity'
56305
- };
56306
- }
56307
- function formatExplain(result) {
56308
- if ("error" in result) return result.error;
56309
- const lines = [];
56310
- lines.push("Rule: " + result.ruleId);
56311
- lines.push("Category: " + result.category);
56312
- lines.push("Severity: " + result.severity);
56313
- lines.push("AI-specific: " + (result.aiSpecific ? "yes (designed to fire on AI tells)" : "no (cross-cutting quality rule)"));
56314
- lines.push("Source: " + result.sourcePath);
56315
- lines.push("Help: " + result.helpUri);
56316
- lines.push("");
56317
- lines.push("Pattern:");
56318
- lines.push(" " + result.pattern);
56319
- lines.push("");
56320
- lines.push("Remediation:");
56321
- lines.push(" " + result.remediation);
56322
- lines.push("");
56323
- lines.push("Suppress / configure in slopbrick.config.mjs:");
56324
- lines.push(" " + result.suppressionSnippet);
56325
- lines.push("");
56326
- return lines.join("\n");
56327
- }
56328
-
56329
56387
  // src/cli/program.ts
56330
56388
  init_validation();
56331
56389
  init_signal_strength2();
@@ -56483,7 +56541,6 @@ function formatMarkdown3(report) {
56483
56541
  }
56484
56542
 
56485
56543
  // src/cli/program.ts
56486
- init_advice();
56487
56544
  init_unified_diff();
56488
56545
  init_heatmap();
56489
56546
 
@@ -57019,7 +57076,7 @@ async function runCli({ start }) {
57019
57076
  logger.info(renderMatrix());
57020
57077
  process.exit(0);
57021
57078
  }
57022
- const cwd = resolve17(options.workspace ?? process.cwd());
57079
+ const cwd = resolve19(options.workspace ?? process.cwd());
57023
57080
  const configPath = join28(cwd, "slopbrick.config.mjs");
57024
57081
  const detected = detectStack(cwd);
57025
57082
  const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
@@ -57115,7 +57172,7 @@ async function runCli({ start }) {
57115
57172
  });
57116
57173
  program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
57117
57174
  const options = command.optsWithGlobals();
57118
- const cwd = resolve17(options.workspace ?? process.cwd());
57175
+ const cwd = resolve19(options.workspace ?? process.cwd());
57119
57176
  const root = getGitRoot(cwd);
57120
57177
  if (!root) {
57121
57178
  logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
@@ -57129,7 +57186,7 @@ async function runCli({ start }) {
57129
57186
  });
57130
57187
  program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
57131
57188
  const options = command.optsWithGlobals();
57132
- const cwd = resolve17(options.workspace ?? process.cwd());
57189
+ const cwd = resolve19(options.workspace ?? process.cwd());
57133
57190
  const root = getGitRoot(cwd);
57134
57191
  if (!root) {
57135
57192
  logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
@@ -57141,34 +57198,12 @@ async function runCli({ start }) {
57141
57198
  }
57142
57199
  process.exit(result.exitCode);
57143
57200
  });
57144
- 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) => {
57145
- const options = command.optsWithGlobals();
57146
- const cwd = resolve17(options.workspace ?? process.cwd());
57147
- const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
57148
- const health = loadHealth2(cwd);
57149
- if (health) {
57150
- const synthetic = {
57151
- slopIndex: 100 - health.repositoryHealth
57152
- };
57153
- logger.info(formatBadge(synthetic));
57154
- process.exit(0);
57155
- }
57156
- const { report } = await runScan(options);
57157
- logger.info(formatBadge(report));
57158
- process.exit(0);
57159
- });
57160
- program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
57161
- const options = command.optsWithGlobals();
57162
- const { report } = await runScan(options);
57163
- const cwd = resolve17(options.workspace ?? process.cwd());
57164
- logger.info(formatAdvice(report));
57165
- const diff = formatUnifiedDiff(report, cwd);
57166
- if (diff) logger.info(diff);
57167
- process.exit(0);
57168
- });
57201
+ registerBadge(program);
57202
+ registerSuggest(program);
57203
+ registerExplain(program);
57169
57204
  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) => {
57170
57205
  const options = command.optsWithGlobals();
57171
- const cwd = resolve17(options.workspace ?? process.cwd());
57206
+ const cwd = resolve19(options.workspace ?? process.cwd());
57172
57207
  const payloads = readTelemetry(cwd);
57173
57208
  if (payloads.length === 0) {
57174
57209
  logger.info("No flywheel telemetry found. Run a scan first.");
@@ -57176,7 +57211,7 @@ async function runCli({ start }) {
57176
57211
  }
57177
57212
  const summary = summarizeTelemetry(payloads);
57178
57213
  if (cmdOptions.export) {
57179
- const exportPath = resolve17(cmdOptions.export);
57214
+ const exportPath = resolve19(cmdOptions.export);
57180
57215
  mkdirSync13(dirname17(exportPath), { recursive: true });
57181
57216
  writeFileSync19(exportPath, JSON.stringify(summary, null, 2), "utf-8");
57182
57217
  logger.info(`Wrote flywheel summary to ${exportPath}`);
@@ -57201,7 +57236,7 @@ async function runCli({ start }) {
57201
57236
  logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
57202
57237
  process.exit(2);
57203
57238
  }
57204
- const cwd = resolve17(options.workspace ?? process.cwd());
57239
+ const cwd = resolve19(options.workspace ?? process.cwd());
57205
57240
  if (options.trend !== void 0) {
57206
57241
  const runs = await readRuns(cwd, fsMemoryIO);
57207
57242
  if (runs.length === 0) {
@@ -57234,7 +57269,7 @@ async function runCli({ start }) {
57234
57269
  const scanElapsed = Math.round(performance.now() - scanStart);
57235
57270
  const totalElapsed = Math.round(performance.now() - start);
57236
57271
  if (options.baseline) {
57237
- const cwd2 = resolve17(options.workspace ?? process.cwd());
57272
+ const cwd2 = resolve19(options.workspace ?? process.cwd());
57238
57273
  const configHash = hashConfig(config);
57239
57274
  const gitHead = await getGitHead(cwd2) ?? "unknown";
57240
57275
  const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
@@ -57328,14 +57363,14 @@ async function runCli({ start }) {
57328
57363
  framework: cmdOptions.framework,
57329
57364
  componentType: cmdOptions.componentType,
57330
57365
  provider,
57331
- outputDir: resolve17(cmdOptions.outputDir),
57366
+ outputDir: resolve19(cmdOptions.outputDir),
57332
57367
  temperature: cmdOptions.temperature
57333
57368
  });
57334
57369
  logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
57335
57370
  });
57336
57371
  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) => {
57337
57372
  try {
57338
- const metadataPath = resolve17(cmdOptions.inputDir, "metadata.json");
57373
+ const metadataPath = resolve19(cmdOptions.inputDir, "metadata.json");
57339
57374
  if (!existsSync28(metadataPath)) {
57340
57375
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
57341
57376
  process.exit(2);
@@ -57343,7 +57378,7 @@ async function runCli({ start }) {
57343
57378
  const samples = JSON.parse(readFileSync38(metadataPath, "utf8"));
57344
57379
  const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
57345
57380
  const analysis = await analyzeSamples(samples, config);
57346
- const outputPath = resolve17(cmdOptions.output);
57381
+ const outputPath = resolve19(cmdOptions.output);
57347
57382
  mkdirSync13(dirname17(outputPath), { recursive: true });
57348
57383
  writeFileSync19(outputPath, JSON.stringify(analysis, null, 2), "utf8");
57349
57384
  logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
@@ -57355,7 +57390,7 @@ async function runCli({ start }) {
57355
57390
  });
57356
57391
  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) => {
57357
57392
  try {
57358
- const metadataPath = resolve17(cmdOptions.inputDir, "metadata.json");
57393
+ const metadataPath = resolve19(cmdOptions.inputDir, "metadata.json");
57359
57394
  if (!existsSync28(metadataPath)) {
57360
57395
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
57361
57396
  process.exit(2);
@@ -57370,7 +57405,7 @@ async function runCli({ start }) {
57370
57405
  const candidates = clustersToCandidates(extraction.clusters, {
57371
57406
  minFrequency: cmdOptions.minFrequency
57372
57407
  });
57373
- const outputPath = resolve17(cmdOptions.output);
57408
+ const outputPath = resolve19(cmdOptions.output);
57374
57409
  mkdirSync13(dirname17(outputPath), { recursive: true });
57375
57410
  const payload = {
57376
57411
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -57396,7 +57431,7 @@ async function runCli({ start }) {
57396
57431
  positiveLimit: cmdOptions.positiveLimit,
57397
57432
  negativeLimit: cmdOptions.negativeLimit
57398
57433
  });
57399
- const outputPath = cmdOptions.output ? resolve17(cwd, cmdOptions.output) : resolve17(cwd, "corpus", "calibration-empirical.md");
57434
+ const outputPath = cmdOptions.output ? resolve19(cwd, cmdOptions.output) : resolve19(cwd, "corpus", "calibration-empirical.md");
57400
57435
  mkdirSync13(dirname17(outputPath), { recursive: true });
57401
57436
  writeFileSync19(outputPath, reportToMarkdown(report), "utf8");
57402
57437
  logger.info(
@@ -57436,7 +57471,7 @@ async function runCli({ start }) {
57436
57471
  const options = command.optsWithGlobals();
57437
57472
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
57438
57473
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
57439
- const cwd = resolve17(options.workspace ?? process.cwd());
57474
+ const cwd = resolve19(options.workspace ?? process.cwd());
57440
57475
  const { config } = await runScan({ ...options, workspace: cwd });
57441
57476
  const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
57442
57477
  logger.info(formatDrift(result, { json: format === "json" }));
@@ -57453,7 +57488,7 @@ async function runCli({ start }) {
57453
57488
  async (cmdOptions, command) => {
57454
57489
  try {
57455
57490
  const options = command.optsWithGlobals();
57456
- const cwd = resolve17(options.workspace ?? process.cwd());
57491
+ const cwd = resolve19(options.workspace ?? process.cwd());
57457
57492
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57458
57493
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57459
57494
  const { config } = await runScan({ ...options, workspace: cwd });
@@ -57480,7 +57515,7 @@ async function runCli({ start }) {
57480
57515
  const options = command.optsWithGlobals();
57481
57516
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
57482
57517
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
57483
- const cwd = resolve17(options.workspace ?? process.cwd());
57518
+ const cwd = resolve19(options.workspace ?? process.cwd());
57484
57519
  const { report } = await runScan({ ...options, workspace: cwd });
57485
57520
  const securityIssues = report.issues.filter((i) => i.category === "security");
57486
57521
  const { risk, findings } = computeAiSecurityRisk(securityIssues);
@@ -57531,7 +57566,7 @@ async function runCli({ start }) {
57531
57566
  const options = command.optsWithGlobals();
57532
57567
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
57533
57568
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
57534
- const cwd = resolve17(options.workspace ?? process.cwd());
57569
+ const cwd = resolve19(options.workspace ?? process.cwd());
57535
57570
  const { config } = await runScan({ ...options, workspace: cwd });
57536
57571
  const { result } = await runTestScan(cwd, config, { strict: options.strict });
57537
57572
  logger.info(formatTestReport(result, { json: format === "json" }));
@@ -57550,7 +57585,7 @@ async function runCli({ start }) {
57550
57585
  const options = command.optsWithGlobals();
57551
57586
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
57552
57587
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
57553
- const cwd = resolve17(options.workspace ?? process.cwd());
57588
+ const cwd = resolve19(options.workspace ?? process.cwd());
57554
57589
  const { config } = await runScan({ ...options, workspace: cwd });
57555
57590
  const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
57556
57591
  const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
@@ -57570,7 +57605,7 @@ async function runCli({ start }) {
57570
57605
  const options = command.optsWithGlobals();
57571
57606
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57572
57607
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57573
- const cwd = resolve17(options.workspace ?? process.cwd());
57608
+ const cwd = resolve19(options.workspace ?? process.cwd());
57574
57609
  const { config } = await runScan({ ...options, workspace: cwd });
57575
57610
  const result = await runBusinessLogicScan(cwd, config, {
57576
57611
  maxFiles: cmdOptions.maxFiles
@@ -57592,7 +57627,7 @@ async function runCli({ start }) {
57592
57627
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57593
57628
  const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
57594
57629
  const strict = options.strict ?? cmdOptions.strict ?? false;
57595
- const cwd = resolve17(options.workspace ?? process.cwd());
57630
+ const cwd = resolve19(options.workspace ?? process.cwd());
57596
57631
  const { config } = await runScan({ ...options, workspace: cwd });
57597
57632
  const result = await runMaintenanceCostScan(cwd, config, {
57598
57633
  maxFiles: cmdOptions.maxFiles,
@@ -57615,7 +57650,7 @@ async function runCli({ start }) {
57615
57650
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57616
57651
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57617
57652
  const strict = options.strict ?? cmdOptions.strict ?? false;
57618
- const cwd = resolve17(options.workspace ?? process.cwd());
57653
+ const cwd = resolve19(options.workspace ?? process.cwd());
57619
57654
  const { config } = await runScan({ ...options, workspace: cwd });
57620
57655
  const result = await runDocsScan(cwd, config, {
57621
57656
  maxDocFiles: cmdOptions.maxFiles,
@@ -57643,7 +57678,7 @@ async function runCli({ start }) {
57643
57678
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57644
57679
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57645
57680
  const strict = options.strict ?? cmdOptions.strict ?? false;
57646
- const cwd = resolve17(options.workspace ?? process.cwd());
57681
+ const cwd = resolve19(options.workspace ?? process.cwd());
57647
57682
  const { config } = await runScan({ ...options, workspace: cwd });
57648
57683
  const result = await runDbScan(cwd, config, {
57649
57684
  maxFiles: cmdOptions.maxFiles,
@@ -57670,7 +57705,7 @@ async function runCli({ start }) {
57670
57705
  const options = command.optsWithGlobals();
57671
57706
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
57672
57707
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
57673
- const cwd = resolve17(options.workspace ?? process.cwd());
57708
+ const cwd = resolve19(options.workspace ?? process.cwd());
57674
57709
  const { config } = await runScan({ ...options, workspace: cwd });
57675
57710
  const result = await runPatternsScan(cwd, config, {
57676
57711
  maxFiles: cmdOptions.maxFiles,
@@ -57701,14 +57736,14 @@ async function runCli({ start }) {
57701
57736
  ...rawGlobals,
57702
57737
  noIncrease: rawGlobals.increase === false
57703
57738
  };
57704
- const cwd = resolve17(options.workspace ?? process.cwd());
57739
+ const cwd = resolve19(options.workspace ?? process.cwd());
57705
57740
  const { watchProject: watchProject2 } = await Promise.resolve().then(() => (init_scan(), scan_exports));
57706
57741
  await scanAction([], options, command);
57707
57742
  await watchProject2(options, cwd, []);
57708
57743
  });
57709
57744
  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(
57710
57745
  (cmdOptions) => {
57711
- const cwd = resolve17(cmdOptions.workspace ?? process.cwd());
57746
+ const cwd = resolve19(cmdOptions.workspace ?? process.cwd());
57712
57747
  const { installHook: installHook2, uninstallHook: uninstallHook2 } = (init_installer(), __toCommonJS(installer_exports));
57713
57748
  if (cmdOptions.uninstall) {
57714
57749
  const result2 = uninstallHook2(cwd);
@@ -57738,7 +57773,7 @@ async function runCli({ start }) {
57738
57773
  // scan only changed files
57739
57774
  format: cmdOptions.format ?? "json"
57740
57775
  };
57741
- const cwd = resolve17(options.workspace ?? process.cwd());
57776
+ const cwd = resolve19(options.workspace ?? process.cwd());
57742
57777
  await scanAction([], options, command);
57743
57778
  const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
57744
57779
  const health = loadHealth2(cwd);
@@ -57766,7 +57801,7 @@ async function runCli({ start }) {
57766
57801
  );
57767
57802
  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(
57768
57803
  async (cmdOptions) => {
57769
- const cwd = resolve17(cmdOptions.workspace ?? process.cwd());
57804
+ const cwd = resolve19(cmdOptions.workspace ?? process.cwd());
57770
57805
  const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
57771
57806
  const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
57772
57807
  if (cmdOptions.regenerate) {
@@ -57799,7 +57834,7 @@ async function runCli({ start }) {
57799
57834
  (cmdOptions, command) => {
57800
57835
  const globals = command.optsWithGlobals();
57801
57836
  const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
57802
- const cwd = resolve17(cmdOptions.workspace ?? process.cwd());
57837
+ const cwd = resolve19(cmdOptions.workspace ?? process.cwd());
57803
57838
  const { runMigrate: runMigrate2, formatMigrate: formatMigrate2 } = (init_migrate(), __toCommonJS(migrate_exports));
57804
57839
  const result = runMigrate2({
57805
57840
  workspace: cwd,
@@ -57894,13 +57929,8 @@ async function runCli({ start }) {
57894
57929
  }
57895
57930
  logger.info(lines.join("\n"));
57896
57931
  });
57897
- program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
57898
- const result = explainRule2(ruleId, builtinRules, RULE_HINTS);
57899
- logger.info(formatExplain(result));
57900
- if ("error" in result) process.exit(2);
57901
- });
57902
57932
  program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
57903
- const path = configPath ? resolve17(configPath) : resolve17(process.cwd(), "slopbrick.config.mjs");
57933
+ const path = configPath ? resolve19(configPath) : resolve19(process.cwd(), "slopbrick.config.mjs");
57904
57934
  if (!existsSync28(path)) {
57905
57935
  logger.error(`Error: config file not found: ${path}`);
57906
57936
  process.exit(2);