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.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.20.0";
22
+ VERSION = "0.21.0";
23
23
  }
24
24
  });
25
25
 
@@ -25921,8 +25921,8 @@ var init_comment_ratio = __esm({
25921
25921
  init_rule();
25922
25922
  init_corpus_baselines2();
25923
25923
  MIN_FILE_LINES = 20;
25924
- FALLBACK_LOW = 0.05;
25925
- FALLBACK_HIGH = 0.45;
25924
+ FALLBACK_LOW = 0.02;
25925
+ FALLBACK_HIGH = 0.55;
25926
25926
  LINE_COMMENT_RE = /^\s*(?:\/\/|#|--|;|%)/;
25927
25927
  BLOCK_COMMENT_OPEN_RE = /\/\*/;
25928
25928
  aiCommentRatioRule = createRule({
@@ -26064,9 +26064,9 @@ var init_compression_profile = __esm({
26064
26064
  init_rule();
26065
26065
  init_ncd_compression();
26066
26066
  MIN_BYTES = 2e3;
26067
- GZIP_RATIO_AI_THRESHOLD = 0.5;
26068
- MEAN_LINE_NCD_HUMAN_MAX = 0.3;
26069
- NCD_CV_AI_MAX = 0.5;
26067
+ GZIP_RATIO_AI_THRESHOLD = 0.6;
26068
+ MEAN_LINE_NCD_HUMAN_MAX = 0.25;
26069
+ NCD_CV_AI_MAX = 0.4;
26070
26070
  aiCompressionProfileRule = createRule({
26071
26071
  id: "ai/compression-profile",
26072
26072
  category: "ai",
@@ -26235,6 +26235,13 @@ var init_default_react_stack = __esm({
26235
26235
  category: "ai",
26236
26236
  severity: "low",
26237
26237
  aiSpecific: true,
26238
+ // v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.001
26239
+ // (fires 1 time per 1,000 files). The "≥3 of default stack"
26240
+ // threshold catches almost nothing on real codebases — the
26241
+ // default stack is now ubiquitous, so the signal is near-zero.
26242
+ // Disable until the threshold is lowered or the rule is split
26243
+ // into per-library detectors.
26244
+ defaultOff: true,
26238
26245
  description: "File uses \u22653 of the default React stack (Next.js + TanStack Query + Zustand + shadcn/ui) \u2014 Sascha 2025 + Nam et al. MSR 2026",
26239
26246
  create(context) {
26240
26247
  return context;
@@ -26508,6 +26515,10 @@ var init_library_reinvention = __esm({
26508
26515
  category: "ai",
26509
26516
  severity: "medium",
26510
26517
  aiSpecific: true,
26518
+ // v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
26519
+ // (fires never on the corpus). The "≥2 reinvented patterns"
26520
+ // threshold is too strict. Disable until rewritten.
26521
+ defaultOff: true,
26511
26522
  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",
26512
26523
  create(context) {
26513
26524
  return context;
@@ -26714,6 +26725,12 @@ var init_markdown_leakage = __esm({
26714
26725
  category: "ai",
26715
26726
  severity: "high",
26716
26727
  aiSpecific: true,
26728
+ // v0.20.0 calibration: v8.5 verdict = INVERTED (lift 65,504× but
26729
+ // recall 0.000 — fires never on the corpus, and when it does fire
26730
+ // it's wrong). The rule was on by default, contributing false
26731
+ // positives to the self-scan. Disable until the rule is rewritten
26732
+ // or the v9 corpus shows it works.
26733
+ defaultOff: true,
26717
26734
  description: "Stray Markdown fence or bare language name at top of file \u2014 AI tools leak the fenced-block format into code",
26718
26735
  create(context) {
26719
26736
  return context;
@@ -27188,6 +27205,11 @@ var init_text_like_ratio = __esm({
27188
27205
  category: "ai",
27189
27206
  severity: "low",
27190
27207
  aiSpecific: true,
27208
+ // v0.20.0 calibration: v8.5 verdict = USEFUL but recall 0.000
27209
+ // (fires never on the corpus). The rule is too specific to ever
27210
+ // fire on real code — the prose-detection heuristic is too
27211
+ // conservative. Disable until rewritten.
27212
+ defaultOff: true,
27191
27213
  description: "High ratio of prose-like lines embedded in code \u2014 AI tools often leave natural-language explanations in source files (Yotkova 2026)",
27192
27214
  create(context) {
27193
27215
  return context;
@@ -28417,7 +28439,7 @@ function collectExports(cwd) {
28417
28439
  /\bexport\s+default\s+(?:function\s+|class\s+)?([A-Za-z_$][\w$]*)/g,
28418
28440
  // v0.18.6: also collect field names from `export interface` and
28419
28441
  // `export type` declarations. Without this, fields like
28420
- // `crossFileDrift`, `aiQuality`, `engineeringHygiene` are
28442
+ // `crossFileDrift`, `aiSlopScore`, `engineeringHygiene` are
28421
28443
  // flagged as stale even though they're valid type fields.
28422
28444
  /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*[?:]/gm
28423
28445
  ]) {
@@ -30082,6 +30104,9 @@ var init_gap_monopoly = __esm({
30082
30104
  GAP_RE = /\bgap(?:-x|-y)?-(\d+)\b/g;
30083
30105
  gapMonopolyRule = createRule({
30084
30106
  id: "layout/gap-monopoly",
30107
+ // v0.20.0 calibration: recall 0.000, fires never. Disable
30108
+ // until rewritten.
30109
+ defaultOff: true,
30085
30110
  category: "layout",
30086
30111
  severity: "medium",
30087
30112
  aiSpecific: false,
@@ -30194,6 +30219,9 @@ var init_math_grid_uniformity = __esm({
30194
30219
  GRID_COLS_RE = /\bgrid-cols-(\d+)\b/g;
30195
30220
  mathGridUniformityRule = createRule({
30196
30221
  id: "layout/math-grid-uniformity",
30222
+ // v0.20.0 calibration: recall 0.000, fires never. Disable
30223
+ // until rewritten.
30224
+ defaultOff: true,
30197
30225
  category: "layout",
30198
30226
  severity: "high",
30199
30227
  aiSpecific: true,
@@ -30306,6 +30334,9 @@ var init_spacing_grid = __esm({
30306
30334
  ]);
30307
30335
  spacingGridRule = createRule({
30308
30336
  id: "layout/spacing-grid",
30337
+ // v0.20.0 calibration: recall 0.000, fires never. Disable
30338
+ // until rewritten.
30339
+ defaultOff: true,
30309
30340
  category: "layout",
30310
30341
  severity: "medium",
30311
30342
  aiSpecific: false,
@@ -30429,15 +30460,9 @@ var init_boundary_violation = __esm({
30429
30460
  if (context.supportsRsc === false) return [];
30430
30461
  if (!facts.v2) return [];
30431
30462
  const issues = [];
30432
- const hookLines = /* @__PURE__ */ new Map();
30433
- for (const h of facts.v2.logic.hooks) {
30434
- const arr = hookLines.get(h.name) ?? [];
30435
- arr.push({ line: h.line, column: h.column });
30436
- hookLines.set(h.name, arr);
30437
- }
30438
30463
  for (const component of facts.v2.components) {
30439
30464
  if (!component.isServerComponent) continue;
30440
- for (const hook of facts.v2.logic.hooks) {
30465
+ for (const hook of component.hookCalls) {
30441
30466
  if (!context.clientHooks.has(hook.name)) continue;
30442
30467
  issues.push({
30443
30468
  ruleId: "logic/boundary-violation",
@@ -30445,8 +30470,8 @@ var init_boundary_violation = __esm({
30445
30470
  severity: "high",
30446
30471
  aiSpecific: true,
30447
30472
  message: `'${hook.name}' only works in interactive client components. This component runs on the server.`,
30448
- line: component.line,
30449
- column: component.column,
30473
+ line: hook.line,
30474
+ column: hook.column,
30450
30475
  advice: "Add the 'use client' directive at the top of this file, or move the stateful part to a separate client component.",
30451
30476
  fix: {
30452
30477
  kind: "insert",
@@ -34703,7 +34728,7 @@ function isHealthFile(value) {
34703
34728
  if (!isVersion3(value.version)) return false;
34704
34729
  if (typeof value.generatedAt !== "string") return false;
34705
34730
  if (typeof value.workspace !== "string") return false;
34706
- if (!isNumber(value.aiQuality)) return false;
34731
+ if (!isNumber(value.aiSlopScore)) return false;
34707
34732
  if (!isNumber(value.engineeringHygiene)) return false;
34708
34733
  if (!isNumber(value.security)) return false;
34709
34734
  if (!isNumber(value.repositoryHealth)) return false;
@@ -34864,7 +34889,7 @@ var init_dist = __esm({
34864
34889
  "../core/dist/index.js"() {
34865
34890
  "use strict";
34866
34891
  init_zod();
34867
- STRUCTURE_SCHEMA_VERSION = "3";
34892
+ STRUCTURE_SCHEMA_VERSION = "5";
34868
34893
  INVENTORY_FILENAME = "inventory.json";
34869
34894
  CONSTITUTION_FILENAME = "constitution.json";
34870
34895
  CACHE_FILENAME = "cache.json";
@@ -35331,11 +35356,11 @@ async function appendRun(cwd, report, version, io, thresholdExceeded2) {
35331
35356
  const run2 = {
35332
35357
  timestamp: report.generatedAt,
35333
35358
  version,
35334
- slopIndex: report.aiQuality,
35359
+ slopIndex: report.aiSlopScore,
35335
35360
  // MemoryAuditRun keeps slopIndex as a historical legacy field
35336
35361
  categoryScores: { ...report.categoryScores },
35337
35362
  topOffenseIds: topOffenseIds(report),
35338
- thresholdExceeded: thresholdExceeded2 ?? report.aiQuality > 0
35363
+ thresholdExceeded: thresholdExceeded2 ?? report.aiSlopScore > 0
35339
35364
  };
35340
35365
  runs.push(run2);
35341
35366
  if (runs.length > MAX_RUNS) {
@@ -35467,7 +35492,7 @@ function buildHealthFromReport(report, workspace, options = {}) {
35467
35492
  version: STRUCTURE_SCHEMA_VERSION,
35468
35493
  generatedAt: report.generatedAt,
35469
35494
  workspace,
35470
- aiQuality: Math.round(report.aiQuality),
35495
+ aiSlopScore: Math.round(report.aiSlopScore),
35471
35496
  engineeringHygiene: Math.round(report.engineeringHygiene),
35472
35497
  security: Math.round(report.security),
35473
35498
  repositoryHealth: Math.round(report.repositoryHealth),
@@ -40355,7 +40380,12 @@ var init_weak_assertion = __esm({
40355
40380
  "toBeUndefined",
40356
40381
  "toBeTruthy",
40357
40382
  "toBeFalsy",
40358
- "toBeNull"
40383
+ "toBeNull",
40384
+ "toBeGreaterThan",
40385
+ "toBeLessThan",
40386
+ "toContain",
40387
+ "toHaveLength",
40388
+ "toBeInstanceOf"
40359
40389
  ]);
40360
40390
  }
40361
40391
  });
@@ -41322,6 +41352,10 @@ var init_math_color_cluster = __esm({
41322
41352
  HEX_COLOR_RE = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
41323
41353
  mathColorClusterRule = createRule({
41324
41354
  id: "visual/math-color-cluster",
41355
+ // v0.20.0 calibration: recall 0.000, fires never. The
41356
+ // "math-color cluster" heuristic is too specific to ever fire
41357
+ // on real code. Disable until rewritten.
41358
+ defaultOff: true,
41325
41359
  category: "visual",
41326
41360
  severity: "high",
41327
41361
  aiSpecific: true,
@@ -41383,6 +41417,9 @@ var init_math_default_font = __esm({
41383
41417
  NEXT_FONT_IMPORT_RE = /import\s+.*from\s+['"]next\/font\/(?:google|local)['"]/;
41384
41418
  mathDefaultFontRule = createRule({
41385
41419
  id: "visual/math-default-font",
41420
+ // v0.20.0 calibration: recall 0.001, fires 1 per 1,000 files.
41421
+ // Too specific to be useful. Disable until rewritten.
41422
+ defaultOff: true,
41386
41423
  category: "visual",
41387
41424
  severity: "high",
41388
41425
  aiSpecific: true,
@@ -41458,6 +41495,9 @@ var init_math_font_entropy = __esm({
41458
41495
  TEXT_SIZE_RE = /\btext-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl)\b/g;
41459
41496
  mathFontEntropyRule = createRule({
41460
41497
  id: "visual/math-font-entropy",
41498
+ // v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
41499
+ // (verified). Disable until rewritten.
41500
+ defaultOff: true,
41461
41501
  category: "visual",
41462
41502
  severity: "high",
41463
41503
  aiSpecific: true,
@@ -41598,6 +41638,10 @@ var init_math_rounded_entropy = __esm({
41598
41638
  ROUNDED_RE = /\brounded(?:-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?\b/g;
41599
41639
  mathRoundedEntropyRule = createRule({
41600
41640
  id: "visual/math-rounded-entropy",
41641
+ // v0.20.0 calibration: recall 0.004, fires 0 times on self-scan
41642
+ // (verified). The entropy heuristic is too specific to ever fire
41643
+ // on real code. Disable until rewritten.
41644
+ defaultOff: true,
41601
41645
  category: "visual",
41602
41646
  severity: "high",
41603
41647
  aiSpecific: true,
@@ -41647,6 +41691,9 @@ var init_math_spacing_entropy = __esm({
41647
41691
  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;
41648
41692
  mathSpacingEntropyRule = createRule({
41649
41693
  id: "visual/math-spacing-entropy",
41694
+ // v0.20.0 calibration: recall 0.002, fires 0 times on self-scan
41695
+ // (verified). Disable until rewritten.
41696
+ defaultOff: true,
41650
41697
  category: "visual",
41651
41698
  severity: "medium",
41652
41699
  aiSpecific: true,
@@ -44492,7 +44539,7 @@ var init_naturalness_anomaly = __esm({
44492
44539
  init_dist2();
44493
44540
  init_visitor();
44494
44541
  MIN_LENGTH = 50;
44495
- DISTINCT_RATIO_FLOOR = 0.3;
44542
+ DISTINCT_RATIO_FLOOR = 0.2;
44496
44543
  naturalnessAnomalyRule = createRule({
44497
44544
  id: "visual/naturalness-anomaly",
44498
44545
  category: "visual",
@@ -44568,6 +44615,9 @@ var init_radius_scale_violation = __esm({
44568
44615
  SCALE_TOLERANCE = 1e-3;
44569
44616
  radiusScaleViolationRule = createRule({
44570
44617
  id: "visual/radius-scale-violation",
44618
+ // v0.20.0 calibration: recall 0.000, fires never. Disable
44619
+ // until rewritten.
44620
+ defaultOff: true,
44571
44621
  category: "visual",
44572
44622
  severity: "medium",
44573
44623
  aiSpecific: false,
@@ -44744,6 +44794,10 @@ var init_dragging_movements = __esm({
44744
44794
  ALT_ROLE_RE = /^(?:button|application|tab|menuitem)$/;
44745
44795
  draggingMovementsRule = createRule({
44746
44796
  id: "wcag/dragging-movements",
44797
+ // v0.20.0 calibration: v8.5 = DORMANT but defaultOff: false
44798
+ // was a bug — DORMANT rules should be off by default. The
44799
+ // rule was contributing 0 useful fires. Disable.
44800
+ defaultOff: true,
44747
44801
  category: "wcag",
44748
44802
  severity: "medium",
44749
44803
  aiSpecific: false,
@@ -44913,6 +44967,11 @@ var init_missing_alt = __esm({
44913
44967
  HAS_PRESENTATION_ROLE_RE = /\brole\s*=\s*["'](?:presentation|none)["']/i;
44914
44968
  missingAltRule = createRule({
44915
44969
  id: "wcag/missing-alt",
44970
+ // v0.20.0 calibration: lift 1.0, recall 0.000. Lift of 1.0
44971
+ // means the rule has zero discriminative power — it fires
44972
+ // equally on positive and negative examples. Dead rule.
44973
+ // Disable until rewritten.
44974
+ defaultOff: true,
44916
44975
  category: "wcag",
44917
44976
  severity: "medium",
44918
44977
  aiSpecific: false,
@@ -46241,17 +46300,11 @@ function colorForSlop(slopIndex) {
46241
46300
  if (slopIndex >= SLOP_BADGE_YELLOW_THRESHOLD) return "yellow";
46242
46301
  return "green";
46243
46302
  }
46244
- function colorForQuality(score) {
46245
- if (score >= SLOP_BADGE_RED_THRESHOLD) return "green";
46246
- if (score >= SLOP_BADGE_ORANGE_THRESHOLD) return "yellow";
46247
- if (score >= SLOP_BADGE_YELLOW_THRESHOLD) return "orange";
46248
- return "red";
46249
- }
46250
46303
  function formatBadge(report) {
46251
- const score = report.aiQuality ?? 0;
46304
+ const score = report.aiSlopScore ?? 0;
46252
46305
  const rounded = Math.round(score);
46253
- const color = colorForQuality(score);
46254
- return `[![Repository Health](https://img.shields.io/badge/repository--health-${rounded}-${color})](https://github.com/usebrick/platform)`;
46306
+ const color = colorForSlop(score);
46307
+ return `[![AI Slop](https://img.shields.io/badge/ai--slop-${rounded}-${color})](https://github.com/usebrick/platform)`;
46255
46308
  }
46256
46309
  function formatSparkline(values) {
46257
46310
  if (values.length === 0) return "";
@@ -46381,18 +46434,25 @@ function scoreBand(score) {
46381
46434
  if (score >= 40) return { label: "needs work", color: chalk.yellow };
46382
46435
  return { label: "concerning", color: chalk.red };
46383
46436
  }
46437
+ function slopScoreBand(score) {
46438
+ if (score >= 70) return { label: "saturated", color: chalk.red };
46439
+ if (score >= 50) return { label: "high", color: chalk.red };
46440
+ if (score >= 30) return { label: "medium", color: chalk.yellow };
46441
+ if (score >= 10) return { label: "low", color: chalk.green };
46442
+ return { label: "no slop", color: chalk.green };
46443
+ }
46384
46444
  function formatDeltaSuffix(report) {
46385
46445
  if (typeof report.previousSlopIndex !== "number") return "";
46386
- const delta = report.aiQuality - report.previousSlopIndex;
46446
+ const delta = report.aiSlopScore - report.previousSlopIndex;
46387
46447
  if (Math.abs(delta) < 0.5) return "";
46388
46448
  const arrow = delta < 0 ? "\u2193" : "\u2191";
46389
46449
  const absDelta = Math.abs(delta).toFixed(0);
46390
- const word = delta > 0 ? "cleaner" : "worse";
46391
- const color = delta > 0 ? chalk.green : chalk.red;
46450
+ const word = delta < 0 ? "cleaner" : "worse";
46451
+ const color = delta < 0 ? chalk.green : chalk.red;
46392
46452
  return color(` ${arrow}${absDelta} (${word})`);
46393
46453
  }
46394
46454
  function formatVerdict(report) {
46395
- const score = report.aiQuality;
46455
+ const score = report.aiSlopScore;
46396
46456
  const band = scoreBand(score);
46397
46457
  const ranked = rankByImpact(report.issues);
46398
46458
  const topRule = ranked[0];
@@ -46476,7 +46536,7 @@ function formatNextStep(report) {
46476
46536
  ` \u2192 \`slopbrick scan --baseline\` to accept today's scores as the new floor`
46477
46537
  )
46478
46538
  );
46479
- if (report.aiQuality < 70) {
46539
+ if (report.aiSlopScore > 30) {
46480
46540
  lines.push(
46481
46541
  chalk.dim(
46482
46542
  ` \u2192 \`slopbrick scan --why-failing\` for the top 5 issues dragging the score down`
@@ -46500,11 +46560,11 @@ function formatWhyFailing(report) {
46500
46560
  if (ranked.length === 0) {
46501
46561
  return chalk.green("Nothing is failing the threshold. Score is clean.");
46502
46562
  }
46503
- const headline = report.aiQuality;
46504
- const status = headline >= 70 ? "PASS" : "FAIL";
46505
- const colorize = headline >= 70 ? chalk.green : chalk.red;
46563
+ const headline = report.aiSlopScore;
46564
+ const status = headline <= 30 ? "PASS" : "FAIL";
46565
+ const colorize = headline <= 30 ? chalk.green : chalk.red;
46506
46566
  const lines = [];
46507
- lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014 below 70)`));
46567
+ lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014 above 30)`));
46508
46568
  lines.push("");
46509
46569
  lines.push(chalk.bold("Top 5 rules dragging the score down:"));
46510
46570
  for (let i = 0; i < ranked.length; i++) {
@@ -46528,30 +46588,42 @@ function severityBadge(severity) {
46528
46588
  const label = severity.toUpperCase().padEnd(8, " ");
46529
46589
  return colorize(label);
46530
46590
  }
46591
+ function scoreToMessage(score, type) {
46592
+ const tiers = SCORE_MESSAGES[type] ?? [];
46593
+ for (const [threshold, message] of tiers) {
46594
+ if (score >= threshold) return message;
46595
+ }
46596
+ const last = tiers[tiers.length - 1];
46597
+ return last ? last[1] : "Score unavailable";
46598
+ }
46531
46599
  function formatCompositeScore(report) {
46532
- const slop = report.aiQuality;
46600
+ const slop = report.aiSlopScore;
46533
46601
  const slopValue = slop.toFixed(0).padStart(3, " ");
46534
- const band = scoreBand(slop);
46602
+ const band = slopScoreBand(slop);
46535
46603
  const status = chalk.bold(`[${band.label.toUpperCase()}]`);
46536
46604
  const lines = [];
46537
46605
  const deltaSuffix = formatDeltaSuffix(report);
46538
46606
  lines.push(
46539
- chalk.bold(`AI Quality: ${slopValue} / 100 ${band.color(status)}${deltaSuffix}`)
46607
+ chalk.bold(`AI Slop Score: ${slopValue} / 100 ${band.color(status)}${deltaSuffix}`)
46540
46608
  );
46609
+ lines.push(` ${chalk.dim(scoreToMessage(slop, "slop"))}`);
46541
46610
  lines.push(
46542
46611
  chalk.dim(
46543
- "higher = better \xB7 measures AI-slop signatures. The same number in .slopbrick/health.json."
46612
+ "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."
46544
46613
  )
46545
46614
  );
46546
46615
  lines.push(
46547
46616
  ` \u251C\u2500 boundary: ${report.boundaryScore.toFixed(0).padStart(3, " ")} (40%) ${chalk.dim("\u2014 structural integrity")}`
46548
46617
  );
46618
+ lines.push(` \u2502 ${chalk.dim(scoreToMessage(report.boundaryScore, "boundary"))}`);
46549
46619
  lines.push(
46550
46620
  ` \u251C\u2500 context: ${report.contextScore.toFixed(0).padStart(3, " ")} (35%) ${chalk.dim("\u2014 props / state / imports")}`
46551
46621
  );
46622
+ lines.push(` \u2502 ${chalk.dim(scoreToMessage(report.contextScore, "context"))}`);
46552
46623
  lines.push(
46553
46624
  ` \u2514\u2500 visual: ${report.visualScore.toFixed(0).padStart(3, " ")} (25%) ${chalk.dim("\u2014 CSS / a11y / layout")}`
46554
46625
  );
46626
+ lines.push(` ${chalk.dim(scoreToMessage(report.visualScore, "visual"))}`);
46555
46627
  const coherence = report.coherence;
46556
46628
  if (typeof coherence === "number") {
46557
46629
  const coherenceValue = coherence.toFixed(0).padStart(3, " ");
@@ -46568,25 +46640,52 @@ function formatCompositeScore(report) {
46568
46640
  function formatCoherenceScores(report) {
46569
46641
  const lines = [];
46570
46642
  const domains = [
46571
- { key: "codeHygiene", label: "Code Hygiene" },
46572
- { key: "accessibility", label: "Accessibility" },
46573
- { key: "performance", label: "Performance" }
46643
+ {
46644
+ key: "codeHygiene",
46645
+ label: "Code Hygiene",
46646
+ measure: "issues per category across arch/logic/layout/component/test",
46647
+ direction: "higher"
46648
+ },
46649
+ {
46650
+ key: "accessibility",
46651
+ label: "Accessibility",
46652
+ measure: "WCAG violations, ARIA misuse, focus management",
46653
+ direction: "higher"
46654
+ },
46655
+ {
46656
+ key: "performance",
46657
+ label: "Performance",
46658
+ measure: "render-blocking, N+1 queries, bundle bloat",
46659
+ direction: "higher"
46660
+ }
46574
46661
  ];
46575
- for (const { key, label } of domains) {
46662
+ for (const { key, label, measure, direction } of domains) {
46576
46663
  const value = report[key];
46577
46664
  if (typeof value === "number") {
46578
46665
  const padded = value.toString().padStart(3, " ");
46579
- lines.push(`${label.padEnd(20)} ${padded}/100`);
46666
+ const arrow = direction === "higher" ? "\u2191" : "\u2193";
46667
+ lines.push(
46668
+ `${label.padEnd(20)} ${padded}/100 (${arrow} ${direction} = better \xB7 ${measure})`
46669
+ );
46670
+ const messageType = key === "codeHygiene" ? "hygiene" : key === "accessibility" ? "accessibility" : key === "performance" ? "performance" : "hygiene";
46671
+ lines.push(`${"".padEnd(20)} ${chalk.dim(scoreToMessage(value, messageType))}`);
46580
46672
  }
46581
46673
  }
46582
46674
  const bl = report.businessLogicCoherence;
46583
46675
  if (typeof bl === "number") {
46584
46676
  const blValue = bl.toString().padStart(3, " ");
46585
- lines.push(`${"Business Logic".padEnd(20)} ${blValue}/100`);
46677
+ lines.push(
46678
+ `${"Business Logic".padEnd(20)} ${blValue}/100 (\u2191 higher = better \xB7 tangled dependencies, cyclic imports, god modules)`
46679
+ );
46586
46680
  }
46587
46681
  if (report.aiSecurityRisk) {
46588
- const secValue = report.aiSecurityRisk.toUpperCase().padEnd(3, " ");
46589
- lines.push(`${"Security Risk".padEnd(20)} ${secValue}`);
46682
+ const secLabel = report.aiSecurityRisk.toUpperCase();
46683
+ lines.push(
46684
+ `${"Security Risk".padEnd(20)} ${secLabel.padEnd(9)} (\u2191 higher = better \xB7 inverted from risk level \xB7 CRITICAL = worst)`
46685
+ );
46686
+ const secToScore = { low: 100, medium: 67, high: 33, critical: 0 };
46687
+ const secNum = secToScore[report.aiSecurityRisk] ?? 0;
46688
+ lines.push(`${"".padEnd(20)} ${chalk.dim(scoreToMessage(secNum, "security"))}`);
46590
46689
  }
46591
46690
  return lines;
46592
46691
  }
@@ -46654,11 +46753,11 @@ function formatDriftSection(report) {
46654
46753
  }
46655
46754
  function formatThresholds(report) {
46656
46755
  const limit = report.thresholds.meanSlop;
46657
- const headline = report.aiQuality;
46756
+ const headline = report.aiSlopScore;
46658
46757
  const passed = headline >= 70;
46659
46758
  const valueText = `${headline.toFixed(1)} \u2265 70`.padStart(12, " ");
46660
46759
  const status = passed ? chalk.green("pass") : chalk.red("fail");
46661
- const result = ["Threshold (CI gate)", ` AI Quality ${valueText} ${status}`];
46760
+ const result = ["Threshold (CI gate)", ` AI Slop Score ${valueText} ${status}`];
46662
46761
  void limit;
46663
46762
  return result;
46664
46763
  }
@@ -46739,7 +46838,7 @@ function formatPretty(report) {
46739
46838
  }
46740
46839
  function formatScoringExplainer(_report) {
46741
46840
  return chalk.dim(
46742
- "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."
46841
+ "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."
46743
46842
  );
46744
46843
  }
46745
46844
  function formatWhyFailingReport(report) {
@@ -46750,7 +46849,7 @@ function formatBriefReport(report) {
46750
46849
  lines.push(formatVerdict(report));
46751
46850
  lines.push("");
46752
46851
  const scoreLines = [
46753
- { label: "AI Quality", field: "aiQuality", value: report.aiQuality },
46852
+ { label: "AI Slop Score", field: "aiSlopScore", value: report.aiSlopScore },
46754
46853
  { label: "Engineering Hygiene", field: "engineeringHygiene", value: report.engineeringHygiene },
46755
46854
  { label: "Security", field: "security", value: report.security },
46756
46855
  { label: "Repository Health", field: "repositoryHealth", value: report.repositoryHealth }
@@ -46765,11 +46864,11 @@ function formatBriefReport(report) {
46765
46864
  ` ${paddedLabel} ${band.color(valueStr)} ${chalk.dim(band.label)} ${chalk.dim.italic(`(${field})`)}${delta}`
46766
46865
  );
46767
46866
  });
46768
- const passed = report.aiQuality >= 70;
46867
+ const passed = report.aiSlopScore <= 30;
46769
46868
  lines.push("");
46770
46869
  lines.push(
46771
46870
  chalk.dim(
46772
- ` CI gate: AI Quality >= 70 -> ${passed ? chalk.green("pass") : chalk.red("fail")}`
46871
+ ` CI gate: AI Slop Score <= 30 -> ${passed ? chalk.green("pass") : chalk.red("fail")}`
46773
46872
  )
46774
46873
  );
46775
46874
  const suppressed = report.defaultOffSuppressedCount ?? 0;
@@ -46789,7 +46888,7 @@ function formatBriefReport(report) {
46789
46888
  );
46790
46889
  return lines.join("\n");
46791
46890
  }
46792
- var CATEGORY_GLOSSARY;
46891
+ var CATEGORY_GLOSSARY, SCORE_MESSAGES;
46793
46892
  var init_pretty = __esm({
46794
46893
  "src/report/pretty.ts"() {
46795
46894
  "use strict";
@@ -46812,6 +46911,96 @@ var init_pretty = __esm({
46812
46911
  product: { short: "product", long: "feature flags, dead code, churn" },
46813
46912
  i18n: { short: "i18n", long: "translations, locale handling" }
46814
46913
  };
46914
+ SCORE_MESSAGES = {
46915
+ // AI Slop Score (v0.21.0: lower = cleaner; field stores the RAW
46916
+ // amount of slop. 0 = no AI slop, 100 = max AI slop. The tier
46917
+ // thresholds are flipped vs the v0.20.1 inverted reading.)
46918
+ slop: [
46919
+ [90, "Repo is saturated with AI slop"],
46920
+ [70, "Repo has a high amount of AI slop"],
46921
+ [50, "Repo has a medium amount of AI slop"],
46922
+ [30, "Repo has a low amount of AI slop"],
46923
+ [0, "Repo has no detectable AI slop"]
46924
+ ],
46925
+ // Engineering Hygiene (higher = better = fewer arch/logic/layout issues)
46926
+ hygiene: [
46927
+ [90, "Repo is clean"],
46928
+ [70, "Repo has minor code quality issues"],
46929
+ [50, "Repo has moderate code quality issues"],
46930
+ [30, "Repo has significant code quality issues"],
46931
+ [0, "Repo has serious code quality issues"]
46932
+ ],
46933
+ // Security (higher = better = inverted from risk level)
46934
+ security: [
46935
+ [90, "Repo has no security risks"],
46936
+ [70, "Repo has low security risk"],
46937
+ [50, "Repo has medium security risk"],
46938
+ [30, "Repo has high security risk"],
46939
+ [0, "Repo has critical security risk"]
46940
+ ],
46941
+ // Repository Health (higher = better = composite of 4 scores)
46942
+ health: [
46943
+ [90, "Repo is healthy"],
46944
+ [70, "Repo has minor concerns"],
46945
+ [50, "Repo has moderate concerns"],
46946
+ [30, "Repo has significant concerns"],
46947
+ [0, "Repo has serious concerns"]
46948
+ ],
46949
+ // Test Quality (higher = better = more test coverage, better assertions)
46950
+ test: [
46951
+ [90, "Tests are comprehensive"],
46952
+ [70, "Good test coverage"],
46953
+ [50, "Moderate test coverage"],
46954
+ [30, "Weak test coverage"],
46955
+ [0, "Tests are inadequate"]
46956
+ ],
46957
+ // Business Logic Coherence (higher = better = fewer cyclic imports, etc.)
46958
+ logic: [
46959
+ [90, "Logic is clean"],
46960
+ [70, "Minor logic tangles"],
46961
+ [50, "Moderate logic tangles"],
46962
+ [30, "Significant logic tangles"],
46963
+ [0, "Serious logic tangles"]
46964
+ ],
46965
+ // Accessibility (higher = better = fewer WCAG violations)
46966
+ accessibility: [
46967
+ [90, "No accessibility issues"],
46968
+ [70, "Minor accessibility issues"],
46969
+ [50, "Moderate accessibility issues"],
46970
+ [30, "Significant accessibility issues"],
46971
+ [0, "Critical accessibility issues"]
46972
+ ],
46973
+ // Performance (higher = better = fewer render-blocking, N+1, bloat)
46974
+ performance: [
46975
+ [90, "No performance issues"],
46976
+ [70, "Minor performance issues"],
46977
+ [50, "Moderate performance issues"],
46978
+ [30, "Significant performance issues"],
46979
+ [0, "Critical performance issues"]
46980
+ ],
46981
+ // Sub-score buckets (boundary/context/visual within AI Slop Score)
46982
+ boundary: [
46983
+ [90, "Boundary is clean"],
46984
+ [70, "Minor boundary issues"],
46985
+ [50, "Moderate boundary issues"],
46986
+ [30, "Significant boundary issues"],
46987
+ [0, "Boundary is broken"]
46988
+ ],
46989
+ context: [
46990
+ [90, "Context is clean"],
46991
+ [70, "Minor context issues"],
46992
+ [50, "Moderate context issues"],
46993
+ [30, "Significant context issues"],
46994
+ [0, "Context is broken"]
46995
+ ],
46996
+ visual: [
46997
+ [90, "Visual is clean"],
46998
+ [70, "Minor visual issues"],
46999
+ [50, "Moderate visual issues"],
47000
+ [30, "Significant visual issues"],
47001
+ [0, "Visual is broken"]
47002
+ ]
47003
+ };
46815
47004
  }
46816
47005
  });
46817
47006
 
@@ -46819,14 +47008,14 @@ var init_pretty = __esm({
46819
47008
  import { readFileSync as readFileSync10, existsSync as existsSync11, writeFileSync as writeFileSync2 } from "fs";
46820
47009
  import { join as join11, resolve as resolve6, relative as relative3 } from "path";
46821
47010
  function thresholdExceeded(report, config) {
46822
- if ((report.aiQuality ?? 0) < config.thresholds.meanSlop) {
47011
+ if ((report.aiSlopScore ?? 0) > config.thresholds.meanSlop) {
46823
47012
  return true;
46824
47013
  }
46825
47014
  return categoryThresholdBreached(report, config.thresholds.categoryThresholds);
46826
47015
  }
46827
47016
  function failedThresholdCount(report, config) {
46828
47017
  let count = 0;
46829
- if (report.aiQuality < config.thresholds.meanSlop) count += 1;
47018
+ if (report.aiSlopScore > config.thresholds.meanSlop) count += 1;
46830
47019
  if (report.p90Score > config.thresholds.p90Slop) count += 1;
46831
47020
  if (report.peakScore > config.thresholds.individualSlopThreshold) count += 1;
46832
47021
  const cat = config.thresholds.categoryThresholds;
@@ -50141,14 +50330,21 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
50141
50330
  }
50142
50331
  }
50143
50332
  const denominator = componentCount || 1;
50333
+ const slopAmount = {
50334
+ boundary: Math.min(100, Math.log10(1 + bucketPoints.boundary / denominator) / Math.log10(11) * 100),
50335
+ context: Math.min(100, Math.log10(1 + bucketPoints.context / denominator) / Math.log10(11) * 100),
50336
+ visual: Math.min(100, Math.log10(1 + bucketPoints.visual / denominator) / Math.log10(11) * 100)
50337
+ };
50144
50338
  const subscore = {
50145
- boundary: Math.min(100, bucketPoints.boundary / denominator * 100),
50146
- context: Math.min(100, bucketPoints.context / denominator * 100),
50147
- visual: Math.min(100, bucketPoints.visual / denominator * 100)
50339
+ boundary: 100 - slopAmount.boundary,
50340
+ context: 100 - slopAmount.context,
50341
+ visual: 100 - slopAmount.visual
50148
50342
  };
50149
50343
  const compositeWeights = config.compositeWeights ?? COMPOSITE_WEIGHTS;
50150
- const slopIndex = compositeWeights.boundary * subscore.boundary + compositeWeights.context * subscore.context + compositeWeights.visual * subscore.visual;
50151
- const assemblyHealth = Math.max(0, 100 - slopIndex);
50344
+ const aiSlopScoreRaw = compositeWeights.boundary * slopAmount.boundary + compositeWeights.context * slopAmount.context + compositeWeights.visual * slopAmount.visual;
50345
+ const aiSlopScore = Math.max(0, Math.min(100, aiSlopScoreRaw));
50346
+ const slopIndex = aiSlopScore;
50347
+ const assemblyHealth = Math.max(0, 100 - aiSlopScore);
50152
50348
  const categoryPoints = {
50153
50349
  visual: 0,
50154
50350
  typo: 0,
@@ -50178,7 +50374,6 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
50178
50374
  categoryScores[category] = categoryScores[category] / componentCount * 100;
50179
50375
  }
50180
50376
  }
50181
- const aiQuality = Math.max(0, Math.min(100, 100 - slopIndex));
50182
50377
  const flatIssues = [];
50183
50378
  for (const group of issueGroups) {
50184
50379
  for (const issue of group.issues) {
@@ -50214,7 +50409,7 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
50214
50409
  const engineeringHygiene = Math.max(0, Math.min(100, 100 - engineeringRaw));
50215
50410
  const testQualityResult = buildTestQualityScore(flatIssues, componentCount);
50216
50411
  const testQuality = Math.max(0, Math.min(100, testQualityResult.score));
50217
- const repositoryHealthRaw = 0.4 * aiQuality + 0.3 * engineeringHygiene + 0.2 * security + 0.1 * testQuality;
50412
+ const repositoryHealthRaw = 0.4 * (100 - aiSlopScore) + 0.3 * engineeringHygiene + 0.2 * security + 0.1 * testQuality;
50218
50413
  const repositoryHealth = Math.max(0, Math.min(100, repositoryHealthRaw));
50219
50414
  let compositeAggregate;
50220
50415
  if (compositeScores && compositeScores.length > 0) {
@@ -50235,7 +50430,7 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
50235
50430
  }
50236
50431
  }
50237
50432
  return {
50238
- aiQuality,
50433
+ aiSlopScore,
50239
50434
  engineeringHygiene,
50240
50435
  security,
50241
50436
  repositoryHealth,
@@ -52395,8 +52590,8 @@ function computeAiMaintenanceCostFromReport(report, options = {}) {
52395
52590
  }
52396
52591
  const approxLoc = options.linesOfCode ?? (report.fileCount ?? 0) * 50;
52397
52592
  const axes = {
52398
- slopIndex: report.aiQuality,
52399
- // The axis is named slopIndex for historical naming; the value is now aiQuality (0-100, higher is better, but axisSlopIndex inverts it)
52593
+ slopIndex: report.aiSlopScore,
52594
+ // The axis is named slopIndex for historical naming; the value is now aiSlopScore (0-100, higher is better, but axisSlopIndex inverts it)
52400
52595
  architectureConsistency: report.architectureConsistency,
52401
52596
  aiSecurityRisk: report.aiSecurityRisk,
52402
52597
  constitutionViolations: options.constitutionViolations,
@@ -52668,10 +52863,10 @@ function aiDebtFromScore(score) {
52668
52863
  }
52669
52864
  function perAxis(inputs) {
52670
52865
  const out = [];
52671
- if (inputs.aiQuality !== void 0 && !Number.isNaN(inputs.aiQuality)) {
52866
+ if (inputs.aiSlopScore !== void 0 && !Number.isNaN(inputs.aiSlopScore)) {
52672
52867
  out.push({
52673
52868
  axis: "slopIndex",
52674
- health: clamp1002(inputs.aiQuality),
52869
+ health: clamp1002(inputs.aiSlopScore),
52675
52870
  weight: REPOSITORY_HEALTH_WEIGHTS.slopIndex
52676
52871
  });
52677
52872
  } else if (inputs.slopIndex !== void 0 && !Number.isNaN(inputs.slopIndex)) {
@@ -52783,10 +52978,10 @@ function buildRepositoryHealthFromReport(report, options = {}) {
52783
52978
  const totalDesignTokens = (options.spacingViolations ?? 0) + (options.radiusViolations ?? 0);
52784
52979
  const inputs = {
52785
52980
  // v0.15.0 U.4+: the v3 headline score. Passed as the
52786
- // `aiQuality` input (higher = better). The perAxis handler
52981
+ // `aiSlopScore` input (higher = better). The perAxis handler
52787
52982
  // maps it to the "slopIndex" axis in the breakdown so
52788
52983
  // dashboards don't have to migrate.
52789
- aiQuality: report.aiQuality,
52984
+ aiSlopScore: report.aiSlopScore,
52790
52985
  architectureConsistency: report.architectureConsistency,
52791
52986
  aiSecurityRisk: report.aiSecurityRisk,
52792
52987
  designTokenViolations: totalDesignTokens > 0 ? {
@@ -53076,7 +53271,7 @@ async function enrichReport(input) {
53076
53271
  const aiSignalCount = sortedIssues.filter((i) => i.aiSpecific === true).length;
53077
53272
  aiMaintenanceCost = computeAiMaintenanceCostFromReport2(
53078
53273
  {
53079
- aiQuality: aggregated.aiQuality ?? 0,
53274
+ aiSlopScore: aggregated.aiSlopScore ?? 0,
53080
53275
  architectureConsistency,
53081
53276
  aiSecurityRisk,
53082
53277
  highSeverityIssueCount: sortedIssues.filter((i) => i.severity === "high").length,
@@ -53135,7 +53330,7 @@ async function enrichReport(input) {
53135
53330
  )?.count ?? 0;
53136
53331
  const composite = buildRepositoryHealthFromReport2(
53137
53332
  {
53138
- aiQuality: aggregated.aiQuality ?? 0,
53333
+ aiSlopScore: aggregated.aiSlopScore ?? 0,
53139
53334
  engineeringHygiene: aggregated.engineeringHygiene ?? 0,
53140
53335
  security: aggregated.security ?? 0,
53141
53336
  repositoryHealth: aggregated.repositoryHealth ?? 0,
@@ -53279,7 +53474,7 @@ function assembleScanReport(input) {
53279
53474
  version: VERSION,
53280
53475
  generatedAt,
53281
53476
  configPath,
53282
- aiQuality: aggregated.aiQuality,
53477
+ aiSlopScore: aggregated.aiSlopScore,
53283
53478
  engineeringHygiene: aggregated.engineeringHygiene,
53284
53479
  security: aggregated.security,
53285
53480
  // repositoryHealth is set later from `enrichment.repositoryHealth`
@@ -53318,7 +53513,7 @@ function assembleScanReport(input) {
53318
53513
  // v0.15.0 U.4+: the previous-run value is stored as
53319
53514
  // `previousSlopIndex` on ProjectReport for backward compat with
53320
53515
  // historical telemetry. The value itself is the previous run's
53321
- // aiQuality (higher = better).
53516
+ // aiSlopScore (higher = better).
53322
53517
  previousSlopIndex: previousRun?.slopIndex,
53323
53518
  previousRunTimestamp: previousRun?.timestamp,
53324
53519
  p90Score: aggregated.p90Score,
@@ -53467,9 +53662,9 @@ function recordTelemetry(cwd, report, results, config) {
53467
53662
  componentCount: report.componentCount,
53468
53663
  // v0.15.0 U.4+: slopIndex is deprecated; the telemetry field is
53469
53664
  // still named slopIndex for backward compat with historical
53470
- // payloads, but the value is now aiQuality (0-100, higher is
53665
+ // payloads, but the value is now aiSlopScore (0-100, higher is
53471
53666
  // better).
53472
- slopIndex: report.aiQuality,
53667
+ slopIndex: report.aiSlopScore,
53473
53668
  assemblyHealth: report.assemblyHealth,
53474
53669
  categoryScores: { ...report.categoryScores },
53475
53670
  p90Score: report.p90Score,
@@ -53736,11 +53931,11 @@ async function persistRun(input) {
53736
53931
  } = input;
53737
53932
  const memoryReport = {
53738
53933
  generatedAt: report.generatedAt,
53739
- aiQuality: report.aiQuality ?? 0,
53934
+ aiSlopScore: report.aiSlopScore ?? 0,
53740
53935
  engineeringHygiene: report.engineeringHygiene ?? 0,
53741
53936
  security: report.security ?? 0,
53742
53937
  repositoryHealth: report.repositoryHealth ?? 0,
53743
- slopIndex: report.slopIndex ?? report.aiQuality ?? 0,
53938
+ slopIndex: report.slopIndex ?? report.aiSlopScore ?? 0,
53744
53939
  categoryScores: report.categoryScores,
53745
53940
  issues: report.issues ?? []
53746
53941
  };
@@ -53839,7 +54034,7 @@ async function persistRun(input) {
53839
54034
  if (!options.quiet && !machineReadableStdout) {
53840
54035
  const compositeSuffix = health.compositeScore ? ` composite=${health.compositeScore.tier}@${health.compositeScore.mean.toFixed(2)}` : "";
53841
54036
  logger.info(
53842
- `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}).`
54037
+ `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}).`
53843
54038
  );
53844
54039
  }
53845
54040
  } catch (err) {
@@ -53915,7 +54110,7 @@ async function finalizeReport(input) {
53915
54110
  config,
53916
54111
  results,
53917
54112
  aggregated: {
53918
- aiQuality: aggregated.aiQuality,
54113
+ aiSlopScore: aggregated.aiSlopScore,
53919
54114
  engineeringHygiene: aggregated.engineeringHygiene,
53920
54115
  security: aggregated.security,
53921
54116
  repositoryHealth: aggregated.repositoryHealth,
@@ -53947,11 +54142,11 @@ async function finalizeReport(input) {
53947
54142
  const previous = (await readRuns(cwd, fsMemoryIO)).at(-1);
53948
54143
  if (previous) {
53949
54144
  const previousBaseline = previous.slopIndex;
53950
- if ((report.aiQuality ?? 0) < previousBaseline) {
54145
+ if ((report.aiSlopScore ?? 0) > previousBaseline) {
53951
54146
  noIncreaseFailure = true;
53952
54147
  if (!options.quiet) {
53953
54148
  logger.error(
53954
- `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.`
54149
+ `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.`
53955
54150
  );
53956
54151
  }
53957
54152
  }
@@ -54383,8 +54578,8 @@ function renderHeader(report) {
54383
54578
  <span class="score-label">Repository Health (composite)</span>
54384
54579
  </div>
54385
54580
  <div class="score-card ai-quality">
54386
- <span class="score-value">${namedScores.aiQuality}</span>
54387
- <span class="score-label">AI Quality</span>
54581
+ <span class="score-value">${namedScores.aiSlopScore}</span>
54582
+ <span class="score-label">AI Slop Score</span>
54388
54583
  </div>
54389
54584
  <div class="score-card engineering-hygiene">
54390
54585
  <span class="score-value">${namedScores.engineeringHygiene}</span>
@@ -54414,7 +54609,7 @@ function extractNamedScores(report) {
54414
54609
  const score = (v) => typeof v === "number" && !Number.isNaN(v) ? Math.round(v).toString() : "\u2013";
54415
54610
  const r = report;
54416
54611
  return {
54417
- aiQuality: score(r.aiQuality),
54612
+ aiSlopScore: score(r.aiSlopScore),
54418
54613
  engineeringHygiene: score(r.engineeringHygiene),
54419
54614
  security: score(r.security),
54420
54615
  repositoryHealth: score(r.repositoryHealth ?? report.repositoryHealth)
@@ -55767,7 +55962,7 @@ async function watchProject(options, cwd, paths) {
55767
55962
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
55768
55963
  configPath,
55769
55964
  // v0.15.0 U.4+: the 4-score model replaces the single slopIndex.
55770
- aiQuality: aggregated.aiQuality,
55965
+ aiSlopScore: aggregated.aiSlopScore,
55771
55966
  engineeringHygiene: aggregated.engineeringHygiene,
55772
55967
  security: aggregated.security,
55773
55968
  repositoryHealth: aggregated.repositoryHealth,
@@ -58377,7 +58572,7 @@ function formatMarkdown(report) {
58377
58572
  const ext = report;
58378
58573
  lines.push(`## Repository Health`);
58379
58574
  lines.push("");
58380
- lines.push(`- **AI Quality** ${Math.round(ext.aiQuality ?? 0)}/100`);
58575
+ lines.push(`- **AI Slop Score** ${Math.round(ext.aiSlopScore ?? 0)}/100`);
58381
58576
  lines.push(`- **Engineering Hygiene** ${Math.round(ext.engineeringHygiene ?? 0)}/100`);
58382
58577
  lines.push(`- **Security** ${Math.round(ext.security ?? 0)}/100`);
58383
58578
  lines.push(`- **Repository Health** ${Math.round(report.repositoryHealth ?? 0)}/100`);
@@ -59996,7 +60191,7 @@ async function runMaintenanceCostScan(cwd, config, options = {}) {
59996
60191
  const hasAiSignals = detectAiSignals(scan.report, config);
59997
60192
  const result = computeAiMaintenanceCostFromReport(
59998
60193
  {
59999
- aiQuality: scan.report.aiQuality,
60194
+ aiSlopScore: scan.report.aiSlopScore,
60000
60195
  engineeringHygiene: scan.report.engineeringHygiene,
60001
60196
  security: scan.report.security,
60002
60197
  repositoryHealth: scan.report.repositoryHealth,