slopbrick 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ var VERSION;
19
19
  var init_header = __esm({
20
20
  "src/types/_header.ts"() {
21
21
  "use strict";
22
- VERSION = "0.42.0";
22
+ VERSION = "0.43.0";
23
23
  }
24
24
  });
25
25
 
@@ -321,8 +321,16 @@ var init_defaults = __esm({
321
321
  // rule definitions are meta-code
322
322
  "**/snippet/**",
323
323
  // RULE_HINTS examples are intentional bad code
324
- "**/tests/**"
324
+ "**/tests/**",
325
325
  // all test files (fixtures + unit + integration)
326
+ // v0.42.0 (post-self-scan cleanup): language visitors are
327
+ // intentional clones — see the §3 v0.42.0 release notes for
328
+ // the design rationale. `dup/identical-block` fires 100+
329
+ // times during self-scan because each visitor shares the
330
+ // same service-suffix arrays and import-walking preamble.
331
+ // Suppress these from the self-scan noise floor; they
332
+ // remain on for user codebases.
333
+ "**/src/engine/visitors/**"
326
334
  ]
327
335
  }
328
336
  };
@@ -25984,7 +25992,7 @@ var init_comment_ratio = __esm({
25984
25992
  message: `Comment-line ratio is ${direction} (${ratio.toFixed(2)} vs corpus mean ${mean3}). Reductive LLMs (GPT-3.5, Claude 3 Haiku) skip comments; expansive LLMs (Claude Haiku 4.5, GPT-OSS) over-comment when prompted for "well-commented code".`,
25985
25993
  line: 1,
25986
25994
  column: 1,
25987
- advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "Trim comments that just restate the next line. Keep comments that explain WHY, not WHAT."
25995
+ advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "High comment ratio \u2014 consider whether the comments are intentional (api docs, type defs, tutorials). For functional code, trim comments that just restate the next line. Keep comments that explain WHY, not WHAT."
25988
25996
  }
25989
25997
  ];
25990
25998
  return issues;
