slopbrick 0.20.0 → 0.21.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.cjs CHANGED
@@ -36,7 +36,7 @@ var VERSION;
36
36
  var init_header = __esm({
37
37
  "src/types/_header.ts"() {
38
38
  "use strict";
39
- VERSION = "0.20.0";
39
+ VERSION = "0.21.0";
40
40
  }
41
41
  });
42
42
 
@@ -25938,8 +25938,8 @@ var init_comment_ratio = __esm({
25938
25938
  init_rule();
25939
25939
  init_corpus_baselines2();
25940
25940
  MIN_FILE_LINES = 20;
25941
- FALLBACK_LOW = 0.05;
25942
- FALLBACK_HIGH = 0.45;
25941
+ FALLBACK_LOW = 0.02;
25942
+ FALLBACK_HIGH = 0.55;
25943
25943
  LINE_COMMENT_RE = /^\s*(?:\/\/|#|--|;|%)/;
25944
25944
  BLOCK_COMMENT_OPEN_RE = /\/\*/;
25945
25945
  aiCommentRatioRule = createRule({
@@ -26082,9 +26082,9 @@ var init_compression_profile = __esm({
26082
26082
  init_rule();
26083
26083
  init_ncd_compression();
26084
26084
  MIN_BYTES = 2e3;
26085
- GZIP_RATIO_AI_THRESHOLD = 0.5;
26086
- MEAN_LINE_NCD_HUMAN_MAX = 0.3;
26087
- NCD_CV_AI_MAX = 0.5;
26085
+ GZIP_RATIO_AI_THRESHOLD = 0.6;
26086
+ MEAN_LINE_NCD_HUMAN_MAX = 0.25;
26087
+ NCD_CV_AI_MAX = 0.4;
26088
26088
  aiCompressionProfileRule = createRule({
26089
26089
  id: "ai/compression-profile",
26090
26090
  category: "ai",
@@ -26253,6 +26253,13 @@ var init_default_react_stack = __esm({
26253
26253
  category: "ai",
26254
26254
  severity: "low",
26255
26255
  aiSpecific: true,
26256
+ // v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.001
26257
+ // (fires 1 time per 1,000 files). The "≥3 of default stack"
26258
+ // threshold catches almost nothing on real codebases — the
26259
+ // default stack is now ubiquitous, so the signal is near-zero.
26260
+ // Disable until the threshold is lowered or the rule is split
26261
+ // into per-library detectors.
26262
+ defaultOff: true,
26256
26263
  description: "File uses \u22653 of the default React stack (Next.js + TanStack Query + Zustand + shadcn/ui) \u2014 Sascha 2025 + Nam et al. MSR 2026",
26257
26264
  create(context) {
26258
26265
  return context;
@@ -26526,6 +26533,10 @@ var init_library_reinvention = __esm({
26526
26533
  category: "ai",
26527
26534
  severity: "medium",
26528
26535
  aiSpecific: true,
26536
+ // v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
26537
+ // (fires never on the corpus). The "≥2 reinvented patterns"
26538
+ // threshold is too strict. Disable until rewritten.
26539
+ defaultOff: true,
26529
26540
  description: "File reinvents \u22652 common patterns (date picker, form validation, chart, modal, etc.) without importing the canonical library \u2014 GitClear 2025 + Cui et al. 2025",
26530
26541
  create(context) {
26531
26542
  return context;
@@ -26732,6 +26743,12 @@ var init_markdown_leakage = __esm({
26732
26743
  category: "ai",
26733
26744
  severity: "high",
26734
26745
  aiSpecific: true,
26746
+ // v0.20.0 calibration: v8.5 verdict = INVERTED (lift 65,504× but
26747
+ // recall 0.000 — fires never on the corpus, and when it does fire
26748
+ // it's wrong). The rule was on by default, contributing false
26749
+ // positives to the self-scan. Disable until the rule is rewritten
26750
+ // or the v9 corpus shows it works.
26751
+ defaultOff: true,
26735
26752
  description: "Stray Markdown fence or bare language name at top of file \u2014 AI tools leak the fenced-block format into code",
26736
26753
  create(context) {
26737
26754
  return context;
@@ -27206,6 +27223,11 @@ var init_text_like_ratio = __esm({
27206
27223
  category: "ai",
27207
27224
  severity: "low",
27208
27225
  aiSpecific: true,
27226
+ // v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
27227
+ // (fires never on the corpus). The rule is too specific to ever
27228
+ // fire on real code — the prose-detection heuristic is too
27229
+ // conservative. Disable until rewritten.
27230
+ defaultOff: true,
27209
27231
  description: "High ratio of prose-like lines embedded in code \u2014 AI tools often leave natural-language explanations in source files (Yotkova 2026)",
27210
27232
  create(context) {
27211
27233
  return context;
@@ -28433,7 +28455,7 @@ function collectExports(cwd) {
28433
28455
  /\bexport\s+default\s+(?:function\s+|class\s+)?([A-Za-z_$][\w$]*)/g,
28434
28456
  // v0.18.6: also collect field names from `export interface` and
28435
28457
  // `export type` declarations. Without this, fields like
28436
- // `crossFileDrift`, `aiQuality`, `engineeringHygiene` are
28458
+ // `crossFileDrift`, `aiSlopScore`, `engineeringHygiene` are
28437
28459
  // flagged as stale even though they're valid type fields.
28438
28460
  /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*[?:]/gm
28439
28461
  ]) {
@@ -30100,6 +30122,9 @@ var init_gap_monopoly = __esm({
30100
30122
  GAP_RE = /\bgap(?:-x|-y)?-(\d+)\b/g;
30101
30123
  gapMonopolyRule = createRule({
30102
30124
  id: "layout/gap-monopoly",
30125
+ // v0.20.0 calibration: recall 0.000, fires never. Disable
30126
+ // until rewritten.
30127
+ defaultOff: true,
30103
30128
  category: "layout",
30104
30129
  severity: "medium",
30105
30130
  aiSpecific: false,
@@ -30212,6 +30237,9 @@ var init_math_grid_uniformity = __esm({
30212
30237
  GRID_COLS_RE = /\bgrid-cols-(\d+)\b/g;
30213
30238
  mathGridUniformityRule = createRule({
30214
30239
  id: "layout/math-grid-uniformity",
30240
+ // v0.20.0 calibration: recall 0.000, fires never. Disable
30241
+ // until rewritten.
30242
+ defaultOff: true,
30215
30243
  category: "layout",
30216
30244
  severity: "high",
30217
30245
  aiSpecific: true,
@@ -30324,6 +30352,9 @@ var init_spacing_grid = __esm({
30324
30352
  ]);
30325
30353
  spacingGridRule = createRule({
30326
30354
  id: "layout/spacing-grid",
30355
+ // v0.20.0 calibration: recall 0.000, fires never. Disable
30356
+ // until rewritten.
30357
+ defaultOff: true,
30327
30358
  category: "layout",
30328
30359
  severity: "medium",
30329
30360
  aiSpecific: false,
@@ -30447,15 +30478,9 @@ var init_boundary_violation = __esm({
30447
30478
  if (context.supportsRsc === false) return [];
30448
30479
  if (!facts.v2) return [];
30449
30480
  const issues = [];
30450
- const hookLines = /* @__PURE__ */ new Map();
30451
- for (const h of facts.v2.logic.hooks) {
30452
- const arr = hookLines.get(h.name) ?? [];
30453
- arr.push({ line: h.line, column: h.column });
30454
- hookLines.set(h.name, arr);
30455
- }
30456
30481
  for (const component of facts.v2.components) {
30457
30482
  if (!component.isServerComponent) continue;
30458
- for (const hook of facts.v2.logic.hooks) {
30483
+ for (const hook of component.hookCalls) {
30459
30484
  if (!context.clientHooks.has(hook.name)) continue;
30460
30485
  issues.push({
30461
30486
  ruleId: "logic/boundary-violation",
@@ -30463,8 +30488,8 @@ var init_boundary_violation = __esm({
30463
30488
  severity: "high",
30464
30489
  aiSpecific: true,
30465
30490
  message: `'${hook.name}' only works in interactive client components. This component runs on the server.`,
30466
- line: component.line,
30467
- column: component.column,
30491
+ line: hook.line,
30492
+ column: hook.column,
30468
30493
  advice: "Add the 'use client' directive at the top of this file, or move the stateful part to a separate client component.",
30469
30494
  fix: {
30470
30495
  kind: "insert",
@@ -34719,7 +34744,7 @@ function isHealthFile(value) {
34719
34744
  if (!isVersion3(value.version)) return false;
34720
34745
  if (typeof value.generatedAt !== "string") return false;
34721
34746
  if (typeof value.workspace !== "string") return false;
34722
- if (!isNumber(value.aiQuality)) return false;
34747
+ if (!isNumber(value.aiSlopScore)) return false;
34723
34748
  if (!isNumber(value.engineeringHygiene)) return false;
34724
34749
  if (!isNumber(value.security)) return false;
34725
34750
  if (!isNumber(value.repositoryHealth)) return false;
@@ -34882,7 +34907,7 @@ var init_dist = __esm({
34882
34907
  import_fs = require("fs");
34883
34908
  import_path = require("path");
34884
34909
  init_zod();
34885
- STRUCTURE_SCHEMA_VERSION = "3";
34910
+ STRUCTURE_SCHEMA_VERSION = "5";
34886
34911
  INVENTORY_FILENAME = "inventory.json";
34887
34912
  CONSTITUTION_FILENAME = "constitution.json";
34888
34913
  CACHE_FILENAME = "cache.json";
@@ -35340,11 +35365,11 @@ async function appendRun(cwd, report, version, io, thresholdExceeded2) {
35340
35365
  const run2 = {
35341
35366
  timestamp: report.generatedAt,
35342
35367
  version,
35343
- slopIndex: report.aiQuality,
35368
+ slopIndex: report.aiSlopScore,
35344
35369
  // MemoryAuditRun keeps slopIndex as a historical legacy field
35345
35370
  categoryScores: { ...report.categoryScores },
35346
35371
  topOffenseIds: topOffenseIds(report),
35347
- thresholdExceeded: thresholdExceeded2 ?? report.aiQuality > 0
35372
+ thresholdExceeded: thresholdExceeded2 ?? report.aiSlopScore > 0
35348
35373
  };
35349
35374
  runs.push(run2);
35350
35375
  if (runs.length > MAX_RUNS) {
@@ -35476,7 +35501,7 @@ function buildHealthFromReport(report, workspace, options = {}) {
35476
35501
  version: STRUCTURE_SCHEMA_VERSION,
35477
35502
  generatedAt: report.generatedAt,
35478
35503
  workspace,
35479
- aiQuality: Math.round(report.aiQuality),
35504
+ aiSlopScore: Math.round(report.aiSlopScore),
35480
35505
  engineeringHygiene: Math.round(report.engineeringHygiene),
35481
35506
  security: Math.round(report.security),
35482
35507
  repositoryHealth: Math.round(report.repositoryHealth),
@@ -40373,7 +40398,12 @@ var init_weak_assertion = __esm({
40373
40398
  "toBeUndefined",
40374
40399
  "toBeTruthy",
40375
40400
  "toBeFalsy",
40376
- "toBeNull"
40401
+ "toBeNull",
40402
+ "toBeGreaterThan",
40403
+ "toBeLessThan",
40404
+ "toContain",
40405
+ "toHaveLength",
40406
+ "toBeInstanceOf"
40377
40407
  ]);
40378
40408
  }
40379
40409
  });
@@ -41340,6 +41370,10 @@ var init_math_color_cluster = __esm({
41340
41370
  HEX_COLOR_RE = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
41341
41371
  mathColorClusterRule = createRule({
41342
41372
  id: "visual/math-color-cluster",
41373
+ // v0.20.0 calibration: recall 0.000, fires never. The
41374
+ // "math-color cluster" heuristic is too specific to ever fire
41375
+ // on real code. Disable until rewritten.
41376
+ defaultOff: true,
41343
41377
  category: "visual",
41344
41378
  severity: "high",
41345
41379
  aiSpecific: true,
@@ -41401,6 +41435,9 @@ var init_math_default_font = __esm({
41401
41435
  NEXT_FONT_IMPORT_RE = /import\s+.*from\s+['"]next\/font\/(?:google|local)['"]/;
41402
41436
  mathDefaultFontRule = createRule({
41403
41437
  id: "visual/math-default-font",
41438
+ // v0.20.0 calibration: recall 0.001, fires 1 per 1,000 files.
41439
+ // Too specific to be useful. Disable until rewritten.
41440
+ defaultOff: true,
41404
41441
  category: "visual",
41405
41442
  severity: "high",
41406
41443
  aiSpecific: true,
@@ -41476,6 +41513,9 @@ var init_math_font_entropy = __esm({
41476
41513
  TEXT_SIZE_RE = /\btext-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl)\b/g;
41477
41514
  mathFontEntropyRule = createRule({
41478
41515
  id: "visual/math-font-entropy",
41516
+ // v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
41517
+ // (verified). Disable until rewritten.
41518
+ defaultOff: true,
41479
41519
  category: "visual",
41480
41520
  severity: "high",
41481
41521
  aiSpecific: true,
@@ -41616,6 +41656,10 @@ var init_math_rounded_entropy = __esm({
41616
41656
  ROUNDED_RE = /\brounded(?:-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?\b/g;
41617
41657
  mathRoundedEntropyRule = createRule({
41618
41658
  id: "visual/math-rounded-entropy",
41659
+ // v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
41660
+ // (verified). The entropy heuristic is too specific to ever fire
41661
+ // on real code. Disable until rewritten.
41662
+ defaultOff: true,
41619
41663
  category: "visual",
41620
41664
  severity: "high",
41621
41665
  aiSpecific: true,
@@ -41665,6 +41709,9 @@ var init_math_spacing_entropy = __esm({
41665
41709
  SPACING_PREFIX_RE2 = /\b(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|gap-x|gap-y)-(\d+)\b/g;
41666
41710
  mathSpacingEntropyRule = createRule({
41667
41711
  id: "visual/math-spacing-entropy",
41712
+ // v0.20.0 calibration: recall 0.002, fires 0 times on self-scan
41713
+ // (verified). Disable until rewritten.
41714
+ defaultOff: true,
41668
41715
  category: "visual",
41669
41716
  severity: "medium",
41670
41717
  aiSpecific: true,
@@ -44510,7 +44557,7 @@ var init_naturalness_anomaly = __esm({
44510
44557
  init_dist2();
44511
44558
  init_visitor();
44512
44559
  MIN_LENGTH = 50;
44513
- DISTINCT_RATIO_FLOOR = 0.3;
44560
+ DISTINCT_RATIO_FLOOR = 0.2;
44514
44561
  naturalnessAnomalyRule = createRule({
44515
44562
  id: "visual/naturalness-anomaly",
44516
44563
  category: "visual",
@@ -44586,6 +44633,9 @@ var init_radius_scale_violation = __esm({
44586
44633
  SCALE_TOLERANCE = 1e-3;
44587
44634
  radiusScaleViolationRule = createRule({
44588
44635
  id: "visual/radius-scale-violation",
44636
+ // v0.20.0 calibration: recall 0.000, fires never. Disable
44637
+ // until rewritten.
44638
+ defaultOff: true,
44589
44639
  category: "visual",
44590
44640
  severity: "medium",
44591
44641
  aiSpecific: false,
@@ -44762,6 +44812,10 @@ var init_dragging_movements = __esm({
44762
44812
  ALT_ROLE_RE = /^(?:button|application|tab|menuitem)$/;
44763
44813
  draggingMovementsRule = createRule({
44764
44814
  id: "wcag/dragging-movements",
44815
+ // v0.20.0 calibration: v8.5 = DORMANT but defaultOff: false
44816
+ // was a bug — DORMANT rules should be off by default. The
44817
+ // rule was contributing 0 useful fires. Disable.
44818
+ defaultOff: true,
44765
44819
  category: "wcag",
44766
44820
  severity: "medium",
44767
44821
  aiSpecific: false,
@@ -44931,6 +44985,11 @@ var init_missing_alt = __esm({
44931
44985
  HAS_PRESENTATION_ROLE_RE = /\brole\s*=\s*["'](?:presentation|none)["']/i;
44932
44986
  missingAltRule = createRule({
44933
44987
  id: "wcag/missing-alt",
44988
+ // v0.20.0 calibration: lift 1.0, recall 0.000. Lift of 1.0
44989
+ // means the rule has zero discriminative power — it fires
44990
+ // equally on positive and negative examples. Dead rule.
44991
+ // Disable until rewritten.
44992
+ defaultOff: true,
44934
44993
  category: "wcag",
44935
44994
  severity: "medium",
44936
44995
  aiSpecific: false,
@@ -46260,17 +46319,11 @@ function colorForSlop(slopIndex) {
46260
46319
  if (slopIndex >= SLOP_BADGE_YELLOW_THRESHOLD) return "yellow";
46261
46320
  return "green";
46262
46321
  }
46263
- function colorForQuality(score) {
46264
- if (score >= SLOP_BADGE_RED_THRESHOLD) return "green";
46265
- if (score >= SLOP_BADGE_ORANGE_THRESHOLD) return "yellow";
46266
- if (score >= SLOP_BADGE_YELLOW_THRESHOLD) return "orange";
46267
- return "red";
46268
- }
46269
46322
  function formatBadge(report) {
46270
- const score = report.aiQuality ?? 0;
46323
+ const score = report.aiSlopScore ?? 0;
46271
46324
  const rounded = Math.round(score);
46272
- const color = colorForQuality(score);
46273
- return `[![Repository Health](https://img.shields.io/badge/repository--health-${rounded}-${color})](https://github.com/usebrick/platform)`;
46325
+ const color = colorForSlop(score);
46326
+ return `[![AI Slop](https://img.shields.io/badge/ai--slop-${rounded}-${color})](https://github.com/usebrick/platform)`;
46274
46327
  }
46275
46328
  function formatSparkline(values) {
46276
46329
  if (values.length === 0) return "";
@@ -46399,18 +46452,25 @@ function scoreBand(score) {
46399
46452
  if (score >= 40) return { label: "needs work", color: import_chalk.default.yellow };
46400
46453
  return { label: "concerning", color: import_chalk.default.red };
46401
46454
  }
46455
+ function slopScoreBand(score) {
46456
+ if (score >= 70) return { label: "saturated", color: import_chalk.default.red };
46457
+ if (score >= 50) return { label: "high", color: import_chalk.default.red };
46458
+ if (score >= 30) return { label: "medium", color: import_chalk.default.yellow };
46459
+ if (score >= 10) return { label: "low", color: import_chalk.default.green };
46460
+ return { label: "no slop", color: import_chalk.default.green };
46461
+ }
46402
46462
  function formatDeltaSuffix(report) {
46403
46463
  if (typeof report.previousSlopIndex !== "number") return "";
46404
- const delta = report.aiQuality - report.previousSlopIndex;
46464
+ const delta = report.aiSlopScore - report.previousSlopIndex;
46405
46465
  if (Math.abs(delta) < 0.5) return "";
46406
46466
  const arrow = delta < 0 ? "\u2193" : "\u2191";
46407
46467
  const absDelta = Math.abs(delta).toFixed(0);
46408
- const word = delta > 0 ? "cleaner" : "worse";
46409
- const color = delta > 0 ? import_chalk.default.green : import_chalk.default.red;
46468
+ const word = delta < 0 ? "cleaner" : "worse";
46469
+ const color = delta < 0 ? import_chalk.default.green : import_chalk.default.red;
46410
46470
  return color(` ${arrow}${absDelta} (${word})`);
46411
46471
  }
46412
46472
  function formatVerdict(report) {
46413
- const score = report.aiQuality;
46473
+ const score = report.aiSlopScore;
46414
46474
  const band = scoreBand(score);
46415
46475
  const ranked = rankByImpact(report.issues);
46416
46476
  const topRule = ranked[0];
@@ -46494,7 +46554,7 @@ function formatNextStep(report) {
46494
46554
  ` \u2192 \`slopbrick scan --baseline\` to accept today's scores as the new floor`
46495
46555
  )
46496
46556
  );
46497
- if (report.aiQuality < 70) {
46557
+ if (report.aiSlopScore > 30) {
46498
46558
  lines.push(
46499
46559
  import_chalk.default.dim(
46500
46560
  ` \u2192 \`slopbrick scan --why-failing\` for the top 5 issues dragging the score down`
@@ -46518,11 +46578,11 @@ function formatWhyFailing(report) {
46518
46578
  if (ranked.length === 0) {
46519
46579
  return import_chalk.default.green("Nothing is failing the threshold. Score is clean.");
46520
46580
  }
46521
- const headline = report.aiQuality;
46522
- const status = headline >= 70 ? "PASS" : "FAIL";
46523
- const colorize = headline >= 70 ? import_chalk.default.green : import_chalk.default.red;
46581
+ const headline = report.aiSlopScore;
46582
+ const status = headline <= 30 ? "PASS" : "FAIL";
46583
+ const colorize = headline <= 30 ? import_chalk.default.green : import_chalk.default.red;
46524
46584
  const lines = [];
46525
- lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014 below 70)`));
46585
+ lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014 above 30)`));
46526
46586
  lines.push("");
46527
46587
  lines.push(import_chalk.default.bold("Top 5 rules dragging the score down:"));
46528
46588
  for (let i = 0; i < ranked.length; i++) {
@@ -46546,30 +46606,42 @@ function severityBadge(severity) {
46546
46606
  const label = severity.toUpperCase().padEnd(8, " ");
46547
46607
  return colorize(label);
46548
46608
  }
46609
+ function scoreToMessage(score, type) {
46610
+ const tiers = SCORE_MESSAGES[type] ?? [];
46611
+ for (const [threshold, message] of tiers) {
46612
+ if (score >= threshold) return message;
46613
+ }
46614
+ const last = tiers[tiers.length - 1];
46615
+ return last ? last[1] : "Score unavailable";
46616
+ }
46549
46617
  function formatCompositeScore(report) {
46550
- const slop = report.aiQuality;
46618
+ const slop = report.aiSlopScore;
46551
46619
  const slopValue = slop.toFixed(0).padStart(3, " ");
46552
- const band = scoreBand(slop);
46620
+ const band = slopScoreBand(slop);
46553
46621
  const status = import_chalk.default.bold(`[${band.label.toUpperCase()}]`);
46554
46622
  const lines = [];
46555
46623
  const deltaSuffix = formatDeltaSuffix(report);
46556
46624
  lines.push(
46557
- import_chalk.default.bold(`AI Quality: ${slopValue} / 100 ${band.color(status)}${deltaSuffix}`)
46625
+ import_chalk.default.bold(`AI Slop Score: ${slopValue} / 100 ${band.color(status)}${deltaSuffix}`)
46558
46626
  );
46627
+ lines.push(` ${import_chalk.default.dim(scoreToMessage(slop, "slop"))}`);
46559
46628
  lines.push(
46560
46629
  import_chalk.default.dim(
46561
- "higher = better \xB7 measures AI-slop signatures. The same number in .slopbrick/health.json."
46630
+ "lower = cleaner \xB7 measures AI-slop signatures (0 = no AI slop detected, 100 = max AI slop). The rules are calibrated to detect AI patterns, so this measures how much AI-style fingerprint the codebase has. Same number in .slopbrick/health.json."
46562
46631
  )
46563
46632
  );
46564
46633
  lines.push(
46565
46634
  ` \u251C\u2500 boundary: ${report.boundaryScore.toFixed(0).padStart(3, " ")} (40%) ${import_chalk.default.dim("\u2014 structural integrity")}`
46566
46635
  );
46636
+ lines.push(` \u2502 ${import_chalk.default.dim(scoreToMessage(report.boundaryScore, "boundary"))}`);
46567
46637
  lines.push(
46568
46638
  ` \u251C\u2500 context: ${report.contextScore.toFixed(0).padStart(3, " ")} (35%) ${import_chalk.default.dim("\u2014 props / state / imports")}`
46569
46639
  );
46640
+ lines.push(` \u2502 ${import_chalk.default.dim(scoreToMessage(report.contextScore, "context"))}`);
46570
46641
  lines.push(
46571
46642
  ` \u2514\u2500 visual: ${report.visualScore.toFixed(0).padStart(3, " ")} (25%) ${import_chalk.default.dim("\u2014 CSS / a11y / layout")}`
46572
46643
  );
46644
+ lines.push(` ${import_chalk.default.dim(scoreToMessage(report.visualScore, "visual"))}`);
46573
46645
  const coherence = report.coherence;
46574
46646
  if (typeof coherence === "number") {
46575
46647
  const coherenceValue = coherence.toFixed(0).padStart(3, " ");
@@ -46586,25 +46658,52 @@ function formatCompositeScore(report) {
46586
46658
  function formatCoherenceScores(report) {
46587
46659
  const lines = [];
46588
46660
  const domains = [
46589
- { key: "codeHygiene", label: "Code Hygiene" },
46590
- { key: "accessibility", label: "Accessibility" },
46591
- { key: "performance", label: "Performance" }
46661
+ {
46662
+ key: "codeHygiene",
46663
+ label: "Code Hygiene",
46664
+ measure: "issues per category across arch/logic/layout/component/test",
46665
+ direction: "higher"
46666
+ },
46667
+ {
46668
+ key: "accessibility",
46669
+ label: "Accessibility",
46670
+ measure: "WCAG violations, ARIA misuse, focus management",
46671
+ direction: "higher"
46672
+ },
46673
+ {
46674
+ key: "performance",
46675
+ label: "Performance",
46676
+ measure: "render-blocking, N+1 queries, bundle bloat",
46677
+ direction: "higher"
46678
+ }
46592
46679
  ];
46593
- for (const { key, label } of domains) {
46680
+ for (const { key, label, measure, direction } of domains) {
46594
46681
  const value = report[key];
46595
46682
  if (typeof value === "number") {
46596
46683
  const padded = value.toString().padStart(3, " ");
46597
- lines.push(`${label.padEnd(20)} ${padded}/100`);
46684
+ const arrow = direction === "higher" ? "\u2191" : "\u2193";
46685
+ lines.push(
46686
+ `${label.padEnd(20)} ${padded}/100 (${arrow} ${direction} = better \xB7 ${measure})`
46687
+ );
46688
+ const messageType = key === "codeHygiene" ? "hygiene" : key === "accessibility" ? "accessibility" : key === "performance" ? "performance" : "hygiene";
46689
+ lines.push(`${"".padEnd(20)} ${import_chalk.default.dim(scoreToMessage(value, messageType))}`);
46598
46690
  }
46599
46691
  }
46600
46692
  const bl = report.businessLogicCoherence;
46601
46693
  if (typeof bl === "number") {
46602
46694
  const blValue = bl.toString().padStart(3, " ");
46603
- lines.push(`${"Business Logic".padEnd(20)} ${blValue}/100`);
46695
+ lines.push(
46696
+ `${"Business Logic".padEnd(20)} ${blValue}/100 (\u2191 higher = better \xB7 tangled dependencies, cyclic imports, god modules)`
46697
+ );
46604
46698
  }
46605
46699
  if (report.aiSecurityRisk) {
46606
- const secValue = report.aiSecurityRisk.toUpperCase().padEnd(3, " ");
46607
- lines.push(`${"Security Risk".padEnd(20)} ${secValue}`);
46700
+ const secLabel = report.aiSecurityRisk.toUpperCase();
46701
+ lines.push(
46702
+ `${"Security Risk".padEnd(20)} ${secLabel.padEnd(9)} (\u2191 higher = better \xB7 inverted from risk level \xB7 CRITICAL = worst)`
46703
+ );
46704
+ const secToScore = { low: 100, medium: 67, high: 33, critical: 0 };
46705
+ const secNum = secToScore[report.aiSecurityRisk] ?? 0;
46706
+ lines.push(`${"".padEnd(20)} ${import_chalk.default.dim(scoreToMessage(secNum, "security"))}`);
46608
46707
  }
46609
46708
  return lines;
46610
46709
  }
@@ -46672,11 +46771,11 @@ function formatDriftSection(report) {
46672
46771
  }
46673
46772
  function formatThresholds(report) {
46674
46773
  const limit = report.thresholds.meanSlop;
46675
- const headline = report.aiQuality;
46774
+ const headline = report.aiSlopScore;
46676
46775
  const passed = headline >= 70;
46677
46776
  const valueText = `${headline.toFixed(1)} \u2265 70`.padStart(12, " ");
46678
46777
  const status = passed ? import_chalk.default.green("pass") : import_chalk.default.red("fail");
46679
- const result = ["Threshold (CI gate)", ` AI Quality ${valueText} ${status}`];
46778
+ const result = ["Threshold (CI gate)", ` AI Slop Score ${valueText} ${status}`];
46680
46779
  void limit;
46681
46780
  return result;
46682
46781
  }
@@ -46757,7 +46856,7 @@ function formatPretty(report) {
46757
46856
  }
46758
46857
  function formatScoringExplainer(_report) {
46759
46858
  return import_chalk.default.dim(
46760
- "Four orthogonal scores (all 0-100, higher = better): AI Quality (AI-slop signatures; the CI gate, AI Quality >= 70), Engineering Hygiene (issues per category across arch/logic/layout/component/test), Security (AI-flagged security risks, inverted from risk level), Repository Health (composite: 0.4*AI Quality + 0.3*Engineering Hygiene + 0.2*Security + 0.1*Test Quality). Only AI Quality gates CI; the others are informational. Default-off rules (INVERTED/NOISY/DORMANT) are suppressed from the scores automatically."
46859
+ "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."
46761
46860
  );
46762
46861
  }
46763
46862
  function formatWhyFailingReport(report) {
@@ -46768,7 +46867,7 @@ function formatBriefReport(report) {
46768
46867
  lines.push(formatVerdict(report));
46769
46868
  lines.push("");
46770
46869
  const scoreLines = [
46771
- { label: "AI Quality", field: "aiQuality", value: report.aiQuality },
46870
+ { label: "AI Slop Score", field: "aiSlopScore", value: report.aiSlopScore },
46772
46871
  { label: "Engineering Hygiene", field: "engineeringHygiene", value: report.engineeringHygiene },
46773
46872
  { label: "Security", field: "security", value: report.security },
46774
46873
  { label: "Repository Health", field: "repositoryHealth", value: report.repositoryHealth }
@@ -46783,11 +46882,11 @@ function formatBriefReport(report) {
46783
46882
  ` ${paddedLabel} ${band.color(valueStr)} ${import_chalk.default.dim(band.label)} ${import_chalk.default.dim.italic(`(${field})`)}${delta}`
46784
46883
  );
46785
46884
  });
46786
- const passed = report.aiQuality >= 70;
46885
+ const passed = report.aiSlopScore <= 30;
46787
46886
  lines.push("");
46788
46887
  lines.push(
46789
46888
  import_chalk.default.dim(
46790
- ` CI gate: AI Quality >= 70 -> ${passed ? import_chalk.default.green("pass") : import_chalk.default.red("fail")}`
46889
+ ` CI gate: AI Slop Score <= 30 -> ${passed ? import_chalk.default.green("pass") : import_chalk.default.red("fail")}`
46791
46890
  )
46792
46891
  );
46793
46892
  const suppressed = report.defaultOffSuppressedCount ?? 0;
@@ -46807,7 +46906,7 @@ function formatBriefReport(report) {
46807
46906
  );
46808
46907
  return lines.join("\n");
46809
46908
  }
46810
- var import_chalk, CATEGORY_GLOSSARY;
46909
+ var import_chalk, CATEGORY_GLOSSARY, SCORE_MESSAGES;
46811
46910
  var init_pretty = __esm({
46812
46911
  "src/report/pretty.ts"() {
46813
46912
  "use strict";
@@ -46831,19 +46930,109 @@ var init_pretty = __esm({
46831
46930
  product: { short: "product", long: "feature flags, dead code, churn" },
46832
46931
  i18n: { short: "i18n", long: "translations, locale handling" }
46833
46932
  };
46933
+ SCORE_MESSAGES = {
46934
+ // AI Slop Score (v0.21.0: lower = cleaner; field stores the RAW
46935
+ // amount of slop. 0 = no AI slop, 100 = max AI slop. The tier
46936
+ // thresholds are flipped vs the v0.20.1 inverted reading.)
46937
+ slop: [
46938
+ [90, "Repo is saturated with AI slop"],
46939
+ [70, "Repo has a high amount of AI slop"],
46940
+ [50, "Repo has a medium amount of AI slop"],
46941
+ [30, "Repo has a low amount of AI slop"],
46942
+ [0, "Repo has no detectable AI slop"]
46943
+ ],
46944
+ // Engineering Hygiene (higher = better = fewer arch/logic/layout issues)
46945
+ hygiene: [
46946
+ [90, "Repo is clean"],
46947
+ [70, "Repo has minor code quality issues"],
46948
+ [50, "Repo has moderate code quality issues"],
46949
+ [30, "Repo has significant code quality issues"],
46950
+ [0, "Repo has serious code quality issues"]
46951
+ ],
46952
+ // Security (higher = better = inverted from risk level)
46953
+ security: [
46954
+ [90, "Repo has no security risks"],
46955
+ [70, "Repo has low security risk"],
46956
+ [50, "Repo has medium security risk"],
46957
+ [30, "Repo has high security risk"],
46958
+ [0, "Repo has critical security risk"]
46959
+ ],
46960
+ // Repository Health (higher = better = composite of 4 scores)
46961
+ health: [
46962
+ [90, "Repo is healthy"],
46963
+ [70, "Repo has minor concerns"],
46964
+ [50, "Repo has moderate concerns"],
46965
+ [30, "Repo has significant concerns"],
46966
+ [0, "Repo has serious concerns"]
46967
+ ],
46968
+ // Test Quality (higher = better = more test coverage, better assertions)
46969
+ test: [
46970
+ [90, "Tests are comprehensive"],
46971
+ [70, "Good test coverage"],
46972
+ [50, "Moderate test coverage"],
46973
+ [30, "Weak test coverage"],
46974
+ [0, "Tests are inadequate"]
46975
+ ],
46976
+ // Business Logic Coherence (higher = better = fewer cyclic imports, etc.)
46977
+ logic: [
46978
+ [90, "Logic is clean"],
46979
+ [70, "Minor logic tangles"],
46980
+ [50, "Moderate logic tangles"],
46981
+ [30, "Significant logic tangles"],
46982
+ [0, "Serious logic tangles"]
46983
+ ],
46984
+ // Accessibility (higher = better = fewer WCAG violations)
46985
+ accessibility: [
46986
+ [90, "No accessibility issues"],
46987
+ [70, "Minor accessibility issues"],
46988
+ [50, "Moderate accessibility issues"],
46989
+ [30, "Significant accessibility issues"],
46990
+ [0, "Critical accessibility issues"]
46991
+ ],
46992
+ // Performance (higher = better = fewer render-blocking, N+1, bloat)
46993
+ performance: [
46994
+ [90, "No performance issues"],
46995
+ [70, "Minor performance issues"],
46996
+ [50, "Moderate performance issues"],
46997
+ [30, "Significant performance issues"],
46998
+ [0, "Critical performance issues"]
46999
+ ],
47000
+ // Sub-score buckets (boundary/context/visual within AI Slop Score)
47001
+ boundary: [
47002
+ [90, "Boundary is clean"],
47003
+ [70, "Minor boundary issues"],
47004
+ [50, "Moderate boundary issues"],
47005
+ [30, "Significant boundary issues"],
47006
+ [0, "Boundary is broken"]
47007
+ ],
47008
+ context: [
47009
+ [90, "Context is clean"],
47010
+ [70, "Minor context issues"],
47011
+ [50, "Moderate context issues"],
47012
+ [30, "Significant context issues"],
47013
+ [0, "Context is broken"]
47014
+ ],
47015
+ visual: [
47016
+ [90, "Visual is clean"],
47017
+ [70, "Minor visual issues"],
47018
+ [50, "Moderate visual issues"],
47019
+ [30, "Significant visual issues"],
47020
+ [0, "Visual is broken"]
47021
+ ]
47022
+ };
46834
47023
  }
46835
47024
  });
46836
47025
 
46837
47026
  // src/cli/threshold.ts
46838
47027
  function thresholdExceeded(report, config) {
46839
- if ((report.aiQuality ?? 0) < config.thresholds.meanSlop) {
47028
+ if ((report.aiSlopScore ?? 0) > config.thresholds.meanSlop) {
46840
47029
  return true;
46841
47030
  }
46842
47031
  return categoryThresholdBreached(report, config.thresholds.categoryThresholds);
46843
47032
  }
46844
47033
  function failedThresholdCount(report, config) {
46845
47034
  let count = 0;
46846
- if (report.aiQuality < config.thresholds.meanSlop) count += 1;
47035
+ if (report.aiSlopScore > config.thresholds.meanSlop) count += 1;
46847
47036
  if (report.p90Score > config.thresholds.p90Slop) count += 1;
46848
47037
  if (report.peakScore > config.thresholds.individualSlopThreshold) count += 1;
46849
47038
  const cat = config.thresholds.categoryThresholds;
@@ -50162,14 +50351,21 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
50162
50351
  }
50163
50352
  }
50164
50353
  const denominator = componentCount || 1;
50354
+ const slopAmount = {
50355
+ boundary: Math.min(100, Math.log10(1 + bucketPoints.boundary / denominator) / Math.log10(11) * 100),
50356
+ context: Math.min(100, Math.log10(1 + bucketPoints.context / denominator) / Math.log10(11) * 100),
50357
+ visual: Math.min(100, Math.log10(1 + bucketPoints.visual / denominator) / Math.log10(11) * 100)
50358
+ };
50165
50359
  const subscore = {
50166
- boundary: Math.min(100, bucketPoints.boundary / denominator * 100),
50167
- context: Math.min(100, bucketPoints.context / denominator * 100),
50168
- visual: Math.min(100, bucketPoints.visual / denominator * 100)
50360
+ boundary: 100 - slopAmount.boundary,
50361
+ context: 100 - slopAmount.context,
50362
+ visual: 100 - slopAmount.visual
50169
50363
  };
50170
50364
  const compositeWeights = config.compositeWeights ?? COMPOSITE_WEIGHTS;
50171
- const slopIndex = compositeWeights.boundary * subscore.boundary + compositeWeights.context * subscore.context + compositeWeights.visual * subscore.visual;
50172
- const assemblyHealth = Math.max(0, 100 - slopIndex);
50365
+ const aiSlopScoreRaw = compositeWeights.boundary * slopAmount.boundary + compositeWeights.context * slopAmount.context + compositeWeights.visual * slopAmount.visual;
50366
+ const aiSlopScore = Math.max(0, Math.min(100, aiSlopScoreRaw));
50367
+ const slopIndex = aiSlopScore;
50368
+ const assemblyHealth = Math.max(0, 100 - aiSlopScore);
50173
50369
  const categoryPoints = {
50174
50370
  visual: 0,
50175
50371
  typo: 0,
@@ -50199,7 +50395,6 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
50199
50395
  categoryScores[category] = categoryScores[category] / componentCount * 100;
50200
50396
  }
50201
50397
  }
50202
- const aiQuality = Math.max(0, Math.min(100, 100 - slopIndex));
50203
50398
  const flatIssues = [];
50204
50399
  for (const group of issueGroups) {
50205
50400
  for (const issue of group.issues) {
@@ -50235,7 +50430,7 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
50235
50430
  const engineeringHygiene = Math.max(0, Math.min(100, 100 - engineeringRaw));
50236
50431
  const testQualityResult = buildTestQualityScore(flatIssues, componentCount);
50237
50432
  const testQuality = Math.max(0, Math.min(100, testQualityResult.score));
50238
- const repositoryHealthRaw = 0.4 * aiQuality + 0.3 * engineeringHygiene + 0.2 * security + 0.1 * testQuality;
50433
+ const repositoryHealthRaw = 0.4 * (100 - aiSlopScore) + 0.3 * engineeringHygiene + 0.2 * security + 0.1 * testQuality;
50239
50434
  const repositoryHealth = Math.max(0, Math.min(100, repositoryHealthRaw));
50240
50435
  let compositeAggregate;
50241
50436
  if (compositeScores && compositeScores.length > 0) {
@@ -50256,7 +50451,7 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
50256
50451
  }
50257
50452
  }
50258
50453
  return {
50259
- aiQuality,
50454
+ aiSlopScore,
50260
50455
  engineeringHygiene,
50261
50456
  security,
50262
50457
  repositoryHealth,
@@ -52417,8 +52612,8 @@ function computeAiMaintenanceCostFromReport(report, options = {}) {
52417
52612
  }
52418
52613
  const approxLoc = options.linesOfCode ?? (report.fileCount ?? 0) * 50;
52419
52614
  const axes = {
52420
- slopIndex: report.aiQuality,
52421
- // The axis is named slopIndex for historical naming; the value is now aiQuality (0-100, higher is better, but axisSlopIndex inverts it)
52615
+ slopIndex: report.aiSlopScore,
52616
+ // The axis is named slopIndex for historical naming; the value is now aiSlopScore (0-100, higher is better, but axisSlopIndex inverts it)
52422
52617
  architectureConsistency: report.architectureConsistency,
52423
52618
  aiSecurityRisk: report.aiSecurityRisk,
52424
52619
  constitutionViolations: options.constitutionViolations,
@@ -52690,10 +52885,10 @@ function aiDebtFromScore(score) {
52690
52885
  }
52691
52886
  function perAxis(inputs) {
52692
52887
  const out = [];
52693
- if (inputs.aiQuality !== void 0 && !Number.isNaN(inputs.aiQuality)) {
52888
+ if (inputs.aiSlopScore !== void 0 && !Number.isNaN(inputs.aiSlopScore)) {
52694
52889
  out.push({
52695
52890
  axis: "slopIndex",
52696
- health: clamp1002(inputs.aiQuality),
52891
+ health: clamp1002(inputs.aiSlopScore),
52697
52892
  weight: REPOSITORY_HEALTH_WEIGHTS.slopIndex
52698
52893
  });
52699
52894
  } else if (inputs.slopIndex !== void 0 && !Number.isNaN(inputs.slopIndex)) {
@@ -52805,10 +53000,10 @@ function buildRepositoryHealthFromReport(report, options = {}) {
52805
53000
  const totalDesignTokens = (options.spacingViolations ?? 0) + (options.radiusViolations ?? 0);
52806
53001
  const inputs = {
52807
53002
  // v0.15.0 U.4+: the v3 headline score. Passed as the
52808
- // `aiQuality` input (higher = better). The perAxis handler
53003
+ // `aiSlopScore` input (higher = better). The perAxis handler
52809
53004
  // maps it to the "slopIndex" axis in the breakdown so
52810
53005
  // dashboards don't have to migrate.
52811
- aiQuality: report.aiQuality,
53006
+ aiSlopScore: report.aiSlopScore,
52812
53007
  architectureConsistency: report.architectureConsistency,
52813
53008
  aiSecurityRisk: report.aiSecurityRisk,
52814
53009
  designTokenViolations: totalDesignTokens > 0 ? {
@@ -53096,7 +53291,7 @@ async function enrichReport(input) {
53096
53291
  const aiSignalCount = sortedIssues.filter((i) => i.aiSpecific === true).length;
53097
53292
  aiMaintenanceCost = computeAiMaintenanceCostFromReport2(
53098
53293
  {
53099
- aiQuality: aggregated.aiQuality ?? 0,
53294
+ aiSlopScore: aggregated.aiSlopScore ?? 0,
53100
53295
  architectureConsistency,
53101
53296
  aiSecurityRisk,
53102
53297
  highSeverityIssueCount: sortedIssues.filter((i) => i.severity === "high").length,
@@ -53155,7 +53350,7 @@ async function enrichReport(input) {
53155
53350
  )?.count ?? 0;
53156
53351
  const composite = buildRepositoryHealthFromReport2(
53157
53352
  {
53158
- aiQuality: aggregated.aiQuality ?? 0,
53353
+ aiSlopScore: aggregated.aiSlopScore ?? 0,
53159
53354
  engineeringHygiene: aggregated.engineeringHygiene ?? 0,
53160
53355
  security: aggregated.security ?? 0,
53161
53356
  repositoryHealth: aggregated.repositoryHealth ?? 0,
@@ -53302,7 +53497,7 @@ function assembleScanReport(input) {
53302
53497
  version: VERSION,
53303
53498
  generatedAt,
53304
53499
  configPath,
53305
- aiQuality: aggregated.aiQuality,
53500
+ aiSlopScore: aggregated.aiSlopScore,
53306
53501
  engineeringHygiene: aggregated.engineeringHygiene,
53307
53502
  security: aggregated.security,
53308
53503
  // repositoryHealth is set later from `enrichment.repositoryHealth`
@@ -53341,7 +53536,7 @@ function assembleScanReport(input) {
53341
53536
  // v0.15.0 U.4+: the previous-run value is stored as
53342
53537
  // `previousSlopIndex` on ProjectReport for backward compat with
53343
53538
  // historical telemetry. The value itself is the previous run's
53344
- // aiQuality (higher = better).
53539
+ // aiSlopScore (higher = better).
53345
53540
  previousSlopIndex: previousRun?.slopIndex,
53346
53541
  previousRunTimestamp: previousRun?.timestamp,
53347
53542
  p90Score: aggregated.p90Score,
@@ -53478,9 +53673,9 @@ function recordTelemetry(cwd, report, results, config) {
53478
53673
  componentCount: report.componentCount,
53479
53674
  // v0.15.0 U.4+: slopIndex is deprecated; the telemetry field is
53480
53675
  // still named slopIndex for backward compat with historical
53481
- // payloads, but the value is now aiQuality (0-100, higher is
53676
+ // payloads, but the value is now aiSlopScore (0-100, higher is
53482
53677
  // better).
53483
- slopIndex: report.aiQuality,
53678
+ slopIndex: report.aiSlopScore,
53484
53679
  assemblyHealth: report.assemblyHealth,
53485
53680
  categoryScores: { ...report.categoryScores },
53486
53681
  p90Score: report.p90Score,
@@ -53748,11 +53943,11 @@ async function persistRun(input) {
53748
53943
  } = input;
53749
53944
  const memoryReport = {
53750
53945
  generatedAt: report.generatedAt,
53751
- aiQuality: report.aiQuality ?? 0,
53946
+ aiSlopScore: report.aiSlopScore ?? 0,
53752
53947
  engineeringHygiene: report.engineeringHygiene ?? 0,
53753
53948
  security: report.security ?? 0,
53754
53949
  repositoryHealth: report.repositoryHealth ?? 0,
53755
- slopIndex: report.slopIndex ?? report.aiQuality ?? 0,
53950
+ slopIndex: report.slopIndex ?? report.aiSlopScore ?? 0,
53756
53951
  categoryScores: report.categoryScores,
53757
53952
  issues: report.issues ?? []
53758
53953
  };
@@ -53851,7 +54046,7 @@ async function persistRun(input) {
53851
54046
  if (!options.quiet && !machineReadableStdout) {
53852
54047
  const compositeSuffix = health.compositeScore ? ` composite=${health.compositeScore.tier}@${health.compositeScore.mean.toFixed(2)}` : "";
53853
54048
  logger.info(
53854
- `Memory persisted to .slopbrick/ (${inventory.patterns.length} patterns, ${inventory.components.length} components, ${md.length} bytes of structure.md, health.json: repo=${health.repositoryHealth} aiQ=${health.aiQuality} eng=${health.engineeringHygiene} sec=${health.security}${compositeSuffix}).`
54049
+ `Memory persisted to .slopbrick/ (${inventory.patterns.length} patterns, ${inventory.components.length} components, ${md.length} bytes of structure.md, health.json: repo=${health.repositoryHealth} aiQ=${health.aiSlopScore} eng=${health.engineeringHygiene} sec=${health.security}${compositeSuffix}).`
53855
54050
  );
53856
54051
  }
53857
54052
  } catch (err) {
@@ -53930,7 +54125,7 @@ async function finalizeReport(input) {
53930
54125
  config,
53931
54126
  results,
53932
54127
  aggregated: {
53933
- aiQuality: aggregated.aiQuality,
54128
+ aiSlopScore: aggregated.aiSlopScore,
53934
54129
  engineeringHygiene: aggregated.engineeringHygiene,
53935
54130
  security: aggregated.security,
53936
54131
  repositoryHealth: aggregated.repositoryHealth,
@@ -53962,11 +54157,11 @@ async function finalizeReport(input) {
53962
54157
  const previous = (await readRuns(cwd, fsMemoryIO)).at(-1);
53963
54158
  if (previous) {
53964
54159
  const previousBaseline = previous.slopIndex;
53965
- if ((report.aiQuality ?? 0) < previousBaseline) {
54160
+ if ((report.aiSlopScore ?? 0) > previousBaseline) {
53966
54161
  noIncreaseFailure = true;
53967
54162
  if (!options.quiet) {
53968
54163
  logger.error(
53969
- `AI Quality went DOWN from ${previousBaseline.toFixed(1)} to ${(report.aiQuality ?? 0).toFixed(1)} \u2014 your code got sloppier. (Both values are 0-100, higher = better; the comparison is against the previous run's aiQuality, stored historically in the legacy slopIndex field.) See which files changed and fix the new issues.`
54164
+ `AI Slop Score went UP from ${previousBaseline.toFixed(1)} to ${(report.aiSlopScore ?? 0).toFixed(1)} \u2014 your code got sloppier. (Both values are 0-100, lower = cleaner; the comparison is against the previous run's raw slop amount.) See which files changed and fix the new issues.`
53970
54165
  );
53971
54166
  }
53972
54167
  }
@@ -54399,8 +54594,8 @@ function renderHeader(report) {
54399
54594
  <span class="score-label">Repository Health (composite)</span>
54400
54595
  </div>
54401
54596
  <div class="score-card ai-quality">
54402
- <span class="score-value">${namedScores.aiQuality}</span>
54403
- <span class="score-label">AI Quality</span>
54597
+ <span class="score-value">${namedScores.aiSlopScore}</span>
54598
+ <span class="score-label">AI Slop Score</span>
54404
54599
  </div>
54405
54600
  <div class="score-card engineering-hygiene">
54406
54601
  <span class="score-value">${namedScores.engineeringHygiene}</span>
@@ -54430,7 +54625,7 @@ function extractNamedScores(report) {
54430
54625
  const score = (v) => typeof v === "number" && !Number.isNaN(v) ? Math.round(v).toString() : "\u2013";
54431
54626
  const r = report;
54432
54627
  return {
54433
- aiQuality: score(r.aiQuality),
54628
+ aiSlopScore: score(r.aiSlopScore),
54434
54629
  engineeringHygiene: score(r.engineeringHygiene),
54435
54630
  security: score(r.security),
54436
54631
  repositoryHealth: score(r.repositoryHealth ?? report.repositoryHealth)
@@ -55780,7 +55975,7 @@ async function watchProject(options, cwd, paths) {
55780
55975
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
55781
55976
  configPath,
55782
55977
  // v0.15.0 U.4+: the 4-score model replaces the single slopIndex.
55783
- aiQuality: aggregated.aiQuality,
55978
+ aiSlopScore: aggregated.aiSlopScore,
55784
55979
  engineeringHygiene: aggregated.engineeringHygiene,
55785
55980
  security: aggregated.security,
55786
55981
  repositoryHealth: aggregated.repositoryHealth,
@@ -58468,7 +58663,7 @@ function formatMarkdown(report) {
58468
58663
  const ext = report;
58469
58664
  lines.push(`## Repository Health`);
58470
58665
  lines.push("");
58471
- lines.push(`- **AI Quality** ${Math.round(ext.aiQuality ?? 0)}/100`);
58666
+ lines.push(`- **AI Slop Score** ${Math.round(ext.aiSlopScore ?? 0)}/100`);
58472
58667
  lines.push(`- **Engineering Hygiene** ${Math.round(ext.engineeringHygiene ?? 0)}/100`);
58473
58668
  lines.push(`- **Security** ${Math.round(ext.security ?? 0)}/100`);
58474
58669
  lines.push(`- **Repository Health** ${Math.round(report.repositoryHealth ?? 0)}/100`);
@@ -60087,7 +60282,7 @@ async function runMaintenanceCostScan(cwd, config, options = {}) {
60087
60282
  const hasAiSignals = detectAiSignals(scan.report, config);
60088
60283
  const result = computeAiMaintenanceCostFromReport(
60089
60284
  {
60090
- aiQuality: scan.report.aiQuality,
60285
+ aiSlopScore: scan.report.aiSlopScore,
60091
60286
  engineeringHygiene: scan.report.engineeringHygiene,
60092
60287
  security: scan.report.security,
60093
60288
  repositoryHealth: scan.report.repositoryHealth,