slopbrick 0.17.0 → 0.17.2

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/README.md CHANGED
@@ -46,7 +46,7 @@ on every scan — your repository, encoded for the next agent.
46
46
  lib, modal system, API client) once. The agent and the linter
47
47
  enforce it together.
48
48
 
49
- **Status:** v0.16.0 (current). See the [CHANGELOG](./CHANGELOG.md) for
49
+ **Status:** v0.17.0 (current). See the [CHANGELOG](./CHANGELOG.md) for
50
50
  the full release notes.
51
51
 
52
52
  ---
@@ -76,9 +76,9 @@ For every other config question, see [`EXAMPLES.md`](./EXAMPLES.md).
76
76
 
77
77
  ---
78
78
 
79
- ## The headlines (4-score model, v0.16.0+)
79
+ ## The headlines (4-score model, v0.17.0+)
80
80
 
81
- > **v0.15.0 introduced the 4-score model; v0.16.0 R3 completed it.**
81
+ > **v0.15.0 introduced the 4-score model; v0.16.0 + v0.17.0 completed it.**
82
82
  > The single `Slop Index` is replaced by **4 independent scores**
83
83
  > (all 0-100, **higher = better**). The legacy `slopIndex` field
84
84
  > is kept as optional on `ProjectReport` for backward compat with
@@ -153,7 +153,7 @@ Next step:
153
153
  | Understand the Slop Index vs Coherence | [`docs/scoring-explained.md`](./docs/scoring-explained.md) |
154
154
  | Connect Claude Code / Cursor / Copilot | [`docs/MCP.md`](./docs/MCP.md) |
155
155
  | See the 4 `.slopbrick/` artifacts (structure, inventory, ...) | [`docs/repository-structure.md`](./docs/repository-structure.md) |
156
- | See the 80 rules (per-rule descriptions + citations) | [`docs/rule-catalog.md`](./docs/rule-catalog.md) |
156
+ | See the 95 rules (per-rule descriptions + citations) | [`docs/rule-catalog.md`](./docs/rule-catalog.md) |
157
157
  | See how the engine works (parser → facts → rules) | [`docs/architecture.md`](./docs/architecture.md) |
158
158
  | See which frameworks are supported | [`docs/framework-parity-matrix.md`](./docs/framework-parity-matrix.md) |
159
159
  | See what's changed in each release | [`CHANGELOG.md`](./CHANGELOG.md) |
package/dist/index.cjs CHANGED
@@ -44844,6 +44844,14 @@ var init_config = __esm({
44844
44844
  });
44845
44845
 
44846
44846
  // src/cli/render.ts