@@ -26834,7 +26842,6 @@ function buildTrigramLM(samples, alpha = 1) {
26834
26842
  logProb(context, t) {
26835
26843
  const ctxKey = context.slice(-2).join("|");
26836
26844
  const triKey = `${ctxKey}|${t}`;
26837
- const biKey = context.slice(-1).join("|") + "|" + t;
26838
26845
  const triCount = tri.get(triKey) ?? 0;
26839
26846
  const biCount = bi.get(ctxKey) ?? 0;
26840
26847
  const V = uni.size;
@@ -28842,7 +28849,7 @@ var init_stale_function_reference = __esm({
28842
28849
  ]);
28843
28850
  SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
28844
28851
  SOURCE_ROOTS = ["src", "lib", "app", "components"];
28845
- CAP = 200;
28852
+ CAP = 4e3;
28846
28853
  staleFunctionReferenceRule = createRule({
28847
28854
  id: "docs/stale-function-reference",
28848
28855
  category: "docs",
@@ -28858,6 +28865,7 @@ var init_stale_function_reference = __esm({
28858
28865
  if (!source) return issues;
28859
28866
  for (const span of extractInlineCodeSpans(source)) {
28860
28867
  const text = span.text;
28868
+ if (span.inBlockComment || span.inComment) continue;
28861
28869
  if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
28862
28870
  if (text.length < 3) continue;
28863
28871
  if (RESERVED.has(text.toLowerCase())) continue;
@@ -29028,6 +29036,7 @@ var init_stale_package_reference = __esm({
29028
29036
  if (!source) return issues;
29029
29037
  const spans = extractInlineCodeSpans(source);
29030
29038
  for (const span of spans) {
29039
+ if (span.inComment) continue;
29031
29040
  const lineStart = source.lastIndexOf("\n", span.index) + 1;
29032
29041
  const lineEnd = source.indexOf("\n", span.index);
29033
29042
  const line = source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd);
@@ -29082,7 +29091,8 @@ __export(doc_freshness_exports, {
29082
29091
  extractExports: () => extractExports,
29083
29092
  extractFencedCodeBlocks: () => extractFencedCodeBlocks,
29084
29093
  extractInlineCodeSpans: () => extractInlineCodeSpans,
29085
- extractMarkdownLinks: () => extractMarkdownLinks
29094
+ extractMarkdownLinks: () => extractMarkdownLinks,
29095
+ findCommentLineRanges: () => findCommentLineRanges
29086
29096
  });
29087
29097
  import { readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
29088
29098
  import { join as join6, dirname as dirname2, relative } from "path";
@@ -29090,6 +29100,8 @@ import { globby } from "globby";
29090
29100
  function extractInlineCodeSpans(source) {
29091
29101
  const hits = [];
29092
29102
  const re = /`([^`\n]+?)`/g;
29103
+ const blockRanges = findBlockCommentRanges(source);
29104
+ const commentLines = findCommentLineRanges(source);
29093
29105
  let m;
29094
29106
  while ((m = re.exec(source)) !== null) {
29095
29107
  const text = m[1] ?? "";
@@ -29097,7 +29109,14 @@ function extractInlineCodeSpans(source) {
29097
29109
  const line = upTo.split("\n").length;
29098
29110
  const lastNl = upTo.lastIndexOf("\n");
29099
29111
  const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
29100
- hits.push({ text, line, column, index: m.index });
29112
+ hits.push({
29113
+ text,
29114
+ line,
29115
+ column,
29116
+ index: m.index,
29117
+ inBlockComment: isInBlockComment(blockRanges, m.index),
29118
+ inComment: commentLines.has(line)
29119
+ });
29101
29120
  }
29102
29121
  return hits;
29103
29122
  }
@@ -29136,6 +29155,8 @@ function extractFencedCodeBlocks(source) {
29136
29155
  function extractMarkdownLinks(source) {
29137
29156
  const hits = [];
29138
29157
  const re = /(?<!\!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
29158
+ const blockRanges = findBlockCommentRanges(source);
29159
+ const commentLines = findCommentLineRanges(source);
29139
29160
  let m;
29140
29161
  while ((m = re.exec(source)) !== null) {
29141
29162
  const target = m[2] ?? "";
@@ -29143,10 +29164,55 @@ function extractMarkdownLinks(source) {
29143
29164
  const line = upTo.split("\n").length;
29144
29165
  const lastNl = upTo.lastIndexOf("\n");
29145
29166
  const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
29146
- hits.push({ target, line, column });
29167
+ hits.push({
29168
+ target,
29169
+ line,
29170
+ column,
29171
+ index: m.index,
29172
+ inBlockComment: isInBlockComment(blockRanges, m.index),
29173
+ inComment: commentLines.has(line)
29174
+ });
29147
29175
  }
29148
29176
  return hits;
29149
29177
  }
29178
+ function findBlockCommentRanges(source) {
29179
+ const ranges = [];
29180
+ const stack = [];
29181
+ let i = 0;
29182
+ while (i < source.length) {
29183
+ if (source[i] === "/" && source[i + 1] === "/") {
29184
+ const eol = source.indexOf("\n", i);
29185
+ i = eol === -1 ? source.length : eol + 1;
29186
+ continue;
29187
+ }
29188
+ if (source[i] === "/" && source[i + 1] === "*") {
29189
+ stack.push(i);
29190
+ i += 2;
29191
+ continue;
29192
+ }
29193
+ if (source[i] === "*" && source[i + 1] === "/" && stack.length > 0) {
29194
+ const start = stack.pop();
29195
+ ranges.push([start, i + 2]);
29196
+ i += 2;
29197
+ continue;
29198
+ }
29199
+ i += 1;
29200
+ }
29201
+ ranges.sort((a, b) => a[0] - b[0]);
29202
+ return ranges;
29203
+ }
29204
+ function isInBlockComment(ranges, pos) {
29205
+ let lo = 0;
29206
+ let hi = ranges.length - 1;
29207
+ while (lo <= hi) {
29208
+ const mid = lo + hi >>> 1;
29209
+ const [start, end] = ranges[mid];
29210
+ if (pos < start) hi = mid - 1;
29211
+ else if (pos >= end) lo = mid + 1;
29212
+ else return true;
29213
+ }
29214
+ return false;
29215
+ }
29150
29216
  function declaredPackages(cwd) {
29151
29217
  const out = /* @__PURE__ */ new Set();
29152
29218
  const pkgPath = join6(cwd, "package.json");
@@ -29301,6 +29367,58 @@ function docDriftFromFreshness(score) {
29301
29367
  if (score >= DOC_FRESHNESS_THRESHOLDS.high) return "high";
29302
29368
  return "critical";
29303
29369
  }
29370
+ function findCommentLineRanges(source) {
29371
+ const out = /* @__PURE__ */ new Set();
29372
+ const stack = [];
29373
+ let inLineComment = false;
29374
+ const lines = source.split("\n");
29375
+ for (let i = 0; i < lines.length; i++) {
29376
+ const line = lines[i];
29377
+ const lineNo = i + 1;
29378
+ let j = 0;
29379
+ while (j < line.length) {
29380
+ const c = line[j];
29381
+ const next = line[j + 1];
29382
+ if (!stack.length && c === "/" && next === "/") {
29383
+ inLineComment = true;
29384
+ out.add(lineNo);
29385
+ break;
29386
+ }
29387
+ if (c === "/" && next === "*") {
29388
+ stack.push(lineNo);
29389
+ out.add(lineNo);
29390
+ j += 2;
29391
+ continue;
29392
+ }
29393
+ if (c === "*" && next === "/" && stack.length > 0) {
29394
+ stack.pop();
29395
+ j += 2;
29396
+ continue;
29397
+ }
29398
+ if (c === '"' || c === "'" || c === "`") {
29399
+ const quote = c;
29400
+ j += 1;
29401
+ while (j < line.length) {
29402
+ if (line[j] === "\\") {
29403
+ j += 2;
29404
+ continue;
29405
+ }
29406
+ if (line[j] === quote) {
29407
+ j += 1;
29408
+ break;
29409
+ }
29410
+ j += 1;
29411
+ }
29412
+ continue;
29413
+ }
29414
+ j += 1;
29415
+ }
29416
+ if (stack.length > 0) {
29417
+ out.add(lineNo);
29418
+ }
29419
+ }
29420
+ return out;
29421
+ }
29304
29422
  var DOC_RULE_WEIGHTS, DOC_FRESHNESS_THRESHOLDS;
29305
29423
  var init_doc_freshness = __esm({
29306
29424
  "src/engine/doc-freshness.ts"() {
@@ -29324,6 +29442,9 @@ var init_doc_freshness = __esm({
29324
29442
  // src/rules/docs/broken-link.ts
29325
29443
  import { existsSync as existsSync6 } from "fs";
29326
29444
  import { dirname as dirname3, join as join7, resolve as resolve4 } from "path";
29445
+ function looksLikeRegexSyntax(target) {
29446
+ return /\[[^\]]*\]/.test(target) || /^\s*[?:=!]/.test(target) || /\\(?:s|d|w|S|D|W|t|n|r|[^\\])/.test(target) || /[{}|]/.test(target);
29447
+ }
29327
29448
  var brokenLinkRule;
29328
29449
  var init_broken_link = __esm({
29329
29450
  "src/rules/docs/broken-link.ts"() {
@@ -29352,6 +29473,8 @@ var init_broken_link = __esm({
29352
29473
  if (target.startsWith("#")) continue;
29353
29474
  if (target.startsWith("//")) continue;
29354
29475
  if (target.startsWith("/")) continue;
29476
+ if (link.inBlockComment) continue;
29477
+ if (looksLikeRegexSyntax(target)) continue;
29355
29478
  const filePart = target.split("#")[0] ?? target;
29356
29479
  if (filePart === "") continue;
29357
29480
  const resolved = join7(docDir, filePart);
@@ -38526,6 +38649,29 @@ var init_stringly_typed = __esm({
38526
38649
  }
38527
38650
  });
38528
38651
 
38652
+ // src/engine/parser-shared.ts
38653
+ function parseTreeSitterSource(parserResult, source) {
38654
+ if (!parserResult.ok) return null;
38655
+ if (!source || source.trim() === "") return null;
38656
+ try {
38657
+ const rawTree = parserResult.parser.parse(source);
38658
+ if (!rawTree) return null;
38659
+ const tree = rawTree;
38660
+ if (!tree || !tree.rootNode) return null;
38661
+ if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
38662
+ return null;
38663
+ }
38664
+ return tree;
38665
+ } catch {
38666
+ return null;
38667
+ }
38668
+ }
38669
+ var init_parser_shared = __esm({
38670
+ "src/engine/parser-shared.ts"() {
38671
+ "use strict";
38672
+ }
38673
+ });
38674
+
38529
38675
  // src/engine/parser-rust.ts
38530
38676
  import Parser from "tree-sitter";
38531
38677
  import Rust from "tree-sitter-rust";
@@ -38551,21 +38697,7 @@ function effectiveParser() {
38551
38697
  return getRustParser();
38552
38698
  }
38553
38699
  function parseRust(source) {
38554
- const result = effectiveParser();
38555
- if (!result.ok) return null;
38556
- if (!source || source.trim() === "") return null;
38557
- try {
38558
- const rawTree = result.parser.parse(source);
38559
- if (!rawTree) return null;
38560
- const tree = rawTree;
38561
- if (!tree || !tree.rootNode) return null;
38562
- if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
38563
- return null;
38564
- }
38565
- return tree;
38566
- } catch {
38567
- return null;
38568
- }
38700
+ return parseTreeSitterSource(effectiveParser(), source);
38569
38701
  }
38570
38702
  function isRustParserAvailable() {
38571
38703
  const result = getRustParser();
@@ -38575,6 +38707,7 @@ var cachedParser, rustLanguage, loadError, forcedFailure;
38575
38707
  var init_parser_rust = __esm({
38576
38708
  "src/engine/parser-rust.ts"() {
38577
38709
  "use strict";
38710
+ init_parser_shared();
38578
38711
  cachedParser = null;
38579
38712
  rustLanguage = null;
38580
38713
  loadError = null;
@@ -43682,8 +43815,6 @@ function mergeTemplateClassNames(filePath, source, facts, templateClassNames) {
43682
43815
  }
43683
43816
  function extractFacts(filePath, ast, source, supportsRsc = true, framework = "react", config) {
43684
43817
  const lineOffsets = buildLineOffsets2(source);
43685
- const isTypeScript = /\.tsx?$/i.test(filePath);
43686
- const NON_JSX_FRAMEWORKS = /* @__PURE__ */ new Set(["vue", "svelte", "astro"]);
43687
43818
  const facts = {
43688
43819
  filePath,
43689
43820
  components: [],
@@ -45622,7 +45753,7 @@ function formatDeltaSuffix(report) {
45622
45753
  }
45623
45754
  function formatVerdict(report) {
45624
45755
  const score = report.aiSlopScore;
45625
- const band = scoreBand(score);
45756
+ const band = slopScoreBand(score);
45626
45757
  const ranked = rankByImpact(report.issues);
45627
45758
  const topRule = ranked[0];
45628
45759
  const topFile = report.topOffenders?.[0];
@@ -45705,7 +45836,8 @@ function formatNextStep(report) {
45705
45836
  ` \u2192 \`slopbrick scan --baseline\` to accept today's scores as the new floor`
45706
45837
  )
45707
45838
  );
45708
- if (report.aiSlopScore > 30) {
45839
+ const meanSlop = report.thresholds?.meanSlop ?? 30;
45840
+ if (report.aiSlopScore > meanSlop) {
45709
45841
  lines.push(
45710
45842
  chalk.dim(
45711
45843
  ` \u2192 \`slopbrick scan --why-failing\` for the top 5 issues dragging the score down`
@@ -45730,17 +45862,18 @@ function formatWhyFailing(report) {
45730
45862
  return chalk.green("Nothing is failing the threshold. Score is clean.");
45731
45863
  }
45732
45864
  const headline = report.aiSlopScore;
45733
- const status = headline <= 30 ? "PASS" : "FAIL";
45734
- const colorize = headline <= 30 ? chalk.green : chalk.red;
45865
+ const meanSlop = report.thresholds?.meanSlop ?? 30;
45866
+ const status = headline <= meanSlop ? "PASS" : "FAIL";
45867
+ const colorize = headline <= meanSlop ? chalk.green : chalk.red;
45868
+ const qualifier = headline <= meanSlop ? `below ${meanSlop}` : `above ${meanSlop}`;
45735
45869
  const lines = [];
45736
- lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014 above 30)`));
45870
+ lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014 ${qualifier})`));
45737
45871
  lines.push("");
45738
45872
  lines.push(chalk.bold("Top 5 rules dragging the score down:"));
45739
45873
  for (let i = 0; i < ranked.length; i++) {
45740
45874
  const [ruleId, info] = ranked[i];
45741
45875
  const fileCount = info.files.size;
45742
45876
  const fileHint = fileCount > 0 ? ` across ${fileCount} file${fileCount === 1 ? "" : "s"}` : "";
45743
- const catShort = CATEGORY_GLOSSARY[info.count > 0 ? "" : ""]?.short;
45744
45877
  const sampleIssue = report.issues.find((i2) => i2.ruleId === ruleId);
45745
45878
  const cat = sampleIssue ? CATEGORY_GLOSSARY[sampleIssue.category]?.short : null;
45746
45879
  const catHint = cat ? chalk.dim(` [${cat}]`) : "";
@@ -45800,7 +45933,7 @@ function formatCompositeScore(report) {
45800
45933
  lines.push("");
45801
45934
  lines.push(
45802
45935
  chalk.dim(
45803
- `Repository Coherence: ${coherenceValue} / 100 [${cohBand.label.toUpperCase()}] \u2014 higher = better \xB7 measures internal consistency. This is a secondary view; the Slop Index above is the gate.`
45936
+ `Repository Coherence: ${coherenceValue} / 100 [${cohBand.label.toUpperCase()}] \u2014 higher = better \xB7 measures internal consistency. This is a secondary view; the AI Slop Score above is the gate.`
45804
45937
  )
45805
45938
  );
45806
45939
  }
@@ -45930,13 +46063,12 @@ function formatDriftSection(report) {
45930
46063
  return lines.join("\n");
45931
46064
  }
45932
46065
  function formatThresholds(report) {
45933
- const limit = report.thresholds.meanSlop;
46066
+ const meanSlop = report.thresholds?.meanSlop ?? 30;
45934
46067
  const headline = report.aiSlopScore;
45935
- const passed = headline >= 70;
45936
- const valueText = `${headline.toFixed(1)} \u2265 70`.padStart(12, " ");
46068
+ const passed = headline <= meanSlop;
46069
+ const valueText = `${headline.toFixed(1)} \u2264 ${meanSlop}`.padStart(12, " ");
45937
46070
  const status = passed ? chalk.green("pass") : chalk.red("fail");
45938
46071
  const result = ["Threshold (CI gate)", ` AI Slop Score ${valueText} ${status}`];
45939
- void limit;
45940
46072
  return result;
45941
46073
  }
45942
46074
  function formatTopComponents(components, topOffenders) {
@@ -46014,9 +46146,10 @@ function formatPretty(report) {
46014
46146
  }
46015
46147
  return sections.join("\n\n");
46016
46148
  }
46017
- function formatScoringExplainer(_report) {
46149
+ function formatScoringExplainer(report) {
46150
+ const meanSlop = report.thresholds?.meanSlop ?? 30;
46018
46151
  return chalk.dim(
46019
- "Four orthogonal scores (all 0-100, higher = better): AI Slop Score (AI-slop signatures; the CI gate, AI Slop Score >= 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 Slop Score + 0.3*Engineering Hygiene + 0.2*Security + 0.1*Test Quality). Only AI Slop Score gates CI; the others are informational. Default-off rules (INVERTED/NOISY/DORMANT) are suppressed from the scores automatically."
46152
+ `Four orthogonal scores (all 0-100, **lower AI Slop Score = cleaner**): AI Slop Score (AI-slop signatures; the CI gate, AI Slop Score \u2264 ${meanSlop} passes), Engineering Hygiene (issues per category across arch/logic/layout/component/test; higher = cleaner), Security (AI-flagged security risks, inverted from risk level; higher = cleaner), Repository Health (composite: 0.4*AI Slop Score + 0.3*Engineering Hygiene + 0.2*Security + 0.1*Test Quality). Only AI Slop Score gates CI; the others are informational. Default-off rules (INVERTED/NOISY/DORMANT) are suppressed from the scores automatically.`
46020
46153
  );
46021
46154
  }
46022
46155
  function formatWhyFailingReport(report) {
@@ -46034,7 +46167,7 @@ function formatBriefReport(report) {
46034
46167
  ];
46035
46168
  const deltaSuffix = formatDeltaSuffix(report);
46036
46169
  scoreLines.forEach(({ label, field, value }, idx) => {
46037
- const band = scoreBand(value);
46170
+ const band = field === "aiSlopScore" ? slopScoreBand(value) : scoreBand(value);
46038
46171
  const paddedLabel = label.padEnd(20, " ");
46039
46172
  const valueStr = value.toFixed(0).padStart(3, " ");
46040
46173
  const delta = idx === 0 ? deltaSuffix : "";
@@ -46042,11 +46175,12 @@ function formatBriefReport(report) {
46042
46175
  ` ${paddedLabel} ${band.color(valueStr)} ${chalk.dim(band.label)} ${chalk.dim.italic(`(${field})`)}${delta}`
46043
46176
  );
46044
46177
  });
46045
- const passed = report.aiSlopScore <= 30;
46178
+ const meanSlop = report.thresholds?.meanSlop ?? 30;
46179
+ const passed = report.aiSlopScore <= meanSlop;
46046
46180
  lines.push("");
46047
46181
  lines.push(
46048
46182
  chalk.dim(
46049
- ` CI gate: AI Slop Score <= 30 -> ${passed ? chalk.green("pass") : chalk.red("fail")}`
46183
+ ` CI gate: AI Slop Score <= ${meanSlop} -> ${passed ? chalk.green("pass") : chalk.red("fail")}`
46050
46184
  )
46051
46185
  );
46052
46186
  const suppressed = report.defaultOffSuppressedCount ?? 0;
@@ -46192,19 +46326,22 @@ function thresholdExceeded(report, config) {
46192
46326
  return categoryThresholdBreached(report, config.thresholds.categoryThresholds);
46193
46327
  }
46194
46328
  function failedThresholdCount(report, config) {
46195
- let count = 0;
46196
- if (report.aiSlopScore > config.thresholds.meanSlop) count += 1;
46197
- if (report.p90Score > config.thresholds.p90Slop) count += 1;
46198
- if (report.peakScore > config.thresholds.individualSlopThreshold) count += 1;
46329
+ return failedThresholds(report, config).length;
46330
+ }
46331
+ function failedThresholds(report, config) {
46332
+ const failed = [];
46333
+ if (report.aiSlopScore > config.thresholds.meanSlop) failed.push("meanSlop");
46334
+ if (report.p90Score > config.thresholds.p90Slop) failed.push("p90Slop");
46335
+ if (report.peakScore > config.thresholds.individualSlopThreshold) failed.push("individualSlopThreshold");
46199
46336
  const cat = config.thresholds.categoryThresholds;
46200
46337
  if (cat) {
46201
46338
  for (const [category, limit] of Object.entries(cat)) {
46202
46339
  if (limit === void 0) continue;
46203
46340
  const score = report.categoryScores[category];
46204
- if (score !== void 0 && score > limit) count += 1;
46341
+ if (score !== void 0 && score > limit) failed.push(`category:${category}`);
46205
46342
  }
46206
46343
  }
46207
- return count;
46344
+ return failed;
46208
46345
  }
46209
46346
  function categoryThresholdBreached(report, categoryThresholds) {
46210
46347
  if (!categoryThresholds) return false;
@@ -50870,9 +51007,22 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
50870
51007
  }
50871
51008
  }
50872
51009
  if (currentTopFiles.length > 0 && runs.length >= CONSECUTIVE_THRESHOLD) {
51010
+ const latestRun = runs[runs.length - 1];
51011
+ const isNoiseTier = (id) => {
51012
+ const rule = ruleById.get(id);
51013
+ if (!rule) return false;
51014
+ if (rule.defaultOff === true) return true;
51015
+ if (rule.aiSpecific && (rule.severity === "medium" || rule.severity === "low")) return true;
51016
+ return false;
51017
+ };
51018
+ const onRulesInLatestTop = latestRun.topOffenseIds.filter(
51019
+ (id) => !isNoiseTier(id)
51020
+ );
51021
+ const skipChronicOnDefaultOffNoise = onRulesInLatestTop.length === 0;
50873
51022
  for (const file of currentTopFiles) {
50874
51023
  const consecutive = countConsecutiveTopFileAppearances(file.hash, recentTopHashes);
50875
51024
  if (consecutive >= CONSECUTIVE_THRESHOLD) {
51025
+ if (skipChronicOnDefaultOffNoise) continue;
50876
51026
  hotspotIssues.push({
50877
51027
  ruleId: "component/chronic-offender",
50878
51028
  category: "component",
@@ -50882,7 +51032,7 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
50882
51032
  filePath: file.filePath,
50883
51033
  line: 1,
50884
51034
  column: 1,
50885
- advice: "Prioritize refactoring this file or add it to the baseline."
51035
+ advice: "Run `slopbrick scan --baseline` to accept current scores as the new floor, or refactor the file to reduce its issue count."
50886
51036
  });
50887
51037
  }
50888
51038
  }
@@ -51570,14 +51720,14 @@ var init_tokens = __esm({
51570
51720
 
51571
51721
  // src/cli/memory-io.ts
51572
51722
  import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, access as access2 } from "fs/promises";
51573
- import { dirname as dirname10 } from "path";
51723
+ import { dirname as dirname9 } from "path";
51574
51724
  var fsMemoryIO;
51575
51725
  var init_memory_io = __esm({
51576
51726
  "src/cli/memory-io.ts"() {
51577
51727
  "use strict";
51578
51728
  fsMemoryIO = {
51579
51729
  read: (path) => readFile3(path, "utf-8").catch(() => null),
51580
- write: (path, content) => mkdir2(dirname10(path), { recursive: true }).then(
51730
+ write: (path, content) => mkdir2(dirname9(path), { recursive: true }).then(
51581
51731
  () => writeFile2(path, content, "utf-8")
51582
51732
  ),
51583
51733
  exists: (path) => access2(path).then(
@@ -53213,16 +53363,16 @@ function axisSlopIndex(slopIndex) {
53213
53363
  if (slopIndex === void 0 || Number.isNaN(slopIndex)) {
53214
53364
  return {
53215
53365
  axis: "slopIndex",
53216
- label: "Slop Index",
53366
+ label: "AI Slop Score (raw)",
53217
53367
  health: 70,
53218
53368
  source: "default (not measured) \u2014 neutral 70"
53219
53369
  };
53220
53370
  }
53221
53371
  return {
53222
53372
  axis: "slopIndex",
53223
- label: "Slop Index",
53373
+ label: "AI Slop Score (raw)",
53224
53374
  health: clamp100(100 - slopIndex),
53225
- source: `100 - slopIndex (${slopIndex.toFixed(1)}) \u2192 inverted`
53375
+ source: `100 - aiSlopScore (${slopIndex.toFixed(1)}) \u2192 inverted to cleanliness`
53226
53376
  };
53227
53377
  }
53228
53378
  function axisArchitecture(architectureConsistency) {
@@ -54169,6 +54319,16 @@ function assembleScanReport(input) {
54169
54319
  enrichment
54170
54320
  } = input;
54171
54321
  const prSlopScore = computePrSlopScore(diffRef, allIssues);
54322
+ const thresholdReport = {
54323
+ aiSlopScore: aggregated.aiSlopScore,
54324
+ p90Score: aggregated.p90Score,
54325
+ peakScore: aggregated.peakScore,
54326
+ categoryScores: aggregated.categoryScores
54327
+ };
54328
+ const failedThresholds_ = failedThresholds(
54329
+ thresholdReport,
54330
+ config
54331
+ );
54172
54332
  return {
54173
54333
  version: VERSION,
54174
54334
  generatedAt,
@@ -54224,6 +54384,11 @@ function assembleScanReport(input) {
54224
54384
  parseErrors: parseErrors.length > 0 ? parseErrors : void 0,
54225
54385
  baseline: baselineMeta,
54226
54386
  thresholds: config.thresholds,
54387
+ // v0.42.0 (user-review fix): expose which gates tripped so CI
54388
+ // consumers can read the JSON output instead of grepping stderr.
54389
+ // Optional field: absent when no thresholds tripped. Computed from
54390
+ // the report's own scores so the JSON is self-describing.
54391
+ failedThresholds: failedThresholds_,
54227
54392
  topOffenders: topOffenders.length > 0 ? topOffenders : void 0,
54228
54393
  coherence: enrichment.coherence,
54229
54394
  coherenceBreakdown: enrichment.coherenceBreakdown,
@@ -54240,6 +54405,7 @@ var PR_SLOP_WEIGHTS;
54240
54405
  var init_assembleScanReport = __esm({
54241
54406
  "src/cli/report/assembleScanReport.ts"() {
54242
54407
  "use strict";
54408
+ init_threshold();
54243
54409
  init_types();
54244
54410
  PR_SLOP_WEIGHTS = { high: 10, medium: 5, low: 1 };
54245
54411
  }
@@ -54257,7 +54423,7 @@ import {
54257
54423
  statSync as statSync5
54258
54424
  } from "fs";
54259
54425
  import { createHash as createHash10 } from "crypto";
54260
- import { dirname as dirname11, join as join17, relative as relative9 } from "path";
54426
+ import { dirname as dirname10, join as join17, relative as relative9 } from "path";
54261
54427
  function telemetryPath2(cwd) {
54262
54428
  return join17(cwd, TELEMETRY_DIR, TELEMETRY_FILE2);
54263
54429
  }
@@ -54334,7 +54500,7 @@ function rotateTelemetry(cwd) {
54334
54500
  if (stats.size < MAX_TELEMETRY_BYTES) {
54335
54501
  return;
54336
54502
  }
54337
- const dir = dirname11(path);
54503
+ const dir = dirname10(path);
54338
54504
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
54339
54505
  renameSync3(path, join17(dir, `scans-${timestamp}.jsonl`));
54340
54506
  const rotated = readdirSync5(dir).filter(isTelemetryFile).map((name) => ({ name, mtime: statSync5(join17(dir, name)).mtimeMs })).sort((a, b) => a.mtime - b.mtime);
@@ -54396,7 +54562,7 @@ function recordTelemetry(cwd, report, results, config, inventory) {
54396
54562
  ...inventory ? { inventory: buildInventorySummary(inventory) } : {}
54397
54563
  };
54398
54564
  const path = telemetryPath2(cwd);
54399
- const dir = dirname11(path);
54565
+ const dir = dirname10(path);
54400
54566
  if (!existsSync15(dir)) {
54401
54567
  mkdirSync5(dir, { recursive: true });
54402
54568
  }
@@ -54424,7 +54590,7 @@ __export(structure_md_exports, {
54424
54590
  writeStructureMarkdown: () => writeStructureMarkdown
54425
54591
  });
54426
54592
  import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync20, writeFileSync as writeFileSync6 } from "fs";
54427
- import { dirname as dirname12, join as join18 } from "path";
54593
+ import { dirname as dirname11, join as join18 } from "path";
54428
54594
  function formatDuration(ms) {
54429
54595
  if (!Number.isFinite(ms) || ms < 0) return "unknown";
54430
54596
  if (ms < 1e3) return `${Math.round(ms)}ms`;
@@ -54568,7 +54734,7 @@ async function writeStructureMarkdown(workspaceDir, md) {
54568
54734
  await new Promise((resolve43, reject) => {
54569
54735
  try {
54570
54736
  const path = join18(workspaceDir, STRUCTURE_MD_FILE);
54571
- mkdirSync6(dirname12(path), { recursive: true });
54737
+ mkdirSync6(dirname11(path), { recursive: true });
54572
54738
  writeFileSync6(path, md, "utf-8");
54573
54739
  resolve43();
54574
54740
  } catch (err) {
@@ -55739,11 +55905,13 @@ function extractNamedScores(report) {
55739
55905
  };
55740
55906
  }
55741
55907
  function renderThresholds(report) {
55742
- const r = report;
55743
- const score = typeof r.repositoryHealth === "number" ? r.repositoryHealth : report.repositoryHealth;
55744
- const limit = 70;
55745
- const available = typeof score === "number";
55746
- const failed = available && score < limit;
55908
+ const meanSlop = report.thresholds?.meanSlop ?? 30;
55909
+ const aiAvailable = typeof report.aiSlopScore === "number";
55910
+ const aiPassed = aiAvailable && report.aiSlopScore <= meanSlop;
55911
+ const rh = report.repositoryHealth;
55912
+ const rhAvailable = typeof rh === "number";
55913
+ const rhLimit = 70;
55914
+ const rhPassed = rhAvailable && rh >= rhLimit;
55747
55915
  return `
55748
55916
  <section class="thresholds-section">
55749
55917
  <h2>Thresholds</h2>
@@ -55757,9 +55925,14 @@ function renderThresholds(report) {
55757
55925
  </thead>
55758
55926
  <tbody>
55759
55927
  <tr>
55760
- <td>Repository Health (composite)</td>
55761
- <td>${available ? `${score.toFixed(1)} / ${limit}` : "\u2013"}</td>
55762
- <td><span class="status-badge ${thresholdStatusClass(failed)}">${available ? failed ? "fail" : "pass" : "n/a"}</span></td>
55928
+ <td>AI Slop Score (CI gate)</td>
55929
+ <td>${aiAvailable ? `${report.aiSlopScore.toFixed(1)} / ${meanSlop}` : "\u2013"}</td>
55930
+ <td><span class="status-badge ${thresholdStatusClass(!aiPassed)}">${aiAvailable ? aiPassed ? "pass" : "fail" : "n/a"}</span></td>
55931
+ </tr>
55932
+ <tr>
55933
+ <td>Repository Health (informational)</td>
55934
+ <td>${rhAvailable ? `${rh.toFixed(1)} / ${rhLimit}` : "\u2013"}</td>
55935
+ <td><span class="status-badge ${thresholdStatusClass(!rhPassed)}">${rhAvailable ? rhPassed ? "pass" : "fail" : "n/a"}</span></td>
55763
55936
  </tr>
55764
55937
  </tbody>
55765
55938
  </table>
@@ -57320,12 +57493,16 @@ async function runScan(options, explicitPaths) {
57320
57493
  files = intersectFiles(files, since, cwd);
57321
57494
  }
57322
57495
  let incrementalSummary;
57496
+ let cachePath3;
57497
+ let existingCache;
57498
+ let unchanged = [];
57323
57499
  if (options.incremental) {
57324
- const cachePath3 = options.cachePath ?? ".slopbrick-cache.json";
57325
- const existing = loadCache(cachePath3);
57326
- const { toScan, unchanged } = partitionByCache(files, existing);
57327
- files = toScan;
57328
- incrementalSummary = { skipped: unchanged.length, rescanned: toScan.length };
57500
+ cachePath3 = options.cachePath ?? ".slopbrick-cache.json";
57501
+ existingCache = loadCache(cachePath3);
57502
+ const partition = partitionByCache(files, existingCache);
57503
+ files = partition.toScan;
57504
+ unchanged = partition.unchanged;
57505
+ incrementalSummary = { skipped: unchanged.length, rescanned: partition.toScan.length };
57329
57506
  }
57330
57507
  const machineReadableStdout = !!options.json || !!options.html || options.format === "json" || options.format === "sarif" || options.format === "html";
57331
57508
  if (files.length === 0 && !options.quiet && !machineReadableStdout) {
@@ -57345,6 +57522,9 @@ async function runScan(options, explicitPaths) {
57345
57522
  logger.warn(
57346
57523
  `No source files matched in ${cwd}. Adjust --include or check your config at ${projectRoot}.`
57347
57524
  );
57525
+ logger.warn(
57526
+ "Tip: run from a directory with source files, or pass --workspace <path> (monorepos usually want `--workspace packages/<name>`)."
57527
+ );
57348
57528
  }
57349
57529
  }
57350
57530
  const configHash = hashConfig(config);
@@ -57494,6 +57674,38 @@ async function runScan(options, explicitPaths) {
57494
57674
  telemetryEnabled,
57495
57675
  machineReadableStdout
57496
57676
  });
57677
+ if (options.incremental && cachePath3 !== void 0) {
57678
+ const cachedFiles = {};
57679
+ for (const r of results) {
57680
+ try {
57681
+ cachedFiles[r.filePath] = {
57682
+ hash: computeFileHash(r.filePath),
57683
+ issueCount: r.issues.length,
57684
+ lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
57685
+ };
57686
+ } catch {
57687
+ }
57688
+ }
57689
+ for (const f of unchanged) {
57690
+ const cached = existingCache?.files[f];
57691
+ if (cached) {
57692
+ cachedFiles[f] = {
57693
+ hash: cached.hash,
57694
+ issueCount: cached.issueCount,
57695
+ lastScannedAt: cached.lastScannedAt
57696
+ };
57697
+ } else {
57698
+ }
57699
+ }
57700
+ try {
57701
+ saveCache(cachePath3, {
57702
+ version: VERSION,
57703
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
57704
+ files: cachedFiles
57705
+ });
57706
+ } catch {
57707
+ }
57708
+ }
57497
57709
  return {
57498
57710
  report,
57499
57711
  scores,
@@ -57646,13 +57858,15 @@ init_discover();
57646
57858
  init_git();
57647
57859
  init_cache();
57648
57860
  init_logger();
57861
+ init_types();
57862
+ import chalk2 from "chalk";
57649
57863
  import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync9, rmSync as rmSync2 } from "fs";
57650
- import { join as join23, dirname as dirname14 } from "path";
57864
+ import { join as join23, dirname as dirname13 } from "path";
57651
57865
  import { createInterface } from "readline";
57652
57866
 
57653
57867
  // src/rules/registry-loader.ts
57654
57868
  import { existsSync as existsSync21, mkdirSync as mkdirSync8, readFileSync as readFileSync25, writeFileSync as writeFileSync11 } from "fs";
57655
- import { dirname as dirname13, join as join21 } from "path";
57869
+ import { dirname as dirname12, join as join21 } from "path";
57656
57870
 
57657
57871
  // src/data/shadcn-registry.json
57658
57872
  var shadcn_registry_default = {
@@ -57789,7 +58003,7 @@ function cachePath2(cwd) {
57789
58003
  return join21(cwd, ".slopbrick", "cache", "registry-snapshot.json");
57790
58004
  }
57791
58005
  function ensureCacheDir(cwd) {
57792
- const dir = dirname13(cachePath2(cwd));
58006
+ const dir = dirname12(cachePath2(cwd));
57793
58007
  if (!existsSync21(dir)) {
57794
58008
  mkdirSync8(dir, { recursive: true });
57795
58009
  }
@@ -57823,7 +58037,12 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
57823
58037
  snapshot: shadcn_registry_default,
57824
58038
  ok: true,
57825
58039
  fresh: false,
57826
- message: "Using bundled snapshot (upstream URL returns 404)."
58040
+ // v0.42.0 (user-review fix): the previous message said
58041
+ // "upstream URL returns 404" which is alarming without context —
58042
+ // upstream has been dead since v0.39.0, and the bundled snapshot
58043
+ // IS the snapshot in use. Replaced with a neutral message that
58044
+ // doesn't suggest "something is wrong".
58045
+ message: "Using bundled shadcn/ui snapshot (built-in; no network fetch)."
57827
58046
  };
57828
58047
  }
57829
58048
  function copyBundledSnapshotToCache(cwd) {
@@ -57994,16 +58213,23 @@ async function runDoctor(cwd) {
57994
58213
  lines.push("slopbrick doctor\n");
57995
58214
  lines.push("Checking your setup:\n");
57996
58215
  const nodeMajor = parseInt(process.versions.node.split(".")[0] ?? "0", 10);
57997
- if (nodeMajor >= 20) ok(`Node ${process.versions.node}`);
58216
+ if (nodeMajor >= 20) ok(`Node ${process.versions.node}, slopbrick ${VERSION}`);
57998
58217
  else fail(`Node ${process.versions.node} \u2014 slopbrick needs Node 20 or newer. Run: nvm install 20`);
57999
58218
  lines.push(`
58000
58219
  Working in: ${cwd}`);
58001
58220
  try {
58221
+ const configPath = resolveConfigPath(cwd);
58002
58222
  const config = await loadConfig(cwd);
58003
- lines.push(`
58004
- \u2713 Config loaded from: ${resolveConfigPath(cwd) ?? "(default)"}`);
58005
- const ruleCount = Object.keys(config.rules ?? {}).length;
58006
- lines.push(` ${ruleCount} rules configured in your config file.`);
58223
+ if (configPath) {
58224
+ const ruleCount = Object.keys(config.rules ?? {}).length;
58225
+ lines.push(`
58226
+ \u2713 Config loaded from: ${configPath}`);
58227
+ lines.push(` ${ruleCount} rule${ruleCount === 1 ? "" : "s"} configured in your config file.`);
58228
+ } else {
58229
+ lines.push(`
58230
+ \u2713 Config: using built-in defaults (no config file found).`);
58231
+ lines.push(chalk2.dim(` Run \`slopbrick init\` to create ${cwd.endsWith("/") ? cwd : cwd + "/"}slopbrick.config.mjs and tune rule severity/rules to your project.`));
58232
+ }
58007
58233
  } catch (error) {
58008
58234
  fail(`Could not read your config: ${error.message}`);
58009
58235
  exitCode = 3;
@@ -58018,7 +58244,7 @@ async function runDoctor(cwd) {
58018
58244
  try {
58019
58245
  const { parseFile: tryParse } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
58020
58246
  const testFile = join23(cwd, ".slopbrick", ".doctor-test.ts");
58021
- mkdirSync9(dirname14(testFile), { recursive: true });
58247
+ mkdirSync9(dirname13(testFile), { recursive: true });
58022
58248
  writeFileSync12(testFile, "export const x = 1;\n");
58023
58249
  await tryParse(testFile);
58024
58250
  rmSync2(testFile, { force: true });
@@ -58060,6 +58286,7 @@ async function runDoctor(cwd) {
58060
58286
  ok(`Found ${scannable.length} source files to scan.`);
58061
58287
  } else {
58062
58288
  warn("No source files (.ts/.tsx/.js/.jsx/.vue/.svelte/.astro/.mdx) found in this directory.");
58289
+ lines.push(chalk2.dim(" Run from a directory with source files, or pass --workspace <path>. Monorepos usually want `--workspace packages/<name>`."));
58063
58290
  }
58064
58291
  } catch {
58065
58292
  }
@@ -58124,7 +58351,7 @@ function registerBadge(program) {
58124
58351
  const health = loadHealth2(cwd);
58125
58352
  if (health) {
58126
58353
  const synthetic = {
58127
- slopIndex: 100 - health.repositoryHealth
58354
+ aiSlopScore: health.aiSlopScore ?? 0
58128
58355
  };
58129
58356
  logger.info(formatBadge(synthetic));
58130
58357
  process.exit(0);
@@ -58454,17 +58681,17 @@ function round3(n) {
58454
58681
  }
58455
58682
 
58456
58683
  // src/rules/composite-loader.ts
58457
- import { existsSync as existsSync23, readFileSync as readFileSync27, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
58458
- import { dirname as dirname15, join as join24 } from "path";
58684
+ import { existsSync as existsSync22, readFileSync as readFileSync26, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
58685
+ import { dirname as dirname14, join as join24 } from "path";
58459
58686
  var COMPOSITES_FILE = "composites.json";
58460
58687
  function compositesPath(cwd) {
58461
58688
  return join24(cwd, COMPOSITES_FILE);
58462
58689
  }
58463
58690
  function readComposites(cwd) {
58464
58691
  const path = compositesPath(cwd);
58465
- if (!existsSync23(path)) return [];
58692
+ if (!existsSync22(path)) return [];
58466
58693
  try {
58467
- const raw = JSON.parse(readFileSync27(path, "utf-8"));
58694
+ const raw = JSON.parse(readFileSync26(path, "utf-8"));
58468
58695
  if (!Array.isArray(raw)) return [];
58469
58696
  return raw.filter(isCompositeRuleEntry);
58470
58697
  } catch {
@@ -58478,7 +58705,7 @@ function isCompositeRuleEntry(value) {
58478
58705
  }
58479
58706
  function writeComposites(cwd, entries) {
58480
58707
  const path = compositesPath(cwd);
58481
- mkdirSync10(dirname15(path), { recursive: true });
58708
+ mkdirSync10(dirname14(path), { recursive: true });
58482
58709
  const tmp = `${path}.tmp`;
58483
58710
  writeFileSync13(tmp, JSON.stringify(entries, null, 2), "utf-8");
58484
58711
  const fs = __require("fs");
@@ -58668,7 +58895,11 @@ init_logger();
58668
58895
  init_builtins();
58669
58896
  init_data();
58670
58897
  function registerExplain(program) {
58671
- program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
58898
+ program.command("explain [ruleId]").description("Print rationale, pattern, and remediation for a single rule. With no ruleId, lists the rules.").action((ruleId) => {
58899
+ if (!ruleId) {
58900
+ logger.warn("Missing ruleId. Run `slopbrick rules` to see available rules, then `slopbrick explain <ruleId>`.");
58901
+ process.exit(2);
58902
+ }
58672
58903
  const result = explainRule(ruleId, builtinRules, RULE_HINTS);
58673
58904
  logger.info(formatExplain(result));
58674
58905
  if ("error" in result) process.exit(2);
@@ -58676,7 +58907,7 @@ function registerExplain(program) {
58676
58907
  }
58677
58908
 
58678
58909
  // src/cli/commands/calibration.ts
58679
- import { dirname as dirname16 } from "path";
58910
+ import { dirname as dirname15 } from "path";
58680
58911
  import { fileURLToPath as fileURLToPath2 } from "url";
58681
58912
 
58682
58913
  // src/rules/signal-strength.json
@@ -62269,7 +62500,7 @@ var signal_strength_default2 = {
62269
62500
  };
62270
62501
 
62271
62502
  // src/cli/commands/calibration.ts
62272
- var __dirname = dirname16(fileURLToPath2(import.meta.url));
62503
+ var __dirname = dirname15(fileURLToPath2(import.meta.url));
62273
62504
  function loadData() {
62274
62505
  return signal_strength_default2;
62275
62506
  }
@@ -62397,12 +62628,12 @@ import { resolve as resolve16 } from "path";
62397
62628
  // src/cli/installer.ts
62398
62629
  import {
62399
62630
  chmodSync,
62400
- existsSync as existsSync24,
62631
+ existsSync as existsSync23,
62401
62632
  mkdirSync as mkdirSync11,
62402
- readFileSync as readFileSync28,
62633
+ readFileSync as readFileSync27,
62403
62634
  writeFileSync as writeFileSync14
62404
62635
  } from "fs";
62405
- import { dirname as dirname17, join as join26 } from "path";
62636
+ import { dirname as dirname16, join as join25 } from "path";
62406
62637
  var BEGIN_SENTINEL = "# slopbrick-hook-begin";
62407
62638
  var END_SENTINEL = "# slopbrick-hook-end";
62408
62639
  var SENTINEL_BLOCK = `${BEGIN_SENTINEL}
@@ -62410,14 +62641,14 @@ npx slopbrick --staged
62410
62641
  ${END_SENTINEL}
62411
62642
  `;
62412
62643
  function hookPath(gitRoot) {
62413
- const huskyDir = join26(gitRoot, ".husky");
62414
- if (existsSync24(huskyDir)) {
62415
- return join26(huskyDir, "pre-commit");
62644
+ const huskyDir = join25(gitRoot, ".husky");
62645
+ if (existsSync23(huskyDir)) {
62646
+ return join25(huskyDir, "pre-commit");
62416
62647
  }
62417
- return join26(gitRoot, ".git", "hooks", "pre-commit");
62648
+ return join25(gitRoot, ".git", "hooks", "pre-commit");
62418
62649
  }
62419
62650
  function readHookContent(path) {
62420
- return readFileSync28(path, "utf8");
62651
+ return readFileSync27(path, "utf8");
62421
62652
  }
62422
62653
  function sentinelsPresent(content) {
62423
62654
  const lines = content.split(/\r?\n/);
@@ -62439,7 +62670,7 @@ function replaceSentinelBlock(content) {
62439
62670
  }
62440
62671
  function installHook(gitRoot) {
62441
62672
  const path = hookPath(gitRoot);
62442
- if (existsSync24(path)) {
62673
+ if (existsSync23(path)) {
62443
62674
  const content = readHookContent(path);
62444
62675
  const { begin, end } = sentinelsPresent(content);
62445
62676
  if (begin && end) {
@@ -62479,7 +62710,7 @@ function installHook(gitRoot) {
62479
62710
  exitCode: 0
62480
62711
  };
62481
62712
  }
62482
- mkdirSync11(dirname17(path), { recursive: true });
62713
+ mkdirSync11(dirname16(path), { recursive: true });
62483
62714
  writeFileSync14(path, SENTINEL_BLOCK, { mode: 493 });
62484
62715
  chmodSync(path, 493);
62485
62716
  return {
@@ -62490,7 +62721,7 @@ function installHook(gitRoot) {
62490
62721
  }
62491
62722
  function uninstallHook(gitRoot) {
62492
62723
  const path = hookPath(gitRoot);
62493
- if (!existsSync24(path)) {
62724
+ if (!existsSync23(path)) {
62494
62725
  return {
62495
62726
  ok: true,
62496
62727
  message: "Hook not installed",
@@ -62588,12 +62819,12 @@ init_patterns();
62588
62819
  init_architecture_score();
62589
62820
  init_business_logic();
62590
62821
  init_structure_md();
62591
- import { readFileSync as readFileSync29 } from "fs";
62822
+ import { readFileSync as readFileSync28 } from "fs";
62592
62823
  import { resolve as resolve18 } from "path";
62593
62824
  var TOOL_DEFINITIONS = [
62594
62825
  {
62595
62826
  name: "slop_scan_file",
62596
- description: "Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, severity, line, column, message, advice) and the file-level Slop Index.",
62827
+ description: 'Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, category, severity, line, column, message, advice), a composite AI-likelihood score (probability + confidenceTier), and a componentCount. The composite score is the Bayesian log-likelihood ratio of all rules that fired, NOT a per-file "Slop Index" \u2014 for project-level scores use slop_suggest.',
62597
62828
  inputSchema: {
62598
62829
  type: "object",
62599
62830
  properties: {
@@ -62609,7 +62840,7 @@ var TOOL_DEFINITIONS = [
62609
62840
  },
62610
62841
  {
62611
62842
  name: "slop_explain_rule",
62612
- description: "Return metadata for a single rule (id, category, severity, aiSpecific) plus a rationale and the recommended fix. Use this before auto-applying --fix to understand what the rule catches.",
62843
+ description: "Return metadata for a single rule: ruleId, category, severity, aiSpecific, a brief rationale string, and a whereToLook path into src/rules/. The actual fix-steps live in the rule source (whereToLook) rather than in this response \u2014 read that file to understand the rule before auto-applying --fix.",
62613
62844
  inputSchema: {
62614
62845
  type: "object",
62615
62846
  properties: {
@@ -62630,7 +62861,7 @@ var TOOL_DEFINITIONS = [
62630
62861
  },
62631
62862
  {
62632
62863
  name: "slop_suggest",
62633
- description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), top issues by rule, hot files by issue count, and the composite Repository Health score. Call this BEFORE writing new code so the agent reuses existing patterns instead of duplicating them.",
62864
+ description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), the declared stack, and (when .slopbrick/health.json exists) a Bayesian composite AI-likelihood score. Call this BEFORE writing new code so the agent reuses existing patterns instead of duplicating them. For per-issue details or per-file hot-spots, use slop_scan_file on each candidate path.",
62634
62865
  inputSchema: {
62635
62866
  type: "object",
62636
62867
  properties: {
@@ -62662,7 +62893,7 @@ var TOOL_DEFINITIONS = [
62662
62893
  // call slop_suggest and read repositoryHealth /
62663
62894
  // architectureConsistency / businessLogicCoherence.
62664
62895
  name: "slop_check_constitution",
62665
- description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns a list of imports that violate declared values or hit the deny-list. Use this on a newly-written or modified file before suggesting a PR.",
62896
+ description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns the file path, total import + violation counts, the parsed imports, the list of violations (each with import, category, and reason), and a conventionSource indicating whether the constitution was declared, detected, or absent. Use this on a newly-written or modified file before suggesting a PR.",
62666
62897
  inputSchema: {
62667
62898
  type: "object",
62668
62899
  properties: {
@@ -62679,7 +62910,7 @@ var TOOL_DEFINITIONS = [
62679
62910
  // (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
62680
62911
  // — no LLM, no embeddings, deterministic. Foundation for StackPick.
62681
62912
  name: "slop_find_similar",
62682
- description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns top-k matches with name, file, line, fingerprint, and similarity score in [0, 1].",
62913
+ description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns up to `limit` matches (default 10) with name, file, line, fingerprint (sha256 over signature), hooks, props, params, and similarity in [0, 1].",
62683
62914
  inputSchema: {
62684
62915
  type: "object",
62685
62916
  properties: {
@@ -62833,7 +63064,7 @@ function runCheckConstitution(args, ctx) {
62833
63064
  const absPath = resolve18(ctx.cwd, path);
62834
63065
  let source;
62835
63066
  try {
62836
- source = readFileSync29(absPath, "utf-8");
63067
+ source = readFileSync28(absPath, "utf-8");
62837
63068
  } catch (err) {
62838
63069
  return toolError(
62839
63070
  `Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -62910,13 +63141,6 @@ async function runFindSimilar(args, ctx) {
62910
63141
  }
62911
63142
  }
62912
63143
  async function handleToolCall(toolName, args, ctx) {
62913
- const deprecation = getDeprecation(toolName);
62914
- const deprecationNotice = deprecation ? {
62915
- tool: toolName,
62916
- replacedBy: deprecation.replacedBy,
62917
- removedIn: deprecation.removedIn ?? "next major",
62918
- reason: deprecation.reason
62919
- } : void 0;
62920
63144
  switch (toolName) {
62921
63145
  case "slop_scan_file":
62922
63146
  return runScanFile(args, ctx);
@@ -63124,7 +63348,7 @@ init_logger();
63124
63348
  init_dist();
63125
63349
  import { resolve as resolve21 } from "path";
63126
63350
  function registerCi(program, scanAction) {
63127
- program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if slopIndex exceeds this number", parseCount).option("--max-new-issues <n>", "exit 1 if new issues (vs .slop-audit-cache.json) exceed this number", parseCount).option("--strict-constitution", "exit 1 on any constitution violation").option("--format <pretty|json>", "output format", "json").action(
63351
+ program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if aiSlopScore exceeds this number (lower = cleaner since v0.21)", parseCount).option("--max-new-issues <n>", "exit 1 if new issues (vs .slop-audit-cache.json) exceed this number", parseCount).option("--strict-constitution", "exit 1 on any constitution violation").option("--format <pretty|json>", "output format", "json").action(
63128
63352
  async (cmdOptions, command) => {
63129
63353
  const globals = command.optsWithGlobals();
63130
63354
  const options = {
@@ -63203,44 +63427,44 @@ import { resolve as resolve23 } from "path";
63203
63427
 
63204
63428
  // src/cli/migrate.ts
63205
63429
  init_logger();
63206
- import { existsSync as existsSync25, readFileSync as readFileSync30, renameSync as renameSync4, writeFileSync as writeFileSync15 } from "fs";
63207
- import { join as join27 } from "path";
63430
+ import { existsSync as existsSync24, readFileSync as readFileSync29, renameSync as renameSync4, writeFileSync as writeFileSync15 } from "fs";
63431
+ import { join as join26 } from "path";
63208
63432
  function planMigration(workspaceDir) {
63209
63433
  const moves = [];
63210
63434
  const rewrites = [];
63211
63435
  const gitignoreEdits = [];
63212
- const oldDir = join27(workspaceDir, ".slop-audit");
63213
- const newDir = join27(workspaceDir, ".slopbrick");
63214
- if (existsSync25(oldDir)) {
63436
+ const oldDir = join26(workspaceDir, ".slop-audit");
63437
+ const newDir = join26(workspaceDir, ".slopbrick");
63438
+ if (existsSync24(oldDir)) {
63215
63439
  moves.push({ from: oldDir, to: newDir, kind: "dir" });
63216
63440
  rewrites.push({
63217
- path: join27(newDir, "inventory.json"),
63441
+ path: join26(newDir, "inventory.json"),
63218
63442
  field: "version",
63219
63443
  from: '"1"',
63220
63444
  to: '"2"'
63221
63445
  });
63222
63446
  rewrites.push({
63223
- path: join27(newDir, "constitution.json"),
63447
+ path: join26(newDir, "constitution.json"),
63224
63448
  field: "version",
63225
63449
  from: '"1"',
63226
63450
  to: '"2"'
63227
63451
  });
63228
63452
  }
63229
- const oldCache = join27(workspaceDir, ".slop-audit-cache.json");
63230
- const newCache = join27(workspaceDir, ".slopbrick-cache.json");
63231
- if (existsSync25(oldCache)) {
63453
+ const oldCache = join26(workspaceDir, ".slop-audit-cache.json");
63454
+ const newCache = join26(workspaceDir, ".slopbrick-cache.json");
63455
+ if (existsSync24(oldCache)) {
63232
63456
  moves.push({ from: oldCache, to: newCache, kind: "file" });
63233
63457
  }
63234
63458
  for (const ext of ["mjs", "cjs", "js"]) {
63235
- const oldCfg = join27(workspaceDir, `slop-audit.config.${ext}`);
63236
- const newCfg = join27(workspaceDir, `slopbrick.config.${ext}`);
63237
- if (existsSync25(oldCfg)) {
63459
+ const oldCfg = join26(workspaceDir, `slop-audit.config.${ext}`);
63460
+ const newCfg = join26(workspaceDir, `slopbrick.config.${ext}`);
63461
+ if (existsSync24(oldCfg)) {
63238
63462
  moves.push({ from: oldCfg, to: newCfg, kind: "config" });
63239
63463
  }
63240
63464
  }
63241
- const gi = join27(workspaceDir, ".gitignore");
63242
- if (existsSync25(gi)) {
63243
- const content = readFileSync30(gi, "utf-8");
63465
+ const gi = join26(workspaceDir, ".gitignore");
63466
+ if (existsSync24(gi)) {
63467
+ const content = readFileSync29(gi, "utf-8");
63244
63468
  if (content.includes(".slop-audit/")) {
63245
63469
  gitignoreEdits.push({
63246
63470
  path: gi,
@@ -63259,7 +63483,7 @@ function planMigration(workspaceDir) {
63259
63483
  return { moves, rewrites, gitignoreEdits };
63260
63484
  }
63261
63485
  function isAlreadyMigrated(workspaceDir) {
63262
- return existsSync25(join27(workspaceDir, ".slopbrick")) && !existsSync25(join27(workspaceDir, ".slop-audit"));
63486
+ return existsSync24(join26(workspaceDir, ".slopbrick")) && !existsSync24(join26(workspaceDir, ".slop-audit"));
63263
63487
  }
63264
63488
  function applyMigration(plan, options = {}) {
63265
63489
  if (options.dryRun) return;
@@ -63267,20 +63491,20 @@ function applyMigration(plan, options = {}) {
63267
63491
  renameSync4(m.from, m.to);
63268
63492
  }
63269
63493
  for (const r of plan.rewrites) {
63270
- if (!existsSync25(r.path)) continue;
63271
- const content = readFileSync30(r.path, "utf-8");
63494
+ if (!existsSync24(r.path)) continue;
63495
+ const content = readFileSync29(r.path, "utf-8");
63272
63496
  const next = content.replace(`"version": ${r.from}`, `"version": ${r.to}`);
63273
63497
  writeFileSync15(r.path, next);
63274
63498
  }
63275
63499
  for (const g of plan.gitignoreEdits) {
63276
- const content = readFileSync30(g.path, "utf-8");
63500
+ const content = readFileSync29(g.path, "utf-8");
63277
63501
  const next = content.replaceAll(g.from, g.to);
63278
63502
  writeFileSync15(g.path, next);
63279
63503
  }
63280
63504
  }
63281
63505
  function runMigrate(options) {
63282
63506
  const { workspace, dryRun = false, force = false } = options;
63283
- if (!existsSync25(workspace)) {
63507
+ if (!existsSync24(workspace)) {
63284
63508
  return {
63285
63509
  ok: false,
63286
63510
  alreadyMigrated: false,
@@ -63290,9 +63514,9 @@ function runMigrate(options) {
63290
63514
  };
63291
63515
  }
63292
63516
  const alreadyMigrated = isAlreadyMigrated(workspace);
63293
- const newDir = join27(workspace, ".slopbrick");
63294
- const oldDir = join27(workspace, ".slop-audit");
63295
- if (existsSync25(newDir) && existsSync25(oldDir) && !force) {
63517
+ const newDir = join26(workspace, ".slopbrick");
63518
+ const oldDir = join26(workspace, ".slop-audit");
63519
+ if (existsSync24(newDir) && existsSync24(oldDir) && !force) {
63296
63520
  return {
63297
63521
  ok: false,
63298
63522
  alreadyMigrated: false,
@@ -63464,7 +63688,25 @@ function registerRules(program) {
63464
63688
  const sev = r.severity.padEnd(8);
63465
63689
  const tag = r.aiSpecific ? "[AI]" : " ";
63466
63690
  lines.push(` ${sev} ${tag} ${r.id}`);
63467
- if (r.description) lines.push(` ${r.description}`);
63691
+ if (r.description) {
63692
+ const cols = process.stdout.isTTY ? process.stdout.columns ?? 100 : 100;
63693
+ const indent = " ";
63694
+ const firstCol = indent.length + 1;
63695
+ const maxWidth = Math.max(40, cols - firstCol);
63696
+ const words = r.description.split(" ");
63697
+ let line = "";
63698
+ for (const word of words) {
63699
+ if (line === "") {
63700
+ line = word;
63701
+ } else if ((line + " " + word).length > maxWidth) {
63702
+ lines.push(indent + line);
63703
+ line = word;
63704
+ } else {
63705
+ line = line + " " + word;
63706
+ }
63707
+ }
63708
+ if (line) lines.push(indent + line);
63709
+ }
63468
63710
  }
63469
63711
  }
63470
63712
  logger.info(lines.join("\n"));
@@ -63474,12 +63716,12 @@ function registerRules(program) {
63474
63716
  // src/cli/commands/validate-config.ts
63475
63717
  init_logger();
63476
63718
  init_validation();
63477
- import { existsSync as existsSync26 } from "fs";
63719
+ import { existsSync as existsSync25 } from "fs";
63478
63720
  import { extname as extname8, resolve as resolve24 } from "path";
63479
63721
  function registerValidateConfig(program) {
63480
63722
  program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
63481
63723
  const path = configPath ? resolve24(configPath) : resolve24(process.cwd(), "slopbrick.config.mjs");
63482
- if (!existsSync26(path)) {
63724
+ if (!existsSync25(path)) {
63483
63725
  logger.error(`Error: config file not found: ${path}`);
63484
63726
  process.exit(2);
63485
63727
  }
@@ -63706,7 +63948,7 @@ ${formatMarkdown(result.report)}`);
63706
63948
  // src/cli/commands/calibrate.ts
63707
63949
  init_logger();
63708
63950
  import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync18 } from "fs";
63709
- import { dirname as dirname19, resolve as resolve26 } from "path";
63951
+ import { dirname as dirname17, resolve as resolve26 } from "path";
63710
63952
 
63711
63953
  // src/research/providers/openai.ts
63712
63954
  var OpenAiProvider = class {
@@ -63762,7 +64004,7 @@ function createProvider(config) {
63762
64004
 
63763
64005
  // src/research/generator.ts
63764
64006
  import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync16 } from "fs";
63765
- import { join as join28 } from "path";
64007
+ import { join as join27 } from "path";
63766
64008
 
63767
64009
  // src/research/prompts.ts
63768
64010
  var DEFAULT_PROMPT_TEMPLATES = [
@@ -63825,13 +64067,13 @@ async function generateSamples(options) {
63825
64067
  }
63826
64068
  const samples = [];
63827
64069
  const ext = extForFramework(framework);
63828
- const dir = join28(outputDir, framework, componentType);
64070
+ const dir = join27(outputDir, framework, componentType);
63829
64071
  mkdirSync12(dir, { recursive: true });
63830
64072
  for (let i = 1; i <= count; i += 1) {
63831
64073
  const raw = await provider.generateSample(renderPrompt(template), { temperature });
63832
64074
  const code = extractCodeFromMarkdown(raw);
63833
64075
  const fileName = `sample-${i}${ext}`;
63834
- const filePath = join28(dir, fileName);
64076
+ const filePath = join27(dir, fileName);
63835
64077
  writeFileSync16(filePath, code, "utf8");
63836
64078
  const sample = {
63837
64079
  filePath,
@@ -63843,7 +64085,7 @@ async function generateSamples(options) {
63843
64085
  };
63844
64086
  samples.push(sample);
63845
64087
  }
63846
- const metadataPath = join28(dir, "metadata.json");
64088
+ const metadataPath = join27(dir, "metadata.json");
63847
64089
  writeFileSync16(metadataPath, JSON.stringify(samples, null, 2), "utf8");
63848
64090
  return samples;
63849
64091
  }
@@ -64050,46 +64292,46 @@ function slugify(value) {
64050
64292
 
64051
64293
  // src/research/calibrator.ts
64052
64294
  import { execFileSync as execFileSync2 } from "child_process";
64053
- import { existsSync as existsSync27, readFileSync as readFileSync31, writeFileSync as writeFileSync17, mkdirSync as mkdirSync13 } from "fs";
64054
- import { join as join30, resolve as resolve25 } from "path";
64295
+ import { existsSync as existsSync26, readFileSync as readFileSync30, writeFileSync as writeFileSync17, mkdirSync as mkdirSync13 } from "fs";
64296
+ import { join as join29, resolve as resolve25 } from "path";
64055
64297
 
64056
64298
  // src/corpus-paths.ts
64057
- import { join as join29 } from "path";
64299
+ import { join as join28 } from "path";
64058
64300
  var CORPUS_ROOT = process.env["SLOPBRICK_CORPUS_DIR"] ?? "/Users/cheng/corpus-expansion";
64059
- var POSITIVE_DIR = join29(CORPUS_ROOT, "positive");
64060
- var NEGATIVE_DIR = join29(CORPUS_ROOT, "negative");
64061
- var FILELISTS_DIR = join29(CORPUS_ROOT, "filelists");
64301
+ var POSITIVE_DIR = join28(CORPUS_ROOT, "positive");
64302
+ var NEGATIVE_DIR = join28(CORPUS_ROOT, "negative");
64303
+ var FILELISTS_DIR = join28(CORPUS_ROOT, "filelists");
64062
64304
 
64063
64305
  // src/research/calibrator.ts
64064
64306
  var DEFAULT_POSITIVE = POSITIVE_DIR;
64065
64307
  var DEFAULT_NEGATIVE = NEGATIVE_DIR;
64066
64308
  function buildFileList(dir, extensions) {
64067
- const tmpList = join30("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
64309
+ const tmpList = join29("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
64068
64310
  const expr = extensions.map((e) => `-name '*.${e}'`).join(" -o ");
64069
64311
  execFileSync2("bash", ["-c", `find ${dir} -maxdepth 8 -type f \\( ${expr} \\) -print0 | xargs -0 realpath > ${tmpList}`]);
64070
- const out = readFileSync31(tmpList, "utf8");
64312
+ const out = readFileSync30(tmpList, "utf8");
64071
64313
  execFileSync2("rm", ["-f", tmpList]);
64072
64314
  return out.trim().split("\n").filter(Boolean);
64073
64315
  }
64074
64316
  function runScan2(fileListPath) {
64075
- const files = readFileSync31(fileListPath, "utf8").trim().split("\n").filter(Boolean);
64317
+ const files = readFileSync30(fileListPath, "utf8").trim().split("\n").filter(Boolean);
64076
64318
  const CHUNK = 600;
64077
64319
  const ruleFires = /* @__PURE__ */ new Map();
64078
64320
  const uniqueFilesPerRule = /* @__PURE__ */ new Map();
64079
64321
  let fileCount = 0;
64080
- const tmpOut = join30("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
64322
+ const tmpOut = join29("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
64081
64323
  for (let i = 0; i < files.length; i += CHUNK) {
64082
64324
  const chunk = files.slice(i, i + CHUNK);
64083
64325
  try {
64084
64326
  execFileSync2(
64085
64327
  "node",
64086
- [join30(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
64328
+ [join29(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
64087
64329
  { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
64088
64330
  );
64089
64331
  } catch {
64090
64332
  }
64091
- if (!existsSync27(tmpOut)) continue;
64092
- const report = JSON.parse(readFileSync31(tmpOut, "utf8"));
64333
+ if (!existsSync26(tmpOut)) continue;
64334
+ const report = JSON.parse(readFileSync30(tmpOut, "utf8"));
64093
64335
  fileCount += report.fileCount;
64094
64336
  for (const issue of report.issues) {
64095
64337
  ruleFires.set(issue.ruleId, (ruleFires.get(issue.ruleId) ?? 0) + 1);
@@ -64117,14 +64359,14 @@ function classify(posFiles, negFiles) {
64117
64359
  async function calibrate(cwd, options = {}) {
64118
64360
  const positiveDir = options.positiveDir ?? DEFAULT_POSITIVE;
64119
64361
  const negativeDir = options.negativeDir ?? DEFAULT_NEGATIVE;
64120
- if (!existsSync27(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
64121
- if (!existsSync27(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
64362
+ if (!existsSync26(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
64363
+ if (!existsSync26(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
64122
64364
  const positiveFiles = buildFileList(positiveDir, ["tsx", "ts", "jsx", "js"]);
64123
64365
  const negativeFiles = buildFileList(negativeDir, ["tsx", "ts"]);
64124
64366
  const posSample = options.positiveLimit ? positiveFiles.slice(0, options.positiveLimit) : positiveFiles;
64125
64367
  const negSample = options.negativeLimit ? negativeFiles.slice(0, options.negativeLimit) : negativeFiles;
64126
- const posListPath = join30("/tmp", `cal-pos-${Date.now()}.txt`);
64127
- const negListPath = join30("/tmp", `cal-neg-${Date.now()}.txt`);
64368
+ const posListPath = join29("/tmp", `cal-pos-${Date.now()}.txt`);
64369
+ const negListPath = join29("/tmp", `cal-neg-${Date.now()}.txt`);
64128
64370
  writeFileSync17(posListPath, posSample.join("\n"));
64129
64371
  writeFileSync17(negListPath, negSample.join("\n"));
64130
64372
  const builtins = await Promise.resolve().then(() => (init_builtins(), builtins_exports));
@@ -64241,7 +64483,7 @@ function registerCalibrate(program) {
64241
64483
  negativeLimit: cmdOptions.negativeLimit
64242
64484
  });
64243
64485
  const outputPath = cmdOptions.output ? resolve26(cwd, cmdOptions.output) : resolve26(cwd, "corpus", "calibration-empirical.md");
64244
- mkdirSync14(dirname19(outputPath), { recursive: true });
64486
+ mkdirSync14(dirname17(outputPath), { recursive: true });
64245
64487
  writeFileSync18(outputPath, reportToMarkdown(report), "utf8");
64246
64488
  logger.info(
64247
64489
  "Calibrated " + report.rules.length + " rules across " + report.positiveFileCount + " positive + " + report.negativeFileCount + " negative files."
@@ -64309,19 +64551,20 @@ function trendToText(report) {
64309
64551
  }
64310
64552
  const slopValues = report.points.map((p) => p.slopIndex);
64311
64553
  const spark = sparkline(slopValues);
64312
- lines.push("Slop Index trend (last " + report.points.length + " scans, " + report.totalScans + " total):");
64554
+ lines.push("AI Slop Score trend (last " + report.points.length + " scans, " + report.totalScans + " total; lower = cleaner since v0.21):");
64313
64555
  lines.push("");
64314
64556
  lines.push(" " + spark + " " + slopValues[0].toFixed(1) + " -> " + slopValues[slopValues.length - 1].toFixed(1));
64315
64557
  lines.push("");
64316
64558
  if (report.first && report.latest) {
64317
64559
  const direction = report.delta < 0 ? "improved" : report.delta > 0 ? "regressed" : "unchanged";
64318
64560
  const arrow = report.delta < 0 ? "v" : report.delta > 0 ? "^" : "=";
64319
- lines.push(" " + arrow + " Slop Index " + direction + " by " + Math.abs(report.delta).toFixed(1) + " points");
64561
+ lines.push(" " + arrow + " AI Slop Score " + direction + " by " + Math.abs(report.delta).toFixed(1) + " points (lower = cleaner since v0.21)");
64320
64562
  lines.push(" " + report.first.timestamp + " -> " + report.latest.timestamp);
64321
64563
  }
64322
64564
  lines.push("");
64323
64565
  lines.push("Recent scans:");
64324
- lines.push(" Timestamp Slop Health Framework Issues");
64566
+ lines.push(" Timestamp AI Slop Health Framework Issues");
64567
+ lines.push(" (lower=cleaner)");
64325
64568
  lines.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500");
64326
64569
  for (const p of report.points.slice(-10)) {
64327
64570
  const ts = p.timestamp.slice(0, 19).replace("T", " ");
@@ -64333,14 +64576,14 @@ function trendToText(report) {
64333
64576
  }
64334
64577
  function trendToMarkdown(report) {
64335
64578
  const lines = [];
64336
- lines.push("# Slop Index Trend");
64579
+ lines.push("# AI Slop Score Trend (lower = cleaner since v0.21)");
64337
64580
  lines.push("");
64338
64581
  if (report.points.length === 0) {
64339
64582
  lines.push("No telemetry data yet. Run `slopbrick scan` to start tracking.");
64340
64583
  return lines.join("\n");
64341
64584
  }
64342
64585
  const slopValues = report.points.map((p) => p.slopIndex);
64343
- lines.push("Slop Index sparkline (last " + report.points.length + " scans):");
64586
+ lines.push("AI Slop Score sparkline (last " + report.points.length + " scans; lower = cleaner since v0.21):");
64344
64587
  lines.push("");
64345
64588
  lines.push("```");
64346
64589
  lines.push(sparkline(slopValues));
@@ -64348,10 +64591,10 @@ function trendToMarkdown(report) {
64348
64591
  lines.push("");
64349
64592
  if (report.first && report.latest) {
64350
64593
  const direction = report.delta < 0 ? "improved" : report.delta > 0 ? "regressed" : "unchanged";
64351
- lines.push("Slop Index " + direction + " by **" + Math.abs(report.delta).toFixed(1) + " points** between first and latest scan.");
64594
+ lines.push("AI Slop Score " + direction + " by **" + Math.abs(report.delta).toFixed(1) + " points** between first and latest scan (lower = cleaner since v0.21).");
64352
64595
  }
64353
64596
  lines.push("");
64354
- lines.push("| Timestamp | Slop Index | Health | Framework | Issues |");
64597
+ lines.push("| Timestamp | AI Slop Score (lower = cleaner) | Health | Framework | Issues |");
64355
64598
  lines.push("|-----------|-----------:|-------:|-----------|-------:|");
64356
64599
  for (const p of report.points) {
64357
64600
  lines.push("| " + p.timestamp + " | " + p.slopIndex.toFixed(1) + " | " + p.assemblyHealth.toFixed(1) + " | " + p.framework + " | " + p.totalIssues + " |");
@@ -64361,7 +64604,7 @@ function trendToMarkdown(report) {
64361
64604
 
64362
64605
  // src/cli/commands/trend.ts
64363
64606
  function registerTrend(program) {
64364
- program.command("trend").description("Slop Index over time, from .slopbrick/flywheel/scans.jsonl").option("--max-points <n>", "how many recent scans to plot", parseCount, 30).option("--render <kind>", "output rendering: text | markdown", "text").action((cmdOptions) => {
64607
+ program.command("trend").description("AI Slop Score over time, from .slopbrick/flywheel/scans.jsonl (lower = cleaner since v0.21)").option("--max-points <n>", "how many recent scans to plot", parseCount, 30).option("--render <kind>", "output rendering: text | markdown", "text").action((cmdOptions) => {
64365
64608
  try {
64366
64609
  const cwd = process.cwd();
64367
64610
  const report = buildTrend(cwd, cmdOptions.maxPoints);
@@ -64369,7 +64612,14 @@ function registerTrend(program) {
64369
64612
  logger.info(out);
64370
64613
  if (report.delta > 1) {
64371
64614
  logger.info("");
64372
- logger.info("Warning: Slop Index increased by " + report.delta.toFixed(1) + " points over this period.");
64615
+ logger.info(
64616
+ // v0.42.0: the legacy 'Slop Index increased' message implied higher
64617
+ // = worse, which is the v0.15-v0.20.1 inverted reading. Since v0.21,
64618
+ // AI Slop Score is lower-is-better; an INCREASE is worse, but
64619
+ // the warning should say "AI Slop Score got worse" or just
64620
+ // "delta went up" so users don't have to remember the direction.
64621
+ `Warning: AI Slop Score got worse by ${report.delta.toFixed(1)} points over this period (lower = cleaner since v0.21).`
64622
+ );
64373
64623
  }
64374
64624
  } catch (error) {
64375
64625
  logger.error(error instanceof Error ? error.message : String(error));
@@ -64387,7 +64637,7 @@ import { resolve as resolve27 } from "path";
64387
64637
  init_discover();
64388
64638
  init_patterns();
64389
64639
  init_telemetry();
64390
- import { readFileSync as readFileSync32 } from "fs";
64640
+ import { readFileSync as readFileSync31 } from "fs";
64391
64641
  import { basename as basename4, relative as relative16 } from "path";
64392
64642
  async function runDrift(cwd, config, options = {}) {
64393
64643
  const maxFiles = options.maxFiles ?? 1e3;
@@ -64399,7 +64649,7 @@ async function runDrift(cwd, config, options = {}) {
64399
64649
  for (const absPath of limited) {
64400
64650
  let source;
64401
64651
  try {
64402
- source = readFileSync32(absPath, "utf-8");
64652
+ source = readFileSync31(absPath, "utf-8");
64403
64653
  } catch {
64404
64654
  continue;
64405
64655
  }
@@ -64722,7 +64972,7 @@ init_worker();
64722
64972
  init_metrics();
64723
64973
  init_patterns();
64724
64974
  init_discover();
64725
- import { readFileSync as readFileSync33 } from "fs";
64975
+ import { readFileSync as readFileSync32 } from "fs";
64726
64976
  import { extname as extname9, relative as relative17, resolve as resolve28 } from "path";
64727
64977
  import { execFile as execFileCb } from "child_process";
64728
64978
  import { promisify as promisify2 } from "util";
@@ -64795,7 +65045,7 @@ async function runPrScan(cwd, config, options = {}) {
64795
65045
  for (const absPath of candidates) {
64796
65046
  let source;
64797
65047
  try {
64798
- source = readFileSync33(absPath, "utf-8");
65048
+ source = readFileSync32(absPath, "utf-8");
64799
65049
  } catch {
64800
65050
  continue;
64801
65051
  }
@@ -64987,13 +65237,13 @@ function parseUnifiedDiffPaths(diff) {
64987
65237
  async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
64988
65238
  const threshold = options.threshold ?? config.prScoreThreshold ?? 20;
64989
65239
  const maxFiles = options.maxFiles ?? 500;
64990
- const { existsSync: existsSync32, readFileSync: readFileSync42 } = await import("fs");
65240
+ const { existsSync: existsSync31, readFileSync: readFileSync41 } = await import("fs");
64991
65241
  const { isAbsolute: isAbsolute4, resolve: resolvePath } = await import("path");
64992
- if (!existsSync32(diffPath)) {
65242
+ if (!existsSync31(diffPath)) {
64993
65243
  throw new Error(`Diff file not found: ${diffPath}`);
64994
65244
  }
64995
65245
  const diffAbs = isAbsolute4(diffPath) ? diffPath : resolvePath(cwd, diffPath);
64996
- const diff = readFileSync42(diffAbs, "utf-8");
65246
+ const diff = readFileSync41(diffAbs, "utf-8");
64997
65247
  const changedPaths = parseUnifiedDiffPaths(diff);
64998
65248
  if (changedPaths.length === 0) {
64999
65249
  return emptyPrResult(threshold);
@@ -65001,7 +65251,7 @@ async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
65001
65251
  return scoreChangedPaths(cwd, config, changedPaths, maxFiles, threshold);
65002
65252
  }
65003
65253
  async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
65004
- const { existsSync: existsSync32, statSync: statSync8 } = await import("fs");
65254
+ const { existsSync: existsSync31, statSync: statSync8 } = await import("fs");
65005
65255
  const { resolve: resolvePath } = await import("path");
65006
65256
  const { readFile: readFile4 } = await import("fs/promises");
65007
65257
  const { scanFile: scanFile2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
@@ -65013,7 +65263,7 @@ async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
65013
65263
  let constitutionViolations = 0;
65014
65264
  for (const rel of limited) {
65015
65265
  const abs = resolvePath(cwd, rel);
65016
- if (!existsSync32(abs) || !statSync8(abs).isFile()) continue;
65266
+ if (!existsSync31(abs) || !statSync8(abs).isFile()) continue;
65017
65267
  let source;
65018
65268
  try {
65019
65269
  source = await readFile4(abs, "utf-8");
@@ -65191,7 +65441,7 @@ var TEST_INCLUDE_GLOBS = [
65191
65441
  "**/__tests__/**/*.{ts,tsx,js,jsx}",
65192
65442
  "**/__fixtures__/**/*.{ts,tsx,js,jsx,json}"
65193
65443
  ];
65194
- async function runTestScan(cwd, config, options = {}) {
65444
+ async function runTestScan(cwd, _config, options = {}) {
65195
65445
  const include = TEST_INCLUDE_GLOBS;
65196
65446
  const cliOptions = {
65197
65447
  include,
@@ -65329,7 +65579,7 @@ import { resolve as resolve34 } from "path";
65329
65579
  // src/cli/business-logic.ts
65330
65580
  init_discover();
65331
65581
  init_business_logic();
65332
- import { readFileSync as readFileSync34 } from "fs";
65582
+ import { readFileSync as readFileSync33 } from "fs";
65333
65583
  import { relative as relative18 } from "path";
65334
65584
  async function runBusinessLogicScan(cwd, config, options = {}) {
65335
65585
  const maxFiles = options.maxFiles ?? 500;
@@ -65340,7 +65590,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
65340
65590
  for (const absPath of limited) {
65341
65591
  let source;
65342
65592
  try {
65343
- source = readFileSync34(absPath, "utf-8");
65593
+ source = readFileSync33(absPath, "utf-8");
65344
65594
  } catch {
65345
65595
  continue;
65346
65596
  }
@@ -65910,7 +66160,7 @@ import { resolve as resolve38 } from "path";
65910
66160
  // src/engine/patterns.ts
65911
66161
  init_patterns();
65912
66162
  init_discover();
65913
- import { readFileSync as readFileSync35 } from "fs";
66163
+ import { readFileSync as readFileSync34 } from "fs";
65914
66164
  import { basename as basename5, relative as relative19 } from "path";
65915
66165
  var PATTERN_CATEGORIES = [
65916
66166
  "modal",
@@ -66013,7 +66263,7 @@ function detectFormsFromFiles(files) {
66013
66263
  for (const f of files) {
66014
66264
  let source;
66015
66265
  try {
66016
- source = readFileSync35(f, "utf-8");
66266
+ source = readFileSync34(f, "utf-8");
66017
66267
  } catch {
66018
66268
  continue;
66019
66269
  }
@@ -66140,7 +66390,7 @@ function formatText2(report) {
66140
66390
  const label = CATEGORY_LABELS2[cat].padEnd(20);
66141
66391
  const count = String(stats.count).padStart(2);
66142
66392
  const baseline = String(stats.baseline).padStart(1);
66143
- const excess = stats.excess > 0 ? `\u26A0 +${stats.excess} over` : "\u2713 clean";
66393
+ const excess = stats.excess > 0 ? `\u26A0 +${stats.excess} over` : stats.count === 0 ? "\u2713 none" : "\u2713 within baseline";
66144
66394
  lines.push(` ${label} ${count} implementations (baseline ${baseline}) ${excess}`);
66145
66395
  }
66146
66396
  if (report.doNotCreate.length > 0) {
@@ -66198,7 +66448,7 @@ function formatMarkdown3(report) {
66198
66448
  for (const cat of PATTERN_CATEGORIES) {
66199
66449
  const stats = report.byCategory[cat];
66200
66450
  const label = CATEGORY_LABELS2[cat];
66201
- const excess = stats.excess > 0 ? `+${stats.excess}` : "\u2014";
66451
+ const excess = stats.excess > 0 ? `+${stats.excess}` : stats.count === 0 ? "\u2014" : "\xB7";
66202
66452
  lines.push(`| ${label} | ${stats.count} | ${stats.baseline} | ${excess} |`);
66203
66453
  }
66204
66454
  if (report.doNotCreate.length > 0) {
@@ -66253,8 +66503,8 @@ function registerPatterns(program) {
66253
66503
  // src/cli/commands/research.ts
66254
66504
  init_logger();
66255
66505
  init_config2();
66256
- import { existsSync as existsSync28, mkdirSync as mkdirSync15, readFileSync as readFileSync36, writeFileSync as writeFileSync19 } from "fs";
66257
- import { dirname as dirname20, resolve as resolve39 } from "path";
66506
+ import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync35, writeFileSync as writeFileSync19 } from "fs";
66507
+ import { dirname as dirname18, resolve as resolve39 } from "path";
66258
66508
  function registerResearch(program) {
66259
66509
  const research = program.command("research").description("research commands for the AI UI learning loop");
66260
66510
  research.command("generate").description("generate synthetic UI samples").requiredOption("--count <n>", "number of samples", parseCount).requiredOption("--framework <name>", "target framework").requiredOption("--component-type <type>", "component type").requiredOption("--provider <name>", "AI provider (openai)").option("--api-key <key>", "API key for provider").option("--model <name>", "model name").option("--temperature <n>", "sampling temperature", parseFloat, 0.7).option("--output-dir <path>", "output directory", ".slopbrick/corpus/generated").action(async (cmdOptions) => {
@@ -66277,15 +66527,15 @@ function registerResearch(program) {
66277
66527
  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) => {
66278
66528
  try {
66279
66529
  const metadataPath = resolve39(cmdOptions.inputDir, "metadata.json");
66280
- if (!existsSync28(metadataPath)) {
66530
+ if (!existsSync27(metadataPath)) {
66281
66531
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
66282
66532
  process.exit(2);
66283
66533
  }
66284
- const samples = JSON.parse(readFileSync36(metadataPath, "utf8"));
66534
+ const samples = JSON.parse(readFileSync35(metadataPath, "utf8"));
66285
66535
  const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
66286
66536
  const analysis = await analyzeSamples(samples, config);
66287
66537
  const outputPath = resolve39(cmdOptions.output);
66288
- mkdirSync15(dirname20(outputPath), { recursive: true });
66538
+ mkdirSync15(dirname18(outputPath), { recursive: true });
66289
66539
  writeFileSync19(outputPath, JSON.stringify(analysis, null, 2), "utf8");
66290
66540
  logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
66291
66541
  logger.info(`Wrote analysis to ${outputPath}`);
@@ -66297,11 +66547,11 @@ function registerResearch(program) {
66297
66547
  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) => {
66298
66548
  try {
66299
66549
  const metadataPath = resolve39(cmdOptions.inputDir, "metadata.json");
66300
- if (!existsSync28(metadataPath)) {
66550
+ if (!existsSync27(metadataPath)) {
66301
66551
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
66302
66552
  process.exit(2);
66303
66553
  }
66304
- const samples = JSON.parse(readFileSync36(metadataPath, "utf8"));
66554
+ const samples = JSON.parse(readFileSync35(metadataPath, "utf8"));
66305
66555
  const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
66306
66556
  const analysis = await analyzeSamples(samples, config);
66307
66557
  const extraction = extractAndCluster(analysis.samples, {
@@ -66312,7 +66562,7 @@ function registerResearch(program) {
66312
66562
  minFrequency: cmdOptions.minFrequency
66313
66563
  });
66314
66564
  const outputPath = resolve39(cmdOptions.output);
66315
- mkdirSync15(dirname20(outputPath), { recursive: true });
66565
+ mkdirSync15(dirname18(outputPath), { recursive: true });
66316
66566
  const payload = {
66317
66567
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
66318
66568
  sampleCount: analysis.summary.total,
@@ -66333,8 +66583,8 @@ function registerResearch(program) {
66333
66583
  // src/cli/commands/init.ts
66334
66584
  init_logger();
66335
66585
  init_builtins();
66336
- import { existsSync as existsSync29, mkdirSync as mkdirSync16, readFileSync as readFileSync37, writeFileSync as writeFileSync20 } from "fs";
66337
- import { dirname as dirname21, join as join32, resolve as resolve40 } from "path";
66586
+ import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync36, writeFileSync as writeFileSync20 } from "fs";
66587
+ import { dirname as dirname19, join as join31, resolve as resolve40 } from "path";
66338
66588
  init_scan2();
66339
66589
  init_config2();
66340
66590
  init_threshold();
@@ -66343,7 +66593,7 @@ init_cache();
66343
66593
 
66344
66594
  // src/snippet/targets.ts
66345
66595
  init_generators();
66346
- import { join as join31 } from "path";
66596
+ import { join as join30 } from "path";
66347
66597
  var SNIPPET_TARGETS2 = [
66348
66598
  {
66349
66599
  flag: "cursor",
@@ -66437,7 +66687,7 @@ var SNIPPET_TARGETS2 = [
66437
66687
  }
66438
66688
  ];
66439
66689
  function resolveTargetPath(target) {
66440
- return target.isFolder ? join31(target.path, target.filename) : target.path;
66690
+ return target.isFolder ? join30(target.path, target.filename) : target.path;
66441
66691
  }
66442
66692
  function renderMatrix() {
66443
66693
  const lines = [];
@@ -66460,12 +66710,12 @@ function registerInit(program) {
66460
66710
  process.exit(0);
66461
66711
  }
66462
66712
  const cwd = resolve40(options.workspace ?? process.cwd());
66463
- const configPath = join32(cwd, "slopbrick.config.mjs");
66713
+ const configPath = join31(cwd, "slopbrick.config.mjs");
66464
66714
  const detected = detectStack(cwd);
66465
66715
  const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
66466
66716
  const proposed = serializeConfig(fallbackConfig);
66467
- if (existsSync29(configPath) && !cmdOptions.yes) {
66468
- const current = readFileSync37(configPath, "utf8");
66717
+ if (existsSync28(configPath) && !cmdOptions.yes) {
66718
+ const current = readFileSync36(configPath, "utf8");
66469
66719
  logger.error(`A config file already exists at ${configPath}.`);
66470
66720
  logger.error("To overwrite it with defaults, run `slopbrick init --yes`.");
66471
66721
  logger.error("");
@@ -66516,11 +66766,11 @@ function registerInit(program) {
66516
66766
  return Boolean(opts[t.flag]);
66517
66767
  });
66518
66768
  for (const target of targetsToWrite) {
66519
- const snippetPath = join32(cwd, resolveTargetPath(target));
66520
- mkdirSync16(dirname21(snippetPath), { recursive: true });
66769
+ const snippetPath = join31(cwd, resolveTargetPath(target));
66770
+ mkdirSync16(dirname19(snippetPath), { recursive: true });
66521
66771
  const generated = target.generator(builtinRules);
66522
- if (!target.isFolder && existsSync29(snippetPath)) {
66523
- const existing = readFileSync37(snippetPath, "utf8");
66772
+ if (!target.isFolder && existsSync28(snippetPath)) {
66773
+ const existing = readFileSync36(snippetPath, "utf8");
66524
66774
  if (existing.includes("<!-- slopbrick:begin -->")) {
66525
66775
  const updated = existing.replace(
66526
66776
  /<!-- slopbrick:begin -->[\s\S]*?<!-- slopbrick:end -->/,
@@ -66559,11 +66809,11 @@ function registerInit(program) {
66559
66809
  init_logger();
66560
66810
  init_telemetry();
66561
66811
  import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync21 } from "fs";
66562
- import { dirname as dirname22, resolve as resolve41 } from "path";
66812
+ import { dirname as dirname20, resolve as resolve41 } from "path";
66563
66813
 
66564
66814
  // src/report/flywheel.ts
66565
- import { existsSync as existsSync30, readFileSync as readFileSync38 } from "fs";
66566
- import { join as join33 } from "path";
66815
+ import { existsSync as existsSync29, readFileSync as readFileSync37 } from "fs";
66816
+ import { join as join32 } from "path";
66567
66817
  function average(values) {
66568
66818
  if (values.length === 0) return 0;
66569
66819
  return values.reduce((a, b) => a + b, 0) / values.length;
@@ -66623,8 +66873,9 @@ function formatFlywheel(summary, options = {}) {
66623
66873
  if (summary.firstRunAt && summary.latestRunAt) {
66624
66874
  lines.push(` Window: ${summary.firstRunAt} \u2192 ${summary.latestRunAt}`);
66625
66875
  }
66626
- lines.push(` Average slop index: ${summary.averageSlopIndex.toFixed(2)}`);
66627
- lines.push(` Latest slop index: ${summary.latestSlopIndex.toFixed(2)}`);
66876
+ const directionNote = " (lower = cleaner since v0.21.0)";
66877
+ lines.push(` Average AI Slop Score: ${summary.averageSlopIndex.toFixed(2)}`);
66878
+ lines.push(` Latest AI Slop Score: ${summary.latestSlopIndex.toFixed(2)}${directionNote}`);
66628
66879
  lines.push(` Average assembly health: ${summary.averageAssemblyHealth.toFixed(2)}`);
66629
66880
  lines.push(` Latest assembly health: ${summary.latestAssemblyHealth.toFixed(2)}`);
66630
66881
  if (summary.topViolations.length > 0) {
@@ -66662,7 +66913,7 @@ function registerFlywheel(program) {
66662
66913
  const summary = summarizeTelemetry(payloads);
66663
66914
  if (cmdOptions.export) {
66664
66915
  const exportPath = resolve41(cmdOptions.export);
66665
- mkdirSync17(dirname22(exportPath), { recursive: true });
66916
+ mkdirSync17(dirname20(exportPath), { recursive: true });
66666
66917
  writeFileSync21(exportPath, JSON.stringify(summary, null, 2), "utf-8");
66667
66918
  logger.info(`Wrote flywheel summary to ${exportPath}`);
66668
66919
  process.exit(0);
@@ -66690,8 +66941,8 @@ init_dist2();
66690
66941
  init_memory_io();
66691
66942
 
66692
66943
  // src/fix/focus-ring.ts
66693
- import { existsSync as existsSync31 } from "fs";
66694
- import { readFileSync as readFileSync39, writeFileSync as writeFileSync22 } from "fs";
66944
+ import { existsSync as existsSync30 } from "fs";
66945
+ import { readFileSync as readFileSync38, writeFileSync as writeFileSync22 } from "fs";
66695
66946
  var ANCHOR_START = "/* @slopbrick:v1.0.0:fix:focus-ring */";
66696
66947
  var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
66697
66948
  :focus-visible {
@@ -66700,10 +66951,10 @@ var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
66700
66951
  }
66701
66952
  /* @slopbrick:v1.0.0:fix:focus-ring-end */`;
66702
66953
  function applyFocusRingFix(targetFile) {
66703
- if (!existsSync31(targetFile)) {
66954
+ if (!existsSync30(targetFile)) {
66704
66955
  return { applied: false, reason: "missing-global-css-target" };
66705
66956
  }
66706
- const content = readFileSync39(targetFile, "utf-8");
66957
+ const content = readFileSync38(targetFile, "utf-8");
66707
66958
  if (content.includes(ANCHOR_START)) {
66708
66959
  return { applied: false, reason: "already-present" };
66709
66960
  }
@@ -66717,9 +66968,9 @@ function applyFocusRingFix(targetFile) {
66717
66968
  init_layout_token();
66718
66969
 
66719
66970
  // src/fix/use-client.ts
66720
- import { readFileSync as readFileSync40, writeFileSync as writeFileSync23 } from "fs";
66971
+ import { readFileSync as readFileSync39, writeFileSync as writeFileSync23 } from "fs";
66721
66972
  function applyUseClientFix(filePath) {
66722
- const content = readFileSync40(filePath, "utf-8");
66973
+ const content = readFileSync39(filePath, "utf-8");
66723
66974
  const trimmed = content.trimStart();
66724
66975
  if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) {
66725
66976
  return { applied: false, reason: "already-present" };
@@ -66731,7 +66982,7 @@ ${content}`);
66731
66982
  }
66732
66983
 
66733
66984
  // src/fix/visual-codemod.ts
66734
- import { readFileSync as readFileSync41, writeFileSync as writeFileSync24 } from "fs";
66985
+ import { readFileSync as readFileSync40, writeFileSync as writeFileSync24 } from "fs";
66735
66986
 
66736
66987
  // src/fix/visual-codemods/tailwind.ts
66737
66988
  var ARBITRARY_ESCAPE_RE = /\b(p|m|px|py|pt|pb|pl|pr|mx|my|mt|mb|ml|mr|gap|space-[xy]|w|h|min-w|max-w|min-h|max-h|text-\w+|leading-\w+|rounded|border)-?\[(-?\d+(?:\.\d+)?)(px|rem|em|%|vh|vw)?\]/g;
@@ -66940,7 +67191,7 @@ var ALL_CODEMODS = [
66940
67191
  { name: "aria-attr-typo", fn: applyAriaAttrTypoCodemod }
66941
67192
  ];
66942
67193
  function applyVisualCodemods(filePath) {
66943
- const original = readFileSync41(filePath, "utf-8");
67194
+ const original = readFileSync40(filePath, "utf-8");
66944
67195
  let content = original;
66945
67196
  const reasons = [];
66946
67197
  const seen = /* @__PURE__ */ new Set();
@@ -66971,7 +67222,7 @@ function applyVisualCodemods(filePath) {
66971
67222
  function collectAllFixes2(issue) {
66972
67223
  return [...issue.fix ? [issue.fix] : [], ...issue.fixes ?? []];
66973
67224
  }
66974
- async function applyFixes(report, config, scannedFiles) {
67225
+ async function applyFixes(report, _config, scannedFiles) {
66975
67226
  const byFile = /* @__PURE__ */ new Map();
66976
67227
  for (const issue of report.issues) {
66977
67228
  const fixes = collectAllFixes2(issue);
@@ -67032,7 +67283,6 @@ async function applyFixes(report, config, scannedFiles) {
67032
67283
  const result = applyLayoutTokenFix(filePath, group.replaces);
67033
67284
  const total = group.replaceApps.length;
67034
67285
  const appliedCount = Math.min(result.applied, total);
67035
- const skippedCount = total - appliedCount;
67036
67286
  applied.push(...group.replaceApps.slice(0, appliedCount));
67037
67287
  skipped.push(...group.replaceApps.slice(appliedCount));
67038
67288
  } catch (err) {
@@ -67227,6 +67477,40 @@ function formatGroupedHelp(cmd) {
67227
67477
  }
67228
67478
  }
67229
67479
  lines.push("");
67480
+ lines.push(
67481
+ "Commands (run `<command> --help` for per-command details):\n" + [
67482
+ " scan score a workspace, with severity gating and report views",
67483
+ " pr score a PR (or a unified-diff file via `--diff <file>`)",
67484
+ " drift detect constitution / forbidden-package violations",
67485
+ " test test quality sub-score (CI gate)",
67486
+ " docs documentation freshness sub-score",
67487
+ " security AI security-risk sub-score",
67488
+ " rules list all built-in rules with categories and severities",
67489
+ " explain show rationale, pattern, and remediation for a single rule",
67490
+ " suggest print per-issue remediation advice",
67491
+ " badge print a shields.io slop-index badge",
67492
+ " doctor check setup, config, and environment for common problems",
67493
+ " init create a slopbrick config file and AGENTS.md/CLAUDE.md snippets",
67494
+ " flywheel summarize aggregated scan telemetry",
67495
+ " composite empirical-composite rule discover / enable (Sprint 3 \xA73b)",
67496
+ " calibrate empirical precision/recall/F1 calibration from held-out corpora",
67497
+ " patterns pattern-fragmentation sub-score",
67498
+ " business-logic business-logic coherence sub-score",
67499
+ " maintenance-cost AI maintenance-cost sub-score",
67500
+ " architecture architecture-consistency sub-score",
67501
+ " memory show patterns detected for `slop_suggest`",
67502
+ " lock install a git pre-commit hook (idempotent)",
67503
+ " migrate v0.10.x slop-audit/ \u2192 v0.11.0+ .slopbrick/ migration",
67504
+ " validate-config validate slopbrick.config.mjs",
67505
+ " tokens merge a design-tokens.json layout values into the allowlist",
67506
+ " report re-render a saved JSON report (from --json <path>)",
67507
+ " install / uninstall manage the git pre-commit hook",
67508
+ " mcp run the MCP server (JSON-RPC 2.0 over stdio)",
67509
+ " watch / --watch re-run scan on file change",
67510
+ " trend sparkline of the last n runs"
67511
+ ].join("\n")
67512
+ );
67513
+ lines.push("");
67230
67514
  lines.push(
67231
67515
  "Use `--help-flat` for the standard un-grouped list. See `https://usebrick.dev/docs/scan-options` for full docs."
67232
67516
  );
@@ -67344,7 +67628,7 @@ process.on("uncaughtException", (err) => {
67344
67628
  });
67345
67629
  async function runCli({ start }) {
67346
67630
  try {
67347
- const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if slop index increased since last run").option("--refresh-snippets", "rewrite the managed slopbrick block in AGENTS.md and CLAUDE.md after a scan").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
67631
+ const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if AI Slop Score increased since last run (lower = cleaner since v0.21)").option("--refresh-snippets", "rewrite the managed slopbrick block in AGENTS.md and CLAUDE.md after a scan").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
67348
67632
  program.helpInformation = () => formatGroupedHelp(program);
67349
67633
  setExitOverride(program);
67350
67634
  registerInit(program);
@@ -67425,20 +67709,20 @@ async function runCli({ start }) {
67425
67709
  const gitHead = await getGitHead(cwd2) ?? "unknown";
67426
67710
  const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
67427
67711
  saveBaseline(cwd2, cache);
67428
- if (!options.quiet) {
67712
+ if (!options.quiet && !machineReadableStdout) {
67429
67713
  logger.info(`Saved baseline to ${baselinePath(cwd2)}`);
67430
67714
  }
67431
67715
  }
67432
67716
  if (options.tighten && baseline) {
67433
67717
  saveBaseline(cwd, baseline);
67434
- if (!options.quiet) {
67718
+ if (!options.quiet && !machineReadableStdout) {
67435
67719
  logger.info(`Tightened baseline saved (revision ${baseline.baseline_revision}).`);
67436
67720
  }
67437
67721
  }
67438
- if (options.doctor && !options.quiet) {
67722
+ if (options.doctor && !options.quiet && !machineReadableStdout) {
67439
67723
  logger.info(`Doctor: bootstrap ${totalElapsed - scanElapsed}ms, scan ${scanElapsed}ms`);
67440
67724
  }
67441
- if (report.baseline && !options.quiet) {
67725
+ if (report.baseline && !options.quiet && !machineReadableStdout) {
67442
67726
  logger.info(baselineStatusMessage(report.baseline));
67443
67727
  }
67444
67728
  if (options.fix) {
@@ -67452,7 +67736,7 @@ async function runCli({ start }) {
67452
67736
  }
67453
67737
  const scannedFiles = results.map((r) => r.filePath);
67454
67738
  const fixResults = await applyFixes(report, config, scannedFiles);
67455
- const { totalApplied, totalSkipped, hasErrors } = printFixSummary(fixResults, options.quiet ?? false);
67739
+ const { hasErrors } = printFixSummary(fixResults, options.quiet ?? false);
67456
67740
  if (!options.showFixesDiff && !options.quiet) {
67457
67741
  logger.info("Tip: pass --show-fixes-diff to see the unified diff of proposed changes.");
67458
67742
  }
@@ -67492,8 +67776,18 @@ async function runCli({ start }) {
67492
67776
  if (options.staged && stagedGatingResult.reason) {
67493
67777
  logger.error(`Gating failure: ${stagedGatingResult.reason}`);
67494
67778
  } else {
67495
- const failed = failedThresholdCount(report, config);
67496
- logger.error(`${failed} threshold${failed === 1 ? "" : "s"} failed. See details above.`);
67779
+ const failedNames = failedThresholds(report, config);
67780
+ const detail = failedNames.map((name) => {
67781
+ if (name === "meanSlop") return `meanSlop (score ${report.aiSlopScore} > ${config.thresholds.meanSlop})`;
67782
+ if (name === "p90Slop") return `p90Slop (score ${report.p90Score} > ${config.thresholds.p90Slop})`;
67783
+ if (name === "individualSlopThreshold") return `individualSlopThreshold (peak ${report.peakScore} > ${config.thresholds.individualSlopThreshold})`;
67784
+ const cat = name.replace(/^category:/, "");
67785
+ const limit = config.thresholds.categoryThresholds?.[cat];
67786
+ const score = report.categoryScores[cat];
67787
+ return `${name} (score ${score} > ${limit})`;
67788
+ }).join(", ");
67789
+ const failed = failedNames.length;
67790
+ logger.error(`${failed} threshold${failed === 1 ? "" : "s"} failed: ${detail}`);
67497
67791
  }
67498
67792
  }
67499
67793
  if (!options.quiet && !machineReadableStdout) {
@@ -67590,6 +67884,7 @@ export {
67590
67884
  directoryScore,
67591
67885
  extractSignatures,
67592
67886
  failedThresholdCount,
67887
+ failedThresholds,
67593
67888
  filterByDisabledDirectives,
67594
67889
  filterIssues,
67595
67890
  findSimilarFunctions,