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