44847
+ function redactSecrets(text) {
44848
+ if (!text) return text;
44849
+ let out = text;
44850
+ for (const pat of SECRET_PATTERNS) {
44851
+ out = out.replace(pat, REDACTED);
44852
+ }
44853
+ return out;
44854
+ }
44847
44855
  function colorForSlop(slopIndex) {
44848
44856
  if (slopIndex >= SLOP_BADGE_RED_THRESHOLD) return "red";
44849
44857
  if (slopIndex >= SLOP_BADGE_ORANGE_THRESHOLD) return "orange";
@@ -44882,22 +44890,64 @@ function renderTrend(runs, count) {
44882
44890
  const sparkline2 = formatSparkline(values);
44883
44891
  return `Slop trend (last ${latest.length} runs): ${values.map((v) => Math.round(v)).join(" ")} ${sparkline2}`;
44884
44892
  }
44885
- function renderProgress(completed, total) {
44886
- const spinner = SPINNER_FRAMES[completed % SPINNER_FRAMES.length];
44887
- process.stdout.write(`\r${spinner} Scanning... ${completed}/${total} files`);
44893
+ function renderProgress(completed, total, startMs = Date.now()) {
44894
+ const safeTotal = total > 0 ? total : 1;
44895
+ const ratio = Math.min(1, completed / safeTotal);
44896
+ const pct = (ratio * 100).toFixed(1);
44897
+ const barWidth = 20;
44898
+ const filled = Math.round(barWidth * ratio);
44899
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
44900
+ const elapsedSec = ((Date.now() - startMs) / 1e3).toFixed(1);
44901
+ let eta = "";
44902
+ if (completed > 0 && ratio < 1) {
44903
+ const totalMs = Date.now() - startMs;
44904
+ const etaMs = totalMs / completed * (total - completed);
44905
+ eta = ` | ETA ${(etaMs / 1e3).toFixed(0)}s`;
44906
+ }
44907
+ const line = `\r[${bar}] ${completed}/${total} files (${pct}%) | ${elapsedSec}s${eta} `;
44908
+ if (process.stdout.isTTY) {
44909
+ process.stdout.write(line);
44910
+ } else {
44911
+ const lastPct = renderProgress._lastPct ?? -1;
44912
+ if (Math.abs(parseFloat(pct) - lastPct) >= 2) {
44913
+ process.stdout.write(`[${bar}] ${completed}/${total} files (${pct}%) | ${elapsedSec}s${eta}
44914
+ `);
44915
+ renderProgress._lastPct = parseFloat(pct);
44916
+ }
44917
+ }
44888
44918
  }
44889
44919
  function clearProgress() {
44890
- process.stdout.write(`\r${" ".repeat(80)}\r`);
44920
+ if (process.stdout.isTTY) {
44921
+ process.stdout.write(`\r${" ".repeat(80)}\r`);
44922
+ }
44891
44923
  }
44892
- var SLOP_BADGE_RED_THRESHOLD, SLOP_BADGE_ORANGE_THRESHOLD, SLOP_BADGE_YELLOW_THRESHOLD, WATCH_DEBOUNCE_MS, SPINNER_FRAMES;
44924
+ var SECRET_PATTERNS, REDACTED, SLOP_BADGE_RED_THRESHOLD, SLOP_BADGE_ORANGE_THRESHOLD, SLOP_BADGE_YELLOW_THRESHOLD, WATCH_DEBOUNCE_MS;
44893
44925
  var init_render = __esm({
44894
44926
  "src/cli/render.ts"() {
44895
44927
  "use strict";
44928
+ SECRET_PATTERNS = [
44929
+ // AWS
44930
+ /\b(AKIA[0-9A-Z]{16})\b/g,
44931
+ // GitHub PAT
44932
+ /\b(ghp_[A-Za-z0-9]{36,})\b/g,
44933
+ /\b(github_pat_[A-Za-z0-9_]{82})\b/g,
44934
+ // Slack
44935
+ /\b(xox[abpr]-[A-Za-z0-9-]{10,})\b/g,
44936
+ // Stripe
44937
+ /\b(sk_live_[A-Za-z0-9]{24,})\b/g,
44938
+ /\b(pk_live_[A-Za-z0-9]{24,})\b/g,
44939
+ // Google API key
44940
+ /\b(AIza[0-9A-Za-z_-]{35})\b/g,
44941
+ // Generic JWT
44942
+ /\b(eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
44943
+ // PEM private key
44944
+ /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g
44945
+ ];
44946
+ REDACTED = "[REDACTED]";
44896
44947
  SLOP_BADGE_RED_THRESHOLD = 76;
44897
44948
  SLOP_BADGE_ORANGE_THRESHOLD = 51;
44898
44949
  SLOP_BADGE_YELLOW_THRESHOLD = 26;
44899
44950
  WATCH_DEBOUNCE_MS = 100;
44900
- SPINNER_FRAMES = ["|", "/", "-", "\\"];
44901
44951
  }
44902
44952
  });
44903
44953
 
@@ -45251,10 +45301,10 @@ function formatIssue(issue) {
45251
45301
  const badge = severityBadge(issue.severity);
45252
45302
  const location = issue.filePath ? `${issue.filePath}:${issue.line}:${issue.column}` : `${issue.line}:${issue.column}`;
45253
45303
  const header = `[${badge}] ${issue.ruleId} \xB7 ${location}`;
45254
- const body = ` ${import_chalk.default.dim(issue.message)}`;
45304
+ const body = ` ${import_chalk.default.dim(redactSecrets(issue.message))}`;
45255
45305
  const lines = [header, body];
45256
45306
  if (issue.advice) {
45257
- lines.push(` ${import_chalk.default.cyan("\u2192")} ${issue.advice}`);
45307
+ lines.push(` ${import_chalk.default.cyan("\u2192")} ${redactSecrets(issue.advice)}`);
45258
45308
  }
45259
45309
  return lines.join("\n");
45260
45310
  }
@@ -45316,26 +45366,26 @@ function formatBriefReport(report) {
45316
45366
  lines.push(formatVerdict(report));
45317
45367
  lines.push("");
45318
45368
  const scoreLines = [
45319
- { name: "aiQuality", value: report.aiQuality },
45320
- { name: "engineeringHygiene", value: report.engineeringHygiene },
45321
- { name: "security", value: report.security },
45322
- { name: "repositoryHealth", value: report.repositoryHealth }
45369
+ { label: "AI Quality", field: "aiQuality", value: report.aiQuality },
45370
+ { label: "Engineering Hygiene", field: "engineeringHygiene", value: report.engineeringHygiene },
45371
+ { label: "Security", field: "security", value: report.security },
45372
+ { label: "Repository Health", field: "repositoryHealth", value: report.repositoryHealth }
45323
45373
  ];
45324
45374
  const deltaSuffix = formatDeltaSuffix(report);
45325
- scoreLines.forEach(({ name, value }, idx) => {
45375
+ scoreLines.forEach(({ label, field, value }, idx) => {
45326
45376
  const band = scoreBand(value);
45327
- const paddedName = name.padEnd(20, " ");
45377
+ const paddedLabel = label.padEnd(20, " ");
45328
45378
  const valueStr = value.toFixed(0).padStart(3, " ");
45329
45379
  const delta = idx === 0 ? deltaSuffix : "";
45330
45380
  lines.push(
45331
- ` ${paddedName} ${band.color(valueStr)} ${import_chalk.default.dim(band.label)}${delta}`
45381
+ ` ${paddedLabel} ${band.color(valueStr)} ${import_chalk.default.dim(band.label)} ${import_chalk.default.dim.italic(`(${field})`)}${delta}`
45332
45382
  );
45333
45383
  });
45334
45384
  const passed = report.aiQuality >= 70;
45335
45385
  lines.push("");
45336
45386
  lines.push(
45337
45387
  import_chalk.default.dim(
45338
- ` CI gate: aiQuality >= 70 -> ${passed ? import_chalk.default.green("pass") : import_chalk.default.red("fail")}`
45388
+ ` CI gate: AI Quality >= 70 -> ${passed ? import_chalk.default.green("pass") : import_chalk.default.red("fail")}`
45339
45389
  )
45340
45390
  );
45341
45391
  const suppressed = report.defaultOffSuppressedCount ?? 0;
@@ -45360,6 +45410,7 @@ var init_pretty = __esm({
45360
45410
  "src/report/pretty.ts"() {
45361
45411
  "use strict";
45362
45412
  import_chalk = __toESM(require("chalk"), 1);
45413
+ init_render();
45363
45414
  CATEGORY_GLOSSARY = {
45364
45415
  visual: { short: "visual style", long: "colors, spacing, font sizes, layout" },
45365
45416
  typo: { short: "typography", long: "font weights, line heights, font choices" },
@@ -52404,7 +52455,7 @@ async function runScan(options, explicitPaths) {
52404
52455
  }
52405
52456
  if (defaultOffApplied > 0 && !options.quiet && !machineReadableStdout) {
52406
52457
  console.error(
52407
- `[v0.14.5i] auto-suppressed ${defaultOffApplied} INVERTED/NOISY issue(s) from ${defaultOff.size} default-off rule(s). See the main output for the trust-signal summary. Re-enable per-rule via \`rules: { 'rule/id': 'medium' }\` in slopbrick.config.mjs.`
52458
+ `[v${VERSION}] auto-suppressed ${defaultOffApplied} INVERTED/NOISY issue(s) from ${defaultOff.size} default-off rule(s). See the main output for the trust-signal summary. Re-enable per-rule via \`rules: { 'rule/id': 'medium' }\` in slopbrick.config.mjs.`
52408
52459
  );
52409
52460
  }
52410
52461
  allIssues.sort((a, b) => SEVERITY_WEIGHTS[b.severity] - SEVERITY_WEIGHTS[a.severity]);
@@ -52463,6 +52514,7 @@ var init_scan = __esm({
52463
52514
  init_signal_strength2();
52464
52515
  init_tokens();
52465
52516
  init_finalizeReport();
52517
+ init_types();
52466
52518
  init_baseline_cache();
52467
52519
  init_printFixSummary();
52468
52520
  init_renderOutput();
@@ -57010,7 +57062,7 @@ process.on("uncaughtException", (err) => {
57010
57062
  });
57011
57063
  async function runCli({ start }) {
57012
57064
  try {
57013
- const program = new import_commander2.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("--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");
57065
+ const program = new import_commander2.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("--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)");
57014
57066
  program.command("init").description("create a slopbrick config file").option("--yes", "overwrite existing config").option("--all", "write snippets for every supported agent").option("--matrix", "print the agent x file matrix and exit").option("--cursor", "also generate .cursor/rules/slopbrick.mdc for Cursor AI").option("--cursorrules", "legacy Cursor .cursorrules format").option("--agents-md", "also generate AGENTS.md for Codex / opencode / Pi / Cline").option("--claude-md", "also generate CLAUDE.md for Claude Code (takes precedence over AGENTS.md)").option("--aider", "also generate CONVENTIONS.md for Aider").option("--windsurf", "also generate .windsurfrules for Windsurf").option("--cline", "also generate .clinerules/AGENTS.md for Cline").option("--gemini", "also generate .gemini/GEMINI.md for Gemini CLI").option("--copilot", "also generate .github/copilot-instructions.md for GitHub Copilot").action(async (cmdOptions, command) => {
57015
57067
  const options = command.optsWithGlobals();
57016
57068
  if (cmdOptions.matrix) {
package/dist/index.js CHANGED
@@ -44836,6 +44836,14 @@ var init_config = __esm({
44836
44836
  });
44837
44837
 
44838
44838
  // src/cli/render.ts
44839
+ function redactSecrets(text) {
44840
+ if (!text) return text;
44841
+ let out = text;
44842
+ for (const pat of SECRET_PATTERNS) {
44843
+ out = out.replace(pat, REDACTED);
44844
+ }
44845
+ return out;
44846
+ }
44839
44847
  function colorForSlop(slopIndex) {
44840
44848
  if (slopIndex >= SLOP_BADGE_RED_THRESHOLD) return "red";
44841
44849
  if (slopIndex >= SLOP_BADGE_ORANGE_THRESHOLD) return "orange";
@@ -44874,22 +44882,64 @@ function renderTrend(runs, count) {
44874
44882
  const sparkline2 = formatSparkline(values);
44875
44883
  return `Slop trend (last ${latest.length} runs): ${values.map((v) => Math.round(v)).join(" ")} ${sparkline2}`;
44876
44884
  }
44877
- function renderProgress(completed, total) {
44878
- const spinner = SPINNER_FRAMES[completed % SPINNER_FRAMES.length];
44879
- process.stdout.write(`\r${spinner} Scanning... ${completed}/${total} files`);
44885
+ function renderProgress(completed, total, startMs = Date.now()) {
44886
+ const safeTotal = total > 0 ? total : 1;
44887
+ const ratio = Math.min(1, completed / safeTotal);
44888
+ const pct = (ratio * 100).toFixed(1);
44889
+ const barWidth = 20;
44890
+ const filled = Math.round(barWidth * ratio);
44891
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
44892
+ const elapsedSec = ((Date.now() - startMs) / 1e3).toFixed(1);
44893
+ let eta = "";
44894
+ if (completed > 0 && ratio < 1) {
44895
+ const totalMs = Date.now() - startMs;
44896
+ const etaMs = totalMs / completed * (total - completed);
44897
+ eta = ` | ETA ${(etaMs / 1e3).toFixed(0)}s`;
44898
+ }
44899
+ const line = `\r[${bar}] ${completed}/${total} files (${pct}%) | ${elapsedSec}s${eta} `;
44900
+ if (process.stdout.isTTY) {
44901
+ process.stdout.write(line);
44902
+ } else {
44903
+ const lastPct = renderProgress._lastPct ?? -1;
44904
+ if (Math.abs(parseFloat(pct) - lastPct) >= 2) {
44905
+ process.stdout.write(`[${bar}] ${completed}/${total} files (${pct}%) | ${elapsedSec}s${eta}
44906
+ `);
44907
+ renderProgress._lastPct = parseFloat(pct);
44908
+ }
44909
+ }
44880
44910
  }
44881
44911
  function clearProgress() {
44882
- process.stdout.write(`\r${" ".repeat(80)}\r`);
44912
+ if (process.stdout.isTTY) {
44913
+ process.stdout.write(`\r${" ".repeat(80)}\r`);
44914
+ }
44883
44915
  }
44884
- var SLOP_BADGE_RED_THRESHOLD, SLOP_BADGE_ORANGE_THRESHOLD, SLOP_BADGE_YELLOW_THRESHOLD, WATCH_DEBOUNCE_MS, SPINNER_FRAMES;
44916
+ var SECRET_PATTERNS, REDACTED, SLOP_BADGE_RED_THRESHOLD, SLOP_BADGE_ORANGE_THRESHOLD, SLOP_BADGE_YELLOW_THRESHOLD, WATCH_DEBOUNCE_MS;
44885
44917
  var init_render = __esm({
44886
44918
  "src/cli/render.ts"() {
44887
44919
  "use strict";
44920
+ SECRET_PATTERNS = [
44921
+ // AWS
44922
+ /\b(AKIA[0-9A-Z]{16})\b/g,
44923
+ // GitHub PAT
44924
+ /\b(ghp_[A-Za-z0-9]{36,})\b/g,
44925
+ /\b(github_pat_[A-Za-z0-9_]{82})\b/g,
44926
+ // Slack
44927
+ /\b(xox[abpr]-[A-Za-z0-9-]{10,})\b/g,
44928
+ // Stripe
44929
+ /\b(sk_live_[A-Za-z0-9]{24,})\b/g,
44930
+ /\b(pk_live_[A-Za-z0-9]{24,})\b/g,
44931
+ // Google API key
44932
+ /\b(AIza[0-9A-Za-z_-]{35})\b/g,
44933
+ // Generic JWT
44934
+ /\b(eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
44935
+ // PEM private key
44936
+ /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g
44937
+ ];
44938
+ REDACTED = "[REDACTED]";
44888
44939
  SLOP_BADGE_RED_THRESHOLD = 76;
44889
44940
  SLOP_BADGE_ORANGE_THRESHOLD = 51;
44890
44941
  SLOP_BADGE_YELLOW_THRESHOLD = 26;
44891
44942
  WATCH_DEBOUNCE_MS = 100;
44892
- SPINNER_FRAMES = ["|", "/", "-", "\\"];
44893
44943
  }
44894
44944
  });
44895
44945
 
@@ -45244,10 +45294,10 @@ function formatIssue(issue) {
45244
45294
  const badge = severityBadge(issue.severity);
45245
45295
  const location = issue.filePath ? `${issue.filePath}:${issue.line}:${issue.column}` : `${issue.line}:${issue.column}`;
45246
45296
  const header = `[${badge}] ${issue.ruleId} \xB7 ${location}`;
45247
- const body = ` ${chalk.dim(issue.message)}`;
45297
+ const body = ` ${chalk.dim(redactSecrets(issue.message))}`;
45248
45298
  const lines = [header, body];
45249
45299
  if (issue.advice) {
45250
- lines.push(` ${chalk.cyan("\u2192")} ${issue.advice}`);
45300
+ lines.push(` ${chalk.cyan("\u2192")} ${redactSecrets(issue.advice)}`);
45251
45301
  }
45252
45302
  return lines.join("\n");
45253
45303
  }
@@ -45309,26 +45359,26 @@ function formatBriefReport(report) {
45309
45359
  lines.push(formatVerdict(report));
45310
45360
  lines.push("");
45311
45361
  const scoreLines = [
45312
- { name: "aiQuality", value: report.aiQuality },
45313
- { name: "engineeringHygiene", value: report.engineeringHygiene },
45314
- { name: "security", value: report.security },
45315
- { name: "repositoryHealth", value: report.repositoryHealth }
45362
+ { label: "AI Quality", field: "aiQuality", value: report.aiQuality },
45363
+ { label: "Engineering Hygiene", field: "engineeringHygiene", value: report.engineeringHygiene },
45364
+ { label: "Security", field: "security", value: report.security },
45365
+ { label: "Repository Health", field: "repositoryHealth", value: report.repositoryHealth }
45316
45366
  ];
45317
45367
  const deltaSuffix = formatDeltaSuffix(report);
45318
- scoreLines.forEach(({ name, value }, idx) => {
45368
+ scoreLines.forEach(({ label, field, value }, idx) => {
45319
45369
  const band = scoreBand(value);
45320
- const paddedName = name.padEnd(20, " ");
45370
+ const paddedLabel = label.padEnd(20, " ");
45321
45371
  const valueStr = value.toFixed(0).padStart(3, " ");
45322
45372
  const delta = idx === 0 ? deltaSuffix : "";
45323
45373
  lines.push(
45324
- ` ${paddedName} ${band.color(valueStr)} ${chalk.dim(band.label)}${delta}`
45374
+ ` ${paddedLabel} ${band.color(valueStr)} ${chalk.dim(band.label)} ${chalk.dim.italic(`(${field})`)}${delta}`
45325
45375
  );
45326
45376
  });
45327
45377
  const passed = report.aiQuality >= 70;
45328
45378
  lines.push("");
45329
45379
  lines.push(
45330
45380
  chalk.dim(
45331
- ` CI gate: aiQuality >= 70 -> ${passed ? chalk.green("pass") : chalk.red("fail")}`
45381
+ ` CI gate: AI Quality >= 70 -> ${passed ? chalk.green("pass") : chalk.red("fail")}`
45332
45382
  )
45333
45383
  );
45334
45384
  const suppressed = report.defaultOffSuppressedCount ?? 0;
@@ -45352,6 +45402,7 @@ var CATEGORY_GLOSSARY;
45352
45402
  var init_pretty = __esm({
45353
45403
  "src/report/pretty.ts"() {
45354
45404
  "use strict";
45405
+ init_render();
45355
45406
  CATEGORY_GLOSSARY = {
45356
45407
  visual: { short: "visual style", long: "colors, spacing, font sizes, layout" },
45357
45408
  typo: { short: "typography", long: "font weights, line heights, font choices" },
@@ -52401,7 +52452,7 @@ async function runScan(options, explicitPaths) {
52401
52452
  }
52402
52453
  if (defaultOffApplied > 0 && !options.quiet && !machineReadableStdout) {
52403
52454
  console.error(
52404
- `[v0.14.5i] auto-suppressed ${defaultOffApplied} INVERTED/NOISY issue(s) from ${defaultOff.size} default-off rule(s). See the main output for the trust-signal summary. Re-enable per-rule via \`rules: { 'rule/id': 'medium' }\` in slopbrick.config.mjs.`
52455
+ `[v${VERSION}] auto-suppressed ${defaultOffApplied} INVERTED/NOISY issue(s) from ${defaultOff.size} default-off rule(s). See the main output for the trust-signal summary. Re-enable per-rule via \`rules: { 'rule/id': 'medium' }\` in slopbrick.config.mjs.`
52405
52456
  );
52406
52457
  }
52407
52458
  allIssues.sort((a, b) => SEVERITY_WEIGHTS[b.severity] - SEVERITY_WEIGHTS[a.severity]);
@@ -52457,6 +52508,7 @@ var init_scan = __esm({
52457
52508
  init_signal_strength2();
52458
52509
  init_tokens();
52459
52510
  init_finalizeReport();
52511
+ init_types();
52460
52512
  init_baseline_cache();
52461
52513
  init_printFixSummary();
52462
52514
  init_renderOutput();
@@ -56929,7 +56981,7 @@ process.on("uncaughtException", (err) => {
56929
56981
  });
56930
56982
  async function runCli({ start }) {
56931
56983
  try {
56932
- 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("--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");
56984
+ 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("--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)");
56933
56985
  program.command("init").description("create a slopbrick config file").option("--yes", "overwrite existing config").option("--all", "write snippets for every supported agent").option("--matrix", "print the agent x file matrix and exit").option("--cursor", "also generate .cursor/rules/slopbrick.mdc for Cursor AI").option("--cursorrules", "legacy Cursor .cursorrules format").option("--agents-md", "also generate AGENTS.md for Codex / opencode / Pi / Cline").option("--claude-md", "also generate CLAUDE.md for Claude Code (takes precedence over AGENTS.md)").option("--aider", "also generate CONVENTIONS.md for Aider").option("--windsurf", "also generate .windsurfrules for Windsurf").option("--cline", "also generate .clinerules/AGENTS.md for Cline").option("--gemini", "also generate .gemini/GEMINI.md for Gemini CLI").option("--copilot", "also generate .github/copilot-instructions.md for GitHub Copilot").action(async (cmdOptions, command) => {
56934
56986
  const options = command.optsWithGlobals();
56935
56987
  if (cmdOptions.matrix) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "slopbrick",
3
- "version": "0.17.0",
4
- "description": "Discovered, modeled, and governed repository structure. SlopBrick scans source code, classifies it against 80+ rules across 13 categories, computes 4 scores (aiQuality, engineeringHygiene, security, repositoryHealth), and persists the structure for AI agents and CI.",
3
+ "version": "0.17.2",
4
+ "description": "Discovered, modeled, and governed repository structure. SlopBrick scans source code, classifies it against 95 rules in 15 categories, computes 4 scores (aiQuality, engineeringHygiene, security, repositoryHealth), and persists the structure for AI agents and CI.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "slopbrick": "bin/slopbrick.js"
@@ -54,7 +54,7 @@
54
54
  "minimatch": "^9.0.5",
55
55
  "pgsql-parser": "^17.9.15"
56
56
  },
57
- "//_usebrick_note": "@usebrick/core and @usebrick/engine are private workspace packages. They are listed in devDependencies to keep pnpm-lock.yaml in sync; tsup.config.ts noExternal bundles them into dist/; the prepack-guard.mjs allowlist includes both. Neither ends up in the published tarball's runtime deps. See https://github.com/usebrick/platform/blob/main/docs/repository-structure.md",
57
+ "//_usebrick_note": "@usebrick/core and @usebrick/engine are private workspace packages. They are listed in devDependencies to keep pnpm-lock.yaml in sync; tsup.config.ts \u2192 noExternal bundles them into dist/; the prepack-guard.mjs allowlist includes both. Neither ends up in the published tarball's runtime deps. See https://github.com/usebrick/platform/blob/main/docs/repository-structure.md",
58
58
  "devDependencies": {
59
59
  "@types/node": "^20.14.0",
60
60
  "@usebrick/core": "workspace:*",
@@ -111,4 +111,4 @@
111
111
  "url": "https://github.com/usebrick/platform/issues"
112
112
  },
113
113
  "homepage": "https://github.com/usebrick/platform#readme"
114
- }
114
+ }