slopbrick 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -55
- package/bin/slopbrick.js +23 -2
- package/dist/engine/worker.cjs +149 -23
- package/dist/engine/worker.js +149 -23
- package/dist/index.cjs +411 -115
- package/dist/index.d.cts +39 -15
- package/dist/index.d.ts +39 -15
- package/dist/index.js +529 -234
- package/package.json +2 -2
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.
|
|
39
|
+
VERSION = "0.43.0";
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -338,8 +338,16 @@ var init_defaults = __esm({
|
|
|
338
338
|
// rule definitions are meta-code
|
|
339
339
|
"**/snippet/**",
|
|
340
340
|
// RULE_HINTS examples are intentional bad code
|
|
341
|
-
"**/tests/**"
|
|
341
|
+
"**/tests/**",
|
|
342
342
|
// all test files (fixtures + unit + integration)
|
|
343
|
+
// v0.42.0 (post-self-scan cleanup): language visitors are
|
|
344
|
+
// intentional clones — see the §3 v0.42.0 release notes for
|
|
345
|
+
// the design rationale. `dup/identical-block` fires 100+
|
|
346
|
+
// times during self-scan because each visitor shares the
|
|
347
|
+
// same service-suffix arrays and import-walking preamble.
|
|
348
|
+
// Suppress these from the self-scan noise floor; they
|
|
349
|
+
// remain on for user codebases.
|
|
350
|
+
"**/src/engine/visitors/**"
|
|
343
351
|
]
|
|
344
352
|
}
|
|
345
353
|
};
|
|
@@ -26001,7 +26009,7 @@ var init_comment_ratio = __esm({
|
|
|
26001
26009
|
message: `Comment-line ratio is ${direction} (${ratio.toFixed(2)} vs corpus mean ${mean3}). Reductive LLMs (GPT-3.5, Claude 3 Haiku) skip comments; expansive LLMs (Claude Haiku 4.5, GPT-OSS) over-comment when prompted for "well-commented code".`,
|
|
26002
26010
|
line: 1,
|
|
26003
26011
|
column: 1,
|
|
26004
|
-
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "
|
|
26012
|
+
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "High comment ratio \u2014 consider whether the comments are intentional (api docs, type defs, tutorials). For functional code, trim comments that just restate the next line. Keep comments that explain WHY, not WHAT."
|
|
26005
26013
|
}
|
|
26006
26014
|
];
|
|
26007
26015
|
return issues;
|
|
@@ -26852,7 +26860,6 @@ function buildTrigramLM(samples, alpha = 1) {
|
|
|
26852
26860
|
logProb(context, t) {
|
|
26853
26861
|
const ctxKey = context.slice(-2).join("|");
|
|
26854
26862
|
const triKey = `${ctxKey}|${t}`;
|
|
26855
|
-
const biKey = context.slice(-1).join("|") + "|" + t;
|
|
26856
26863
|
const triCount = tri.get(triKey) ?? 0;
|
|
26857
26864
|
const biCount = bi.get(ctxKey) ?? 0;
|
|
26858
26865
|
const V = uni.size;
|
|
@@ -28860,7 +28867,7 @@ var init_stale_function_reference = __esm({
|
|
|
28860
28867
|
]);
|
|
28861
28868
|
SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
28862
28869
|
SOURCE_ROOTS = ["src", "lib", "app", "components"];
|
|
28863
|
-
CAP =
|
|
28870
|
+
CAP = 4e3;
|
|
28864
28871
|
staleFunctionReferenceRule = createRule({
|
|
28865
28872
|
id: "docs/stale-function-reference",
|
|
28866
28873
|
category: "docs",
|
|
@@ -28876,6 +28883,7 @@ var init_stale_function_reference = __esm({
|
|
|
28876
28883
|
if (!source) return issues;
|
|
28877
28884
|
for (const span of extractInlineCodeSpans(source)) {
|
|
28878
28885
|
const text = span.text;
|
|
28886
|
+
if (span.inBlockComment || span.inComment) continue;
|
|
28879
28887
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
|
|
28880
28888
|
if (text.length < 3) continue;
|
|
28881
28889
|
if (RESERVED.has(text.toLowerCase())) continue;
|
|
@@ -29046,6 +29054,7 @@ var init_stale_package_reference = __esm({
|
|
|
29046
29054
|
if (!source) return issues;
|
|
29047
29055
|
const spans = extractInlineCodeSpans(source);
|
|
29048
29056
|
for (const span of spans) {
|
|
29057
|
+
if (span.inComment) continue;
|
|
29049
29058
|
const lineStart = source.lastIndexOf("\n", span.index) + 1;
|
|
29050
29059
|
const lineEnd = source.indexOf("\n", span.index);
|
|
29051
29060
|
const line = source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd);
|
|
@@ -29100,11 +29109,14 @@ __export(doc_freshness_exports, {
|
|
|
29100
29109
|
extractExports: () => extractExports,
|
|
29101
29110
|
extractFencedCodeBlocks: () => extractFencedCodeBlocks,
|
|
29102
29111
|
extractInlineCodeSpans: () => extractInlineCodeSpans,
|
|
29103
|
-
extractMarkdownLinks: () => extractMarkdownLinks
|
|
29112
|
+
extractMarkdownLinks: () => extractMarkdownLinks,
|
|
29113
|
+
findCommentLineRanges: () => findCommentLineRanges
|
|
29104
29114
|
});
|
|
29105
29115
|
function extractInlineCodeSpans(source) {
|
|
29106
29116
|
const hits = [];
|
|
29107
29117
|
const re = /`([^`\n]+?)`/g;
|
|
29118
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
29119
|
+
const commentLines = findCommentLineRanges(source);
|
|
29108
29120
|
let m;
|
|
29109
29121
|
while ((m = re.exec(source)) !== null) {
|
|
29110
29122
|
const text = m[1] ?? "";
|
|
@@ -29112,7 +29124,14 @@ function extractInlineCodeSpans(source) {
|
|
|
29112
29124
|
const line = upTo.split("\n").length;
|
|
29113
29125
|
const lastNl = upTo.lastIndexOf("\n");
|
|
29114
29126
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
29115
|
-
hits.push({
|
|
29127
|
+
hits.push({
|
|
29128
|
+
text,
|
|
29129
|
+
line,
|
|
29130
|
+
column,
|
|
29131
|
+
index: m.index,
|
|
29132
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
29133
|
+
inComment: commentLines.has(line)
|
|
29134
|
+
});
|
|
29116
29135
|
}
|
|
29117
29136
|
return hits;
|
|
29118
29137
|
}
|
|
@@ -29151,6 +29170,8 @@ function extractFencedCodeBlocks(source) {
|
|
|
29151
29170
|
function extractMarkdownLinks(source) {
|
|
29152
29171
|
const hits = [];
|
|
29153
29172
|
const re = /(?<!\!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
29173
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
29174
|
+
const commentLines = findCommentLineRanges(source);
|
|
29154
29175
|
let m;
|
|
29155
29176
|
while ((m = re.exec(source)) !== null) {
|
|
29156
29177
|
const target = m[2] ?? "";
|
|
@@ -29158,10 +29179,55 @@ function extractMarkdownLinks(source) {
|
|
|
29158
29179
|
const line = upTo.split("\n").length;
|
|
29159
29180
|
const lastNl = upTo.lastIndexOf("\n");
|
|
29160
29181
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
29161
|
-
hits.push({
|
|
29182
|
+
hits.push({
|
|
29183
|
+
target,
|
|
29184
|
+
line,
|
|
29185
|
+
column,
|
|
29186
|
+
index: m.index,
|
|
29187
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
29188
|
+
inComment: commentLines.has(line)
|
|
29189
|
+
});
|
|
29162
29190
|
}
|
|
29163
29191
|
return hits;
|
|
29164
29192
|
}
|
|
29193
|
+
function findBlockCommentRanges(source) {
|
|
29194
|
+
const ranges = [];
|
|
29195
|
+
const stack = [];
|
|
29196
|
+
let i = 0;
|
|
29197
|
+
while (i < source.length) {
|
|
29198
|
+
if (source[i] === "/" && source[i + 1] === "/") {
|
|
29199
|
+
const eol = source.indexOf("\n", i);
|
|
29200
|
+
i = eol === -1 ? source.length : eol + 1;
|
|
29201
|
+
continue;
|
|
29202
|
+
}
|
|
29203
|
+
if (source[i] === "/" && source[i + 1] === "*") {
|
|
29204
|
+
stack.push(i);
|
|
29205
|
+
i += 2;
|
|
29206
|
+
continue;
|
|
29207
|
+
}
|
|
29208
|
+
if (source[i] === "*" && source[i + 1] === "/" && stack.length > 0) {
|
|
29209
|
+
const start = stack.pop();
|
|
29210
|
+
ranges.push([start, i + 2]);
|
|
29211
|
+
i += 2;
|
|
29212
|
+
continue;
|
|
29213
|
+
}
|
|
29214
|
+
i += 1;
|
|
29215
|
+
}
|
|
29216
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
29217
|
+
return ranges;
|
|
29218
|
+
}
|
|
29219
|
+
function isInBlockComment(ranges, pos) {
|
|
29220
|
+
let lo = 0;
|
|
29221
|
+
let hi = ranges.length - 1;
|
|
29222
|
+
while (lo <= hi) {
|
|
29223
|
+
const mid = lo + hi >>> 1;
|
|
29224
|
+
const [start, end] = ranges[mid];
|
|
29225
|
+
if (pos < start) hi = mid - 1;
|
|
29226
|
+
else if (pos >= end) lo = mid + 1;
|
|
29227
|
+
else return true;
|
|
29228
|
+
}
|
|
29229
|
+
return false;
|
|
29230
|
+
}
|
|
29165
29231
|
function declaredPackages(cwd) {
|
|
29166
29232
|
const out = /* @__PURE__ */ new Set();
|
|
29167
29233
|
const pkgPath = (0, import_node_path6.join)(cwd, "package.json");
|
|
@@ -29316,6 +29382,58 @@ function docDriftFromFreshness(score) {
|
|
|
29316
29382
|
if (score >= DOC_FRESHNESS_THRESHOLDS.high) return "high";
|
|
29317
29383
|
return "critical";
|
|
29318
29384
|
}
|
|
29385
|
+
function findCommentLineRanges(source) {
|
|
29386
|
+
const out = /* @__PURE__ */ new Set();
|
|
29387
|
+
const stack = [];
|
|
29388
|
+
let inLineComment = false;
|
|
29389
|
+
const lines = source.split("\n");
|
|
29390
|
+
for (let i = 0; i < lines.length; i++) {
|
|
29391
|
+
const line = lines[i];
|
|
29392
|
+
const lineNo = i + 1;
|
|
29393
|
+
let j = 0;
|
|
29394
|
+
while (j < line.length) {
|
|
29395
|
+
const c = line[j];
|
|
29396
|
+
const next = line[j + 1];
|
|
29397
|
+
if (!stack.length && c === "/" && next === "/") {
|
|
29398
|
+
inLineComment = true;
|
|
29399
|
+
out.add(lineNo);
|
|
29400
|
+
break;
|
|
29401
|
+
}
|
|
29402
|
+
if (c === "/" && next === "*") {
|
|
29403
|
+
stack.push(lineNo);
|
|
29404
|
+
out.add(lineNo);
|
|
29405
|
+
j += 2;
|
|
29406
|
+
continue;
|
|
29407
|
+
}
|
|
29408
|
+
if (c === "*" && next === "/" && stack.length > 0) {
|
|
29409
|
+
stack.pop();
|
|
29410
|
+
j += 2;
|
|
29411
|
+
continue;
|
|
29412
|
+
}
|
|
29413
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
29414
|
+
const quote = c;
|
|
29415
|
+
j += 1;
|
|
29416
|
+
while (j < line.length) {
|
|
29417
|
+
if (line[j] === "\\") {
|
|
29418
|
+
j += 2;
|
|
29419
|
+
continue;
|
|
29420
|
+
}
|
|
29421
|
+
if (line[j] === quote) {
|
|
29422
|
+
j += 1;
|
|
29423
|
+
break;
|
|
29424
|
+
}
|
|
29425
|
+
j += 1;
|
|
29426
|
+
}
|
|
29427
|
+
continue;
|
|
29428
|
+
}
|
|
29429
|
+
j += 1;
|
|
29430
|
+
}
|
|
29431
|
+
if (stack.length > 0) {
|
|
29432
|
+
out.add(lineNo);
|
|
29433
|
+
}
|
|
29434
|
+
}
|
|
29435
|
+
return out;
|
|
29436
|
+
}
|
|
29319
29437
|
var import_node_fs6, import_node_path6, import_globby, DOC_RULE_WEIGHTS, DOC_FRESHNESS_THRESHOLDS;
|
|
29320
29438
|
var init_doc_freshness = __esm({
|
|
29321
29439
|
"src/engine/doc-freshness.ts"() {
|
|
@@ -29340,6 +29458,9 @@ var init_doc_freshness = __esm({
|
|
|
29340
29458
|
});
|
|
29341
29459
|
|
|
29342
29460
|
// src/rules/docs/broken-link.ts
|
|
29461
|
+
function looksLikeRegexSyntax(target) {
|
|
29462
|
+
return /\[[^\]]*\]/.test(target) || /^\s*[?:=!]/.test(target) || /\\(?:s|d|w|S|D|W|t|n|r|[^\\])/.test(target) || /[{}|]/.test(target);
|
|
29463
|
+
}
|
|
29343
29464
|
var import_node_fs7, import_node_path7, brokenLinkRule;
|
|
29344
29465
|
var init_broken_link = __esm({
|
|
29345
29466
|
"src/rules/docs/broken-link.ts"() {
|
|
@@ -29370,6 +29491,8 @@ var init_broken_link = __esm({
|
|
|
29370
29491
|
if (target.startsWith("#")) continue;
|
|
29371
29492
|
if (target.startsWith("//")) continue;
|
|
29372
29493
|
if (target.startsWith("/")) continue;
|
|
29494
|
+
if (link.inBlockComment) continue;
|
|
29495
|
+
if (looksLikeRegexSyntax(target)) continue;
|
|
29373
29496
|
const filePart = target.split("#")[0] ?? target;
|
|
29374
29497
|
if (filePart === "") continue;
|
|
29375
29498
|
const resolved = (0, import_node_path7.join)(docDir, filePart);
|
|
@@ -38545,6 +38668,29 @@ var init_stringly_typed = __esm({
|
|
|
38545
38668
|
}
|
|
38546
38669
|
});
|
|
38547
38670
|
|
|
38671
|
+
// src/engine/parser-shared.ts
|
|
38672
|
+
function parseTreeSitterSource(parserResult, source) {
|
|
38673
|
+
if (!parserResult.ok) return null;
|
|
38674
|
+
if (!source || source.trim() === "") return null;
|
|
38675
|
+
try {
|
|
38676
|
+
const rawTree = parserResult.parser.parse(source);
|
|
38677
|
+
if (!rawTree) return null;
|
|
38678
|
+
const tree = rawTree;
|
|
38679
|
+
if (!tree || !tree.rootNode) return null;
|
|
38680
|
+
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
38681
|
+
return null;
|
|
38682
|
+
}
|
|
38683
|
+
return tree;
|
|
38684
|
+
} catch {
|
|
38685
|
+
return null;
|
|
38686
|
+
}
|
|
38687
|
+
}
|
|
38688
|
+
var init_parser_shared = __esm({
|
|
38689
|
+
"src/engine/parser-shared.ts"() {
|
|
38690
|
+
"use strict";
|
|
38691
|
+
}
|
|
38692
|
+
});
|
|
38693
|
+
|
|
38548
38694
|
// src/engine/parser-rust.ts
|
|
38549
38695
|
function getRustParser() {
|
|
38550
38696
|
if (cachedParser) return { ok: true, parser: cachedParser };
|
|
@@ -38568,21 +38714,7 @@ function effectiveParser() {
|
|
|
38568
38714
|
return getRustParser();
|
|
38569
38715
|
}
|
|
38570
38716
|
function parseRust(source) {
|
|
38571
|
-
|
|
38572
|
-
if (!result.ok) return null;
|
|
38573
|
-
if (!source || source.trim() === "") return null;
|
|
38574
|
-
try {
|
|
38575
|
-
const rawTree = result.parser.parse(source);
|
|
38576
|
-
if (!rawTree) return null;
|
|
38577
|
-
const tree = rawTree;
|
|
38578
|
-
if (!tree || !tree.rootNode) return null;
|
|
38579
|
-
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
38580
|
-
return null;
|
|
38581
|
-
}
|
|
38582
|
-
return tree;
|
|
38583
|
-
} catch {
|
|
38584
|
-
return null;
|
|
38585
|
-
}
|
|
38717
|
+
return parseTreeSitterSource(effectiveParser(), source);
|
|
38586
38718
|
}
|
|
38587
38719
|
function isRustParserAvailable() {
|
|
38588
38720
|
const result = getRustParser();
|
|
@@ -38594,6 +38726,7 @@ var init_parser_rust = __esm({
|
|
|
38594
38726
|
"use strict";
|
|
38595
38727
|
import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
38596
38728
|
import_tree_sitter_rust = __toESM(require("tree-sitter-rust"), 1);
|
|
38729
|
+
init_parser_shared();
|
|
38597
38730
|
cachedParser = null;
|
|
38598
38731
|
rustLanguage = null;
|
|
38599
38732
|
loadError = null;
|
|
@@ -43701,8 +43834,6 @@ function mergeTemplateClassNames(filePath, source, facts, templateClassNames) {
|
|
|
43701
43834
|
}
|
|
43702
43835
|
function extractFacts(filePath, ast, source, supportsRsc = true, framework = "react", config) {
|
|
43703
43836
|
const lineOffsets = buildLineOffsets2(source);
|
|
43704
|
-
const isTypeScript = /\.tsx?$/i.test(filePath);
|
|
43705
|
-
const NON_JSX_FRAMEWORKS = /* @__PURE__ */ new Set(["vue", "svelte", "astro"]);
|
|
43706
43837
|
const facts = {
|
|
43707
43838
|
filePath,
|
|
43708
43839
|
components: [],
|
|
@@ -45641,7 +45772,7 @@ function formatDeltaSuffix(report) {
|
|
|
45641
45772
|
}
|
|
45642
45773
|
function formatVerdict(report) {
|
|
45643
45774
|
const score = report.aiSlopScore;
|
|
45644
|
-
const band =
|
|
45775
|
+
const band = slopScoreBand(score);
|
|
45645
45776
|
const ranked = rankByImpact(report.issues);
|
|
45646
45777
|
const topRule = ranked[0];
|
|
45647
45778
|
const topFile = report.topOffenders?.[0];
|
|
@@ -45724,7 +45855,8 @@ function formatNextStep(report) {
|
|
|
45724
45855
|
` \u2192 \`slopbrick scan --baseline\` to accept today's scores as the new floor`
|
|
45725
45856
|
)
|
|
45726
45857
|
);
|
|
45727
|
-
|
|
45858
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
45859
|
+
if (report.aiSlopScore > meanSlop) {
|
|
45728
45860
|
lines.push(
|
|
45729
45861
|
import_chalk.default.dim(
|
|
45730
45862
|
` \u2192 \`slopbrick scan --why-failing\` for the top 5 issues dragging the score down`
|
|
@@ -45749,17 +45881,18 @@ function formatWhyFailing(report) {
|
|
|
45749
45881
|
return import_chalk.default.green("Nothing is failing the threshold. Score is clean.");
|
|
45750
45882
|
}
|
|
45751
45883
|
const headline = report.aiSlopScore;
|
|
45752
|
-
const
|
|
45753
|
-
const
|
|
45884
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
45885
|
+
const status = headline <= meanSlop ? "PASS" : "FAIL";
|
|
45886
|
+
const colorize = headline <= meanSlop ? import_chalk.default.green : import_chalk.default.red;
|
|
45887
|
+
const qualifier = headline <= meanSlop ? `below ${meanSlop}` : `above ${meanSlop}`;
|
|
45754
45888
|
const lines = [];
|
|
45755
|
-
lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014
|
|
45889
|
+
lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014 ${qualifier})`));
|
|
45756
45890
|
lines.push("");
|
|
45757
45891
|
lines.push(import_chalk.default.bold("Top 5 rules dragging the score down:"));
|
|
45758
45892
|
for (let i = 0; i < ranked.length; i++) {
|
|
45759
45893
|
const [ruleId, info] = ranked[i];
|
|
45760
45894
|
const fileCount = info.files.size;
|
|
45761
45895
|
const fileHint = fileCount > 0 ? ` across ${fileCount} file${fileCount === 1 ? "" : "s"}` : "";
|
|
45762
|
-
const catShort = CATEGORY_GLOSSARY[info.count > 0 ? "" : ""]?.short;
|
|
45763
45896
|
const sampleIssue = report.issues.find((i2) => i2.ruleId === ruleId);
|
|
45764
45897
|
const cat = sampleIssue ? CATEGORY_GLOSSARY[sampleIssue.category]?.short : null;
|
|
45765
45898
|
const catHint = cat ? import_chalk.default.dim(` [${cat}]`) : "";
|
|
@@ -45819,7 +45952,7 @@ function formatCompositeScore(report) {
|
|
|
45819
45952
|
lines.push("");
|
|
45820
45953
|
lines.push(
|
|
45821
45954
|
import_chalk.default.dim(
|
|
45822
|
-
`Repository Coherence: ${coherenceValue} / 100 [${cohBand.label.toUpperCase()}] \u2014 higher = better \xB7 measures internal consistency. This is a secondary view; the Slop
|
|
45955
|
+
`Repository Coherence: ${coherenceValue} / 100 [${cohBand.label.toUpperCase()}] \u2014 higher = better \xB7 measures internal consistency. This is a secondary view; the AI Slop Score above is the gate.`
|
|
45823
45956
|
)
|
|
45824
45957
|
);
|
|
45825
45958
|
}
|
|
@@ -45949,13 +46082,12 @@ function formatDriftSection(report) {
|
|
|
45949
46082
|
return lines.join("\n");
|
|
45950
46083
|
}
|
|
45951
46084
|
function formatThresholds(report) {
|
|
45952
|
-
const
|
|
46085
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
45953
46086
|
const headline = report.aiSlopScore;
|
|
45954
|
-
const passed = headline
|
|
45955
|
-
const valueText = `${headline.toFixed(1)} \
|
|
46087
|
+
const passed = headline <= meanSlop;
|
|
46088
|
+
const valueText = `${headline.toFixed(1)} \u2264 ${meanSlop}`.padStart(12, " ");
|
|
45956
46089
|
const status = passed ? import_chalk.default.green("pass") : import_chalk.default.red("fail");
|
|
45957
46090
|
const result = ["Threshold (CI gate)", ` AI Slop Score ${valueText} ${status}`];
|
|
45958
|
-
void limit;
|
|
45959
46091
|
return result;
|
|
45960
46092
|
}
|
|
45961
46093
|
function formatTopComponents(components, topOffenders) {
|
|
@@ -46033,9 +46165,10 @@ function formatPretty(report) {
|
|
|
46033
46165
|
}
|
|
46034
46166
|
return sections.join("\n\n");
|
|
46035
46167
|
}
|
|
46036
|
-
function formatScoringExplainer(
|
|
46168
|
+
function formatScoringExplainer(report) {
|
|
46169
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
46037
46170
|
return import_chalk.default.dim(
|
|
46038
|
-
|
|
46171
|
+
`Four orthogonal scores (all 0-100, **lower AI Slop Score = cleaner**): AI Slop Score (AI-slop signatures; the CI gate, AI Slop Score \u2264 ${meanSlop} passes), Engineering Hygiene (issues per category across arch/logic/layout/component/test; higher = cleaner), Security (AI-flagged security risks, inverted from risk level; higher = cleaner), Repository Health (composite: 0.4*AI Slop Score + 0.3*Engineering Hygiene + 0.2*Security + 0.1*Test Quality). Only AI Slop Score gates CI; the others are informational. Default-off rules (INVERTED/NOISY/DORMANT) are suppressed from the scores automatically.`
|
|
46039
46172
|
);
|
|
46040
46173
|
}
|
|
46041
46174
|
function formatWhyFailingReport(report) {
|
|
@@ -46053,7 +46186,7 @@ function formatBriefReport(report) {
|
|
|
46053
46186
|
];
|
|
46054
46187
|
const deltaSuffix = formatDeltaSuffix(report);
|
|
46055
46188
|
scoreLines.forEach(({ label, field, value }, idx) => {
|
|
46056
|
-
const band = scoreBand(value);
|
|
46189
|
+
const band = field === "aiSlopScore" ? slopScoreBand(value) : scoreBand(value);
|
|
46057
46190
|
const paddedLabel = label.padEnd(20, " ");
|
|
46058
46191
|
const valueStr = value.toFixed(0).padStart(3, " ");
|
|
46059
46192
|
const delta = idx === 0 ? deltaSuffix : "";
|
|
@@ -46061,11 +46194,12 @@ function formatBriefReport(report) {
|
|
|
46061
46194
|
` ${paddedLabel} ${band.color(valueStr)} ${import_chalk.default.dim(band.label)} ${import_chalk.default.dim.italic(`(${field})`)}${delta}`
|
|
46062
46195
|
);
|
|
46063
46196
|
});
|
|
46064
|
-
const
|
|
46197
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
46198
|
+
const passed = report.aiSlopScore <= meanSlop;
|
|
46065
46199
|
lines.push("");
|
|
46066
46200
|
lines.push(
|
|
46067
46201
|
import_chalk.default.dim(
|
|
46068
|
-
` CI gate: AI Slop Score <=
|
|
46202
|
+
` CI gate: AI Slop Score <= ${meanSlop} -> ${passed ? import_chalk.default.green("pass") : import_chalk.default.red("fail")}`
|
|
46069
46203
|
)
|
|
46070
46204
|
);
|
|
46071
46205
|
const suppressed = report.defaultOffSuppressedCount ?? 0;
|
|
@@ -46210,19 +46344,22 @@ function thresholdExceeded(report, config) {
|
|
|
46210
46344
|
return categoryThresholdBreached(report, config.thresholds.categoryThresholds);
|
|
46211
46345
|
}
|
|
46212
46346
|
function failedThresholdCount(report, config) {
|
|
46213
|
-
|
|
46214
|
-
|
|
46215
|
-
|
|
46216
|
-
|
|
46347
|
+
return failedThresholds(report, config).length;
|
|
46348
|
+
}
|
|
46349
|
+
function failedThresholds(report, config) {
|
|
46350
|
+
const failed = [];
|
|
46351
|
+
if (report.aiSlopScore > config.thresholds.meanSlop) failed.push("meanSlop");
|
|
46352
|
+
if (report.p90Score > config.thresholds.p90Slop) failed.push("p90Slop");
|
|
46353
|
+
if (report.peakScore > config.thresholds.individualSlopThreshold) failed.push("individualSlopThreshold");
|
|
46217
46354
|
const cat = config.thresholds.categoryThresholds;
|
|
46218
46355
|
if (cat) {
|
|
46219
46356
|
for (const [category, limit] of Object.entries(cat)) {
|
|
46220
46357
|
if (limit === void 0) continue;
|
|
46221
46358
|
const score = report.categoryScores[category];
|
|
46222
|
-
if (score !== void 0 && score > limit)
|
|
46359
|
+
if (score !== void 0 && score > limit) failed.push(`category:${category}`);
|
|
46223
46360
|
}
|
|
46224
46361
|
}
|
|
46225
|
-
return
|
|
46362
|
+
return failed;
|
|
46226
46363
|
}
|
|
46227
46364
|
function categoryThresholdBreached(report, categoryThresholds) {
|
|
46228
46365
|
if (!categoryThresholds) return false;
|
|
@@ -50890,9 +51027,22 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
|
|
|
50890
51027
|
}
|
|
50891
51028
|
}
|
|
50892
51029
|
if (currentTopFiles.length > 0 && runs.length >= CONSECUTIVE_THRESHOLD) {
|
|
51030
|
+
const latestRun = runs[runs.length - 1];
|
|
51031
|
+
const isNoiseTier = (id) => {
|
|
51032
|
+
const rule = ruleById.get(id);
|
|
51033
|
+
if (!rule) return false;
|
|
51034
|
+
if (rule.defaultOff === true) return true;
|
|
51035
|
+
if (rule.aiSpecific && (rule.severity === "medium" || rule.severity === "low")) return true;
|
|
51036
|
+
return false;
|
|
51037
|
+
};
|
|
51038
|
+
const onRulesInLatestTop = latestRun.topOffenseIds.filter(
|
|
51039
|
+
(id) => !isNoiseTier(id)
|
|
51040
|
+
);
|
|
51041
|
+
const skipChronicOnDefaultOffNoise = onRulesInLatestTop.length === 0;
|
|
50893
51042
|
for (const file of currentTopFiles) {
|
|
50894
51043
|
const consecutive = countConsecutiveTopFileAppearances(file.hash, recentTopHashes);
|
|
50895
51044
|
if (consecutive >= CONSECUTIVE_THRESHOLD) {
|
|
51045
|
+
if (skipChronicOnDefaultOffNoise) continue;
|
|
50896
51046
|
hotspotIssues.push({
|
|
50897
51047
|
ruleId: "component/chronic-offender",
|
|
50898
51048
|
category: "component",
|
|
@@ -50902,7 +51052,7 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
|
|
|
50902
51052
|
filePath: file.filePath,
|
|
50903
51053
|
line: 1,
|
|
50904
51054
|
column: 1,
|
|
50905
|
-
advice: "
|
|
51055
|
+
advice: "Run `slopbrick scan --baseline` to accept current scores as the new floor, or refactor the file to reduce its issue count."
|
|
50906
51056
|
});
|
|
50907
51057
|
}
|
|
50908
51058
|
}
|
|
@@ -53236,16 +53386,16 @@ function axisSlopIndex(slopIndex) {
|
|
|
53236
53386
|
if (slopIndex === void 0 || Number.isNaN(slopIndex)) {
|
|
53237
53387
|
return {
|
|
53238
53388
|
axis: "slopIndex",
|
|
53239
|
-
label: "Slop
|
|
53389
|
+
label: "AI Slop Score (raw)",
|
|
53240
53390
|
health: 70,
|
|
53241
53391
|
source: "default (not measured) \u2014 neutral 70"
|
|
53242
53392
|
};
|
|
53243
53393
|
}
|
|
53244
53394
|
return {
|
|
53245
53395
|
axis: "slopIndex",
|
|
53246
|
-
label: "Slop
|
|
53396
|
+
label: "AI Slop Score (raw)",
|
|
53247
53397
|
health: clamp100(100 - slopIndex),
|
|
53248
|
-
source: `100 -
|
|
53398
|
+
source: `100 - aiSlopScore (${slopIndex.toFixed(1)}) \u2192 inverted to cleanliness`
|
|
53249
53399
|
};
|
|
53250
53400
|
}
|
|
53251
53401
|
function axisArchitecture(architectureConsistency) {
|
|
@@ -54193,6 +54343,16 @@ function assembleScanReport(input) {
|
|
|
54193
54343
|
enrichment
|
|
54194
54344
|
} = input;
|
|
54195
54345
|
const prSlopScore = computePrSlopScore(diffRef, allIssues);
|
|
54346
|
+
const thresholdReport = {
|
|
54347
|
+
aiSlopScore: aggregated.aiSlopScore,
|
|
54348
|
+
p90Score: aggregated.p90Score,
|
|
54349
|
+
peakScore: aggregated.peakScore,
|
|
54350
|
+
categoryScores: aggregated.categoryScores
|
|
54351
|
+
};
|
|
54352
|
+
const failedThresholds_ = failedThresholds(
|
|
54353
|
+
thresholdReport,
|
|
54354
|
+
config
|
|
54355
|
+
);
|
|
54196
54356
|
return {
|
|
54197
54357
|
version: VERSION,
|
|
54198
54358
|
generatedAt,
|
|
@@ -54248,6 +54408,11 @@ function assembleScanReport(input) {
|
|
|
54248
54408
|
parseErrors: parseErrors.length > 0 ? parseErrors : void 0,
|
|
54249
54409
|
baseline: baselineMeta,
|
|
54250
54410
|
thresholds: config.thresholds,
|
|
54411
|
+
// v0.42.0 (user-review fix): expose which gates tripped so CI
|
|
54412
|
+
// consumers can read the JSON output instead of grepping stderr.
|
|
54413
|
+
// Optional field: absent when no thresholds tripped. Computed from
|
|
54414
|
+
// the report's own scores so the JSON is self-describing.
|
|
54415
|
+
failedThresholds: failedThresholds_,
|
|
54251
54416
|
topOffenders: topOffenders.length > 0 ? topOffenders : void 0,
|
|
54252
54417
|
coherence: enrichment.coherence,
|
|
54253
54418
|
coherenceBreakdown: enrichment.coherenceBreakdown,
|
|
@@ -54264,6 +54429,7 @@ var PR_SLOP_WEIGHTS;
|
|
|
54264
54429
|
var init_assembleScanReport = __esm({
|
|
54265
54430
|
"src/cli/report/assembleScanReport.ts"() {
|
|
54266
54431
|
"use strict";
|
|
54432
|
+
init_threshold();
|
|
54267
54433
|
init_types();
|
|
54268
54434
|
PR_SLOP_WEIGHTS = { high: 10, medium: 5, low: 1 };
|
|
54269
54435
|
}
|
|
@@ -55756,11 +55922,13 @@ function extractNamedScores(report) {
|
|
|
55756
55922
|
};
|
|
55757
55923
|
}
|
|
55758
55924
|
function renderThresholds(report) {
|
|
55759
|
-
const
|
|
55760
|
-
const
|
|
55761
|
-
const
|
|
55762
|
-
const
|
|
55763
|
-
const
|
|
55925
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
55926
|
+
const aiAvailable = typeof report.aiSlopScore === "number";
|
|
55927
|
+
const aiPassed = aiAvailable && report.aiSlopScore <= meanSlop;
|
|
55928
|
+
const rh = report.repositoryHealth;
|
|
55929
|
+
const rhAvailable = typeof rh === "number";
|
|
55930
|
+
const rhLimit = 70;
|
|
55931
|
+
const rhPassed = rhAvailable && rh >= rhLimit;
|
|
55764
55932
|
return `
|
|
55765
55933
|
<section class="thresholds-section">
|
|
55766
55934
|
<h2>Thresholds</h2>
|
|
@@ -55774,9 +55942,14 @@ function renderThresholds(report) {
|
|
|
55774
55942
|
</thead>
|
|
55775
55943
|
<tbody>
|
|
55776
55944
|
<tr>
|
|
55777
|
-
<td>
|
|
55778
|
-
<td>${
|
|
55779
|
-
<td><span class="status-badge ${thresholdStatusClass(
|
|
55945
|
+
<td>AI Slop Score (CI gate)</td>
|
|
55946
|
+
<td>${aiAvailable ? `${report.aiSlopScore.toFixed(1)} / ${meanSlop}` : "\u2013"}</td>
|
|
55947
|
+
<td><span class="status-badge ${thresholdStatusClass(!aiPassed)}">${aiAvailable ? aiPassed ? "pass" : "fail" : "n/a"}</span></td>
|
|
55948
|
+
</tr>
|
|
55949
|
+
<tr>
|
|
55950
|
+
<td>Repository Health (informational)</td>
|
|
55951
|
+
<td>${rhAvailable ? `${rh.toFixed(1)} / ${rhLimit}` : "\u2013"}</td>
|
|
55952
|
+
<td><span class="status-badge ${thresholdStatusClass(!rhPassed)}">${rhAvailable ? rhPassed ? "pass" : "fail" : "n/a"}</span></td>
|
|
55780
55953
|
</tr>
|
|
55781
55954
|
</tbody>
|
|
55782
55955
|
</table>
|
|
@@ -57337,12 +57510,16 @@ async function runScan(options, explicitPaths) {
|
|
|
57337
57510
|
files = intersectFiles(files, since, cwd);
|
|
57338
57511
|
}
|
|
57339
57512
|
let incrementalSummary;
|
|
57513
|
+
let cachePath3;
|
|
57514
|
+
let existingCache;
|
|
57515
|
+
let unchanged = [];
|
|
57340
57516
|
if (options.incremental) {
|
|
57341
|
-
|
|
57342
|
-
|
|
57343
|
-
const
|
|
57344
|
-
files = toScan;
|
|
57345
|
-
|
|
57517
|
+
cachePath3 = options.cachePath ?? ".slopbrick-cache.json";
|
|
57518
|
+
existingCache = loadCache(cachePath3);
|
|
57519
|
+
const partition = partitionByCache(files, existingCache);
|
|
57520
|
+
files = partition.toScan;
|
|
57521
|
+
unchanged = partition.unchanged;
|
|
57522
|
+
incrementalSummary = { skipped: unchanged.length, rescanned: partition.toScan.length };
|
|
57346
57523
|
}
|
|
57347
57524
|
const machineReadableStdout = !!options.json || !!options.html || options.format === "json" || options.format === "sarif" || options.format === "html";
|
|
57348
57525
|
if (files.length === 0 && !options.quiet && !machineReadableStdout) {
|
|
@@ -57362,6 +57539,9 @@ async function runScan(options, explicitPaths) {
|
|
|
57362
57539
|
logger.warn(
|
|
57363
57540
|
`No source files matched in ${cwd}. Adjust --include or check your config at ${projectRoot}.`
|
|
57364
57541
|
);
|
|
57542
|
+
logger.warn(
|
|
57543
|
+
"Tip: run from a directory with source files, or pass --workspace <path> (monorepos usually want `--workspace packages/<name>`)."
|
|
57544
|
+
);
|
|
57365
57545
|
}
|
|
57366
57546
|
}
|
|
57367
57547
|
const configHash = hashConfig(config);
|
|
@@ -57511,6 +57691,38 @@ async function runScan(options, explicitPaths) {
|
|
|
57511
57691
|
telemetryEnabled,
|
|
57512
57692
|
machineReadableStdout
|
|
57513
57693
|
});
|
|
57694
|
+
if (options.incremental && cachePath3 !== void 0) {
|
|
57695
|
+
const cachedFiles = {};
|
|
57696
|
+
for (const r of results) {
|
|
57697
|
+
try {
|
|
57698
|
+
cachedFiles[r.filePath] = {
|
|
57699
|
+
hash: computeFileHash(r.filePath),
|
|
57700
|
+
issueCount: r.issues.length,
|
|
57701
|
+
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
57702
|
+
};
|
|
57703
|
+
} catch {
|
|
57704
|
+
}
|
|
57705
|
+
}
|
|
57706
|
+
for (const f of unchanged) {
|
|
57707
|
+
const cached = existingCache?.files[f];
|
|
57708
|
+
if (cached) {
|
|
57709
|
+
cachedFiles[f] = {
|
|
57710
|
+
hash: cached.hash,
|
|
57711
|
+
issueCount: cached.issueCount,
|
|
57712
|
+
lastScannedAt: cached.lastScannedAt
|
|
57713
|
+
};
|
|
57714
|
+
} else {
|
|
57715
|
+
}
|
|
57716
|
+
}
|
|
57717
|
+
try {
|
|
57718
|
+
saveCache(cachePath3, {
|
|
57719
|
+
version: VERSION,
|
|
57720
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
57721
|
+
files: cachedFiles
|
|
57722
|
+
});
|
|
57723
|
+
} catch {
|
|
57724
|
+
}
|
|
57725
|
+
}
|
|
57514
57726
|
return {
|
|
57515
57727
|
report,
|
|
57516
57728
|
scores,
|
|
@@ -57657,6 +57869,7 @@ __export(src_exports, {
|
|
|
57657
57869
|
directoryScore: () => directoryScore,
|
|
57658
57870
|
extractSignatures: () => extractSignatures,
|
|
57659
57871
|
failedThresholdCount: () => failedThresholdCount,
|
|
57872
|
+
failedThresholds: () => failedThresholds,
|
|
57660
57873
|
filterByDisabledDirectives: () => filterByDisabledDirectives,
|
|
57661
57874
|
filterIssues: () => filterIssues,
|
|
57662
57875
|
findSimilarFunctions: () => findSimilarFunctions,
|
|
@@ -57742,6 +57955,7 @@ init_scan2();
|
|
|
57742
57955
|
init_scan2();
|
|
57743
57956
|
|
|
57744
57957
|
// src/cli/init.ts
|
|
57958
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
57745
57959
|
var import_node_fs31 = require("fs");
|
|
57746
57960
|
var import_node_path34 = require("path");
|
|
57747
57961
|
var import_node_readline = require("readline");
|
|
@@ -57750,6 +57964,7 @@ init_discover();
|
|
|
57750
57964
|
init_git();
|
|
57751
57965
|
init_cache();
|
|
57752
57966
|
init_logger();
|
|
57967
|
+
init_types();
|
|
57753
57968
|
|
|
57754
57969
|
// src/rules/registry-loader.ts
|
|
57755
57970
|
var import_node_fs30 = require("fs");
|
|
@@ -57924,7 +58139,12 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
|
|
|
57924
58139
|
snapshot: shadcn_registry_default,
|
|
57925
58140
|
ok: true,
|
|
57926
58141
|
fresh: false,
|
|
57927
|
-
|
|
58142
|
+
// v0.42.0 (user-review fix): the previous message said
|
|
58143
|
+
// "upstream URL returns 404" which is alarming without context —
|
|
58144
|
+
// upstream has been dead since v0.39.0, and the bundled snapshot
|
|
58145
|
+
// IS the snapshot in use. Replaced with a neutral message that
|
|
58146
|
+
// doesn't suggest "something is wrong".
|
|
58147
|
+
message: "Using bundled shadcn/ui snapshot (built-in; no network fetch)."
|
|
57928
58148
|
};
|
|
57929
58149
|
}
|
|
57930
58150
|
function copyBundledSnapshotToCache(cwd) {
|
|
@@ -58095,16 +58315,23 @@ async function runDoctor(cwd) {
|
|
|
58095
58315
|
lines.push("slopbrick doctor\n");
|
|
58096
58316
|
lines.push("Checking your setup:\n");
|
|
58097
58317
|
const nodeMajor = parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
58098
|
-
if (nodeMajor >= 20) ok(`Node ${process.versions.node}`);
|
|
58318
|
+
if (nodeMajor >= 20) ok(`Node ${process.versions.node}, slopbrick ${VERSION}`);
|
|
58099
58319
|
else fail(`Node ${process.versions.node} \u2014 slopbrick needs Node 20 or newer. Run: nvm install 20`);
|
|
58100
58320
|
lines.push(`
|
|
58101
58321
|
Working in: ${cwd}`);
|
|
58102
58322
|
try {
|
|
58323
|
+
const configPath = resolveConfigPath(cwd);
|
|
58103
58324
|
const config = await loadConfig(cwd);
|
|
58104
|
-
|
|
58105
|
-
|
|
58106
|
-
|
|
58107
|
-
|
|
58325
|
+
if (configPath) {
|
|
58326
|
+
const ruleCount = Object.keys(config.rules ?? {}).length;
|
|
58327
|
+
lines.push(`
|
|
58328
|
+
\u2713 Config loaded from: ${configPath}`);
|
|
58329
|
+
lines.push(` ${ruleCount} rule${ruleCount === 1 ? "" : "s"} configured in your config file.`);
|
|
58330
|
+
} else {
|
|
58331
|
+
lines.push(`
|
|
58332
|
+
\u2713 Config: using built-in defaults (no config file found).`);
|
|
58333
|
+
lines.push(import_chalk2.default.dim(` Run \`slopbrick init\` to create ${cwd.endsWith("/") ? cwd : cwd + "/"}slopbrick.config.mjs and tune rule severity/rules to your project.`));
|
|
58334
|
+
}
|
|
58108
58335
|
} catch (error) {
|
|
58109
58336
|
fail(`Could not read your config: ${error.message}`);
|
|
58110
58337
|
exitCode = 3;
|
|
@@ -58161,6 +58388,7 @@ async function runDoctor(cwd) {
|
|
|
58161
58388
|
ok(`Found ${scannable.length} source files to scan.`);
|
|
58162
58389
|
} else {
|
|
58163
58390
|
warn("No source files (.ts/.tsx/.js/.jsx/.vue/.svelte/.astro/.mdx) found in this directory.");
|
|
58391
|
+
lines.push(import_chalk2.default.dim(" Run from a directory with source files, or pass --workspace <path>. Monorepos usually want `--workspace packages/<name>`."));
|
|
58164
58392
|
}
|
|
58165
58393
|
} catch {
|
|
58166
58394
|
}
|
|
@@ -58225,7 +58453,7 @@ function registerBadge(program) {
|
|
|
58225
58453
|
const health = loadHealth2(cwd);
|
|
58226
58454
|
if (health) {
|
|
58227
58455
|
const synthetic = {
|
|
58228
|
-
|
|
58456
|
+
aiSlopScore: health.aiSlopScore ?? 0
|
|
58229
58457
|
};
|
|
58230
58458
|
logger.info(formatBadge(synthetic));
|
|
58231
58459
|
process.exit(0);
|
|
@@ -58769,7 +58997,11 @@ init_logger();
|
|
|
58769
58997
|
init_builtins();
|
|
58770
58998
|
init_data();
|
|
58771
58999
|
function registerExplain(program) {
|
|
58772
|
-
program.command("explain
|
|
59000
|
+
program.command("explain [ruleId]").description("Print rationale, pattern, and remediation for a single rule. With no ruleId, lists the rules.").action((ruleId) => {
|
|
59001
|
+
if (!ruleId) {
|
|
59002
|
+
logger.warn("Missing ruleId. Run `slopbrick rules` to see available rules, then `slopbrick explain <ruleId>`.");
|
|
59003
|
+
process.exit(2);
|
|
59004
|
+
}
|
|
58773
59005
|
const result = explainRule(ruleId, builtinRules, RULE_HINTS);
|
|
58774
59006
|
logger.info(formatExplain(result));
|
|
58775
59007
|
if ("error" in result) process.exit(2);
|
|
@@ -62688,7 +62920,7 @@ init_structure_md();
|
|
|
62688
62920
|
var TOOL_DEFINITIONS = [
|
|
62689
62921
|
{
|
|
62690
62922
|
name: "slop_scan_file",
|
|
62691
|
-
description:
|
|
62923
|
+
description: 'Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, category, severity, line, column, message, advice), a composite AI-likelihood score (probability + confidenceTier), and a componentCount. The composite score is the Bayesian log-likelihood ratio of all rules that fired, NOT a per-file "Slop Index" \u2014 for project-level scores use slop_suggest.',
|
|
62692
62924
|
inputSchema: {
|
|
62693
62925
|
type: "object",
|
|
62694
62926
|
properties: {
|
|
@@ -62704,7 +62936,7 @@ var TOOL_DEFINITIONS = [
|
|
|
62704
62936
|
},
|
|
62705
62937
|
{
|
|
62706
62938
|
name: "slop_explain_rule",
|
|
62707
|
-
description: "Return metadata for a single rule
|
|
62939
|
+
description: "Return metadata for a single rule: ruleId, category, severity, aiSpecific, a brief rationale string, and a whereToLook path into src/rules/. The actual fix-steps live in the rule source (whereToLook) rather than in this response \u2014 read that file to understand the rule before auto-applying --fix.",
|
|
62708
62940
|
inputSchema: {
|
|
62709
62941
|
type: "object",
|
|
62710
62942
|
properties: {
|
|
@@ -62725,7 +62957,7 @@ var TOOL_DEFINITIONS = [
|
|
|
62725
62957
|
},
|
|
62726
62958
|
{
|
|
62727
62959
|
name: "slop_suggest",
|
|
62728
|
-
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate),
|
|
62960
|
+
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), the declared stack, and (when .slopbrick/health.json exists) a Bayesian composite AI-likelihood score. Call this BEFORE writing new code so the agent reuses existing patterns instead of duplicating them. For per-issue details or per-file hot-spots, use slop_scan_file on each candidate path.",
|
|
62729
62961
|
inputSchema: {
|
|
62730
62962
|
type: "object",
|
|
62731
62963
|
properties: {
|
|
@@ -62757,7 +62989,7 @@ var TOOL_DEFINITIONS = [
|
|
|
62757
62989
|
// call slop_suggest and read repositoryHealth /
|
|
62758
62990
|
// architectureConsistency / businessLogicCoherence.
|
|
62759
62991
|
name: "slop_check_constitution",
|
|
62760
|
-
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns
|
|
62992
|
+
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns the file path, total import + violation counts, the parsed imports, the list of violations (each with import, category, and reason), and a conventionSource indicating whether the constitution was declared, detected, or absent. Use this on a newly-written or modified file before suggesting a PR.",
|
|
62761
62993
|
inputSchema: {
|
|
62762
62994
|
type: "object",
|
|
62763
62995
|
properties: {
|
|
@@ -62774,7 +63006,7 @@ var TOOL_DEFINITIONS = [
|
|
|
62774
63006
|
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
62775
63007
|
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
62776
63008
|
name: "slop_find_similar",
|
|
62777
|
-
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns
|
|
63009
|
+
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns up to `limit` matches (default 10) with name, file, line, fingerprint (sha256 over signature), hooks, props, params, and similarity in [0, 1].",
|
|
62778
63010
|
inputSchema: {
|
|
62779
63011
|
type: "object",
|
|
62780
63012
|
properties: {
|
|
@@ -63005,13 +63237,6 @@ async function runFindSimilar(args, ctx) {
|
|
|
63005
63237
|
}
|
|
63006
63238
|
}
|
|
63007
63239
|
async function handleToolCall(toolName, args, ctx) {
|
|
63008
|
-
const deprecation = getDeprecation(toolName);
|
|
63009
|
-
const deprecationNotice = deprecation ? {
|
|
63010
|
-
tool: toolName,
|
|
63011
|
-
replacedBy: deprecation.replacedBy,
|
|
63012
|
-
removedIn: deprecation.removedIn ?? "next major",
|
|
63013
|
-
reason: deprecation.reason
|
|
63014
|
-
} : void 0;
|
|
63015
63240
|
switch (toolName) {
|
|
63016
63241
|
case "slop_scan_file":
|
|
63017
63242
|
return runScanFile(args, ctx);
|
|
@@ -63219,7 +63444,7 @@ var import_node_path46 = require("path");
|
|
|
63219
63444
|
init_logger();
|
|
63220
63445
|
init_dist();
|
|
63221
63446
|
function registerCi(program, scanAction) {
|
|
63222
|
-
program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if
|
|
63447
|
+
program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if aiSlopScore exceeds this number (lower = cleaner since v0.21)", parseCount).option("--max-new-issues <n>", "exit 1 if new issues (vs .slop-audit-cache.json) exceed this number", parseCount).option("--strict-constitution", "exit 1 on any constitution violation").option("--format <pretty|json>", "output format", "json").action(
|
|
63223
63448
|
async (cmdOptions, command) => {
|
|
63224
63449
|
const globals = command.optsWithGlobals();
|
|
63225
63450
|
const options = {
|
|
@@ -63559,7 +63784,25 @@ function registerRules(program) {
|
|
|
63559
63784
|
const sev = r.severity.padEnd(8);
|
|
63560
63785
|
const tag = r.aiSpecific ? "[AI]" : " ";
|
|
63561
63786
|
lines.push(` ${sev} ${tag} ${r.id}`);
|
|
63562
|
-
if (r.description)
|
|
63787
|
+
if (r.description) {
|
|
63788
|
+
const cols = process.stdout.isTTY ? process.stdout.columns ?? 100 : 100;
|
|
63789
|
+
const indent = " ";
|
|
63790
|
+
const firstCol = indent.length + 1;
|
|
63791
|
+
const maxWidth = Math.max(40, cols - firstCol);
|
|
63792
|
+
const words = r.description.split(" ");
|
|
63793
|
+
let line = "";
|
|
63794
|
+
for (const word of words) {
|
|
63795
|
+
if (line === "") {
|
|
63796
|
+
line = word;
|
|
63797
|
+
} else if ((line + " " + word).length > maxWidth) {
|
|
63798
|
+
lines.push(indent + line);
|
|
63799
|
+
line = word;
|
|
63800
|
+
} else {
|
|
63801
|
+
line = line + " " + word;
|
|
63802
|
+
}
|
|
63803
|
+
}
|
|
63804
|
+
if (line) lines.push(indent + line);
|
|
63805
|
+
}
|
|
63563
63806
|
}
|
|
63564
63807
|
}
|
|
63565
63808
|
logger.info(lines.join("\n"));
|
|
@@ -64404,19 +64647,20 @@ function trendToText(report) {
|
|
|
64404
64647
|
}
|
|
64405
64648
|
const slopValues = report.points.map((p) => p.slopIndex);
|
|
64406
64649
|
const spark = sparkline(slopValues);
|
|
64407
|
-
lines.push("Slop
|
|
64650
|
+
lines.push("AI Slop Score trend (last " + report.points.length + " scans, " + report.totalScans + " total; lower = cleaner since v0.21):");
|
|
64408
64651
|
lines.push("");
|
|
64409
64652
|
lines.push(" " + spark + " " + slopValues[0].toFixed(1) + " -> " + slopValues[slopValues.length - 1].toFixed(1));
|
|
64410
64653
|
lines.push("");
|
|
64411
64654
|
if (report.first && report.latest) {
|
|
64412
64655
|
const direction = report.delta < 0 ? "improved" : report.delta > 0 ? "regressed" : "unchanged";
|
|
64413
64656
|
const arrow = report.delta < 0 ? "v" : report.delta > 0 ? "^" : "=";
|
|
64414
|
-
lines.push(" " + arrow + " Slop
|
|
64657
|
+
lines.push(" " + arrow + " AI Slop Score " + direction + " by " + Math.abs(report.delta).toFixed(1) + " points (lower = cleaner since v0.21)");
|
|
64415
64658
|
lines.push(" " + report.first.timestamp + " -> " + report.latest.timestamp);
|
|
64416
64659
|
}
|
|
64417
64660
|
lines.push("");
|
|
64418
64661
|
lines.push("Recent scans:");
|
|
64419
|
-
lines.push(" Timestamp Slop
|
|
64662
|
+
lines.push(" Timestamp AI Slop Health Framework Issues");
|
|
64663
|
+
lines.push(" (lower=cleaner)");
|
|
64420
64664
|
lines.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500");
|
|
64421
64665
|
for (const p of report.points.slice(-10)) {
|
|
64422
64666
|
const ts = p.timestamp.slice(0, 19).replace("T", " ");
|
|
@@ -64428,14 +64672,14 @@ function trendToText(report) {
|
|
|
64428
64672
|
}
|
|
64429
64673
|
function trendToMarkdown(report) {
|
|
64430
64674
|
const lines = [];
|
|
64431
|
-
lines.push("# Slop
|
|
64675
|
+
lines.push("# AI Slop Score Trend (lower = cleaner since v0.21)");
|
|
64432
64676
|
lines.push("");
|
|
64433
64677
|
if (report.points.length === 0) {
|
|
64434
64678
|
lines.push("No telemetry data yet. Run `slopbrick scan` to start tracking.");
|
|
64435
64679
|
return lines.join("\n");
|
|
64436
64680
|
}
|
|
64437
64681
|
const slopValues = report.points.map((p) => p.slopIndex);
|
|
64438
|
-
lines.push("Slop
|
|
64682
|
+
lines.push("AI Slop Score sparkline (last " + report.points.length + " scans; lower = cleaner since v0.21):");
|
|
64439
64683
|
lines.push("");
|
|
64440
64684
|
lines.push("```");
|
|
64441
64685
|
lines.push(sparkline(slopValues));
|
|
@@ -64443,10 +64687,10 @@ function trendToMarkdown(report) {
|
|
|
64443
64687
|
lines.push("");
|
|
64444
64688
|
if (report.first && report.latest) {
|
|
64445
64689
|
const direction = report.delta < 0 ? "improved" : report.delta > 0 ? "regressed" : "unchanged";
|
|
64446
|
-
lines.push("Slop
|
|
64690
|
+
lines.push("AI Slop Score " + direction + " by **" + Math.abs(report.delta).toFixed(1) + " points** between first and latest scan (lower = cleaner since v0.21).");
|
|
64447
64691
|
}
|
|
64448
64692
|
lines.push("");
|
|
64449
|
-
lines.push("| Timestamp | Slop
|
|
64693
|
+
lines.push("| Timestamp | AI Slop Score (lower = cleaner) | Health | Framework | Issues |");
|
|
64450
64694
|
lines.push("|-----------|-----------:|-------:|-----------|-------:|");
|
|
64451
64695
|
for (const p of report.points) {
|
|
64452
64696
|
lines.push("| " + p.timestamp + " | " + p.slopIndex.toFixed(1) + " | " + p.assemblyHealth.toFixed(1) + " | " + p.framework + " | " + p.totalIssues + " |");
|
|
@@ -64456,7 +64700,7 @@ function trendToMarkdown(report) {
|
|
|
64456
64700
|
|
|
64457
64701
|
// src/cli/commands/trend.ts
|
|
64458
64702
|
function registerTrend(program) {
|
|
64459
|
-
program.command("trend").description("Slop
|
|
64703
|
+
program.command("trend").description("AI Slop Score over time, from .slopbrick/flywheel/scans.jsonl (lower = cleaner since v0.21)").option("--max-points <n>", "how many recent scans to plot", parseCount, 30).option("--render <kind>", "output rendering: text | markdown", "text").action((cmdOptions) => {
|
|
64460
64704
|
try {
|
|
64461
64705
|
const cwd = process.cwd();
|
|
64462
64706
|
const report = buildTrend(cwd, cmdOptions.maxPoints);
|
|
@@ -64464,7 +64708,14 @@ function registerTrend(program) {
|
|
|
64464
64708
|
logger.info(out);
|
|
64465
64709
|
if (report.delta > 1) {
|
|
64466
64710
|
logger.info("");
|
|
64467
|
-
logger.info(
|
|
64711
|
+
logger.info(
|
|
64712
|
+
// v0.42.0: the legacy 'Slop Index increased' message implied higher
|
|
64713
|
+
// = worse, which is the v0.15-v0.20.1 inverted reading. Since v0.21,
|
|
64714
|
+
// AI Slop Score is lower-is-better; an INCREASE is worse, but
|
|
64715
|
+
// the warning should say "AI Slop Score got worse" or just
|
|
64716
|
+
// "delta went up" so users don't have to remember the direction.
|
|
64717
|
+
`Warning: AI Slop Score got worse by ${report.delta.toFixed(1)} points over this period (lower = cleaner since v0.21).`
|
|
64718
|
+
);
|
|
64468
64719
|
}
|
|
64469
64720
|
} catch (error) {
|
|
64470
64721
|
logger.error(error instanceof Error ? error.message : String(error));
|
|
@@ -65082,13 +65333,13 @@ function parseUnifiedDiffPaths(diff) {
|
|
|
65082
65333
|
async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
|
|
65083
65334
|
const threshold = options.threshold ?? config.prScoreThreshold ?? 20;
|
|
65084
65335
|
const maxFiles = options.maxFiles ?? 500;
|
|
65085
|
-
const { existsSync:
|
|
65336
|
+
const { existsSync: existsSync31, readFileSync: readFileSync41 } = await import("fs");
|
|
65086
65337
|
const { isAbsolute: isAbsolute4, resolve: resolvePath } = await import("path");
|
|
65087
|
-
if (!
|
|
65338
|
+
if (!existsSync31(diffPath)) {
|
|
65088
65339
|
throw new Error(`Diff file not found: ${diffPath}`);
|
|
65089
65340
|
}
|
|
65090
65341
|
const diffAbs = isAbsolute4(diffPath) ? diffPath : resolvePath(cwd, diffPath);
|
|
65091
|
-
const diff =
|
|
65342
|
+
const diff = readFileSync41(diffAbs, "utf-8");
|
|
65092
65343
|
const changedPaths = parseUnifiedDiffPaths(diff);
|
|
65093
65344
|
if (changedPaths.length === 0) {
|
|
65094
65345
|
return emptyPrResult(threshold);
|
|
@@ -65096,7 +65347,7 @@ async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
|
|
|
65096
65347
|
return scoreChangedPaths(cwd, config, changedPaths, maxFiles, threshold);
|
|
65097
65348
|
}
|
|
65098
65349
|
async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
|
|
65099
|
-
const { existsSync:
|
|
65350
|
+
const { existsSync: existsSync31, statSync: statSync8 } = await import("fs");
|
|
65100
65351
|
const { resolve: resolvePath } = await import("path");
|
|
65101
65352
|
const { readFile: readFile4 } = await import("fs/promises");
|
|
65102
65353
|
const { scanFile: scanFile2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
|
|
@@ -65108,7 +65359,7 @@ async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
|
|
|
65108
65359
|
let constitutionViolations = 0;
|
|
65109
65360
|
for (const rel of limited) {
|
|
65110
65361
|
const abs = resolvePath(cwd, rel);
|
|
65111
|
-
if (!
|
|
65362
|
+
if (!existsSync31(abs) || !statSync8(abs).isFile()) continue;
|
|
65112
65363
|
let source;
|
|
65113
65364
|
try {
|
|
65114
65365
|
source = await readFile4(abs, "utf-8");
|
|
@@ -65286,7 +65537,7 @@ var TEST_INCLUDE_GLOBS = [
|
|
|
65286
65537
|
"**/__tests__/**/*.{ts,tsx,js,jsx}",
|
|
65287
65538
|
"**/__fixtures__/**/*.{ts,tsx,js,jsx,json}"
|
|
65288
65539
|
];
|
|
65289
|
-
async function runTestScan(cwd,
|
|
65540
|
+
async function runTestScan(cwd, _config, options = {}) {
|
|
65290
65541
|
const include = TEST_INCLUDE_GLOBS;
|
|
65291
65542
|
const cliOptions = {
|
|
65292
65543
|
include,
|
|
@@ -66235,7 +66486,7 @@ function formatText2(report) {
|
|
|
66235
66486
|
const label = CATEGORY_LABELS2[cat].padEnd(20);
|
|
66236
66487
|
const count = String(stats.count).padStart(2);
|
|
66237
66488
|
const baseline = String(stats.baseline).padStart(1);
|
|
66238
|
-
const excess = stats.excess > 0 ? `\u26A0 +${stats.excess} over` : "\u2713
|
|
66489
|
+
const excess = stats.excess > 0 ? `\u26A0 +${stats.excess} over` : stats.count === 0 ? "\u2713 none" : "\u2713 within baseline";
|
|
66239
66490
|
lines.push(` ${label} ${count} implementations (baseline ${baseline}) ${excess}`);
|
|
66240
66491
|
}
|
|
66241
66492
|
if (report.doNotCreate.length > 0) {
|
|
@@ -66293,7 +66544,7 @@ function formatMarkdown3(report) {
|
|
|
66293
66544
|
for (const cat of PATTERN_CATEGORIES) {
|
|
66294
66545
|
const stats = report.byCategory[cat];
|
|
66295
66546
|
const label = CATEGORY_LABELS2[cat];
|
|
66296
|
-
const excess = stats.excess > 0 ? `+${stats.excess}` : "\u2014";
|
|
66547
|
+
const excess = stats.excess > 0 ? `+${stats.excess}` : stats.count === 0 ? "\u2014" : "\xB7";
|
|
66297
66548
|
lines.push(`| ${label} | ${stats.count} | ${stats.baseline} | ${excess} |`);
|
|
66298
66549
|
}
|
|
66299
66550
|
if (report.doNotCreate.length > 0) {
|
|
@@ -66718,8 +66969,9 @@ function formatFlywheel(summary, options = {}) {
|
|
|
66718
66969
|
if (summary.firstRunAt && summary.latestRunAt) {
|
|
66719
66970
|
lines.push(` Window: ${summary.firstRunAt} \u2192 ${summary.latestRunAt}`);
|
|
66720
66971
|
}
|
|
66721
|
-
|
|
66722
|
-
lines.push(`
|
|
66972
|
+
const directionNote = " (lower = cleaner since v0.21.0)";
|
|
66973
|
+
lines.push(` Average AI Slop Score: ${summary.averageSlopIndex.toFixed(2)}`);
|
|
66974
|
+
lines.push(` Latest AI Slop Score: ${summary.latestSlopIndex.toFixed(2)}${directionNote}`);
|
|
66723
66975
|
lines.push(` Average assembly health: ${summary.averageAssemblyHealth.toFixed(2)}`);
|
|
66724
66976
|
lines.push(` Latest assembly health: ${summary.latestAssemblyHealth.toFixed(2)}`);
|
|
66725
66977
|
if (summary.topViolations.length > 0) {
|
|
@@ -67066,7 +67318,7 @@ function applyVisualCodemods(filePath) {
|
|
|
67066
67318
|
function collectAllFixes2(issue) {
|
|
67067
67319
|
return [...issue.fix ? [issue.fix] : [], ...issue.fixes ?? []];
|
|
67068
67320
|
}
|
|
67069
|
-
async function applyFixes(report,
|
|
67321
|
+
async function applyFixes(report, _config, scannedFiles) {
|
|
67070
67322
|
const byFile = /* @__PURE__ */ new Map();
|
|
67071
67323
|
for (const issue of report.issues) {
|
|
67072
67324
|
const fixes = collectAllFixes2(issue);
|
|
@@ -67127,7 +67379,6 @@ async function applyFixes(report, config, scannedFiles) {
|
|
|
67127
67379
|
const result = applyLayoutTokenFix(filePath, group.replaces);
|
|
67128
67380
|
const total = group.replaceApps.length;
|
|
67129
67381
|
const appliedCount = Math.min(result.applied, total);
|
|
67130
|
-
const skippedCount = total - appliedCount;
|
|
67131
67382
|
applied.push(...group.replaceApps.slice(0, appliedCount));
|
|
67132
67383
|
skipped.push(...group.replaceApps.slice(appliedCount));
|
|
67133
67384
|
} catch (err) {
|
|
@@ -67322,6 +67573,40 @@ function formatGroupedHelp(cmd) {
|
|
|
67322
67573
|
}
|
|
67323
67574
|
}
|
|
67324
67575
|
lines.push("");
|
|
67576
|
+
lines.push(
|
|
67577
|
+
"Commands (run `<command> --help` for per-command details):\n" + [
|
|
67578
|
+
" scan score a workspace, with severity gating and report views",
|
|
67579
|
+
" pr score a PR (or a unified-diff file via `--diff <file>`)",
|
|
67580
|
+
" drift detect constitution / forbidden-package violations",
|
|
67581
|
+
" test test quality sub-score (CI gate)",
|
|
67582
|
+
" docs documentation freshness sub-score",
|
|
67583
|
+
" security AI security-risk sub-score",
|
|
67584
|
+
" rules list all built-in rules with categories and severities",
|
|
67585
|
+
" explain show rationale, pattern, and remediation for a single rule",
|
|
67586
|
+
" suggest print per-issue remediation advice",
|
|
67587
|
+
" badge print a shields.io slop-index badge",
|
|
67588
|
+
" doctor check setup, config, and environment for common problems",
|
|
67589
|
+
" init create a slopbrick config file and AGENTS.md/CLAUDE.md snippets",
|
|
67590
|
+
" flywheel summarize aggregated scan telemetry",
|
|
67591
|
+
" composite empirical-composite rule discover / enable (Sprint 3 \xA73b)",
|
|
67592
|
+
" calibrate empirical precision/recall/F1 calibration from held-out corpora",
|
|
67593
|
+
" patterns pattern-fragmentation sub-score",
|
|
67594
|
+
" business-logic business-logic coherence sub-score",
|
|
67595
|
+
" maintenance-cost AI maintenance-cost sub-score",
|
|
67596
|
+
" architecture architecture-consistency sub-score",
|
|
67597
|
+
" memory show patterns detected for `slop_suggest`",
|
|
67598
|
+
" lock install a git pre-commit hook (idempotent)",
|
|
67599
|
+
" migrate v0.10.x slop-audit/ \u2192 v0.11.0+ .slopbrick/ migration",
|
|
67600
|
+
" validate-config validate slopbrick.config.mjs",
|
|
67601
|
+
" tokens merge a design-tokens.json layout values into the allowlist",
|
|
67602
|
+
" report re-render a saved JSON report (from --json <path>)",
|
|
67603
|
+
" install / uninstall manage the git pre-commit hook",
|
|
67604
|
+
" mcp run the MCP server (JSON-RPC 2.0 over stdio)",
|
|
67605
|
+
" watch / --watch re-run scan on file change",
|
|
67606
|
+
" trend sparkline of the last n runs"
|
|
67607
|
+
].join("\n")
|
|
67608
|
+
);
|
|
67609
|
+
lines.push("");
|
|
67325
67610
|
lines.push(
|
|
67326
67611
|
"Use `--help-flat` for the standard un-grouped list. See `https://usebrick.dev/docs/scan-options` for full docs."
|
|
67327
67612
|
);
|
|
@@ -67439,7 +67724,7 @@ process.on("uncaughtException", (err) => {
|
|
|
67439
67724
|
});
|
|
67440
67725
|
async function runCli({ start }) {
|
|
67441
67726
|
try {
|
|
67442
|
-
const program = new import_commander3.Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if
|
|
67727
|
+
const program = new import_commander3.Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if AI Slop Score increased since last run (lower = cleaner since v0.21)").option("--refresh-snippets", "rewrite the managed slopbrick block in AGENTS.md and CLAUDE.md after a scan").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
|
|
67443
67728
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
67444
67729
|
setExitOverride(program);
|
|
67445
67730
|
registerInit(program);
|
|
@@ -67520,20 +67805,20 @@ async function runCli({ start }) {
|
|
|
67520
67805
|
const gitHead = await getGitHead(cwd2) ?? "unknown";
|
|
67521
67806
|
const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
|
|
67522
67807
|
saveBaseline(cwd2, cache);
|
|
67523
|
-
if (!options.quiet) {
|
|
67808
|
+
if (!options.quiet && !machineReadableStdout) {
|
|
67524
67809
|
logger.info(`Saved baseline to ${baselinePath(cwd2)}`);
|
|
67525
67810
|
}
|
|
67526
67811
|
}
|
|
67527
67812
|
if (options.tighten && baseline) {
|
|
67528
67813
|
saveBaseline(cwd, baseline);
|
|
67529
|
-
if (!options.quiet) {
|
|
67814
|
+
if (!options.quiet && !machineReadableStdout) {
|
|
67530
67815
|
logger.info(`Tightened baseline saved (revision ${baseline.baseline_revision}).`);
|
|
67531
67816
|
}
|
|
67532
67817
|
}
|
|
67533
|
-
if (options.doctor && !options.quiet) {
|
|
67818
|
+
if (options.doctor && !options.quiet && !machineReadableStdout) {
|
|
67534
67819
|
logger.info(`Doctor: bootstrap ${totalElapsed - scanElapsed}ms, scan ${scanElapsed}ms`);
|
|
67535
67820
|
}
|
|
67536
|
-
if (report.baseline && !options.quiet) {
|
|
67821
|
+
if (report.baseline && !options.quiet && !machineReadableStdout) {
|
|
67537
67822
|
logger.info(baselineStatusMessage(report.baseline));
|
|
67538
67823
|
}
|
|
67539
67824
|
if (options.fix) {
|
|
@@ -67547,7 +67832,7 @@ async function runCli({ start }) {
|
|
|
67547
67832
|
}
|
|
67548
67833
|
const scannedFiles = results.map((r) => r.filePath);
|
|
67549
67834
|
const fixResults = await applyFixes(report, config, scannedFiles);
|
|
67550
|
-
const {
|
|
67835
|
+
const { hasErrors } = printFixSummary(fixResults, options.quiet ?? false);
|
|
67551
67836
|
if (!options.showFixesDiff && !options.quiet) {
|
|
67552
67837
|
logger.info("Tip: pass --show-fixes-diff to see the unified diff of proposed changes.");
|
|
67553
67838
|
}
|
|
@@ -67587,8 +67872,18 @@ async function runCli({ start }) {
|
|
|
67587
67872
|
if (options.staged && stagedGatingResult.reason) {
|
|
67588
67873
|
logger.error(`Gating failure: ${stagedGatingResult.reason}`);
|
|
67589
67874
|
} else {
|
|
67590
|
-
const
|
|
67591
|
-
|
|
67875
|
+
const failedNames = failedThresholds(report, config);
|
|
67876
|
+
const detail = failedNames.map((name) => {
|
|
67877
|
+
if (name === "meanSlop") return `meanSlop (score ${report.aiSlopScore} > ${config.thresholds.meanSlop})`;
|
|
67878
|
+
if (name === "p90Slop") return `p90Slop (score ${report.p90Score} > ${config.thresholds.p90Slop})`;
|
|
67879
|
+
if (name === "individualSlopThreshold") return `individualSlopThreshold (peak ${report.peakScore} > ${config.thresholds.individualSlopThreshold})`;
|
|
67880
|
+
const cat = name.replace(/^category:/, "");
|
|
67881
|
+
const limit = config.thresholds.categoryThresholds?.[cat];
|
|
67882
|
+
const score = report.categoryScores[cat];
|
|
67883
|
+
return `${name} (score ${score} > ${limit})`;
|
|
67884
|
+
}).join(", ");
|
|
67885
|
+
const failed = failedNames.length;
|
|
67886
|
+
logger.error(`${failed} threshold${failed === 1 ? "" : "s"} failed: ${detail}`);
|
|
67592
67887
|
}
|
|
67593
67888
|
}
|
|
67594
67889
|
if (!options.quiet && !machineReadableStdout) {
|
|
@@ -67686,6 +67981,7 @@ init_render();
|
|
|
67686
67981
|
directoryScore,
|
|
67687
67982
|
extractSignatures,
|
|
67688
67983
|
failedThresholdCount,
|
|
67984
|
+
failedThresholds,
|
|
67689
67985
|
filterByDisabledDirectives,
|
|
67690
67986
|
filterIssues,
|
|
67691
67987
|
findSimilarFunctions,
|