slopbrick 0.41.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 +198 -40
- package/dist/engine/worker.js +198 -40
- package/dist/index.cjs +1792 -715
- package/dist/index.d.cts +141 -15
- package/dist/index.d.ts +141 -15
- package/dist/index.js +1740 -664
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -36,7 +36,7 @@ var VERSION;
|
|
|
36
36
|
var init_header = __esm({
|
|
37
37
|
"src/types/_header.ts"() {
|
|
38
38
|
"use strict";
|
|
39
|
-
VERSION = "0.
|
|
39
|
+
VERSION = "0.43.0";
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -338,8 +338,16 @@ var init_defaults = __esm({
|
|
|
338
338
|
// rule definitions are meta-code
|
|
339
339
|
"**/snippet/**",
|
|
340
340
|
// RULE_HINTS examples are intentional bad code
|
|
341
|
-
"**/tests/**"
|
|
341
|
+
"**/tests/**",
|
|
342
342
|
// all test files (fixtures + unit + integration)
|
|
343
|
+
// v0.42.0 (post-self-scan cleanup): language visitors are
|
|
344
|
+
// intentional clones — see the §3 v0.42.0 release notes for
|
|
345
|
+
// the design rationale. `dup/identical-block` fires 100+
|
|
346
|
+
// times during self-scan because each visitor shares the
|
|
347
|
+
// same service-suffix arrays and import-walking preamble.
|
|
348
|
+
// Suppress these from the self-scan noise floor; they
|
|
349
|
+
// remain on for user codebases.
|
|
350
|
+
"**/src/engine/visitors/**"
|
|
343
351
|
]
|
|
344
352
|
}
|
|
345
353
|
};
|
|
@@ -26001,7 +26009,7 @@ var init_comment_ratio = __esm({
|
|
|
26001
26009
|
message: `Comment-line ratio is ${direction} (${ratio.toFixed(2)} vs corpus mean ${mean3}). Reductive LLMs (GPT-3.5, Claude 3 Haiku) skip comments; expansive LLMs (Claude Haiku 4.5, GPT-OSS) over-comment when prompted for "well-commented code".`,
|
|
26002
26010
|
line: 1,
|
|
26003
26011
|
column: 1,
|
|
26004
|
-
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "
|
|
26012
|
+
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "High comment ratio \u2014 consider whether the comments are intentional (api docs, type defs, tutorials). For functional code, trim comments that just restate the next line. Keep comments that explain WHY, not WHAT."
|
|
26005
26013
|
}
|
|
26006
26014
|
];
|
|
26007
26015
|
return issues;
|
|
@@ -26852,7 +26860,6 @@ function buildTrigramLM(samples, alpha = 1) {
|
|
|
26852
26860
|
logProb(context, t) {
|
|
26853
26861
|
const ctxKey = context.slice(-2).join("|");
|
|
26854
26862
|
const triKey = `${ctxKey}|${t}`;
|
|
26855
|
-
const biKey = context.slice(-1).join("|") + "|" + t;
|
|
26856
26863
|
const triCount = tri.get(triKey) ?? 0;
|
|
26857
26864
|
const biCount = bi.get(ctxKey) ?? 0;
|
|
26858
26865
|
const V = uni.size;
|
|
@@ -28860,7 +28867,7 @@ var init_stale_function_reference = __esm({
|
|
|
28860
28867
|
]);
|
|
28861
28868
|
SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
28862
28869
|
SOURCE_ROOTS = ["src", "lib", "app", "components"];
|
|
28863
|
-
CAP =
|
|
28870
|
+
CAP = 4e3;
|
|
28864
28871
|
staleFunctionReferenceRule = createRule({
|
|
28865
28872
|
id: "docs/stale-function-reference",
|
|
28866
28873
|
category: "docs",
|
|
@@ -28876,6 +28883,7 @@ var init_stale_function_reference = __esm({
|
|
|
28876
28883
|
if (!source) return issues;
|
|
28877
28884
|
for (const span of extractInlineCodeSpans(source)) {
|
|
28878
28885
|
const text = span.text;
|
|
28886
|
+
if (span.inBlockComment || span.inComment) continue;
|
|
28879
28887
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
|
|
28880
28888
|
if (text.length < 3) continue;
|
|
28881
28889
|
if (RESERVED.has(text.toLowerCase())) continue;
|
|
@@ -29046,6 +29054,7 @@ var init_stale_package_reference = __esm({
|
|
|
29046
29054
|
if (!source) return issues;
|
|
29047
29055
|
const spans = extractInlineCodeSpans(source);
|
|
29048
29056
|
for (const span of spans) {
|
|
29057
|
+
if (span.inComment) continue;
|
|
29049
29058
|
const lineStart = source.lastIndexOf("\n", span.index) + 1;
|
|
29050
29059
|
const lineEnd = source.indexOf("\n", span.index);
|
|
29051
29060
|
const line = source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd);
|
|
@@ -29100,11 +29109,14 @@ __export(doc_freshness_exports, {
|
|
|
29100
29109
|
extractExports: () => extractExports,
|
|
29101
29110
|
extractFencedCodeBlocks: () => extractFencedCodeBlocks,
|
|
29102
29111
|
extractInlineCodeSpans: () => extractInlineCodeSpans,
|
|
29103
|
-
extractMarkdownLinks: () => extractMarkdownLinks
|
|
29112
|
+
extractMarkdownLinks: () => extractMarkdownLinks,
|
|
29113
|
+
findCommentLineRanges: () => findCommentLineRanges
|
|
29104
29114
|
});
|
|
29105
29115
|
function extractInlineCodeSpans(source) {
|
|
29106
29116
|
const hits = [];
|
|
29107
29117
|
const re = /`([^`\n]+?)`/g;
|
|
29118
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
29119
|
+
const commentLines = findCommentLineRanges(source);
|
|
29108
29120
|
let m;
|
|
29109
29121
|
while ((m = re.exec(source)) !== null) {
|
|
29110
29122
|
const text = m[1] ?? "";
|
|
@@ -29112,7 +29124,14 @@ function extractInlineCodeSpans(source) {
|
|
|
29112
29124
|
const line = upTo.split("\n").length;
|
|
29113
29125
|
const lastNl = upTo.lastIndexOf("\n");
|
|
29114
29126
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
29115
|
-
hits.push({
|
|
29127
|
+
hits.push({
|
|
29128
|
+
text,
|
|
29129
|
+
line,
|
|
29130
|
+
column,
|
|
29131
|
+
index: m.index,
|
|
29132
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
29133
|
+
inComment: commentLines.has(line)
|
|
29134
|
+
});
|
|
29116
29135
|
}
|
|
29117
29136
|
return hits;
|
|
29118
29137
|
}
|
|
@@ -29151,6 +29170,8 @@ function extractFencedCodeBlocks(source) {
|
|
|
29151
29170
|
function extractMarkdownLinks(source) {
|
|
29152
29171
|
const hits = [];
|
|
29153
29172
|
const re = /(?<!\!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
29173
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
29174
|
+
const commentLines = findCommentLineRanges(source);
|
|
29154
29175
|
let m;
|
|
29155
29176
|
while ((m = re.exec(source)) !== null) {
|
|
29156
29177
|
const target = m[2] ?? "";
|
|
@@ -29158,10 +29179,55 @@ function extractMarkdownLinks(source) {
|
|
|
29158
29179
|
const line = upTo.split("\n").length;
|
|
29159
29180
|
const lastNl = upTo.lastIndexOf("\n");
|
|
29160
29181
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
29161
|
-
hits.push({
|
|
29182
|
+
hits.push({
|
|
29183
|
+
target,
|
|
29184
|
+
line,
|
|
29185
|
+
column,
|
|
29186
|
+
index: m.index,
|
|
29187
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
29188
|
+
inComment: commentLines.has(line)
|
|
29189
|
+
});
|
|
29162
29190
|
}
|
|
29163
29191
|
return hits;
|
|
29164
29192
|
}
|
|
29193
|
+
function findBlockCommentRanges(source) {
|
|
29194
|
+
const ranges = [];
|
|
29195
|
+
const stack = [];
|
|
29196
|
+
let i = 0;
|
|
29197
|
+
while (i < source.length) {
|
|
29198
|
+
if (source[i] === "/" && source[i + 1] === "/") {
|
|
29199
|
+
const eol = source.indexOf("\n", i);
|
|
29200
|
+
i = eol === -1 ? source.length : eol + 1;
|
|
29201
|
+
continue;
|
|
29202
|
+
}
|
|
29203
|
+
if (source[i] === "/" && source[i + 1] === "*") {
|
|
29204
|
+
stack.push(i);
|
|
29205
|
+
i += 2;
|
|
29206
|
+
continue;
|
|
29207
|
+
}
|
|
29208
|
+
if (source[i] === "*" && source[i + 1] === "/" && stack.length > 0) {
|
|
29209
|
+
const start = stack.pop();
|
|
29210
|
+
ranges.push([start, i + 2]);
|
|
29211
|
+
i += 2;
|
|
29212
|
+
continue;
|
|
29213
|
+
}
|
|
29214
|
+
i += 1;
|
|
29215
|
+
}
|
|
29216
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
29217
|
+
return ranges;
|
|
29218
|
+
}
|
|
29219
|
+
function isInBlockComment(ranges, pos) {
|
|
29220
|
+
let lo = 0;
|
|
29221
|
+
let hi = ranges.length - 1;
|
|
29222
|
+
while (lo <= hi) {
|
|
29223
|
+
const mid = lo + hi >>> 1;
|
|
29224
|
+
const [start, end] = ranges[mid];
|
|
29225
|
+
if (pos < start) hi = mid - 1;
|
|
29226
|
+
else if (pos >= end) lo = mid + 1;
|
|
29227
|
+
else return true;
|
|
29228
|
+
}
|
|
29229
|
+
return false;
|
|
29230
|
+
}
|
|
29165
29231
|
function declaredPackages(cwd) {
|
|
29166
29232
|
const out = /* @__PURE__ */ new Set();
|
|
29167
29233
|
const pkgPath = (0, import_node_path6.join)(cwd, "package.json");
|
|
@@ -29316,6 +29382,58 @@ function docDriftFromFreshness(score) {
|
|
|
29316
29382
|
if (score >= DOC_FRESHNESS_THRESHOLDS.high) return "high";
|
|
29317
29383
|
return "critical";
|
|
29318
29384
|
}
|
|
29385
|
+
function findCommentLineRanges(source) {
|
|
29386
|
+
const out = /* @__PURE__ */ new Set();
|
|
29387
|
+
const stack = [];
|
|
29388
|
+
let inLineComment = false;
|
|
29389
|
+
const lines = source.split("\n");
|
|
29390
|
+
for (let i = 0; i < lines.length; i++) {
|
|
29391
|
+
const line = lines[i];
|
|
29392
|
+
const lineNo = i + 1;
|
|
29393
|
+
let j = 0;
|
|
29394
|
+
while (j < line.length) {
|
|
29395
|
+
const c = line[j];
|
|
29396
|
+
const next = line[j + 1];
|
|
29397
|
+
if (!stack.length && c === "/" && next === "/") {
|
|
29398
|
+
inLineComment = true;
|
|
29399
|
+
out.add(lineNo);
|
|
29400
|
+
break;
|
|
29401
|
+
}
|
|
29402
|
+
if (c === "/" && next === "*") {
|
|
29403
|
+
stack.push(lineNo);
|
|
29404
|
+
out.add(lineNo);
|
|
29405
|
+
j += 2;
|
|
29406
|
+
continue;
|
|
29407
|
+
}
|
|
29408
|
+
if (c === "*" && next === "/" && stack.length > 0) {
|
|
29409
|
+
stack.pop();
|
|
29410
|
+
j += 2;
|
|
29411
|
+
continue;
|
|
29412
|
+
}
|
|
29413
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
29414
|
+
const quote = c;
|
|
29415
|
+
j += 1;
|
|
29416
|
+
while (j < line.length) {
|
|
29417
|
+
if (line[j] === "\\") {
|
|
29418
|
+
j += 2;
|
|
29419
|
+
continue;
|
|
29420
|
+
}
|
|
29421
|
+
if (line[j] === quote) {
|
|
29422
|
+
j += 1;
|
|
29423
|
+
break;
|
|
29424
|
+
}
|
|
29425
|
+
j += 1;
|
|
29426
|
+
}
|
|
29427
|
+
continue;
|
|
29428
|
+
}
|
|
29429
|
+
j += 1;
|
|
29430
|
+
}
|
|
29431
|
+
if (stack.length > 0) {
|
|
29432
|
+
out.add(lineNo);
|
|
29433
|
+
}
|
|
29434
|
+
}
|
|
29435
|
+
return out;
|
|
29436
|
+
}
|
|
29319
29437
|
var import_node_fs6, import_node_path6, import_globby, DOC_RULE_WEIGHTS, DOC_FRESHNESS_THRESHOLDS;
|
|
29320
29438
|
var init_doc_freshness = __esm({
|
|
29321
29439
|
"src/engine/doc-freshness.ts"() {
|
|
@@ -29340,6 +29458,9 @@ var init_doc_freshness = __esm({
|
|
|
29340
29458
|
});
|
|
29341
29459
|
|
|
29342
29460
|
// src/rules/docs/broken-link.ts
|
|
29461
|
+
function looksLikeRegexSyntax(target) {
|
|
29462
|
+
return /\[[^\]]*\]/.test(target) || /^\s*[?:=!]/.test(target) || /\\(?:s|d|w|S|D|W|t|n|r|[^\\])/.test(target) || /[{}|]/.test(target);
|
|
29463
|
+
}
|
|
29343
29464
|
var import_node_fs7, import_node_path7, brokenLinkRule;
|
|
29344
29465
|
var init_broken_link = __esm({
|
|
29345
29466
|
"src/rules/docs/broken-link.ts"() {
|
|
@@ -29370,6 +29491,8 @@ var init_broken_link = __esm({
|
|
|
29370
29491
|
if (target.startsWith("#")) continue;
|
|
29371
29492
|
if (target.startsWith("//")) continue;
|
|
29372
29493
|
if (target.startsWith("/")) continue;
|
|
29494
|
+
if (link.inBlockComment) continue;
|
|
29495
|
+
if (looksLikeRegexSyntax(target)) continue;
|
|
29373
29496
|
const filePart = target.split("#")[0] ?? target;
|
|
29374
29497
|
if (filePart === "") continue;
|
|
29375
29498
|
const resolved = (0, import_node_path7.join)(docDir, filePart);
|
|
@@ -38545,6 +38668,29 @@ var init_stringly_typed = __esm({
|
|
|
38545
38668
|
}
|
|
38546
38669
|
});
|
|
38547
38670
|
|
|
38671
|
+
// src/engine/parser-shared.ts
|
|
38672
|
+
function parseTreeSitterSource(parserResult, source) {
|
|
38673
|
+
if (!parserResult.ok) return null;
|
|
38674
|
+
if (!source || source.trim() === "") return null;
|
|
38675
|
+
try {
|
|
38676
|
+
const rawTree = parserResult.parser.parse(source);
|
|
38677
|
+
if (!rawTree) return null;
|
|
38678
|
+
const tree = rawTree;
|
|
38679
|
+
if (!tree || !tree.rootNode) return null;
|
|
38680
|
+
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
38681
|
+
return null;
|
|
38682
|
+
}
|
|
38683
|
+
return tree;
|
|
38684
|
+
} catch {
|
|
38685
|
+
return null;
|
|
38686
|
+
}
|
|
38687
|
+
}
|
|
38688
|
+
var init_parser_shared = __esm({
|
|
38689
|
+
"src/engine/parser-shared.ts"() {
|
|
38690
|
+
"use strict";
|
|
38691
|
+
}
|
|
38692
|
+
});
|
|
38693
|
+
|
|
38548
38694
|
// src/engine/parser-rust.ts
|
|
38549
38695
|
function getRustParser() {
|
|
38550
38696
|
if (cachedParser) return { ok: true, parser: cachedParser };
|
|
@@ -38568,21 +38714,7 @@ function effectiveParser() {
|
|
|
38568
38714
|
return getRustParser();
|
|
38569
38715
|
}
|
|
38570
38716
|
function parseRust(source) {
|
|
38571
|
-
|
|
38572
|
-
if (!result.ok) return null;
|
|
38573
|
-
if (!source || source.trim() === "") return null;
|
|
38574
|
-
try {
|
|
38575
|
-
const rawTree = result.parser.parse(source);
|
|
38576
|
-
if (!rawTree) return null;
|
|
38577
|
-
const tree = rawTree;
|
|
38578
|
-
if (!tree || !tree.rootNode) return null;
|
|
38579
|
-
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
38580
|
-
return null;
|
|
38581
|
-
}
|
|
38582
|
-
return tree;
|
|
38583
|
-
} catch {
|
|
38584
|
-
return null;
|
|
38585
|
-
}
|
|
38717
|
+
return parseTreeSitterSource(effectiveParser(), source);
|
|
38586
38718
|
}
|
|
38587
38719
|
function isRustParserAvailable() {
|
|
38588
38720
|
const result = getRustParser();
|
|
@@ -38594,6 +38726,7 @@ var init_parser_rust = __esm({
|
|
|
38594
38726
|
"use strict";
|
|
38595
38727
|
import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
38596
38728
|
import_tree_sitter_rust = __toESM(require("tree-sitter-rust"), 1);
|
|
38729
|
+
init_parser_shared();
|
|
38597
38730
|
cachedParser = null;
|
|
38598
38731
|
rustLanguage = null;
|
|
38599
38732
|
loadError = null;
|
|
@@ -43701,8 +43834,6 @@ function mergeTemplateClassNames(filePath, source, facts, templateClassNames) {
|
|
|
43701
43834
|
}
|
|
43702
43835
|
function extractFacts(filePath, ast, source, supportsRsc = true, framework = "react", config) {
|
|
43703
43836
|
const lineOffsets = buildLineOffsets2(source);
|
|
43704
|
-
const isTypeScript = /\.tsx?$/i.test(filePath);
|
|
43705
|
-
const NON_JSX_FRAMEWORKS = /* @__PURE__ */ new Set(["vue", "svelte", "astro"]);
|
|
43706
43837
|
const facts = {
|
|
43707
43838
|
filePath,
|
|
43708
43839
|
components: [],
|
|
@@ -45641,7 +45772,7 @@ function formatDeltaSuffix(report) {
|
|
|
45641
45772
|
}
|
|
45642
45773
|
function formatVerdict(report) {
|
|
45643
45774
|
const score = report.aiSlopScore;
|
|
45644
|
-
const band =
|
|
45775
|
+
const band = slopScoreBand(score);
|
|
45645
45776
|
const ranked = rankByImpact(report.issues);
|
|
45646
45777
|
const topRule = ranked[0];
|
|
45647
45778
|
const topFile = report.topOffenders?.[0];
|
|
@@ -45724,7 +45855,8 @@ function formatNextStep(report) {
|
|
|
45724
45855
|
` \u2192 \`slopbrick scan --baseline\` to accept today's scores as the new floor`
|
|
45725
45856
|
)
|
|
45726
45857
|
);
|
|
45727
|
-
|
|
45858
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
45859
|
+
if (report.aiSlopScore > meanSlop) {
|
|
45728
45860
|
lines.push(
|
|
45729
45861
|
import_chalk.default.dim(
|
|
45730
45862
|
` \u2192 \`slopbrick scan --why-failing\` for the top 5 issues dragging the score down`
|
|
@@ -45749,17 +45881,18 @@ function formatWhyFailing(report) {
|
|
|
45749
45881
|
return import_chalk.default.green("Nothing is failing the threshold. Score is clean.");
|
|
45750
45882
|
}
|
|
45751
45883
|
const headline = report.aiSlopScore;
|
|
45752
|
-
const
|
|
45753
|
-
const
|
|
45884
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
45885
|
+
const status = headline <= meanSlop ? "PASS" : "FAIL";
|
|
45886
|
+
const colorize = headline <= meanSlop ? import_chalk.default.green : import_chalk.default.red;
|
|
45887
|
+
const qualifier = headline <= meanSlop ? `below ${meanSlop}` : `above ${meanSlop}`;
|
|
45754
45888
|
const lines = [];
|
|
45755
|
-
lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014
|
|
45889
|
+
lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014 ${qualifier})`));
|
|
45756
45890
|
lines.push("");
|
|
45757
45891
|
lines.push(import_chalk.default.bold("Top 5 rules dragging the score down:"));
|
|
45758
45892
|
for (let i = 0; i < ranked.length; i++) {
|
|
45759
45893
|
const [ruleId, info] = ranked[i];
|
|
45760
45894
|
const fileCount = info.files.size;
|
|
45761
45895
|
const fileHint = fileCount > 0 ? ` across ${fileCount} file${fileCount === 1 ? "" : "s"}` : "";
|
|
45762
|
-
const catShort = CATEGORY_GLOSSARY[info.count > 0 ? "" : ""]?.short;
|
|
45763
45896
|
const sampleIssue = report.issues.find((i2) => i2.ruleId === ruleId);
|
|
45764
45897
|
const cat = sampleIssue ? CATEGORY_GLOSSARY[sampleIssue.category]?.short : null;
|
|
45765
45898
|
const catHint = cat ? import_chalk.default.dim(` [${cat}]`) : "";
|
|
@@ -45819,7 +45952,7 @@ function formatCompositeScore(report) {
|
|
|
45819
45952
|
lines.push("");
|
|
45820
45953
|
lines.push(
|
|
45821
45954
|
import_chalk.default.dim(
|
|
45822
|
-
`Repository Coherence: ${coherenceValue} / 100 [${cohBand.label.toUpperCase()}] \u2014 higher = better \xB7 measures internal consistency. This is a secondary view; the Slop
|
|
45955
|
+
`Repository Coherence: ${coherenceValue} / 100 [${cohBand.label.toUpperCase()}] \u2014 higher = better \xB7 measures internal consistency. This is a secondary view; the AI Slop Score above is the gate.`
|
|
45823
45956
|
)
|
|
45824
45957
|
);
|
|
45825
45958
|
}
|
|
@@ -45949,13 +46082,12 @@ function formatDriftSection(report) {
|
|
|
45949
46082
|
return lines.join("\n");
|
|
45950
46083
|
}
|
|
45951
46084
|
function formatThresholds(report) {
|
|
45952
|
-
const
|
|
46085
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
45953
46086
|
const headline = report.aiSlopScore;
|
|
45954
|
-
const passed = headline
|
|
45955
|
-
const valueText = `${headline.toFixed(1)} \
|
|
46087
|
+
const passed = headline <= meanSlop;
|
|
46088
|
+
const valueText = `${headline.toFixed(1)} \u2264 ${meanSlop}`.padStart(12, " ");
|
|
45956
46089
|
const status = passed ? import_chalk.default.green("pass") : import_chalk.default.red("fail");
|
|
45957
46090
|
const result = ["Threshold (CI gate)", ` AI Slop Score ${valueText} ${status}`];
|
|
45958
|
-
void limit;
|
|
45959
46091
|
return result;
|
|
45960
46092
|
}
|
|
45961
46093
|
function formatTopComponents(components, topOffenders) {
|
|
@@ -46033,9 +46165,10 @@ function formatPretty(report) {
|
|
|
46033
46165
|
}
|
|
46034
46166
|
return sections.join("\n\n");
|
|
46035
46167
|
}
|
|
46036
|
-
function formatScoringExplainer(
|
|
46168
|
+
function formatScoringExplainer(report) {
|
|
46169
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
46037
46170
|
return import_chalk.default.dim(
|
|
46038
|
-
|
|
46171
|
+
`Four orthogonal scores (all 0-100, **lower AI Slop Score = cleaner**): AI Slop Score (AI-slop signatures; the CI gate, AI Slop Score \u2264 ${meanSlop} passes), Engineering Hygiene (issues per category across arch/logic/layout/component/test; higher = cleaner), Security (AI-flagged security risks, inverted from risk level; higher = cleaner), Repository Health (composite: 0.4*AI Slop Score + 0.3*Engineering Hygiene + 0.2*Security + 0.1*Test Quality). Only AI Slop Score gates CI; the others are informational. Default-off rules (INVERTED/NOISY/DORMANT) are suppressed from the scores automatically.`
|
|
46039
46172
|
);
|
|
46040
46173
|
}
|
|
46041
46174
|
function formatWhyFailingReport(report) {
|
|
@@ -46053,7 +46186,7 @@ function formatBriefReport(report) {
|
|
|
46053
46186
|
];
|
|
46054
46187
|
const deltaSuffix = formatDeltaSuffix(report);
|
|
46055
46188
|
scoreLines.forEach(({ label, field, value }, idx) => {
|
|
46056
|
-
const band = scoreBand(value);
|
|
46189
|
+
const band = field === "aiSlopScore" ? slopScoreBand(value) : scoreBand(value);
|
|
46057
46190
|
const paddedLabel = label.padEnd(20, " ");
|
|
46058
46191
|
const valueStr = value.toFixed(0).padStart(3, " ");
|
|
46059
46192
|
const delta = idx === 0 ? deltaSuffix : "";
|
|
@@ -46061,11 +46194,12 @@ function formatBriefReport(report) {
|
|
|
46061
46194
|
` ${paddedLabel} ${band.color(valueStr)} ${import_chalk.default.dim(band.label)} ${import_chalk.default.dim.italic(`(${field})`)}${delta}`
|
|
46062
46195
|
);
|
|
46063
46196
|
});
|
|
46064
|
-
const
|
|
46197
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
46198
|
+
const passed = report.aiSlopScore <= meanSlop;
|
|
46065
46199
|
lines.push("");
|
|
46066
46200
|
lines.push(
|
|
46067
46201
|
import_chalk.default.dim(
|
|
46068
|
-
` CI gate: AI Slop Score <=
|
|
46202
|
+
` CI gate: AI Slop Score <= ${meanSlop} -> ${passed ? import_chalk.default.green("pass") : import_chalk.default.red("fail")}`
|
|
46069
46203
|
)
|
|
46070
46204
|
);
|
|
46071
46205
|
const suppressed = report.defaultOffSuppressedCount ?? 0;
|
|
@@ -46210,19 +46344,22 @@ function thresholdExceeded(report, config) {
|
|
|
46210
46344
|
return categoryThresholdBreached(report, config.thresholds.categoryThresholds);
|
|
46211
46345
|
}
|
|
46212
46346
|
function failedThresholdCount(report, config) {
|
|
46213
|
-
|
|
46214
|
-
|
|
46215
|
-
|
|
46216
|
-
|
|
46347
|
+
return failedThresholds(report, config).length;
|
|
46348
|
+
}
|
|
46349
|
+
function failedThresholds(report, config) {
|
|
46350
|
+
const failed = [];
|
|
46351
|
+
if (report.aiSlopScore > config.thresholds.meanSlop) failed.push("meanSlop");
|
|
46352
|
+
if (report.p90Score > config.thresholds.p90Slop) failed.push("p90Slop");
|
|
46353
|
+
if (report.peakScore > config.thresholds.individualSlopThreshold) failed.push("individualSlopThreshold");
|
|
46217
46354
|
const cat = config.thresholds.categoryThresholds;
|
|
46218
46355
|
if (cat) {
|
|
46219
46356
|
for (const [category, limit] of Object.entries(cat)) {
|
|
46220
46357
|
if (limit === void 0) continue;
|
|
46221
46358
|
const score = report.categoryScores[category];
|
|
46222
|
-
if (score !== void 0 && score > limit)
|
|
46359
|
+
if (score !== void 0 && score > limit) failed.push(`category:${category}`);
|
|
46223
46360
|
}
|
|
46224
46361
|
}
|
|
46225
|
-
return
|
|
46362
|
+
return failed;
|
|
46226
46363
|
}
|
|
46227
46364
|
function categoryThresholdBreached(report, categoryThresholds) {
|
|
46228
46365
|
if (!categoryThresholds) return false;
|
|
@@ -46884,11 +47021,11 @@ var init_pool = __esm({
|
|
|
46884
47021
|
for (let i = 0; i < this.threadCount; i++) {
|
|
46885
47022
|
spawnWorker();
|
|
46886
47023
|
}
|
|
46887
|
-
return new Promise((
|
|
47024
|
+
return new Promise((resolve43) => {
|
|
46888
47025
|
const check = setInterval(() => {
|
|
46889
47026
|
if (resolved) {
|
|
46890
47027
|
clearInterval(check);
|
|
46891
|
-
|
|
47028
|
+
resolve43(results);
|
|
46892
47029
|
}
|
|
46893
47030
|
}, 10);
|
|
46894
47031
|
});
|
|
@@ -46905,23 +47042,16 @@ var init_registry = __esm({
|
|
|
46905
47042
|
init_builtins();
|
|
46906
47043
|
RuleRegistry = class {
|
|
46907
47044
|
rules = /* @__PURE__ */ new Map();
|
|
46908
|
-
|
|
46909
|
-
|
|
46910
|
-
|
|
46911
|
-
|
|
46912
|
-
|
|
46913
|
-
|
|
46914
|
-
|
|
46915
|
-
|
|
46916
|
-
|
|
46917
|
-
|
|
46918
|
-
create: factory.create,
|
|
46919
|
-
analyze: factory.analyze
|
|
46920
|
-
};
|
|
46921
|
-
this.rules.set(ruleOrId, rule);
|
|
46922
|
-
} else {
|
|
46923
|
-
this.rules.set(ruleOrId.id, ruleOrId);
|
|
46924
|
-
}
|
|
47045
|
+
/**
|
|
47046
|
+
* Register a rule. The 2-arg overload `register(id, factory)` was
|
|
47047
|
+
* deleted in v0.42.0 (architecture review F6): no callers used it,
|
|
47048
|
+
* every `Rule` carries its own `id`, and the per-id registration
|
|
47049
|
+
* was ergonomically redundant with the 1-arg form (`register(rule)`).
|
|
47050
|
+
* External consumers that needed `register-by-id` semantics can
|
|
47051
|
+
* construct a `Rule` directly and pass it in.
|
|
47052
|
+
*/
|
|
47053
|
+
register(rule) {
|
|
47054
|
+
this.rules.set(rule.id, rule);
|
|
46925
47055
|
}
|
|
46926
47056
|
loadBuiltins(onlyRuleId) {
|
|
46927
47057
|
for (const rule of builtinRules) {
|
|
@@ -46950,6 +47080,15 @@ var init_registry = __esm({
|
|
|
46950
47080
|
all() {
|
|
46951
47081
|
return Array.from(this.rules.values());
|
|
46952
47082
|
}
|
|
47083
|
+
/** v0.42.0 (§3b.5): look up a registered rule by id.
|
|
47084
|
+
* Returns undefined if not registered. */
|
|
47085
|
+
get(id) {
|
|
47086
|
+
return this.rules.get(id);
|
|
47087
|
+
}
|
|
47088
|
+
/** v0.42.0 (§3b.5): check whether a rule id is registered. */
|
|
47089
|
+
has(id) {
|
|
47090
|
+
return this.rules.has(id);
|
|
47091
|
+
}
|
|
46953
47092
|
createContexts(config, filePath, cwd, hotspotIssues = []) {
|
|
46954
47093
|
const context = {
|
|
46955
47094
|
config,
|
|
@@ -50601,6 +50740,10 @@ var init_signal_strength2 = __esm({
|
|
|
50601
50740
|
});
|
|
50602
50741
|
|
|
50603
50742
|
// src/engine/worker.ts
|
|
50743
|
+
var worker_exports = {};
|
|
50744
|
+
__export(worker_exports, {
|
|
50745
|
+
scanFile: () => scanFile
|
|
50746
|
+
});
|
|
50604
50747
|
function buildParserCacheConfig(cwd) {
|
|
50605
50748
|
const envVal = process.env.SLOP_AUDIT_CACHE;
|
|
50606
50749
|
const enabled = envVal === "1" || envVal === "true";
|
|
@@ -50677,6 +50820,36 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
|
50677
50820
|
const issues = applyRuleOverrides(rawIssues, config.rules);
|
|
50678
50821
|
const triggeredRuleIds = Array.from(new Set(issues.map((i) => i.ruleId)));
|
|
50679
50822
|
const compScore = compositeScore(triggeredRuleIds, loadSignalStrength());
|
|
50823
|
+
const fireSet = new Set(triggeredRuleIds);
|
|
50824
|
+
const factsWithFireSet = Object.assign(facts, {
|
|
50825
|
+
compositeFireSet: fireSet
|
|
50826
|
+
});
|
|
50827
|
+
for (const { rule } of rules) {
|
|
50828
|
+
if (!rule.id.startsWith("composite/")) continue;
|
|
50829
|
+
try {
|
|
50830
|
+
const compositeIssues = rule.analyze(
|
|
50831
|
+
rule.create({
|
|
50832
|
+
config,
|
|
50833
|
+
filePath,
|
|
50834
|
+
cwd,
|
|
50835
|
+
framework: config.framework,
|
|
50836
|
+
uiLibraries: config.uiLibraries,
|
|
50837
|
+
hasTailwind: config.hasTailwind,
|
|
50838
|
+
supportsRsc: config.supportsRsc,
|
|
50839
|
+
hotspotIssues: []
|
|
50840
|
+
}),
|
|
50841
|
+
factsWithFireSet
|
|
50842
|
+
);
|
|
50843
|
+
if (compositeIssues.length > 0) {
|
|
50844
|
+
issues.push(...compositeIssues);
|
|
50845
|
+
for (const ci of compositeIssues) fireSet.add(ci.ruleId);
|
|
50846
|
+
}
|
|
50847
|
+
} catch (err) {
|
|
50848
|
+
if (process.env.SLOP_AUDIT_DEBUG === "1") {
|
|
50849
|
+
console.error(`[worker] composite ${rule.id} threw: ${err.message}`);
|
|
50850
|
+
}
|
|
50851
|
+
}
|
|
50852
|
+
}
|
|
50680
50853
|
const gapValues = collectGapValues(facts);
|
|
50681
50854
|
const styleSources = collectStyleSources(facts);
|
|
50682
50855
|
const elementTags = facts.v2.jsx.elements.map((e) => e.tag);
|
|
@@ -50854,9 +51027,22 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
|
|
|
50854
51027
|
}
|
|
50855
51028
|
}
|
|
50856
51029
|
if (currentTopFiles.length > 0 && runs.length >= CONSECUTIVE_THRESHOLD) {
|
|
51030
|
+
const latestRun = runs[runs.length - 1];
|
|
51031
|
+
const isNoiseTier = (id) => {
|
|
51032
|
+
const rule = ruleById.get(id);
|
|
51033
|
+
if (!rule) return false;
|
|
51034
|
+
if (rule.defaultOff === true) return true;
|
|
51035
|
+
if (rule.aiSpecific && (rule.severity === "medium" || rule.severity === "low")) return true;
|
|
51036
|
+
return false;
|
|
51037
|
+
};
|
|
51038
|
+
const onRulesInLatestTop = latestRun.topOffenseIds.filter(
|
|
51039
|
+
(id) => !isNoiseTier(id)
|
|
51040
|
+
);
|
|
51041
|
+
const skipChronicOnDefaultOffNoise = onRulesInLatestTop.length === 0;
|
|
50857
51042
|
for (const file of currentTopFiles) {
|
|
50858
51043
|
const consecutive = countConsecutiveTopFileAppearances(file.hash, recentTopHashes);
|
|
50859
51044
|
if (consecutive >= CONSECUTIVE_THRESHOLD) {
|
|
51045
|
+
if (skipChronicOnDefaultOffNoise) continue;
|
|
50860
51046
|
hotspotIssues.push({
|
|
50861
51047
|
ruleId: "component/chronic-offender",
|
|
50862
51048
|
category: "component",
|
|
@@ -50866,7 +51052,7 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
|
|
|
50866
51052
|
filePath: file.filePath,
|
|
50867
51053
|
line: 1,
|
|
50868
51054
|
column: 1,
|
|
50869
|
-
advice: "
|
|
51055
|
+
advice: "Run `slopbrick scan --baseline` to accept current scores as the new floor, or refactor the file to reduce its issue count."
|
|
50870
51056
|
});
|
|
50871
51057
|
}
|
|
50872
51058
|
}
|
|
@@ -53200,16 +53386,16 @@ function axisSlopIndex(slopIndex) {
|
|
|
53200
53386
|
if (slopIndex === void 0 || Number.isNaN(slopIndex)) {
|
|
53201
53387
|
return {
|
|
53202
53388
|
axis: "slopIndex",
|
|
53203
|
-
label: "Slop
|
|
53389
|
+
label: "AI Slop Score (raw)",
|
|
53204
53390
|
health: 70,
|
|
53205
53391
|
source: "default (not measured) \u2014 neutral 70"
|
|
53206
53392
|
};
|
|
53207
53393
|
}
|
|
53208
53394
|
return {
|
|
53209
53395
|
axis: "slopIndex",
|
|
53210
|
-
label: "Slop
|
|
53396
|
+
label: "AI Slop Score (raw)",
|
|
53211
53397
|
health: clamp100(100 - slopIndex),
|
|
53212
|
-
source: `100 -
|
|
53398
|
+
source: `100 - aiSlopScore (${slopIndex.toFixed(1)}) \u2192 inverted to cleanliness`
|
|
53213
53399
|
};
|
|
53214
53400
|
}
|
|
53215
53401
|
function axisArchitecture(architectureConsistency) {
|
|
@@ -54157,6 +54343,16 @@ function assembleScanReport(input) {
|
|
|
54157
54343
|
enrichment
|
|
54158
54344
|
} = input;
|
|
54159
54345
|
const prSlopScore = computePrSlopScore(diffRef, allIssues);
|
|
54346
|
+
const thresholdReport = {
|
|
54347
|
+
aiSlopScore: aggregated.aiSlopScore,
|
|
54348
|
+
p90Score: aggregated.p90Score,
|
|
54349
|
+
peakScore: aggregated.peakScore,
|
|
54350
|
+
categoryScores: aggregated.categoryScores
|
|
54351
|
+
};
|
|
54352
|
+
const failedThresholds_ = failedThresholds(
|
|
54353
|
+
thresholdReport,
|
|
54354
|
+
config
|
|
54355
|
+
);
|
|
54160
54356
|
return {
|
|
54161
54357
|
version: VERSION,
|
|
54162
54358
|
generatedAt,
|
|
@@ -54212,6 +54408,11 @@ function assembleScanReport(input) {
|
|
|
54212
54408
|
parseErrors: parseErrors.length > 0 ? parseErrors : void 0,
|
|
54213
54409
|
baseline: baselineMeta,
|
|
54214
54410
|
thresholds: config.thresholds,
|
|
54411
|
+
// v0.42.0 (user-review fix): expose which gates tripped so CI
|
|
54412
|
+
// consumers can read the JSON output instead of grepping stderr.
|
|
54413
|
+
// Optional field: absent when no thresholds tripped. Computed from
|
|
54414
|
+
// the report's own scores so the JSON is self-describing.
|
|
54415
|
+
failedThresholds: failedThresholds_,
|
|
54215
54416
|
topOffenders: topOffenders.length > 0 ? topOffenders : void 0,
|
|
54216
54417
|
coherence: enrichment.coherence,
|
|
54217
54418
|
coherenceBreakdown: enrichment.coherenceBreakdown,
|
|
@@ -54228,6 +54429,7 @@ var PR_SLOP_WEIGHTS;
|
|
|
54228
54429
|
var init_assembleScanReport = __esm({
|
|
54229
54430
|
"src/cli/report/assembleScanReport.ts"() {
|
|
54230
54431
|
"use strict";
|
|
54432
|
+
init_threshold();
|
|
54231
54433
|
init_types();
|
|
54232
54434
|
PR_SLOP_WEIGHTS = { high: 10, medium: 5, low: 1 };
|
|
54233
54435
|
}
|
|
@@ -54542,29 +54744,29 @@ function renderStructureMarkdown(inventory, constitution) {
|
|
|
54542
54744
|
return lines.join("\n");
|
|
54543
54745
|
}
|
|
54544
54746
|
async function writeStructureMarkdown(workspaceDir, md) {
|
|
54545
|
-
await new Promise((
|
|
54747
|
+
await new Promise((resolve43, reject) => {
|
|
54546
54748
|
try {
|
|
54547
54749
|
const path = (0, import_node_path23.join)(workspaceDir, STRUCTURE_MD_FILE);
|
|
54548
54750
|
(0, import_node_fs21.mkdirSync)((0, import_node_path23.dirname)(path), { recursive: true });
|
|
54549
54751
|
(0, import_node_fs21.writeFileSync)(path, md, "utf-8");
|
|
54550
|
-
|
|
54752
|
+
resolve43();
|
|
54551
54753
|
} catch (err) {
|
|
54552
54754
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
54553
54755
|
}
|
|
54554
54756
|
});
|
|
54555
54757
|
}
|
|
54556
54758
|
async function readStructureMarkdown(workspaceDir) {
|
|
54557
|
-
return new Promise((
|
|
54759
|
+
return new Promise((resolve43) => {
|
|
54558
54760
|
try {
|
|
54559
54761
|
const path = (0, import_node_path23.join)(workspaceDir, STRUCTURE_MD_FILE);
|
|
54560
54762
|
if (!(0, import_node_fs21.existsSync)(path)) {
|
|
54561
|
-
|
|
54763
|
+
resolve43(null);
|
|
54562
54764
|
return;
|
|
54563
54765
|
}
|
|
54564
54766
|
const content = (0, import_node_fs21.readFileSync)(path, "utf-8");
|
|
54565
|
-
|
|
54767
|
+
resolve43(content);
|
|
54566
54768
|
} catch {
|
|
54567
|
-
|
|
54769
|
+
resolve43(null);
|
|
54568
54770
|
}
|
|
54569
54771
|
});
|
|
54570
54772
|
}
|
|
@@ -54614,6 +54816,388 @@ var init_structure_md = __esm({
|
|
|
54614
54816
|
}
|
|
54615
54817
|
});
|
|
54616
54818
|
|
|
54819
|
+
// src/snippet/data.ts
|
|
54820
|
+
var CATEGORY_DIRECTIVES, RULE_HINTS;
|
|
54821
|
+
var init_data = __esm({
|
|
54822
|
+
"src/snippet/data.ts"() {
|
|
54823
|
+
"use strict";
|
|
54824
|
+
CATEGORY_DIRECTIVES = {
|
|
54825
|
+
visual: 'Avoid the saturated "vibe purple" Tailwind palette (violet-400-700, indigo-400-700). Prefer emerald, sky, amber, rose for accents. Never use arbitrary color values like bg-[#7c3aed] when a token exists.',
|
|
54826
|
+
logic: "Never use explicit `any`. Use `unknown` and narrow. Always add an AbortSignal to fetches. Handle errors with try/catch, never swallow with empty catch. Use `as const` instead of `as Type` casts.",
|
|
54827
|
+
wcag: 'All form inputs must have an accessible label (visible <label>, aria-label, or aria-labelledby). Decorative images must have empty alt="". Buttons must be <button>, never <div onClick>. Touch targets must be \u2265 24\xD724 CSS px.',
|
|
54828
|
+
security: 'Never store tokens in localStorage or sessionStorage \u2014 use httpOnly Secure SameSite cookies. Never put secrets in NEXT_PUBLIC_* / REACT_APP_* / VITE_* env vars. Validate e.origin in postMessage handlers. Never dangerouslySetInnerHTML with user input. Never use target="_blank" without rel="noopener".',
|
|
54829
|
+
perf: "Always use AbortController with fetch. Use image width/height attributes to prevent CLS. Use <Suspense> around async client components. Don't load all images eagerly.",
|
|
54830
|
+
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
54831
|
+
layout: "Don't stack badge-above-h1 hero patterns. Don't build 3-stat banner rows without explicit user request. Don't use emoji inside nav items (use SVG icons). Use the project's spacing scale (4px or 8px grid), never arbitrary values like p-[13px].",
|
|
54832
|
+
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
54833
|
+
arch: "For Astro: server-render everything by default; only opt-in to client islands when you need interactivity. Don't put secrets in client-side code.",
|
|
54834
|
+
test: "Use domain-specific fixture data, assert on value shapes not just truthiness, and consolidate repeated setup into helpers. Avoid `expect(x).toBeDefined()` placeholders and textbook fixtures like 'John Doe' or 'test@test.com'."
|
|
54835
|
+
};
|
|
54836
|
+
RULE_HINTS = {
|
|
54837
|
+
// v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
|
|
54838
|
+
// remaining key matches a rule in src/rules/builtins.ts. No orphans.
|
|
54839
|
+
// (v0.16.0 previously moved 35 out-of-scope orphan hints to
|
|
54840
|
+
// docs/research/backlog-rule-hints.md
|
|
54841
|
+
// for future implementers to paste back when the corresponding rules ship.)
|
|
54842
|
+
"security/hardcoded-secret": "Never inline API keys, JWT secrets, or database passwords in source. Load them from env vars and never commit a .env file. Assume any published secret is compromised and rotate it.",
|
|
54843
|
+
"security/exposed-env-var": "NEVER prefix a secret with NEXT_PUBLIC_, VITE_, REACT_APP_, EXPO_PUBLIC_, GATSBY_, or PUBLIC_ \u2014 those vars are inlined into every browser build.",
|
|
54844
|
+
"security/dangerous-cors": "Don't set Access-Control-Allow-Origin: * on production endpoints. Restrict to an explicit allowlist; never combine wildcard origin with credentials: true.",
|
|
54845
|
+
"security/missing-auth-check": "Every server route handler must perform an authentication + authorization check at the top. Reachability of an endpoint by any user (authenticated or not) is a vulnerability, not a feature.",
|
|
54846
|
+
"security/unsafe-html-render": "Sanitize any non-literal value passed to dangerouslySetInnerHTML with DOMPurify. Better: avoid the prop entirely and let React escape via children.",
|
|
54847
|
+
"security/fail-open-auth": "Don't gate auth bypasses on NODE_ENV. Replace dev-env checks with an explicit AUTH_BYPASS flag that's never set in production.",
|
|
54848
|
+
"security/sql-construction": 'Never build SQL with string concatenation or template-literal interpolation. Use parameterized queries: pg client.query("... WHERE id = $1", [id]) or your ORM query builder.',
|
|
54849
|
+
"security/public-admin-route": "Routes under /admin, /internal, /debug, /staff, /manage, /private need an explicit role check on top of standard auth \u2014 auth alone is not enough for privileged paths.",
|
|
54850
|
+
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
54851
|
+
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
54852
|
+
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
54853
|
+
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
54854
|
+
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
54855
|
+
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
54856
|
+
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
54857
|
+
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
54858
|
+
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
54859
|
+
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
54860
|
+
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
54861
|
+
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
54862
|
+
"component/multiple-components-per-file": "One component per file. Move subcomponents into their own files so the Context Window stays small and boundary tests are easy.",
|
|
54863
|
+
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
54864
|
+
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
54865
|
+
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
54866
|
+
"layout/math-element-uniformity": "Human files have lopsided interactive counts (1 button + 12 inputs). AI tends to balance them. Build forms with many inputs and few buttons.",
|
|
54867
|
+
"layout/math-grid-uniformity": "Vary grid-cols-N (grid-cols-2, grid-cols-3, grid-cols-4, grid-cols-6) across sections instead of repeating grid-cols-3.",
|
|
54868
|
+
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
54869
|
+
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
54870
|
+
"logic/heaps-deviation": "Inspect for LLM-style vocabulary patterns: this file's vocabulary grows faster (high Heaps \u03BB) or slower (low \u03BB) than typical source code. Verify authorship if unexpected. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
|
|
54871
|
+
"logic/ks-distribution-shift": "Inspect the shifted features. KS detects both AI anomalies and production-rot anomalies (it is symmetric); combine with Heaps/Zipf for AI-specific signal. (v0.12.0 \u2014 arXiv:2510.15996, Oct 2025.)",
|
|
54872
|
+
"logic/zipf-slope-anomaly": "Inspect for LLM-style frequency distribution: this file's identifier usage is more peaked or flatter than typical human code. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
|
|
54873
|
+
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
54874
|
+
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
54875
|
+
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
54876
|
+
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
54877
|
+
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
54878
|
+
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
54879
|
+
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
54880
|
+
"dead/unused-import": "Remove the import or use the symbol somewhere. Unused imports are the most common AI-iteration signature \u2014 the model added it for a feature, then rewrote the function without cleaning up.",
|
|
54881
|
+
"dead/unused-local": "Remove the declaration or use the variable. AI-iteration signature: the model declared a binding for a feature, then rewrote the function without cleaning up.",
|
|
54882
|
+
"dead/unused-parameter": "Remove the parameter (and update every call site) or use it in the function body. AI-iteration signature: the model added a parameter for a feature, then rewrote the function without removing parameters the new code does not need.",
|
|
54883
|
+
"dead/dead-branch": "Replace the literal boolean with a real condition, or remove the dead branch. AI-iteration signature: a feature flag toggled to a constant, or a wrapper from a previous refactor that was never cleaned up.",
|
|
54884
|
+
"dead/unreachable": "Remove this statement \u2014 code after a return/throw/break/continue is unreachable. AI-iteration signature: the model added an early return for a new error path, then forgot the rest of the function body was still sitting below it.",
|
|
54885
|
+
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
54886
|
+
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
54887
|
+
"typo/math-button-label-uniformity": 'Mix button lengths deliberately \u2014 pair a short "Save" with a longer "Mark as complete" \u2014 instead of repeating the same template.',
|
|
54888
|
+
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
54889
|
+
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
54890
|
+
"visual/math-font-entropy": "Use a wider range of text sizes (text-xs, text-sm, text-lg, text-xl, text-2xl, text-3xl) for a more deliberate type scale.",
|
|
54891
|
+
"visual/math-rounded-entropy": "Use a wider range of border-radius values (sm, md, 2xl, 3xl) instead of repeating the same lg/xl/full pattern.",
|
|
54892
|
+
"visual/math-spacing-entropy": "Mix more spacing values from the design scale (e.g. 3, 5, 7, 10, 14, 20, 28) instead of repeating the same 4/8 pattern.",
|
|
54893
|
+
"visual/naturalness-anomaly": "Use domain-specific identifier names so the identifier stream reflects the actual problem domain. Hindle 2012 \xA74.3: LLM-generated code reuses a narrow band of training-data identifiers, dropping distinct-token ratio below 30%.",
|
|
54894
|
+
"visual/math-color-cluster": "Use at least 3 distinct hue families (e.g. blue + amber + green) instead of clustering every color in the violet/fuchsia band.",
|
|
54895
|
+
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
54896
|
+
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
54897
|
+
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
54898
|
+
"test/duplicate-setup": "Extract shared `beforeEach` / `setupServer` blocks into a single helper (e.g. `renderWithProviders`) so each describe block calls it instead of repeating setup.",
|
|
54899
|
+
"test/fake-placeholder": "Use domain-specific fixture values (`alice@acme-corp.com`, `Order#48231`) or a factory like @faker-js/faker. Avoid textbook placeholders (`John Doe`, `test@test.com`, `id: 1`).",
|
|
54900
|
+
"product/terminology-drift": "Keep the leading noun consistent across files: `PostList`, `PostDetail`, `PostCard` are one entity, not three. AI agents pick slightly different words each invocation; product copy drifts.",
|
|
54901
|
+
"product/ux-pattern-fragmentation": "Keep the per-category count tight: modal \u22643, toast \u22642, button \u22644, input \u22643, card \u22643. Pick the canonical one and alias the rest. `slopbrick patterns` reports the per-category count.",
|
|
54902
|
+
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
54903
|
+
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
54904
|
+
"ai/comment-ratio": "AI tools either skip comments (reductive models) or over-comment (expansive models). Match the corpus mean \xB1 2\u03C3 (Rahman et al. 2024, Bisztray et al. 2025).",
|
|
54905
|
+
"ai/whitespace-regularity": "Vary inter-token spacing (single spaces mostly, occasional alignment in tables). Uniform runs are an AI tell (Shi et al. DetectCodeGPT 2024).",
|
|
54906
|
+
"ai/text-like-ratio": "Move natural-language explanations to README files or doc comments. Inline prose in source code is hard to maintain (Yotkova 2026).",
|
|
54907
|
+
"ai/errors-near-eof": "Check whether the file was truncated by a token limit. Unbalanced delimiters near EOF suggest the model ran out of output budget (Yotkova 2026).",
|
|
54908
|
+
"ai/any-density": "Replace `any` with `unknown`, `Record<string, unknown>`, or a domain type. The `: any` annotation propagates type-errors and defeats TS safety (Lee, Hassan, Hindle MSR 2026).",
|
|
54909
|
+
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
54910
|
+
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
54911
|
+
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
54912
|
+
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
54913
|
+
// reclassified post-v7 calibration in v0.14.5d)
|
|
54914
|
+
"ai/tailwind-color-overuse": "If most utility classes are bg-violet-500, text-violet-600, ring-violet-400 \u2014 the project is on the AI-default palette. Audit and replace with the project's design tokens.",
|
|
54915
|
+
"ai/default-react-stack": "Every new file is a Vite + React + Tailwind + Zustand + React Hook Form clone. Verify the project actually needs each piece before adding it.",
|
|
54916
|
+
"ai/library-reinvention": "Re-implementing zustand, react-hook-form, or date-fns inline (custom event emitters, useState reducers, manual date math) is a sign of LLM completion-mode code. Use the libraries the project already depends on.",
|
|
54917
|
+
"ai/state-default-overuse": "Wrapping every component in useState + useEffect for transient UI state is the React tutorial default. Real production code uses refs, uncontrolled inputs, or the project's state lib.",
|
|
54918
|
+
"ai/fetch-default-overuse": "Calling fetch() inline in components instead of going through the project's data-fetching layer (react-query, swr, or your own client) bypasses the cache, error boundary, and abort handling.",
|
|
54919
|
+
"ai/console-debug-storm": "5+ console.log calls in a single file is debug-by-print-statement, the LLM training-data default. Remove before commit; use the project's logger or a real debugger.",
|
|
54920
|
+
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
54921
|
+
"db/sql-concat": 'Never build SQL with template-literal interpolation \u2014 `db.query(`SELECT \u2026 WHERE id = ${id}`)` is a SQL injection vector. Use parameterized queries (`db.query("\u2026 WHERE id = $1", [id])`) or your ORM query builder.',
|
|
54922
|
+
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
54923
|
+
"docs/stale-package-reference": "Update the doc to reference an installed package, or add the package to package.json. Copy-pasted install commands from a previous project are the #1 doc-drift failure mode.",
|
|
54924
|
+
"docs/stale-function-reference": "Rename the doc reference to match a current export, or add a wrapper export. Stale function callouts in tutorials cost readers 10+ minutes of debugging.",
|
|
54925
|
+
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
|
|
54926
|
+
// v0.18.9 — Rust-specific rules (tree-sitter-backed)
|
|
54927
|
+
"rust/unused-pub-fn": "Remove the function or call it. Rust's compiler doesn't warn on `pub fn` references unless `#![warn(dead_code)]` is set at the crate root. AI agents leave these behind during iterative refactors.",
|
|
54928
|
+
"rust/unwrap-in-production": "Replace `.unwrap()` / `.expect()` with `?` (early-return on Err), `.map_err(...)` for error conversion, or an explicit `match`. They panic in production; reserve them for `#[cfg(test)]` or `#[test]` scopes.",
|
|
54929
|
+
"rust/todo-macro": "Implement the function body or remove the stub. `todo!()` / `unimplemented!()` both expand to `panic!()`, so a hit at runtime crashes the program. Move them into a `#[cfg(test)] mod tests` block to suppress.",
|
|
54930
|
+
"rust/stringly-typed": 'Replace the `String` / `&str` parameter with the typed enum that already exists in the file. Stringly-typed APIs lose type information at the boundary \u2014 typos (`"Click"` vs `"click"`) only fail at runtime.',
|
|
54931
|
+
// v0.19.0 — TypeScript-specific rules
|
|
54932
|
+
"ts/enum-vs-as-const": "Replace `enum Foo { A, B }` with `const Foo = { A: 'A', B: 'B' } as const` (or `const Foo = ['A', 'B'] as const`). Modern TS style guides (Google, TS-eslint) prefer `as const` because enums have surprising runtime semantics.",
|
|
54933
|
+
"ts/import-type-misuse": "Use `import type { X } from '...'` instead of `import { type X } from '...'`. The split form is more common in real codebases and makes the type-only intent unambiguous.",
|
|
54934
|
+
"ts/never-vs-unknown": "The `never` return type means 'this function never returns'. Reserve it for functions that always throw, always loop, or always exit. For 'impossible' branches, use a concrete type (`void`, `Error`, `unknown`) and an exhaustive check.",
|
|
54935
|
+
"ts/excessive-type-assertion": "More than 3 `as` assertions in one function is a strong signal that the type is wrong, not the code. Fix the type definition (or use a type guard) instead of bypassing the type system.",
|
|
54936
|
+
// v0.19.0 — Go-specific rules
|
|
54937
|
+
"go/struct-tag-inconsistency": 'Pick one tag style per struct. If most fields are `json:"foo"`, this field should be too. Real Go code maintains consistency within a struct (or within a package).',
|
|
54938
|
+
"go/nil-slice-vs-empty": "Either declare as `var x = []int{}` or assign with `make([]int, 0)`. The nil/empty inconsistency is an AI signal \u2014 real code picks one form and sticks with it.",
|
|
54939
|
+
// v0.19.0 — Duplication detector
|
|
54940
|
+
"dup/identical-block": "Refactor to a shared helper. This is a Type-1 clone (byte-for-byte identical after normalization). Common in AI-generated code that copy-pastes from training data.",
|
|
54941
|
+
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
54942
|
+
"dup/near-duplicate": "Refactor to a shared helper. Type-2 near-duplicate: same identifiers + structure, different whitespace/comments. AI-iterated code regenerates the same function with cosmetic edits. (v0.23.0 \u2014 MinHash + LSH, k=5, thr 0.7.)",
|
|
54943
|
+
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
54944
|
+
"dup/structural-clone": "Refactor to a shared helper. Type-3 clone: same shape as another file after identifier canonicalization, with renames and/or added/removed statements. AI agents and copy-paste both produce this. Extract the shared part. (v0.24.0.)",
|
|
54945
|
+
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
54946
|
+
// v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
|
|
54947
|
+
// v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
|
|
54948
|
+
"java/sql-string-concat": "Use a PreparedStatement (JDBC), setParameter (jOOQ), or an ORM (Hibernate, MyBatis with #{}). String concat into SQL is the canonical SQL-injection pattern. OWASP A03:2021. (v0.30.0 \u2014 DORMANT, ratio 0.59.)",
|
|
54949
|
+
"java/thread-sleep-in-loop": "Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the polling anti-pattern \u2014 ties up Tomcat/Jetty/Netty threads. (v0.30.0 \u2014 DORMANT, era-confounded ratio 0.97.)",
|
|
54950
|
+
// v0.35.0 — content-based detection (CoCoNUTS-inspired)
|
|
54951
|
+
"java/suspicious-implementation": "Function name claims validate/encrypt/hash/sanitize but body is empty, returns null/true, or returns the input. This is a content mismatch \u2014 the function's claimed behavior doesn't match its actual behavior. OWASP A04:2021.",
|
|
54952
|
+
// v0.35.1 — Raidar-inspired content-based detection
|
|
54953
|
+
"java/lost-stack-trace": 'catch block throws a new exception without the original cause \u2014 stack trace is lost. Pass the original exception as the second arg to the new exception\'s constructor: `throw new XxxException("msg", e)`.',
|
|
54954
|
+
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
54955
|
+
"swift/force-unwrap": "Replace with the safe form: `as?` + guard/if let, `try?`, or `guard let x = optional else { return }`. `!` crashes unconditionally in release. (v0.24.0 \u2014 DORMANT.)",
|
|
54956
|
+
"swift/print-debug": "Replace with `Logger(subsystem:..., category:...).info(...)` (os.log). `print` writes to stdout with no level and no redaction. (v0.24.0 \u2014 DORMANT.)",
|
|
54957
|
+
"swift/fatal-error-thrown": "Replace with `return` of a typed default (e.g. `nil`, `[]()`) or throw a typed `enum MyError: Error`. `fatalError` survives release builds. (v0.24.0 \u2014 DORMANT.)",
|
|
54958
|
+
"swift/implicitly-unwrapped-optional": "Declare as `var name: Type?` and unwrap with `guard let`. IUOs exist for Obj-C bridging / IBOutlets but using them everywhere is an AI signal. (v0.24.0 \u2014 DORMANT.)",
|
|
54959
|
+
"swift/strong-self-capture": "Capture with `[weak self]` (or `[unowned self]` if guaranteed non-nil): `[weak self] in self?.foo = bar`. Strong self capture creates a retain cycle. (v0.24.0 \u2014 DORMANT.)",
|
|
54960
|
+
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
54961
|
+
"cpp/raw-new-delete": "Replace with `auto p = std::make_unique<T>(...)`. Smart pointers delete on scope exit, never leak on early return / exception, never double-free. (v0.24.0 \u2014 DORMANT.)",
|
|
54962
|
+
"cpp/c-style-cast": "Use the named cast: `static_cast<int>(x)`, `reinterpret_cast<...>(p)`, `const_cast<...>(ref)`. C-style casts silently pick static / reinterpret / const \u2014 impossible to grep. (v0.24.0 \u2014 DORMANT.)",
|
|
54963
|
+
"cpp/printf-debug": "Replace with `spdlog::info(...)`, `LOG(INFO) << ...` (glog), or `ABSL_LOG(INFO) << ...`. `printf` / `std::cout` have no levels, no redaction, no sink routing. (v0.24.0 \u2014 DORMANT.)",
|
|
54964
|
+
"cpp/magic-numbers": "Replace with `constexpr int MAX = 1024;` (or `static constexpr`). Named constants live next to their value, can be searched, force the reader to mean what they say. (v0.24.0 \u2014 DORMANT.)"
|
|
54965
|
+
};
|
|
54966
|
+
}
|
|
54967
|
+
});
|
|
54968
|
+
|
|
54969
|
+
// src/snippet/generators.ts
|
|
54970
|
+
function wrapWithMarkers(level, body) {
|
|
54971
|
+
const head = level === 1 ? "<!-- slopbrick-managed-section -->\n" : "";
|
|
54972
|
+
return `${head}${MARKER_BEGIN}
|
|
54973
|
+
${body}
|
|
54974
|
+
${MARKER_END}
|
|
54975
|
+
`;
|
|
54976
|
+
}
|
|
54977
|
+
function generateCursorSnippet(rules) {
|
|
54978
|
+
const lines = [];
|
|
54979
|
+
lines.push("# slopbrick cursor rules");
|
|
54980
|
+
lines.push("");
|
|
54981
|
+
lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
|
|
54982
|
+
lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
|
|
54983
|
+
lines.push("");
|
|
54984
|
+
lines.push("## Category-level directives");
|
|
54985
|
+
lines.push("");
|
|
54986
|
+
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
54987
|
+
lines.push("- **" + category + "**: " + directive);
|
|
54988
|
+
}
|
|
54989
|
+
lines.push("");
|
|
54990
|
+
lines.push("## Per-rule directives");
|
|
54991
|
+
lines.push("");
|
|
54992
|
+
const ai = aiSpecificRules(rules);
|
|
54993
|
+
const byCategory = categorizeRules(ai);
|
|
54994
|
+
for (const [category, list] of byCategory) {
|
|
54995
|
+
lines.push("### " + category);
|
|
54996
|
+
lines.push("");
|
|
54997
|
+
for (const rule of list) {
|
|
54998
|
+
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
54999
|
+
lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
|
|
55000
|
+
}
|
|
55001
|
+
lines.push("");
|
|
55002
|
+
}
|
|
55003
|
+
lines.push("## Verification");
|
|
55004
|
+
lines.push("");
|
|
55005
|
+
lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
|
|
55006
|
+
lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
|
|
55007
|
+
lines.push("");
|
|
55008
|
+
return lines.join("\n");
|
|
55009
|
+
}
|
|
55010
|
+
function generateAgentsMdSnippet(rules) {
|
|
55011
|
+
return wrapWithMarkers(
|
|
55012
|
+
1,
|
|
55013
|
+
baseContent(
|
|
55014
|
+
rules,
|
|
55015
|
+
"AGENTS.md",
|
|
55016
|
+
"Generated by `slopbrick init --agents-md`. Read these directives when writing or modifying code in this project. AGENTS.md is the de facto standard read by OpenAI Codex, opencode, Pi, Cline, Continue, Gemini, Claude Code (fallback), and others."
|
|
55017
|
+
) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n"
|
|
55018
|
+
);
|
|
55019
|
+
}
|
|
55020
|
+
function generateClaudeMdSnippet(rules) {
|
|
55021
|
+
return wrapWithMarkers(
|
|
55022
|
+
1,
|
|
55023
|
+
baseContent(
|
|
55024
|
+
rules,
|
|
55025
|
+
"CLAUDE.md",
|
|
55026
|
+
"Generated by `slopbrick init --claude-md`. Read this file at the start of every Claude Code session in this project. CLAUDE.md takes precedence over AGENTS.md for Claude Code."
|
|
55027
|
+
) + "## Verification\n\nRun `slopbrick scan` after edits. If MCP is configured (`slopbrick mcp`), use `slop_scan_file` proactively to check before reporting code as done.\n"
|
|
55028
|
+
);
|
|
55029
|
+
}
|
|
55030
|
+
function generateAiderSnippet(rules) {
|
|
55031
|
+
return baseContent(
|
|
55032
|
+
rules,
|
|
55033
|
+
"CONVENTIONS.md",
|
|
55034
|
+
"Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
|
|
55035
|
+
);
|
|
55036
|
+
}
|
|
55037
|
+
function generateWindsurfSnippet(rules) {
|
|
55038
|
+
return baseContent(
|
|
55039
|
+
rules,
|
|
55040
|
+
".windsurfrules",
|
|
55041
|
+
"Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
|
|
55042
|
+
);
|
|
55043
|
+
}
|
|
55044
|
+
function generateClineRules(rules) {
|
|
55045
|
+
return baseContent(
|
|
55046
|
+
rules,
|
|
55047
|
+
"AGENTS.md",
|
|
55048
|
+
"Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
|
|
55049
|
+
);
|
|
55050
|
+
}
|
|
55051
|
+
function generateCursorrulesLegacySnippet(rules) {
|
|
55052
|
+
return baseContent(
|
|
55053
|
+
rules,
|
|
55054
|
+
".cursorrules",
|
|
55055
|
+
"Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
|
|
55056
|
+
);
|
|
55057
|
+
}
|
|
55058
|
+
function generateGeminiSnippet(rules) {
|
|
55059
|
+
return baseContent(
|
|
55060
|
+
rules,
|
|
55061
|
+
"GEMINI.md",
|
|
55062
|
+
"Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
|
|
55063
|
+
);
|
|
55064
|
+
}
|
|
55065
|
+
function generateCopilotSnippet(rules) {
|
|
55066
|
+
return baseContent(
|
|
55067
|
+
rules,
|
|
55068
|
+
"copilot-instructions.md",
|
|
55069
|
+
"Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
|
|
55070
|
+
);
|
|
55071
|
+
}
|
|
55072
|
+
var MARKER_BEGIN, MARKER_END;
|
|
55073
|
+
var init_generators = __esm({
|
|
55074
|
+
"src/snippet/generators.ts"() {
|
|
55075
|
+
"use strict";
|
|
55076
|
+
init_data();
|
|
55077
|
+
init_render2();
|
|
55078
|
+
MARKER_BEGIN = "<!-- slopbrick:begin:v3 -->";
|
|
55079
|
+
MARKER_END = "<!-- slopbrick:end:v3 -->";
|
|
55080
|
+
}
|
|
55081
|
+
});
|
|
55082
|
+
|
|
55083
|
+
// src/snippet/render.ts
|
|
55084
|
+
function aiSpecificRules(rules) {
|
|
55085
|
+
return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
|
|
55086
|
+
}
|
|
55087
|
+
function categorizeRules(rules) {
|
|
55088
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
55089
|
+
for (const r of rules) {
|
|
55090
|
+
const list = byCategory.get(r.category) ?? [];
|
|
55091
|
+
list.push(r);
|
|
55092
|
+
byCategory.set(r.category, list);
|
|
55093
|
+
}
|
|
55094
|
+
for (const list of byCategory.values()) {
|
|
55095
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
55096
|
+
}
|
|
55097
|
+
return byCategory;
|
|
55098
|
+
}
|
|
55099
|
+
function baseContent(rules, toolLabel, intro) {
|
|
55100
|
+
const lines = [];
|
|
55101
|
+
lines.push("# " + toolLabel);
|
|
55102
|
+
lines.push("");
|
|
55103
|
+
lines.push(intro);
|
|
55104
|
+
lines.push("");
|
|
55105
|
+
const ai = aiSpecificRules(rules);
|
|
55106
|
+
const byCategory = categorizeRules(ai);
|
|
55107
|
+
lines.push("## Category-level directives");
|
|
55108
|
+
lines.push("");
|
|
55109
|
+
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
55110
|
+
lines.push("- **" + category + "**: " + directive);
|
|
55111
|
+
}
|
|
55112
|
+
lines.push("");
|
|
55113
|
+
lines.push("## Per-rule directives");
|
|
55114
|
+
lines.push("");
|
|
55115
|
+
for (const [category, list] of byCategory) {
|
|
55116
|
+
lines.push("### " + category);
|
|
55117
|
+
lines.push("");
|
|
55118
|
+
for (const rule of list) {
|
|
55119
|
+
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
55120
|
+
lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
|
|
55121
|
+
}
|
|
55122
|
+
lines.push("");
|
|
55123
|
+
}
|
|
55124
|
+
return lines.join("\n");
|
|
55125
|
+
}
|
|
55126
|
+
function safeRewrite(existing, freshBlockBody) {
|
|
55127
|
+
const beginIdx = existing.indexOf(MARKER_BEGIN);
|
|
55128
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
55129
|
+
if (beginIdx === -1 && endIdx === -1) {
|
|
55130
|
+
return { content: existing, rewritten: false, failClosed: true, mismatched: false };
|
|
55131
|
+
}
|
|
55132
|
+
if (beginIdx === -1 || endIdx === -1) {
|
|
55133
|
+
return { content: existing, rewritten: false, failClosed: false, mismatched: true };
|
|
55134
|
+
}
|
|
55135
|
+
if (endIdx <= beginIdx) {
|
|
55136
|
+
return { content: existing, rewritten: false, failClosed: false, mismatched: true };
|
|
55137
|
+
}
|
|
55138
|
+
const before = existing.slice(0, beginIdx + MARKER_BEGIN.length);
|
|
55139
|
+
const after = existing.slice(endIdx);
|
|
55140
|
+
const next = `${before}
|
|
55141
|
+
${freshBlockBody}
|
|
55142
|
+
${after}`;
|
|
55143
|
+
return { content: next, rewritten: true, failClosed: false, mismatched: false };
|
|
55144
|
+
}
|
|
55145
|
+
var init_render2 = __esm({
|
|
55146
|
+
"src/snippet/render.ts"() {
|
|
55147
|
+
"use strict";
|
|
55148
|
+
init_data();
|
|
55149
|
+
init_generators();
|
|
55150
|
+
}
|
|
55151
|
+
});
|
|
55152
|
+
|
|
55153
|
+
// src/snippet/refresh.ts
|
|
55154
|
+
var refresh_exports = {};
|
|
55155
|
+
__export(refresh_exports, {
|
|
55156
|
+
SNIPPET_TARGETS: () => SNIPPET_TARGETS,
|
|
55157
|
+
refreshSnippets: () => refreshSnippets
|
|
55158
|
+
});
|
|
55159
|
+
function refreshSnippets(cwd, rules, targets = [...SNIPPET_TARGETS]) {
|
|
55160
|
+
const result = { rewritten: [], failClosed: [], mismatched: [], absent: [] };
|
|
55161
|
+
for (const filename of targets) {
|
|
55162
|
+
const path = (0, import_node_path24.join)(cwd, filename);
|
|
55163
|
+
if (!(0, import_node_fs22.existsSync)(path)) {
|
|
55164
|
+
result.absent.push(filename);
|
|
55165
|
+
continue;
|
|
55166
|
+
}
|
|
55167
|
+
const existing = (0, import_node_fs22.readFileSync)(path, "utf-8");
|
|
55168
|
+
const agentName = filename.replace(/\.md$/i, "").toLowerCase();
|
|
55169
|
+
const generator = agentName === "claude" ? generateAgentsMdSnippet : generateAgentsMdSnippet;
|
|
55170
|
+
const newBlock = generator([...rules]).trim();
|
|
55171
|
+
const outcome = safeRewrite(existing, newBlock);
|
|
55172
|
+
if (outcome.rewritten) {
|
|
55173
|
+
const tmp = `${path}.tmp`;
|
|
55174
|
+
(0, import_node_fs22.writeFileSync)(tmp, outcome.content, "utf-8");
|
|
55175
|
+
const fs = require("fs");
|
|
55176
|
+
fs.renameSync(tmp, path);
|
|
55177
|
+
result.rewritten.push(filename);
|
|
55178
|
+
} else if (outcome.failClosed) {
|
|
55179
|
+
result.failClosed.push(filename);
|
|
55180
|
+
} else if (outcome.mismatched) {
|
|
55181
|
+
result.mismatched.push(filename);
|
|
55182
|
+
}
|
|
55183
|
+
}
|
|
55184
|
+
return result;
|
|
55185
|
+
}
|
|
55186
|
+
var import_node_fs22, import_node_path24, SNIPPET_TARGETS;
|
|
55187
|
+
var init_refresh = __esm({
|
|
55188
|
+
"src/snippet/refresh.ts"() {
|
|
55189
|
+
"use strict";
|
|
55190
|
+
import_node_fs22 = require("fs");
|
|
55191
|
+
import_node_path24 = require("path");
|
|
55192
|
+
init_render2();
|
|
55193
|
+
init_generators();
|
|
55194
|
+
SNIPPET_TARGETS = [
|
|
55195
|
+
"AGENTS.md",
|
|
55196
|
+
"CLAUDE.md"
|
|
55197
|
+
];
|
|
55198
|
+
}
|
|
55199
|
+
});
|
|
55200
|
+
|
|
54617
55201
|
// src/cli/report/persistRun.ts
|
|
54618
55202
|
async function persistRun(input) {
|
|
54619
55203
|
const {
|
|
@@ -54676,7 +55260,7 @@ async function persistRun(input) {
|
|
|
54676
55260
|
const recentTopHashes = telemetryPayloads.map(
|
|
54677
55261
|
(payload) => [...payload.files].sort((a, b) => b.score - a.score).slice(0, 10).map((file) => file.hash)
|
|
54678
55262
|
);
|
|
54679
|
-
const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile((0,
|
|
55263
|
+
const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile((0, import_node_path25.relative)(cwd, c.filePath)) }));
|
|
54680
55264
|
const unmatchedStringLiterals = results.flatMap((r) => r.unmatchedStringLiterals ?? []);
|
|
54681
55265
|
const flywheelOutput = computeFlywheelOutput(
|
|
54682
55266
|
runs,
|
|
@@ -54696,10 +55280,10 @@ async function persistRun(input) {
|
|
|
54696
55280
|
report.research = state.research;
|
|
54697
55281
|
}
|
|
54698
55282
|
if (flywheelOutput.suggestions.length > 0) {
|
|
54699
|
-
const suggestionsDir = (0,
|
|
54700
|
-
if (!(0,
|
|
54701
|
-
(0,
|
|
54702
|
-
(0,
|
|
55283
|
+
const suggestionsDir = (0, import_node_path25.join)(cwd, ".slopbrick", "flywheel");
|
|
55284
|
+
if (!(0, import_node_fs23.existsSync)(suggestionsDir)) (0, import_node_fs23.mkdirSync)(suggestionsDir, { recursive: true });
|
|
55285
|
+
(0, import_node_fs23.writeFileSync)(
|
|
55286
|
+
(0, import_node_path25.join)(suggestionsDir, "rule-suggestions.json"),
|
|
54703
55287
|
JSON.stringify(flywheelOutput.suggestions, null, 2)
|
|
54704
55288
|
);
|
|
54705
55289
|
}
|
|
@@ -54744,13 +55328,28 @@ async function persistRun(input) {
|
|
|
54744
55328
|
}
|
|
54745
55329
|
}
|
|
54746
55330
|
recordTelemetry(cwd, report, results, config, patternInventory);
|
|
55331
|
+
if (input.autoRefreshSnippets) {
|
|
55332
|
+
try {
|
|
55333
|
+
const { refreshSnippets: refreshSnippets2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
|
|
55334
|
+
const outcome = refreshSnippets2(cwd, registry.getRules());
|
|
55335
|
+
if (outcome.rewritten.length > 0 && !options.quiet && !machineReadableStdout) {
|
|
55336
|
+
logger.info(
|
|
55337
|
+
`Snippet refresh: rewrote ${outcome.rewritten.length} file(s) (${outcome.rewritten.join(", ")}).`
|
|
55338
|
+
);
|
|
55339
|
+
}
|
|
55340
|
+
} catch (err) {
|
|
55341
|
+
if (options.quiet !== true && !machineReadableStdout) {
|
|
55342
|
+
logger.warn(`snippet refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
55343
|
+
}
|
|
55344
|
+
}
|
|
55345
|
+
}
|
|
54747
55346
|
}
|
|
54748
|
-
var
|
|
55347
|
+
var import_node_fs23, import_node_path25;
|
|
54749
55348
|
var init_persistRun = __esm({
|
|
54750
55349
|
"src/cli/report/persistRun.ts"() {
|
|
54751
55350
|
"use strict";
|
|
54752
|
-
|
|
54753
|
-
|
|
55351
|
+
import_node_fs23 = require("fs");
|
|
55352
|
+
import_node_path25 = require("path");
|
|
54754
55353
|
init_logger();
|
|
54755
55354
|
init_cache_incremental();
|
|
54756
55355
|
init_flywheel();
|
|
@@ -54889,7 +55488,7 @@ var init_finalizeReport = __esm({
|
|
|
54889
55488
|
function buildBaselineCache(report, configHash, gitHead, cwd) {
|
|
54890
55489
|
const scores = {};
|
|
54891
55490
|
for (const component of report.components) {
|
|
54892
|
-
scores[(0,
|
|
55491
|
+
scores[(0, import_node_path26.relative)(cwd, component.filePath)] = {
|
|
54893
55492
|
baselineScore: component.componentScore,
|
|
54894
55493
|
componentCount: component.componentCount
|
|
54895
55494
|
};
|
|
@@ -54904,11 +55503,11 @@ function buildBaselineCache(report, configHash, gitHead, cwd) {
|
|
|
54904
55503
|
scores
|
|
54905
55504
|
};
|
|
54906
55505
|
}
|
|
54907
|
-
var
|
|
55506
|
+
var import_node_path26;
|
|
54908
55507
|
var init_baseline_cache = __esm({
|
|
54909
55508
|
"src/cli/report/baseline-cache.ts"() {
|
|
54910
55509
|
"use strict";
|
|
54911
|
-
|
|
55510
|
+
import_node_path26 = require("path");
|
|
54912
55511
|
init_types();
|
|
54913
55512
|
}
|
|
54914
55513
|
});
|
|
@@ -54972,24 +55571,24 @@ function buildArtifactUri(filePath, cwd) {
|
|
|
54972
55571
|
return ".";
|
|
54973
55572
|
}
|
|
54974
55573
|
if (cwd) {
|
|
54975
|
-
const absoluteFilePath = (0,
|
|
54976
|
-
const rel = (0,
|
|
55574
|
+
const absoluteFilePath = (0, import_node_path27.isAbsolute)(filePath) ? filePath : (0, import_node_path27.resolve)(cwd, filePath);
|
|
55575
|
+
const rel = (0, import_node_path27.relative)(cwd, absoluteFilePath);
|
|
54977
55576
|
if (rel.startsWith("..")) {
|
|
54978
|
-
return (0,
|
|
55577
|
+
return (0, import_node_path27.basename)(filePath);
|
|
54979
55578
|
}
|
|
54980
55579
|
return rel;
|
|
54981
55580
|
}
|
|
54982
|
-
if ((0,
|
|
54983
|
-
return (0,
|
|
55581
|
+
if ((0, import_node_path27.isAbsolute)(filePath)) {
|
|
55582
|
+
return (0, import_node_path27.basename)(filePath);
|
|
54984
55583
|
}
|
|
54985
55584
|
return filePath;
|
|
54986
55585
|
}
|
|
54987
55586
|
function resolveSourcePath(filePath, cwd) {
|
|
54988
55587
|
if (!filePath) return null;
|
|
54989
55588
|
if (cwd) {
|
|
54990
|
-
return (0,
|
|
55589
|
+
return (0, import_node_path27.isAbsolute)(filePath) ? filePath : (0, import_node_path27.resolve)(cwd, filePath);
|
|
54991
55590
|
}
|
|
54992
|
-
return (0,
|
|
55591
|
+
return (0, import_node_path27.isAbsolute)(filePath) ? filePath : null;
|
|
54993
55592
|
}
|
|
54994
55593
|
function severityToSarifLevel(severity) {
|
|
54995
55594
|
switch (severity) {
|
|
@@ -55072,7 +55671,7 @@ function buildResultFromIssue(issue, cwd, fileContentCache) {
|
|
|
55072
55671
|
let content = fileContentCache.get(sourcePath);
|
|
55073
55672
|
if (content === void 0) {
|
|
55074
55673
|
try {
|
|
55075
|
-
content = (0,
|
|
55674
|
+
content = (0, import_node_fs24.readFileSync)(sourcePath, "utf-8");
|
|
55076
55675
|
fileContentCache.set(sourcePath, content);
|
|
55077
55676
|
} catch {
|
|
55078
55677
|
fileContentCache.set(sourcePath, "");
|
|
@@ -55146,13 +55745,13 @@ function formatSarif(report, options) {
|
|
|
55146
55745
|
};
|
|
55147
55746
|
return JSON.stringify(log, null, 2);
|
|
55148
55747
|
}
|
|
55149
|
-
var import_node_crypto9,
|
|
55748
|
+
var import_node_crypto9, import_node_fs24, import_node_path27, REPO_INFORMATION_URI, RULES_BASE_URL;
|
|
55150
55749
|
var init_sarif = __esm({
|
|
55151
55750
|
"src/report/sarif.ts"() {
|
|
55152
55751
|
"use strict";
|
|
55153
55752
|
import_node_crypto9 = require("crypto");
|
|
55154
|
-
|
|
55155
|
-
|
|
55753
|
+
import_node_fs24 = require("fs");
|
|
55754
|
+
import_node_path27 = require("path");
|
|
55156
55755
|
REPO_INFORMATION_URI = "https://github.com/usebrick/platform";
|
|
55157
55756
|
RULES_BASE_URL = "https://github.com/usebrick/platform/blob/main/packages/slopbrick/src/rules";
|
|
55158
55757
|
}
|
|
@@ -55323,11 +55922,13 @@ function extractNamedScores(report) {
|
|
|
55323
55922
|
};
|
|
55324
55923
|
}
|
|
55325
55924
|
function renderThresholds(report) {
|
|
55326
|
-
const
|
|
55327
|
-
const
|
|
55328
|
-
const
|
|
55329
|
-
const
|
|
55330
|
-
const
|
|
55925
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
55926
|
+
const aiAvailable = typeof report.aiSlopScore === "number";
|
|
55927
|
+
const aiPassed = aiAvailable && report.aiSlopScore <= meanSlop;
|
|
55928
|
+
const rh = report.repositoryHealth;
|
|
55929
|
+
const rhAvailable = typeof rh === "number";
|
|
55930
|
+
const rhLimit = 70;
|
|
55931
|
+
const rhPassed = rhAvailable && rh >= rhLimit;
|
|
55331
55932
|
return `
|
|
55332
55933
|
<section class="thresholds-section">
|
|
55333
55934
|
<h2>Thresholds</h2>
|
|
@@ -55341,9 +55942,14 @@ function renderThresholds(report) {
|
|
|
55341
55942
|
</thead>
|
|
55342
55943
|
<tbody>
|
|
55343
55944
|
<tr>
|
|
55344
|
-
<td>
|
|
55345
|
-
<td>${
|
|
55346
|
-
<td><span class="status-badge ${thresholdStatusClass(
|
|
55945
|
+
<td>AI Slop Score (CI gate)</td>
|
|
55946
|
+
<td>${aiAvailable ? `${report.aiSlopScore.toFixed(1)} / ${meanSlop}` : "\u2013"}</td>
|
|
55947
|
+
<td><span class="status-badge ${thresholdStatusClass(!aiPassed)}">${aiAvailable ? aiPassed ? "pass" : "fail" : "n/a"}</span></td>
|
|
55948
|
+
</tr>
|
|
55949
|
+
<tr>
|
|
55950
|
+
<td>Repository Health (informational)</td>
|
|
55951
|
+
<td>${rhAvailable ? `${rh.toFixed(1)} / ${rhLimit}` : "\u2013"}</td>
|
|
55952
|
+
<td><span class="status-badge ${thresholdStatusClass(!rhPassed)}">${rhAvailable ? rhPassed ? "pass" : "fail" : "n/a"}</span></td>
|
|
55347
55953
|
</tr>
|
|
55348
55954
|
</tbody>
|
|
55349
55955
|
</table>
|
|
@@ -56337,18 +56943,18 @@ function applyReplaceFixes(content, fixes) {
|
|
|
56337
56943
|
return { content, applied, skipped, reasons };
|
|
56338
56944
|
}
|
|
56339
56945
|
function applyLayoutTokenFix(filePath, fixes) {
|
|
56340
|
-
let content = (0,
|
|
56946
|
+
let content = (0, import_node_fs25.readFileSync)(filePath, "utf-8");
|
|
56341
56947
|
const { content: patched, applied, skipped, reasons } = applyReplaceFixes(content, fixes);
|
|
56342
56948
|
if (patched !== content) {
|
|
56343
|
-
(0,
|
|
56949
|
+
(0, import_node_fs25.writeFileSync)(filePath, patched);
|
|
56344
56950
|
}
|
|
56345
56951
|
return { applied, skipped, reasons };
|
|
56346
56952
|
}
|
|
56347
|
-
var
|
|
56953
|
+
var import_node_fs25;
|
|
56348
56954
|
var init_layout_token = __esm({
|
|
56349
56955
|
"src/fix/layout-token.ts"() {
|
|
56350
56956
|
"use strict";
|
|
56351
|
-
|
|
56957
|
+
import_node_fs25 = require("fs");
|
|
56352
56958
|
}
|
|
56353
56959
|
});
|
|
56354
56960
|
|
|
@@ -56428,8 +57034,8 @@ function formatUnifiedDiff(report, cwd) {
|
|
|
56428
57034
|
const parts = [];
|
|
56429
57035
|
let hasHunk = false;
|
|
56430
57036
|
for (const [filePath, fixes] of byFile) {
|
|
56431
|
-
if (!(0,
|
|
56432
|
-
const original = (0,
|
|
57037
|
+
if (!(0, import_node_fs26.existsSync)(filePath)) continue;
|
|
57038
|
+
const original = (0, import_node_fs26.readFileSync)(filePath, "utf-8");
|
|
56433
57039
|
const patched = applyFixesToString(original, fixes);
|
|
56434
57040
|
if (patched === original) continue;
|
|
56435
57041
|
if (!hasHunk) {
|
|
@@ -56438,7 +57044,7 @@ function formatUnifiedDiff(report, cwd) {
|
|
|
56438
57044
|
parts.push("");
|
|
56439
57045
|
hasHunk = true;
|
|
56440
57046
|
}
|
|
56441
|
-
const rel = (0,
|
|
57047
|
+
const rel = (0, import_node_path28.relative)(cwd, filePath);
|
|
56442
57048
|
parts.push(`--- a/${rel}`);
|
|
56443
57049
|
parts.push(`+++ b/${rel}`);
|
|
56444
57050
|
parts.push(...formatHunk(original, patched));
|
|
@@ -56449,12 +57055,12 @@ function formatUnifiedDiff(report, cwd) {
|
|
|
56449
57055
|
}
|
|
56450
57056
|
return parts.join("\n");
|
|
56451
57057
|
}
|
|
56452
|
-
var
|
|
57058
|
+
var import_node_fs26, import_node_path28;
|
|
56453
57059
|
var init_unified_diff = __esm({
|
|
56454
57060
|
"src/report/unified-diff.ts"() {
|
|
56455
57061
|
"use strict";
|
|
56456
|
-
|
|
56457
|
-
|
|
57062
|
+
import_node_fs26 = require("fs");
|
|
57063
|
+
import_node_path28 = require("path");
|
|
56458
57064
|
init_layout_token();
|
|
56459
57065
|
}
|
|
56460
57066
|
});
|
|
@@ -56480,7 +57086,7 @@ async function buildHeatmap(report, cwd, helpers = { getFileEditCount, getFileLa
|
|
|
56480
57086
|
const reference = new Date(report.generatedAt);
|
|
56481
57087
|
const entries = await Promise.all(
|
|
56482
57088
|
report.components.map(async (component) => {
|
|
56483
|
-
const relPath = (0,
|
|
57089
|
+
const relPath = (0, import_node_path29.relative)(cwd, component.filePath) || component.filePath;
|
|
56484
57090
|
const [edits, lastModified] = await Promise.all([
|
|
56485
57091
|
helpers.getFileEditCount(cwd, relPath, RECENCY_DAYS),
|
|
56486
57092
|
helpers.getFileLastModifiedDate(cwd, relPath)
|
|
@@ -56525,11 +57131,11 @@ function formatHeatmap(entries, options = {}) {
|
|
|
56525
57131
|
);
|
|
56526
57132
|
return [header, ...rows].join("\n");
|
|
56527
57133
|
}
|
|
56528
|
-
var
|
|
57134
|
+
var import_node_path29, RECENCY_DAYS, MAX_EDITS;
|
|
56529
57135
|
var init_heatmap = __esm({
|
|
56530
57136
|
"src/report/heatmap.ts"() {
|
|
56531
57137
|
"use strict";
|
|
56532
|
-
|
|
57138
|
+
import_node_path29 = require("path");
|
|
56533
57139
|
init_git();
|
|
56534
57140
|
RECENCY_DAYS = 30;
|
|
56535
57141
|
MAX_EDITS = 10;
|
|
@@ -56570,7 +57176,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56570
57176
|
if (options.html) {
|
|
56571
57177
|
const html = formatHtml(report);
|
|
56572
57178
|
if (typeof options.html === "string") {
|
|
56573
|
-
(0,
|
|
57179
|
+
(0, import_node_fs27.writeFileSync)((0, import_node_path30.resolve)(options.html), html);
|
|
56574
57180
|
if (!options.quiet) {
|
|
56575
57181
|
logger.info(`Wrote HTML report to ${options.html}`);
|
|
56576
57182
|
}
|
|
@@ -56582,7 +57188,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56582
57188
|
if (options.json) {
|
|
56583
57189
|
const json = formatJson(report);
|
|
56584
57190
|
if (typeof options.json === "string") {
|
|
56585
|
-
(0,
|
|
57191
|
+
(0, import_node_fs27.writeFileSync)((0, import_node_path30.resolve)(options.json), json);
|
|
56586
57192
|
if (!options.quiet) {
|
|
56587
57193
|
logger.info(`Wrote JSON report to ${options.json}`);
|
|
56588
57194
|
}
|
|
@@ -56596,7 +57202,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56596
57202
|
return;
|
|
56597
57203
|
}
|
|
56598
57204
|
if (options.format === "sarif") {
|
|
56599
|
-
const cwdSarif = (0,
|
|
57205
|
+
const cwdSarif = (0, import_node_path30.resolve)(options.workspace ?? process.cwd());
|
|
56600
57206
|
logger.info(formatSarif(report, { cwd: cwdSarif }));
|
|
56601
57207
|
return;
|
|
56602
57208
|
}
|
|
@@ -56617,12 +57223,12 @@ async function outputScanResults(report, options, cwd) {
|
|
|
56617
57223
|
}
|
|
56618
57224
|
renderOutput(report, options, cwd);
|
|
56619
57225
|
}
|
|
56620
|
-
var
|
|
57226
|
+
var import_node_path30, import_node_fs27, VALID_FORMATS;
|
|
56621
57227
|
var init_renderOutput = __esm({
|
|
56622
57228
|
"src/cli/report/renderOutput.ts"() {
|
|
56623
57229
|
"use strict";
|
|
56624
|
-
|
|
56625
|
-
|
|
57230
|
+
import_node_path30 = require("path");
|
|
57231
|
+
import_node_fs27 = require("fs");
|
|
56626
57232
|
init_logger();
|
|
56627
57233
|
init_json();
|
|
56628
57234
|
init_pretty();
|
|
@@ -56647,7 +57253,7 @@ async function watchProject(options, cwd, paths) {
|
|
|
56647
57253
|
let currentBaseline;
|
|
56648
57254
|
function getBaselineMtime() {
|
|
56649
57255
|
try {
|
|
56650
|
-
return (0,
|
|
57256
|
+
return (0, import_node_fs28.statSync)(baselinePath(cwd)).mtimeMs;
|
|
56651
57257
|
} catch {
|
|
56652
57258
|
return void 0;
|
|
56653
57259
|
}
|
|
@@ -56755,12 +57361,12 @@ async function watchProject(options, cwd, paths) {
|
|
|
56755
57361
|
await doScan(false);
|
|
56756
57362
|
if (closed) return;
|
|
56757
57363
|
const currentFiles = new Set(issueGroupsMap.keys());
|
|
56758
|
-
watcher = (0,
|
|
57364
|
+
watcher = (0, import_node_fs28.watch)(
|
|
56759
57365
|
cwd,
|
|
56760
57366
|
{ recursive: true },
|
|
56761
57367
|
(_eventType, filename) => {
|
|
56762
57368
|
if (closed || !filename) return;
|
|
56763
|
-
const changedPath = (0,
|
|
57369
|
+
const changedPath = (0, import_node_path31.resolve)(cwd, filename.toString());
|
|
56764
57370
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
56765
57371
|
debounceTimer = setTimeout(() => {
|
|
56766
57372
|
debounceTimer = void 0;
|
|
@@ -56795,12 +57401,12 @@ async function watchProject(options, cwd, paths) {
|
|
|
56795
57401
|
}
|
|
56796
57402
|
);
|
|
56797
57403
|
}
|
|
56798
|
-
var
|
|
57404
|
+
var import_node_fs28, import_node_path31;
|
|
56799
57405
|
var init_watch = __esm({
|
|
56800
57406
|
"src/cli/watch.ts"() {
|
|
56801
57407
|
"use strict";
|
|
56802
|
-
|
|
56803
|
-
|
|
57408
|
+
import_node_fs28 = require("fs");
|
|
57409
|
+
import_node_path31 = require("path");
|
|
56804
57410
|
init_metrics();
|
|
56805
57411
|
init_worker();
|
|
56806
57412
|
init_threshold();
|
|
@@ -56820,11 +57426,11 @@ async function runScan(options, explicitPaths) {
|
|
|
56820
57426
|
setLoggerQuiet(!!options.quiet);
|
|
56821
57427
|
const startTime = Date.now();
|
|
56822
57428
|
const scanId = (0, import_node_crypto10.randomUUID)();
|
|
56823
|
-
const cwd = (0,
|
|
56824
|
-
if (!(0,
|
|
57429
|
+
const cwd = (0, import_node_path32.resolve)(options.workspace ?? process.cwd());
|
|
57430
|
+
if (!(0, import_node_fs29.existsSync)(cwd)) {
|
|
56825
57431
|
throw new Error(`Workspace not found: ${cwd}`);
|
|
56826
57432
|
}
|
|
56827
|
-
const cwdStat = (0,
|
|
57433
|
+
const cwdStat = (0, import_node_fs29.statSync)(cwd);
|
|
56828
57434
|
if (!cwdStat.isDirectory()) {
|
|
56829
57435
|
throw new Error(`Workspace is not a directory: ${cwd}`);
|
|
56830
57436
|
}
|
|
@@ -56851,7 +57457,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56851
57457
|
process.env.SLOP_AUDIT_CACHE = "1";
|
|
56852
57458
|
}
|
|
56853
57459
|
if (options.tokens) {
|
|
56854
|
-
const tokenResult = readDtcgTokensFile((0,
|
|
57460
|
+
const tokenResult = readDtcgTokensFile((0, import_node_path32.resolve)(cwd, options.tokens));
|
|
56855
57461
|
if (tokenResult.ok) {
|
|
56856
57462
|
const extra = tokensToAllowlist(tokenResult.tree);
|
|
56857
57463
|
config.arbitraryValueAllowlist = [...config.arbitraryValueAllowlist, ...extra];
|
|
@@ -56863,14 +57469,14 @@ async function runScan(options, explicitPaths) {
|
|
|
56863
57469
|
if (explicitPaths && explicitPaths.length > 0) {
|
|
56864
57470
|
const { globby: globby4 } = await import("globby");
|
|
56865
57471
|
const { minimatch: minimatch4 } = await import("minimatch");
|
|
56866
|
-
const resolved = explicitPaths.map((p) => (0,
|
|
57472
|
+
const resolved = explicitPaths.map((p) => (0, import_node_path32.resolve)(cwd, p));
|
|
56867
57473
|
const expanded = [];
|
|
56868
57474
|
for (const p of resolved) {
|
|
56869
|
-
if ((0,
|
|
57475
|
+
if ((0, import_node_fs29.existsSync)(p) && (0, import_node_fs29.statSync)(p).isDirectory()) {
|
|
56870
57476
|
const found = await globby4(`${p}/**/*`, { absolute: true, onlyFiles: true });
|
|
56871
57477
|
for (const f of found) {
|
|
56872
|
-
if (!ALL_SOURCE_EXTENSIONS.has((0,
|
|
56873
|
-
const rel = (0,
|
|
57478
|
+
if (!ALL_SOURCE_EXTENSIONS.has((0, import_node_path32.extname)(f).toLowerCase())) continue;
|
|
57479
|
+
const rel = (0, import_node_path32.relative)(cwd, f).split(import_node_path32.sep).join("/");
|
|
56874
57480
|
if (config.include.length > 0 && !config.include.some((pattern) => minimatch4(rel, pattern))) {
|
|
56875
57481
|
continue;
|
|
56876
57482
|
}
|
|
@@ -56904,12 +57510,16 @@ async function runScan(options, explicitPaths) {
|
|
|
56904
57510
|
files = intersectFiles(files, since, cwd);
|
|
56905
57511
|
}
|
|
56906
57512
|
let incrementalSummary;
|
|
57513
|
+
let cachePath3;
|
|
57514
|
+
let existingCache;
|
|
57515
|
+
let unchanged = [];
|
|
56907
57516
|
if (options.incremental) {
|
|
56908
|
-
|
|
56909
|
-
|
|
56910
|
-
const
|
|
56911
|
-
files = toScan;
|
|
56912
|
-
|
|
57517
|
+
cachePath3 = options.cachePath ?? ".slopbrick-cache.json";
|
|
57518
|
+
existingCache = loadCache(cachePath3);
|
|
57519
|
+
const partition = partitionByCache(files, existingCache);
|
|
57520
|
+
files = partition.toScan;
|
|
57521
|
+
unchanged = partition.unchanged;
|
|
57522
|
+
incrementalSummary = { skipped: unchanged.length, rescanned: partition.toScan.length };
|
|
56913
57523
|
}
|
|
56914
57524
|
const machineReadableStdout = !!options.json || !!options.html || options.format === "json" || options.format === "sarif" || options.format === "html";
|
|
56915
57525
|
if (files.length === 0 && !options.quiet && !machineReadableStdout) {
|
|
@@ -56929,6 +57539,9 @@ async function runScan(options, explicitPaths) {
|
|
|
56929
57539
|
logger.warn(
|
|
56930
57540
|
`No source files matched in ${cwd}. Adjust --include or check your config at ${projectRoot}.`
|
|
56931
57541
|
);
|
|
57542
|
+
logger.warn(
|
|
57543
|
+
"Tip: run from a directory with source files, or pass --workspace <path> (monorepos usually want `--workspace packages/<name>`)."
|
|
57544
|
+
);
|
|
56932
57545
|
}
|
|
56933
57546
|
}
|
|
56934
57547
|
const configHash = hashConfig(config);
|
|
@@ -57020,7 +57633,7 @@ async function runScan(options, explicitPaths) {
|
|
|
57020
57633
|
const scannedPaths = new Set(results.map((result) => result.filePath));
|
|
57021
57634
|
for (const [filePath, cached] of Object.entries(baseline.scores)) {
|
|
57022
57635
|
if (scannedPaths.has(filePath)) continue;
|
|
57023
|
-
if (!(0,
|
|
57636
|
+
if (!(0, import_node_fs29.existsSync)(filePath)) continue;
|
|
57024
57637
|
scores.push({
|
|
57025
57638
|
filePath,
|
|
57026
57639
|
rawScore: 0,
|
|
@@ -57059,7 +57672,12 @@ async function runScan(options, explicitPaths) {
|
|
|
57059
57672
|
const { report, noIncreaseFailure } = await finalizeReport({
|
|
57060
57673
|
cwd,
|
|
57061
57674
|
config,
|
|
57062
|
-
options
|
|
57675
|
+
options: {
|
|
57676
|
+
...options,
|
|
57677
|
+
// v0.42.0 (§3a.4): forward the opt-in flag through to
|
|
57678
|
+
// `persistRun`, which calls `refreshSnippets` post-scan.
|
|
57679
|
+
autoRefreshSnippets: Boolean(options.autoRefreshSnippets) || Boolean(config.autoRefreshSnippets)
|
|
57680
|
+
},
|
|
57063
57681
|
results,
|
|
57064
57682
|
aggregated,
|
|
57065
57683
|
allIssues,
|
|
@@ -57073,6 +57691,38 @@ async function runScan(options, explicitPaths) {
|
|
|
57073
57691
|
telemetryEnabled,
|
|
57074
57692
|
machineReadableStdout
|
|
57075
57693
|
});
|
|
57694
|
+
if (options.incremental && cachePath3 !== void 0) {
|
|
57695
|
+
const cachedFiles = {};
|
|
57696
|
+
for (const r of results) {
|
|
57697
|
+
try {
|
|
57698
|
+
cachedFiles[r.filePath] = {
|
|
57699
|
+
hash: computeFileHash(r.filePath),
|
|
57700
|
+
issueCount: r.issues.length,
|
|
57701
|
+
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
57702
|
+
};
|
|
57703
|
+
} catch {
|
|
57704
|
+
}
|
|
57705
|
+
}
|
|
57706
|
+
for (const f of unchanged) {
|
|
57707
|
+
const cached = existingCache?.files[f];
|
|
57708
|
+
if (cached) {
|
|
57709
|
+
cachedFiles[f] = {
|
|
57710
|
+
hash: cached.hash,
|
|
57711
|
+
issueCount: cached.issueCount,
|
|
57712
|
+
lastScannedAt: cached.lastScannedAt
|
|
57713
|
+
};
|
|
57714
|
+
} else {
|
|
57715
|
+
}
|
|
57716
|
+
}
|
|
57717
|
+
try {
|
|
57718
|
+
saveCache(cachePath3, {
|
|
57719
|
+
version: VERSION,
|
|
57720
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
57721
|
+
files: cachedFiles
|
|
57722
|
+
});
|
|
57723
|
+
} catch {
|
|
57724
|
+
}
|
|
57725
|
+
}
|
|
57076
57726
|
return {
|
|
57077
57727
|
report,
|
|
57078
57728
|
scores,
|
|
@@ -57098,12 +57748,12 @@ async function scanProject(options) {
|
|
|
57098
57748
|
const { report } = await runScan({ ...options, workspace: options.cwd });
|
|
57099
57749
|
return report;
|
|
57100
57750
|
}
|
|
57101
|
-
var
|
|
57751
|
+
var import_node_fs29, import_node_path32, import_node_crypto10;
|
|
57102
57752
|
var init_scan2 = __esm({
|
|
57103
57753
|
"src/cli/scan.ts"() {
|
|
57104
57754
|
"use strict";
|
|
57105
|
-
|
|
57106
|
-
|
|
57755
|
+
import_node_fs29 = require("fs");
|
|
57756
|
+
import_node_path32 = require("path");
|
|
57107
57757
|
import_node_crypto10 = require("crypto");
|
|
57108
57758
|
init_render();
|
|
57109
57759
|
init_threshold();
|
|
@@ -57131,6 +57781,50 @@ var init_scan2 = __esm({
|
|
|
57131
57781
|
}
|
|
57132
57782
|
});
|
|
57133
57783
|
|
|
57784
|
+
// src/rules/composite-seed.ts
|
|
57785
|
+
var composite_seed_exports = {};
|
|
57786
|
+
__export(composite_seed_exports, {
|
|
57787
|
+
HAND_CURATED_SEED: () => HAND_CURATED_SEED,
|
|
57788
|
+
maybeAppendSeed: () => maybeAppendSeed
|
|
57789
|
+
});
|
|
57790
|
+
function maybeAppendSeed(entries) {
|
|
57791
|
+
if (entries.length === 0) {
|
|
57792
|
+
return [...HAND_CURATED_SEED];
|
|
57793
|
+
}
|
|
57794
|
+
return entries;
|
|
57795
|
+
}
|
|
57796
|
+
var HAND_CURATED_SEED;
|
|
57797
|
+
var init_composite_seed = __esm({
|
|
57798
|
+
"src/rules/composite-seed.ts"() {
|
|
57799
|
+
"use strict";
|
|
57800
|
+
HAND_CURATED_SEED = [
|
|
57801
|
+
{
|
|
57802
|
+
id: "composite/ai-fingerprint-cluster",
|
|
57803
|
+
ruleIds: ["ai/compression-profile", "ai/comment-ratio", "ai/segment-surprisal-cv"],
|
|
57804
|
+
minMatch: 2,
|
|
57805
|
+
severity: "medium",
|
|
57806
|
+
defaultOff: true,
|
|
57807
|
+
description: "Composite rule: at least 2 of {ai/compression-profile+ai/comment-ratio+...} fire on the same file. Hand-curated seed from the v0.42.0 brief; superseded by `slopbrick composite discover` once empirical clusters dominate the ledger.",
|
|
57808
|
+
calibration: {
|
|
57809
|
+
recall: 0,
|
|
57810
|
+
FP: 0,
|
|
57811
|
+
precision: 0,
|
|
57812
|
+
F1: 0,
|
|
57813
|
+
nFiles: 0
|
|
57814
|
+
},
|
|
57815
|
+
provenance: {
|
|
57816
|
+
seed: "hand-curated-by-brief",
|
|
57817
|
+
discoveredAt: "2026-07-05T00:00:00.000Z",
|
|
57818
|
+
nFiles: 0,
|
|
57819
|
+
members: 3,
|
|
57820
|
+
npmi: 0,
|
|
57821
|
+
fisherP: 1
|
|
57822
|
+
}
|
|
57823
|
+
}
|
|
57824
|
+
];
|
|
57825
|
+
}
|
|
57826
|
+
});
|
|
57827
|
+
|
|
57134
57828
|
// src/index.ts
|
|
57135
57829
|
var src_exports = {};
|
|
57136
57830
|
__export(src_exports, {
|
|
@@ -57175,6 +57869,7 @@ __export(src_exports, {
|
|
|
57175
57869
|
directoryScore: () => directoryScore,
|
|
57176
57870
|
extractSignatures: () => extractSignatures,
|
|
57177
57871
|
failedThresholdCount: () => failedThresholdCount,
|
|
57872
|
+
failedThresholds: () => failedThresholds,
|
|
57178
57873
|
filterByDisabledDirectives: () => filterByDisabledDirectives,
|
|
57179
57874
|
filterIssues: () => filterIssues,
|
|
57180
57875
|
findSimilarFunctions: () => findSimilarFunctions,
|
|
@@ -57217,7 +57912,7 @@ init_config2();
|
|
|
57217
57912
|
init_dist2();
|
|
57218
57913
|
|
|
57219
57914
|
// src/cli/program.ts
|
|
57220
|
-
var
|
|
57915
|
+
var import_node_path75 = require("path");
|
|
57221
57916
|
var import_commander3 = require("commander");
|
|
57222
57917
|
|
|
57223
57918
|
// src/cli/options.ts
|
|
@@ -57260,18 +57955,20 @@ init_scan2();
|
|
|
57260
57955
|
init_scan2();
|
|
57261
57956
|
|
|
57262
57957
|
// src/cli/init.ts
|
|
57263
|
-
var
|
|
57264
|
-
var
|
|
57958
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
57959
|
+
var import_node_fs31 = require("fs");
|
|
57960
|
+
var import_node_path34 = require("path");
|
|
57265
57961
|
var import_node_readline = require("readline");
|
|
57266
57962
|
init_config2();
|
|
57267
57963
|
init_discover();
|
|
57268
57964
|
init_git();
|
|
57269
57965
|
init_cache();
|
|
57270
57966
|
init_logger();
|
|
57967
|
+
init_types();
|
|
57271
57968
|
|
|
57272
57969
|
// src/rules/registry-loader.ts
|
|
57273
|
-
var
|
|
57274
|
-
var
|
|
57970
|
+
var import_node_fs30 = require("fs");
|
|
57971
|
+
var import_node_path33 = require("path");
|
|
57275
57972
|
|
|
57276
57973
|
// src/data/shadcn-registry.json
|
|
57277
57974
|
var shadcn_registry_default = {
|
|
@@ -57405,12 +58102,12 @@ var shadcn_registry_default = {
|
|
|
57405
58102
|
var REGISTRY_URL = "https://ui.shadcn.com/registry.json";
|
|
57406
58103
|
var BUNDLED_REGISTRY_VERSION = shadcn_registry_default.version;
|
|
57407
58104
|
function cachePath2(cwd) {
|
|
57408
|
-
return (0,
|
|
58105
|
+
return (0, import_node_path33.join)(cwd, ".slopbrick", "cache", "registry-snapshot.json");
|
|
57409
58106
|
}
|
|
57410
58107
|
function ensureCacheDir(cwd) {
|
|
57411
|
-
const dir = (0,
|
|
57412
|
-
if (!(0,
|
|
57413
|
-
(0,
|
|
58108
|
+
const dir = (0, import_node_path33.dirname)(cachePath2(cwd));
|
|
58109
|
+
if (!(0, import_node_fs30.existsSync)(dir)) {
|
|
58110
|
+
(0, import_node_fs30.mkdirSync)(dir, { recursive: true });
|
|
57414
58111
|
}
|
|
57415
58112
|
}
|
|
57416
58113
|
function isValidSnapshot(value) {
|
|
@@ -57422,9 +58119,9 @@ function isValidSnapshot(value) {
|
|
|
57422
58119
|
}
|
|
57423
58120
|
function isRegistryFresh(cwd) {
|
|
57424
58121
|
const cached = cachePath2(cwd);
|
|
57425
|
-
if (!(0,
|
|
58122
|
+
if (!(0, import_node_fs30.existsSync)(cached)) return false;
|
|
57426
58123
|
try {
|
|
57427
|
-
const parsed = JSON.parse((0,
|
|
58124
|
+
const parsed = JSON.parse((0, import_node_fs30.readFileSync)(cached, "utf8"));
|
|
57428
58125
|
if (!isValidSnapshot(parsed)) return false;
|
|
57429
58126
|
return parsed.version === BUNDLED_REGISTRY_VERSION;
|
|
57430
58127
|
} catch {
|
|
@@ -57435,19 +58132,24 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
|
|
|
57435
58132
|
try {
|
|
57436
58133
|
const cacheFile = cachePath2(cwd);
|
|
57437
58134
|
ensureCacheDir(cwd);
|
|
57438
|
-
(0,
|
|
58135
|
+
(0, import_node_fs30.writeFileSync)(cacheFile, JSON.stringify(shadcn_registry_default, null, 2), "utf-8");
|
|
57439
58136
|
} catch {
|
|
57440
58137
|
}
|
|
57441
58138
|
return {
|
|
57442
58139
|
snapshot: shadcn_registry_default,
|
|
57443
58140
|
ok: true,
|
|
57444
58141
|
fresh: false,
|
|
57445
|
-
|
|
58142
|
+
// v0.42.0 (user-review fix): the previous message said
|
|
58143
|
+
// "upstream URL returns 404" which is alarming without context —
|
|
58144
|
+
// upstream has been dead since v0.39.0, and the bundled snapshot
|
|
58145
|
+
// IS the snapshot in use. Replaced with a neutral message that
|
|
58146
|
+
// doesn't suggest "something is wrong".
|
|
58147
|
+
message: "Using bundled shadcn/ui snapshot (built-in; no network fetch)."
|
|
57446
58148
|
};
|
|
57447
58149
|
}
|
|
57448
58150
|
function copyBundledSnapshotToCache(cwd) {
|
|
57449
58151
|
ensureCacheDir(cwd);
|
|
57450
|
-
(0,
|
|
58152
|
+
(0, import_node_fs30.writeFileSync)(cachePath2(cwd), JSON.stringify(shadcn_registry_default, null, 2));
|
|
57451
58153
|
}
|
|
57452
58154
|
|
|
57453
58155
|
// src/cli/init.ts
|
|
@@ -57476,17 +58178,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
|
|
|
57476
58178
|
var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
|
|
57477
58179
|
var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
|
|
57478
58180
|
function promptText(rl, question, detected) {
|
|
57479
|
-
return new Promise((
|
|
58181
|
+
return new Promise((resolve43) => {
|
|
57480
58182
|
const lines = [
|
|
57481
58183
|
`? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
|
|
57482
58184
|
];
|
|
57483
58185
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57484
58186
|
const trimmed = answer.trim();
|
|
57485
58187
|
if (trimmed === "") {
|
|
57486
|
-
|
|
58188
|
+
resolve43(void 0);
|
|
57487
58189
|
return;
|
|
57488
58190
|
}
|
|
57489
|
-
|
|
58191
|
+
resolve43(trimmed);
|
|
57490
58192
|
});
|
|
57491
58193
|
});
|
|
57492
58194
|
}
|
|
@@ -57494,7 +58196,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57494
58196
|
const defaultIndex = options.indexOf(defaultValue);
|
|
57495
58197
|
const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
|
|
57496
58198
|
const safeDefaultValue = options[safeDefaultIndex];
|
|
57497
|
-
return new Promise((
|
|
58199
|
+
return new Promise((resolve43) => {
|
|
57498
58200
|
function ask() {
|
|
57499
58201
|
const lines = [
|
|
57500
58202
|
`? ${question} (detected: ${safeDefaultValue}):`,
|
|
@@ -57504,7 +58206,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57504
58206
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57505
58207
|
const trimmed = answer.trim();
|
|
57506
58208
|
if (trimmed === "") {
|
|
57507
|
-
|
|
58209
|
+
resolve43(safeDefaultValue);
|
|
57508
58210
|
return;
|
|
57509
58211
|
}
|
|
57510
58212
|
const num = parseInt(trimmed, 10);
|
|
@@ -57513,7 +58215,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57513
58215
|
ask();
|
|
57514
58216
|
return;
|
|
57515
58217
|
}
|
|
57516
|
-
|
|
58218
|
+
resolve43(options[num - 1]);
|
|
57517
58219
|
});
|
|
57518
58220
|
}
|
|
57519
58221
|
ask();
|
|
@@ -57523,7 +58225,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57523
58225
|
const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
|
|
57524
58226
|
const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
|
|
57525
58227
|
const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
|
|
57526
|
-
return new Promise((
|
|
58228
|
+
return new Promise((resolve43) => {
|
|
57527
58229
|
function ask() {
|
|
57528
58230
|
const lines = [
|
|
57529
58231
|
`? ${question} (detected: ${defaultDisplay}):`,
|
|
@@ -57534,7 +58236,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57534
58236
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57535
58237
|
const trimmed = answer.trim();
|
|
57536
58238
|
if (trimmed === "") {
|
|
57537
|
-
|
|
58239
|
+
resolve43(defaultValue.length > 0 ? defaultValue : []);
|
|
57538
58240
|
return;
|
|
57539
58241
|
}
|
|
57540
58242
|
const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
|
|
@@ -57544,11 +58246,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57544
58246
|
return;
|
|
57545
58247
|
}
|
|
57546
58248
|
if (numbers.includes(1)) {
|
|
57547
|
-
|
|
58249
|
+
resolve43([]);
|
|
57548
58250
|
return;
|
|
57549
58251
|
}
|
|
57550
58252
|
const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
|
|
57551
|
-
|
|
58253
|
+
resolve43(selected);
|
|
57552
58254
|
});
|
|
57553
58255
|
}
|
|
57554
58256
|
ask();
|
|
@@ -57613,16 +58315,23 @@ async function runDoctor(cwd) {
|
|
|
57613
58315
|
lines.push("slopbrick doctor\n");
|
|
57614
58316
|
lines.push("Checking your setup:\n");
|
|
57615
58317
|
const nodeMajor = parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
57616
|
-
if (nodeMajor >= 20) ok(`Node ${process.versions.node}`);
|
|
58318
|
+
if (nodeMajor >= 20) ok(`Node ${process.versions.node}, slopbrick ${VERSION}`);
|
|
57617
58319
|
else fail(`Node ${process.versions.node} \u2014 slopbrick needs Node 20 or newer. Run: nvm install 20`);
|
|
57618
58320
|
lines.push(`
|
|
57619
58321
|
Working in: ${cwd}`);
|
|
57620
58322
|
try {
|
|
58323
|
+
const configPath = resolveConfigPath(cwd);
|
|
57621
58324
|
const config = await loadConfig(cwd);
|
|
57622
|
-
|
|
57623
|
-
|
|
57624
|
-
|
|
57625
|
-
|
|
58325
|
+
if (configPath) {
|
|
58326
|
+
const ruleCount = Object.keys(config.rules ?? {}).length;
|
|
58327
|
+
lines.push(`
|
|
58328
|
+
\u2713 Config loaded from: ${configPath}`);
|
|
58329
|
+
lines.push(` ${ruleCount} rule${ruleCount === 1 ? "" : "s"} configured in your config file.`);
|
|
58330
|
+
} else {
|
|
58331
|
+
lines.push(`
|
|
58332
|
+
\u2713 Config: using built-in defaults (no config file found).`);
|
|
58333
|
+
lines.push(import_chalk2.default.dim(` Run \`slopbrick init\` to create ${cwd.endsWith("/") ? cwd : cwd + "/"}slopbrick.config.mjs and tune rule severity/rules to your project.`));
|
|
58334
|
+
}
|
|
57626
58335
|
} catch (error) {
|
|
57627
58336
|
fail(`Could not read your config: ${error.message}`);
|
|
57628
58337
|
exitCode = 3;
|
|
@@ -57636,11 +58345,11 @@ async function runDoctor(cwd) {
|
|
|
57636
58345
|
}
|
|
57637
58346
|
try {
|
|
57638
58347
|
const { parseFile: tryParse } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
57639
|
-
const testFile = (0,
|
|
57640
|
-
(0,
|
|
57641
|
-
(0,
|
|
58348
|
+
const testFile = (0, import_node_path34.join)(cwd, ".slopbrick", ".doctor-test.ts");
|
|
58349
|
+
(0, import_node_fs31.mkdirSync)((0, import_node_path34.dirname)(testFile), { recursive: true });
|
|
58350
|
+
(0, import_node_fs31.writeFileSync)(testFile, "export const x = 1;\n");
|
|
57642
58351
|
await tryParse(testFile);
|
|
57643
|
-
(0,
|
|
58352
|
+
(0, import_node_fs31.rmSync)(testFile, { force: true });
|
|
57644
58353
|
ok("Parser is working.");
|
|
57645
58354
|
} catch (err) {
|
|
57646
58355
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -57679,6 +58388,7 @@ async function runDoctor(cwd) {
|
|
|
57679
58388
|
ok(`Found ${scannable.length} source files to scan.`);
|
|
57680
58389
|
} else {
|
|
57681
58390
|
warn("No source files (.ts/.tsx/.js/.jsx/.vue/.svelte/.astro/.mdx) found in this directory.");
|
|
58391
|
+
lines.push(import_chalk2.default.dim(" Run from a directory with source files, or pass --workspace <path>. Monorepos usually want `--workspace packages/<name>`."));
|
|
57682
58392
|
}
|
|
57683
58393
|
} catch {
|
|
57684
58394
|
}
|
|
@@ -57729,7 +58439,7 @@ async function runDoctor(cwd) {
|
|
|
57729
58439
|
}
|
|
57730
58440
|
|
|
57731
58441
|
// src/cli/commands/badge.ts
|
|
57732
|
-
var
|
|
58442
|
+
var import_node_path35 = require("path");
|
|
57733
58443
|
init_render();
|
|
57734
58444
|
init_logger();
|
|
57735
58445
|
init_scan2();
|
|
@@ -57738,12 +58448,12 @@ function registerBadge(program) {
|
|
|
57738
58448
|
"print a shields.io slop-index badge. Reads .slopbrick/health.json if present (no re-scan); falls back to a fresh scan."
|
|
57739
58449
|
).action(async (_cmdOptions, command) => {
|
|
57740
58450
|
const options = command.optsWithGlobals();
|
|
57741
|
-
const cwd = (0,
|
|
58451
|
+
const cwd = (0, import_node_path35.resolve)(options.workspace ?? process.cwd());
|
|
57742
58452
|
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
57743
58453
|
const health = loadHealth2(cwd);
|
|
57744
58454
|
if (health) {
|
|
57745
58455
|
const synthetic = {
|
|
57746
|
-
|
|
58456
|
+
aiSlopScore: health.aiSlopScore ?? 0
|
|
57747
58457
|
};
|
|
57748
58458
|
logger.info(formatBadge(synthetic));
|
|
57749
58459
|
process.exit(0);
|
|
@@ -57755,7 +58465,7 @@ function registerBadge(program) {
|
|
|
57755
58465
|
}
|
|
57756
58466
|
|
|
57757
58467
|
// src/cli/commands/suggest.ts
|
|
57758
|
-
var
|
|
58468
|
+
var import_node_path36 = require("path");
|
|
57759
58469
|
init_advice();
|
|
57760
58470
|
init_unified_diff();
|
|
57761
58471
|
init_logger();
|
|
@@ -57764,7 +58474,7 @@ function registerSuggest(program) {
|
|
|
57764
58474
|
program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
|
|
57765
58475
|
const options = command.optsWithGlobals();
|
|
57766
58476
|
const { report } = await runScan(options);
|
|
57767
|
-
const cwd = (0,
|
|
58477
|
+
const cwd = (0, import_node_path36.resolve)(options.workspace ?? process.cwd());
|
|
57768
58478
|
logger.info(formatAdvice(report));
|
|
57769
58479
|
const diff = formatUnifiedDiff(report, cwd);
|
|
57770
58480
|
if (diff) logger.info(diff);
|
|
@@ -57772,6 +58482,470 @@ function registerSuggest(program) {
|
|
|
57772
58482
|
});
|
|
57773
58483
|
}
|
|
57774
58484
|
|
|
58485
|
+
// src/cli/commands/composite.ts
|
|
58486
|
+
var import_node_path38 = require("path");
|
|
58487
|
+
init_logger();
|
|
58488
|
+
init_telemetry();
|
|
58489
|
+
|
|
58490
|
+
// src/engine/cluster.ts
|
|
58491
|
+
var import_node_crypto11 = require("crypto");
|
|
58492
|
+
var DEFAULT_CLUSTER_PARAMS = {
|
|
58493
|
+
minSupport: 0.05,
|
|
58494
|
+
// 5% of files
|
|
58495
|
+
minNPMI: 0.3,
|
|
58496
|
+
// Cohen's-Landis-and-Koch "fair" agreement
|
|
58497
|
+
fisherAlpha: 0.01,
|
|
58498
|
+
// Bonferroni-style floor for many-pair tests
|
|
58499
|
+
minClusterSize: 2
|
|
58500
|
+
};
|
|
58501
|
+
function adaptiveMinSupport(fileCount) {
|
|
58502
|
+
if (fileCount <= 0) return DEFAULT_CLUSTER_PARAMS.minSupport;
|
|
58503
|
+
return Math.max(DEFAULT_CLUSTER_PARAMS.minSupport, 5 / fileCount);
|
|
58504
|
+
}
|
|
58505
|
+
function computeRuleSupport(fireMatrix) {
|
|
58506
|
+
const support = /* @__PURE__ */ new Map();
|
|
58507
|
+
for (const ruleIds of fireMatrix.values()) {
|
|
58508
|
+
for (const ruleId of ruleIds) {
|
|
58509
|
+
support.set(ruleId, (support.get(ruleId) ?? 0) + 1);
|
|
58510
|
+
}
|
|
58511
|
+
}
|
|
58512
|
+
return support;
|
|
58513
|
+
}
|
|
58514
|
+
function prefilterBySupport(support, totalFiles, minSupport) {
|
|
58515
|
+
const kept = /* @__PURE__ */ new Set();
|
|
58516
|
+
const minCount = Math.max(1, Math.ceil(minSupport * totalFiles));
|
|
58517
|
+
for (const [ruleId, count] of support) {
|
|
58518
|
+
if (count >= minCount) kept.add(ruleId);
|
|
58519
|
+
}
|
|
58520
|
+
return kept;
|
|
58521
|
+
}
|
|
58522
|
+
function buildContingency(rA, rB, totalFiles) {
|
|
58523
|
+
let aAndB = 0;
|
|
58524
|
+
let aOnly = 0;
|
|
58525
|
+
let bOnly = 0;
|
|
58526
|
+
for (const file of rA) {
|
|
58527
|
+
if (rB.has(file)) aAndB++;
|
|
58528
|
+
else aOnly++;
|
|
58529
|
+
}
|
|
58530
|
+
for (const file of rB) {
|
|
58531
|
+
if (!rA.has(file)) bOnly++;
|
|
58532
|
+
}
|
|
58533
|
+
return {
|
|
58534
|
+
aAndB,
|
|
58535
|
+
aOnly,
|
|
58536
|
+
bOnly,
|
|
58537
|
+
neither: Math.max(0, totalFiles - aAndB - aOnly - bOnly)
|
|
58538
|
+
};
|
|
58539
|
+
}
|
|
58540
|
+
function fisherExactTwoSided(c) {
|
|
58541
|
+
const N = c.aAndB + c.aOnly + c.bOnly + c.neither;
|
|
58542
|
+
const K1 = c.aAndB + c.aOnly;
|
|
58543
|
+
const K2 = c.aAndB + c.bOnly;
|
|
58544
|
+
const n = K1;
|
|
58545
|
+
const kObs = c.aAndB;
|
|
58546
|
+
const logPmf = (k) => {
|
|
58547
|
+
if (k < Math.max(0, K1 + K2 - N) || k > Math.min(K1, K2)) return -Infinity;
|
|
58548
|
+
return lnChoose(K2, k) + lnChoose(N - K2, K1 - k) - lnChoose(N, K1);
|
|
58549
|
+
};
|
|
58550
|
+
const pObs = Math.exp(logPmf(kObs));
|
|
58551
|
+
const kMin = Math.max(0, K1 + K2 - N);
|
|
58552
|
+
const kMax = Math.min(K1, K2);
|
|
58553
|
+
let p = 0;
|
|
58554
|
+
for (let k = kMin; k <= kMax; k++) {
|
|
58555
|
+
const pk = Math.exp(logPmf(k));
|
|
58556
|
+
if (pk <= pObs + 1e-12) {
|
|
58557
|
+
p += pk;
|
|
58558
|
+
}
|
|
58559
|
+
}
|
|
58560
|
+
return Math.min(1, p);
|
|
58561
|
+
}
|
|
58562
|
+
function lnChoose(n, k) {
|
|
58563
|
+
if (k < 0 || k > n) return -Infinity;
|
|
58564
|
+
if (k === 0 || k === n) return 0;
|
|
58565
|
+
k = Math.min(k, n - k);
|
|
58566
|
+
let s = 0;
|
|
58567
|
+
for (let i = 1; i <= k; i++) {
|
|
58568
|
+
s += Math.log((n - k + i) / i);
|
|
58569
|
+
}
|
|
58570
|
+
return s;
|
|
58571
|
+
}
|
|
58572
|
+
function npmi(c, total) {
|
|
58573
|
+
if (total === 0 || c.aAndB === 0) return -1;
|
|
58574
|
+
const pXY = c.aAndB / total;
|
|
58575
|
+
const pX = (c.aAndB + c.aOnly) / total;
|
|
58576
|
+
const pY = (c.aAndB + c.bOnly) / total;
|
|
58577
|
+
if (pX === 0 || pY === 0 || pXY === 0) return -1;
|
|
58578
|
+
const pmi = Math.log2(pXY / (pX * pY));
|
|
58579
|
+
const norm = -Math.log2(pXY);
|
|
58580
|
+
return norm === 0 ? 1 : pmi / norm;
|
|
58581
|
+
}
|
|
58582
|
+
function computeEdges(fireMatrix, rules, total, params) {
|
|
58583
|
+
const ruleToFiles = /* @__PURE__ */ new Map();
|
|
58584
|
+
for (const ruleId of rules) ruleToFiles.set(ruleId, /* @__PURE__ */ new Set());
|
|
58585
|
+
for (const [file, fired] of fireMatrix) {
|
|
58586
|
+
for (const ruleId of fired) {
|
|
58587
|
+
if (rules.has(ruleId)) ruleToFiles.get(ruleId).add(file);
|
|
58588
|
+
}
|
|
58589
|
+
}
|
|
58590
|
+
const ruleList = Array.from(rules).sort();
|
|
58591
|
+
const edges = [];
|
|
58592
|
+
for (let i = 0; i < ruleList.length; i++) {
|
|
58593
|
+
for (let j = i + 1; j < ruleList.length; j++) {
|
|
58594
|
+
const a = ruleList[i];
|
|
58595
|
+
const b = ruleList[j];
|
|
58596
|
+
const rA = ruleToFiles.get(a);
|
|
58597
|
+
const rB = ruleToFiles.get(b);
|
|
58598
|
+
const c = buildContingency(rA, rB, total);
|
|
58599
|
+
const n = npmi(c, total);
|
|
58600
|
+
if (n < params.minNPMI) continue;
|
|
58601
|
+
const p = fisherExactTwoSided(c);
|
|
58602
|
+
if (p > params.fisherAlpha) continue;
|
|
58603
|
+
edges.push({ ruleIds: [a, b], weight: n, fisherP: p });
|
|
58604
|
+
}
|
|
58605
|
+
}
|
|
58606
|
+
return edges;
|
|
58607
|
+
}
|
|
58608
|
+
function singleLinkageCluster(edges, minClusterSize) {
|
|
58609
|
+
const parent = /* @__PURE__ */ new Map();
|
|
58610
|
+
const find = (x) => {
|
|
58611
|
+
if (!parent.has(x)) {
|
|
58612
|
+
parent.set(x, x);
|
|
58613
|
+
return x;
|
|
58614
|
+
}
|
|
58615
|
+
if (parent.get(x) === x) return x;
|
|
58616
|
+
const root = find(parent.get(x));
|
|
58617
|
+
parent.set(x, root);
|
|
58618
|
+
return root;
|
|
58619
|
+
};
|
|
58620
|
+
const union = (a, b) => {
|
|
58621
|
+
const ra = find(a);
|
|
58622
|
+
const rb = find(b);
|
|
58623
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
58624
|
+
};
|
|
58625
|
+
for (const e of edges) {
|
|
58626
|
+
union(e.ruleIds[0], e.ruleIds[1]);
|
|
58627
|
+
}
|
|
58628
|
+
const groups = /* @__PURE__ */ new Map();
|
|
58629
|
+
for (const k of parent.keys()) {
|
|
58630
|
+
const root = find(k);
|
|
58631
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
58632
|
+
groups.get(root).push(k);
|
|
58633
|
+
}
|
|
58634
|
+
const clusters = [];
|
|
58635
|
+
for (const members of groups.values()) {
|
|
58636
|
+
if (members.length < minClusterSize) continue;
|
|
58637
|
+
members.sort();
|
|
58638
|
+
clusters.push(members);
|
|
58639
|
+
}
|
|
58640
|
+
clusters.sort((a, b) => b.length - a.length || a[0].localeCompare(b[0]));
|
|
58641
|
+
return clusters;
|
|
58642
|
+
}
|
|
58643
|
+
function filesForComposite(fireMatrix, members, minMatch) {
|
|
58644
|
+
const fired = /* @__PURE__ */ new Set();
|
|
58645
|
+
for (const [file, ids] of fireMatrix) {
|
|
58646
|
+
let n = 0;
|
|
58647
|
+
for (const m of members) if (ids.has(m)) n++;
|
|
58648
|
+
if (n >= minMatch) fired.add(file);
|
|
58649
|
+
}
|
|
58650
|
+
return fired;
|
|
58651
|
+
}
|
|
58652
|
+
var DEFAULT_CALIBRATION = {
|
|
58653
|
+
// 1.5× is the documented floor.
|
|
58654
|
+
recallFpRatio: 1.5,
|
|
58655
|
+
// 0.05 absolute FP — the same `min` value most rules get via the
|
|
58656
|
+
// v0.18.x calibration contract.
|
|
58657
|
+
minFpFloor: 0.05
|
|
58658
|
+
};
|
|
58659
|
+
function calibrateCluster(cluster2, edges, fireMatrix, positiveFiles, totalFiles, thresholds = DEFAULT_CALIBRATION) {
|
|
58660
|
+
if (cluster2.length < 2) return null;
|
|
58661
|
+
const clusterSet = new Set(cluster2);
|
|
58662
|
+
const clusterEdges = edges.filter(
|
|
58663
|
+
(e) => clusterSet.has(e.ruleIds[0]) && clusterSet.has(e.ruleIds[1])
|
|
58664
|
+
);
|
|
58665
|
+
if (clusterEdges.length === 0) return null;
|
|
58666
|
+
const strongest = clusterEdges.reduce((a, b) => b.weight > a.weight ? b : a);
|
|
58667
|
+
let best = null;
|
|
58668
|
+
for (let k = 1; k <= cluster2.length; k++) {
|
|
58669
|
+
const firedFiles = filesForComposite(fireMatrix, cluster2, k);
|
|
58670
|
+
const nFiles = firedFiles.size;
|
|
58671
|
+
if (nFiles === 0) continue;
|
|
58672
|
+
let recall = 0;
|
|
58673
|
+
let fp = 0;
|
|
58674
|
+
let precision = 0;
|
|
58675
|
+
let canCalibrate = false;
|
|
58676
|
+
if (positiveFiles && positiveFiles.size > 0) {
|
|
58677
|
+
let tp = 0;
|
|
58678
|
+
let fpc = 0;
|
|
58679
|
+
for (const file of firedFiles) {
|
|
58680
|
+
if (positiveFiles.has(file)) tp++;
|
|
58681
|
+
else fpc++;
|
|
58682
|
+
}
|
|
58683
|
+
const totalPos = positiveFiles.size;
|
|
58684
|
+
const totalNeg = Math.max(1, totalFiles - totalPos);
|
|
58685
|
+
recall = totalPos > 0 ? tp / totalPos : 0;
|
|
58686
|
+
fp = fpc / totalNeg;
|
|
58687
|
+
precision = tp + fpc > 0 ? tp / (tp + fpc) : 0;
|
|
58688
|
+
canCalibrate = true;
|
|
58689
|
+
}
|
|
58690
|
+
const F1 = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0;
|
|
58691
|
+
if (canCalibrate) {
|
|
58692
|
+
if (recall / Math.max(fp, 1e-9) < thresholds.recallFpRatio) continue;
|
|
58693
|
+
if (fp < thresholds.minFpFloor && recall < 0.5) continue;
|
|
58694
|
+
}
|
|
58695
|
+
if (best === null || F1 > best.F1 + 1e-9 || Math.abs(F1 - best.F1) < 1e-9 && k > best.minMatch) {
|
|
58696
|
+
best = {
|
|
58697
|
+
minMatch: k,
|
|
58698
|
+
npmi: strongest.weight,
|
|
58699
|
+
fisherP: strongest.fisherP,
|
|
58700
|
+
recall,
|
|
58701
|
+
FP: fp,
|
|
58702
|
+
precision,
|
|
58703
|
+
F1,
|
|
58704
|
+
nFiles
|
|
58705
|
+
};
|
|
58706
|
+
}
|
|
58707
|
+
}
|
|
58708
|
+
return best;
|
|
58709
|
+
}
|
|
58710
|
+
function compositeId(members) {
|
|
58711
|
+
const sorted = [...members].sort();
|
|
58712
|
+
return "composite/" + (0, import_node_crypto11.createHash)("sha1").update(sorted.join("\n")).digest("hex").slice(0, 12);
|
|
58713
|
+
}
|
|
58714
|
+
function worstSeverity(members, memberSeverities) {
|
|
58715
|
+
let worst = "low";
|
|
58716
|
+
const order = { low: 0, medium: 1, high: 2 };
|
|
58717
|
+
for (const m of members) {
|
|
58718
|
+
const s = memberSeverities?.get(m);
|
|
58719
|
+
if (s && order[s] > order[worst]) worst = s;
|
|
58720
|
+
}
|
|
58721
|
+
return worst;
|
|
58722
|
+
}
|
|
58723
|
+
function describeComposite(members, minMatch) {
|
|
58724
|
+
const clusterPart = members.length <= 4 ? members.join("+") : `${members.slice(0, 3).join("+")}+${members.length - 3} more`;
|
|
58725
|
+
return `Composite rule: at least ${minMatch} of {${clusterPart}} fire on the same file.`;
|
|
58726
|
+
}
|
|
58727
|
+
function runClusterer(input) {
|
|
58728
|
+
const total = input.fireMatrix.size;
|
|
58729
|
+
const minSupport = input.params?.minSupport ?? adaptiveMinSupport(total);
|
|
58730
|
+
const params = {
|
|
58731
|
+
minSupport,
|
|
58732
|
+
minNPMI: input.params?.minNPMI ?? DEFAULT_CLUSTER_PARAMS.minNPMI,
|
|
58733
|
+
fisherAlpha: input.params?.fisherAlpha ?? DEFAULT_CLUSTER_PARAMS.fisherAlpha,
|
|
58734
|
+
minClusterSize: input.params?.minClusterSize ?? DEFAULT_CLUSTER_PARAMS.minClusterSize
|
|
58735
|
+
};
|
|
58736
|
+
const support = computeRuleSupport(input.fireMatrix);
|
|
58737
|
+
const rulesKept = prefilterBySupport(support, total, params.minSupport);
|
|
58738
|
+
const edges = computeEdges(input.fireMatrix, rulesKept, total, params);
|
|
58739
|
+
const clusters = singleLinkageCluster(edges, params.minClusterSize);
|
|
58740
|
+
const entries = [];
|
|
58741
|
+
const clusterMaxNPMI = /* @__PURE__ */ new Map();
|
|
58742
|
+
for (const cluster2 of clusters) {
|
|
58743
|
+
const cal = calibrateCluster(
|
|
58744
|
+
cluster2,
|
|
58745
|
+
edges,
|
|
58746
|
+
input.fireMatrix,
|
|
58747
|
+
input.positiveFiles,
|
|
58748
|
+
total
|
|
58749
|
+
);
|
|
58750
|
+
if (!cal) continue;
|
|
58751
|
+
const id = compositeId(cluster2);
|
|
58752
|
+
const severity = worstSeverity(cluster2, input.memberSeverities);
|
|
58753
|
+
entries.push({
|
|
58754
|
+
id,
|
|
58755
|
+
ruleIds: [...cluster2].sort(),
|
|
58756
|
+
minMatch: cal.minMatch,
|
|
58757
|
+
severity,
|
|
58758
|
+
defaultOff: true,
|
|
58759
|
+
description: describeComposite(cluster2, cal.minMatch),
|
|
58760
|
+
calibration: {
|
|
58761
|
+
recall: round3(cal.recall),
|
|
58762
|
+
FP: round3(cal.FP),
|
|
58763
|
+
precision: round3(cal.precision),
|
|
58764
|
+
F1: round3(cal.F1),
|
|
58765
|
+
nFiles: cal.nFiles
|
|
58766
|
+
},
|
|
58767
|
+
provenance: {
|
|
58768
|
+
seed: "auto-cluster",
|
|
58769
|
+
discoveredAt: input.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
58770
|
+
nFiles: total,
|
|
58771
|
+
members: cluster2.length,
|
|
58772
|
+
npmi: round3(cal.npmi),
|
|
58773
|
+
fisherP: round3(cal.fisherP)
|
|
58774
|
+
}
|
|
58775
|
+
});
|
|
58776
|
+
clusterMaxNPMI.set(id, cal.npmi);
|
|
58777
|
+
}
|
|
58778
|
+
entries.sort((a, b) => b.calibration.F1 - a.calibration.F1 || a.id.localeCompare(b.id));
|
|
58779
|
+
return { entries, supportCounts: support, clusterMaxNPMI, filesScanned: total, params };
|
|
58780
|
+
}
|
|
58781
|
+
function round3(n) {
|
|
58782
|
+
return Math.round(n * 1e3) / 1e3;
|
|
58783
|
+
}
|
|
58784
|
+
|
|
58785
|
+
// src/rules/composite-loader.ts
|
|
58786
|
+
var import_node_fs32 = require("fs");
|
|
58787
|
+
var import_node_path37 = require("path");
|
|
58788
|
+
var COMPOSITES_FILE = "composites.json";
|
|
58789
|
+
function compositesPath(cwd) {
|
|
58790
|
+
return (0, import_node_path37.join)(cwd, COMPOSITES_FILE);
|
|
58791
|
+
}
|
|
58792
|
+
function readComposites(cwd) {
|
|
58793
|
+
const path = compositesPath(cwd);
|
|
58794
|
+
if (!(0, import_node_fs32.existsSync)(path)) return [];
|
|
58795
|
+
try {
|
|
58796
|
+
const raw = JSON.parse((0, import_node_fs32.readFileSync)(path, "utf-8"));
|
|
58797
|
+
if (!Array.isArray(raw)) return [];
|
|
58798
|
+
return raw.filter(isCompositeRuleEntry);
|
|
58799
|
+
} catch {
|
|
58800
|
+
return [];
|
|
58801
|
+
}
|
|
58802
|
+
}
|
|
58803
|
+
function isCompositeRuleEntry(value) {
|
|
58804
|
+
if (!value || typeof value !== "object") return false;
|
|
58805
|
+
const v = value;
|
|
58806
|
+
return typeof v.id === "string" && Array.isArray(v.ruleIds) && typeof v.minMatch === "number";
|
|
58807
|
+
}
|
|
58808
|
+
function writeComposites(cwd, entries) {
|
|
58809
|
+
const path = compositesPath(cwd);
|
|
58810
|
+
(0, import_node_fs32.mkdirSync)((0, import_node_path37.dirname)(path), { recursive: true });
|
|
58811
|
+
const tmp = `${path}.tmp`;
|
|
58812
|
+
(0, import_node_fs32.writeFileSync)(tmp, JSON.stringify(entries, null, 2), "utf-8");
|
|
58813
|
+
const fs = require("fs");
|
|
58814
|
+
fs.renameSync(tmp, path);
|
|
58815
|
+
}
|
|
58816
|
+
|
|
58817
|
+
// src/cli/commands/composite.ts
|
|
58818
|
+
function registerComposite(program) {
|
|
58819
|
+
const cmd = program.command("composite").description(
|
|
58820
|
+
"discover and enable composite rules (Sprint 3, \xA73b). Run `slopbrick composite discover` to scan scans.jsonl, audit the candidates, then `slopbrick composite enable <id>` to opt a specific composite into slopbrick.config.mjs."
|
|
58821
|
+
);
|
|
58822
|
+
cmd.command("discover").description(
|
|
58823
|
+
"run the empirical composite clusterer on .slopbrick/flywheel/scans.jsonl; writes composites.json"
|
|
58824
|
+
).action(async (_cmdOptions, command) => {
|
|
58825
|
+
const options = command.optsWithGlobals();
|
|
58826
|
+
const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
|
|
58827
|
+
const payloads = readTelemetry(cwd);
|
|
58828
|
+
if (payloads.length === 0) {
|
|
58829
|
+
logger.error(
|
|
58830
|
+
"`composite discover` needs at least 2 scans in .slopbrick/flywheel/scans.jsonl. Run `slopbrick scan` a few times first."
|
|
58831
|
+
);
|
|
58832
|
+
process.exit(1);
|
|
58833
|
+
}
|
|
58834
|
+
const fireMatrix = /* @__PURE__ */ new Map();
|
|
58835
|
+
for (const payload of payloads) {
|
|
58836
|
+
for (const f of payload.files) {
|
|
58837
|
+
const set = fireMatrix.get(f.hash) ?? /* @__PURE__ */ new Set();
|
|
58838
|
+
for (const id of f.ruleIds) set.add(id);
|
|
58839
|
+
if (set.size > 0) fireMatrix.set(f.hash, set);
|
|
58840
|
+
}
|
|
58841
|
+
}
|
|
58842
|
+
const out = runClusterer({
|
|
58843
|
+
fireMatrix,
|
|
58844
|
+
// No labeled positive set; let the clusterer surface for audit.
|
|
58845
|
+
positiveFiles: void 0,
|
|
58846
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
58847
|
+
});
|
|
58848
|
+
const finalEntries = out.entries.length === 0 ? [...(await Promise.resolve().then(() => (init_composite_seed(), composite_seed_exports))).HAND_CURATED_SEED] : out.entries;
|
|
58849
|
+
writeComposites(cwd, finalEntries);
|
|
58850
|
+
if (out.entries.length === 0) {
|
|
58851
|
+
logger.info("Composite discovery: no clusters passed the empirical gate.");
|
|
58852
|
+
logger.info(
|
|
58853
|
+
" (Try lowering --min-support, or run more scans to densify the ledger.)"
|
|
58854
|
+
);
|
|
58855
|
+
return;
|
|
58856
|
+
}
|
|
58857
|
+
logger.info(
|
|
58858
|
+
`Composite discovery: ${out.entries.length} candidate(s) written to composites.json`
|
|
58859
|
+
);
|
|
58860
|
+
const lines = [];
|
|
58861
|
+
for (const e of out.entries) {
|
|
58862
|
+
lines.push("");
|
|
58863
|
+
lines.push(
|
|
58864
|
+
` ${e.id} minMatch=${e.minMatch} F1=${e.calibration.F1} (${e.ruleIds.length} members)`
|
|
58865
|
+
);
|
|
58866
|
+
lines.push(` ${describeComposite(e.ruleIds, e.minMatch)}`);
|
|
58867
|
+
lines.push(
|
|
58868
|
+
` provenance: ${e.provenance.seed} npmi=${e.provenance.npmi} fisherP=${e.provenance.fisherP}`
|
|
58869
|
+
);
|
|
58870
|
+
}
|
|
58871
|
+
logger.info(lines.join("\n"));
|
|
58872
|
+
logger.info(
|
|
58873
|
+
"\nNext step: review each entry. Opt in with `slopbrick composite enable <id>`; opt out by removing the entry."
|
|
58874
|
+
);
|
|
58875
|
+
});
|
|
58876
|
+
cmd.command("enable <id>").description(
|
|
58877
|
+
"opt a specific composite into slopbrick.config.mjs#compositeRules (one composite per call)"
|
|
58878
|
+
).action(async (id, _cmdOptions, command) => {
|
|
58879
|
+
const options = command.optsWithGlobals();
|
|
58880
|
+
const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
|
|
58881
|
+
const composites = readComposites(cwd);
|
|
58882
|
+
const target = composites.find((c) => c.id === id);
|
|
58883
|
+
if (!target) {
|
|
58884
|
+
logger.error(
|
|
58885
|
+
`Composite "${id}" not found in composites.json. Run \`slopbrick composite discover\` first to populate the ledger.`
|
|
58886
|
+
);
|
|
58887
|
+
process.exit(1);
|
|
58888
|
+
}
|
|
58889
|
+
const cfgPath = (0, import_node_path38.resolve)(cwd, "slopbrick.config.mjs");
|
|
58890
|
+
const fs = require("fs");
|
|
58891
|
+
if (!fs.existsSync(cfgPath)) {
|
|
58892
|
+
logger.error(`No slopbrick.config.mjs found at ${cfgPath}.`);
|
|
58893
|
+
process.exit(1);
|
|
58894
|
+
}
|
|
58895
|
+
const src = fs.readFileSync(cfgPath, "utf-8");
|
|
58896
|
+
const compositeSnippet = JSON.stringify(
|
|
58897
|
+
{
|
|
58898
|
+
id: target.id,
|
|
58899
|
+
category: "ai",
|
|
58900
|
+
severity: target.severity,
|
|
58901
|
+
aiSpecific: true,
|
|
58902
|
+
description: target.description,
|
|
58903
|
+
defaultOff: true,
|
|
58904
|
+
ruleIds: target.ruleIds,
|
|
58905
|
+
minMatch: target.minMatch,
|
|
58906
|
+
create: "() => ({})",
|
|
58907
|
+
analyze: "() => []"
|
|
58908
|
+
},
|
|
58909
|
+
null,
|
|
58910
|
+
2
|
|
58911
|
+
);
|
|
58912
|
+
if (src.includes(`id: '${target.id}'`) || src.includes(`id: "${target.id}"`)) {
|
|
58913
|
+
logger.info(`Composite "${id}" is already enabled in slopbrick.config.mjs (idempotent).`);
|
|
58914
|
+
return;
|
|
58915
|
+
}
|
|
58916
|
+
const updated = src.replace(
|
|
58917
|
+
/(export default\s*{[\s\S]*?)(}\s*;?\s*)$/,
|
|
58918
|
+
(_match, head, tail) => `${head} compositeRules: [
|
|
58919
|
+
${compositeSnippet}
|
|
58920
|
+
],
|
|
58921
|
+
${tail}`
|
|
58922
|
+
);
|
|
58923
|
+
fs.writeFileSync(cfgPath, updated, "utf-8");
|
|
58924
|
+
logger.info(`Composite "${id}" enabled.`);
|
|
58925
|
+
logger.info("Next scan will apply it; rerun `slopbrick scan` to see the effect.");
|
|
58926
|
+
});
|
|
58927
|
+
cmd.command("list").description("list composites currently registered (composites.json + config-declared)").action(async (_cmdOptions, command) => {
|
|
58928
|
+
const options = command.optsWithGlobals();
|
|
58929
|
+
const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
|
|
58930
|
+
const composites = readComposites(cwd);
|
|
58931
|
+
if (composites.length === 0) {
|
|
58932
|
+
logger.info("No composites registered. Run `slopbrick composite discover` first.");
|
|
58933
|
+
return;
|
|
58934
|
+
}
|
|
58935
|
+
const lines = ["Registered composites:"];
|
|
58936
|
+
for (const e of composites) {
|
|
58937
|
+
lines.push(
|
|
58938
|
+
`
|
|
58939
|
+
${e.id}
|
|
58940
|
+
${describeComposite(e.ruleIds, e.minMatch)}
|
|
58941
|
+
members=[${e.ruleIds.join(", ")}] minMatch=${e.minMatch}
|
|
58942
|
+
F1=${e.calibration.F1} recall=${e.calibration.recall} FP=${e.calibration.FP}`
|
|
58943
|
+
);
|
|
58944
|
+
}
|
|
58945
|
+
logger.info(lines.join("\n"));
|
|
58946
|
+
});
|
|
58947
|
+
}
|
|
58948
|
+
|
|
57775
58949
|
// src/cli/explain.ts
|
|
57776
58950
|
var RULES_BASE_URL2 = "https://github.com/usebrick/platform/blob/main/packages/slopbrick/src/rules";
|
|
57777
58951
|
function ruleIdToFilename2(ruleId) {
|
|
@@ -57821,154 +58995,13 @@ function formatExplain(result) {
|
|
|
57821
58995
|
// src/cli/commands/explain.ts
|
|
57822
58996
|
init_logger();
|
|
57823
58997
|
init_builtins();
|
|
57824
|
-
|
|
57825
|
-
// src/snippet/data.ts
|
|
57826
|
-
var CATEGORY_DIRECTIVES = {
|
|
57827
|
-
visual: 'Avoid the saturated "vibe purple" Tailwind palette (violet-400-700, indigo-400-700). Prefer emerald, sky, amber, rose for accents. Never use arbitrary color values like bg-[#7c3aed] when a token exists.',
|
|
57828
|
-
logic: "Never use explicit `any`. Use `unknown` and narrow. Always add an AbortSignal to fetches. Handle errors with try/catch, never swallow with empty catch. Use `as const` instead of `as Type` casts.",
|
|
57829
|
-
wcag: 'All form inputs must have an accessible label (visible <label>, aria-label, or aria-labelledby). Decorative images must have empty alt="". Buttons must be <button>, never <div onClick>. Touch targets must be \u2265 24\xD724 CSS px.',
|
|
57830
|
-
security: 'Never store tokens in localStorage or sessionStorage \u2014 use httpOnly Secure SameSite cookies. Never put secrets in NEXT_PUBLIC_* / REACT_APP_* / VITE_* env vars. Validate e.origin in postMessage handlers. Never dangerouslySetInnerHTML with user input. Never use target="_blank" without rel="noopener".',
|
|
57831
|
-
perf: "Always use AbortController with fetch. Use image width/height attributes to prevent CLS. Use <Suspense> around async client components. Don't load all images eagerly.",
|
|
57832
|
-
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
57833
|
-
layout: "Don't stack badge-above-h1 hero patterns. Don't build 3-stat banner rows without explicit user request. Don't use emoji inside nav items (use SVG icons). Use the project's spacing scale (4px or 8px grid), never arbitrary values like p-[13px].",
|
|
57834
|
-
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
57835
|
-
arch: "For Astro: server-render everything by default; only opt-in to client islands when you need interactivity. Don't put secrets in client-side code.",
|
|
57836
|
-
test: "Use domain-specific fixture data, assert on value shapes not just truthiness, and consolidate repeated setup into helpers. Avoid `expect(x).toBeDefined()` placeholders and textbook fixtures like 'John Doe' or 'test@test.com'."
|
|
57837
|
-
};
|
|
57838
|
-
var RULE_HINTS = {
|
|
57839
|
-
// v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
|
|
57840
|
-
// remaining key matches a rule in src/rules/builtins.ts. No orphans.
|
|
57841
|
-
// (v0.16.0 previously moved 35 out-of-scope orphan hints to
|
|
57842
|
-
// docs/research/backlog-rule-hints.md
|
|
57843
|
-
// for future implementers to paste back when the corresponding rules ship.)
|
|
57844
|
-
"security/hardcoded-secret": "Never inline API keys, JWT secrets, or database passwords in source. Load them from env vars and never commit a .env file. Assume any published secret is compromised and rotate it.",
|
|
57845
|
-
"security/exposed-env-var": "NEVER prefix a secret with NEXT_PUBLIC_, VITE_, REACT_APP_, EXPO_PUBLIC_, GATSBY_, or PUBLIC_ \u2014 those vars are inlined into every browser build.",
|
|
57846
|
-
"security/dangerous-cors": "Don't set Access-Control-Allow-Origin: * on production endpoints. Restrict to an explicit allowlist; never combine wildcard origin with credentials: true.",
|
|
57847
|
-
"security/missing-auth-check": "Every server route handler must perform an authentication + authorization check at the top. Reachability of an endpoint by any user (authenticated or not) is a vulnerability, not a feature.",
|
|
57848
|
-
"security/unsafe-html-render": "Sanitize any non-literal value passed to dangerouslySetInnerHTML with DOMPurify. Better: avoid the prop entirely and let React escape via children.",
|
|
57849
|
-
"security/fail-open-auth": "Don't gate auth bypasses on NODE_ENV. Replace dev-env checks with an explicit AUTH_BYPASS flag that's never set in production.",
|
|
57850
|
-
"security/sql-construction": 'Never build SQL with string concatenation or template-literal interpolation. Use parameterized queries: pg client.query("... WHERE id = $1", [id]) or your ORM query builder.',
|
|
57851
|
-
"security/public-admin-route": "Routes under /admin, /internal, /debug, /staff, /manage, /private need an explicit role check on top of standard auth \u2014 auth alone is not enough for privileged paths.",
|
|
57852
|
-
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
57853
|
-
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
57854
|
-
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
57855
|
-
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
57856
|
-
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
57857
|
-
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
57858
|
-
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
57859
|
-
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
57860
|
-
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
57861
|
-
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
57862
|
-
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
57863
|
-
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
57864
|
-
"component/multiple-components-per-file": "One component per file. Move subcomponents into their own files so the Context Window stays small and boundary tests are easy.",
|
|
57865
|
-
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
57866
|
-
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
57867
|
-
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
57868
|
-
"layout/math-element-uniformity": "Human files have lopsided interactive counts (1 button + 12 inputs). AI tends to balance them. Build forms with many inputs and few buttons.",
|
|
57869
|
-
"layout/math-grid-uniformity": "Vary grid-cols-N (grid-cols-2, grid-cols-3, grid-cols-4, grid-cols-6) across sections instead of repeating grid-cols-3.",
|
|
57870
|
-
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
57871
|
-
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
57872
|
-
"logic/heaps-deviation": "Inspect for LLM-style vocabulary patterns: this file's vocabulary grows faster (high Heaps \u03BB) or slower (low \u03BB) than typical source code. Verify authorship if unexpected. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
|
|
57873
|
-
"logic/ks-distribution-shift": "Inspect the shifted features. KS detects both AI anomalies and production-rot anomalies (it is symmetric); combine with Heaps/Zipf for AI-specific signal. (v0.12.0 \u2014 arXiv:2510.15996, Oct 2025.)",
|
|
57874
|
-
"logic/zipf-slope-anomaly": "Inspect for LLM-style frequency distribution: this file's identifier usage is more peaked or flatter than typical human code. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
|
|
57875
|
-
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
57876
|
-
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
57877
|
-
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
57878
|
-
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
57879
|
-
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
57880
|
-
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
57881
|
-
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
57882
|
-
"dead/unused-import": "Remove the import or use the symbol somewhere. Unused imports are the most common AI-iteration signature \u2014 the model added it for a feature, then rewrote the function without cleaning up.",
|
|
57883
|
-
"dead/unused-local": "Remove the declaration or use the variable. AI-iteration signature: the model declared a binding for a feature, then rewrote the function without cleaning up.",
|
|
57884
|
-
"dead/unused-parameter": "Remove the parameter (and update every call site) or use it in the function body. AI-iteration signature: the model added a parameter for a feature, then rewrote the function without removing parameters the new code does not need.",
|
|
57885
|
-
"dead/dead-branch": "Replace the literal boolean with a real condition, or remove the dead branch. AI-iteration signature: a feature flag toggled to a constant, or a wrapper from a previous refactor that was never cleaned up.",
|
|
57886
|
-
"dead/unreachable": "Remove this statement \u2014 code after a return/throw/break/continue is unreachable. AI-iteration signature: the model added an early return for a new error path, then forgot the rest of the function body was still sitting below it.",
|
|
57887
|
-
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
57888
|
-
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
57889
|
-
"typo/math-button-label-uniformity": 'Mix button lengths deliberately \u2014 pair a short "Save" with a longer "Mark as complete" \u2014 instead of repeating the same template.',
|
|
57890
|
-
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
57891
|
-
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
57892
|
-
"visual/math-font-entropy": "Use a wider range of text sizes (text-xs, text-sm, text-lg, text-xl, text-2xl, text-3xl) for a more deliberate type scale.",
|
|
57893
|
-
"visual/math-rounded-entropy": "Use a wider range of border-radius values (sm, md, 2xl, 3xl) instead of repeating the same lg/xl/full pattern.",
|
|
57894
|
-
"visual/math-spacing-entropy": "Mix more spacing values from the design scale (e.g. 3, 5, 7, 10, 14, 20, 28) instead of repeating the same 4/8 pattern.",
|
|
57895
|
-
"visual/naturalness-anomaly": "Use domain-specific identifier names so the identifier stream reflects the actual problem domain. Hindle 2012 \xA74.3: LLM-generated code reuses a narrow band of training-data identifiers, dropping distinct-token ratio below 30%.",
|
|
57896
|
-
"visual/math-color-cluster": "Use at least 3 distinct hue families (e.g. blue + amber + green) instead of clustering every color in the violet/fuchsia band.",
|
|
57897
|
-
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
57898
|
-
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
57899
|
-
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
57900
|
-
"test/duplicate-setup": "Extract shared `beforeEach` / `setupServer` blocks into a single helper (e.g. `renderWithProviders`) so each describe block calls it instead of repeating setup.",
|
|
57901
|
-
"test/fake-placeholder": "Use domain-specific fixture values (`alice@acme-corp.com`, `Order#48231`) or a factory like @faker-js/faker. Avoid textbook placeholders (`John Doe`, `test@test.com`, `id: 1`).",
|
|
57902
|
-
"product/terminology-drift": "Keep the leading noun consistent across files: `PostList`, `PostDetail`, `PostCard` are one entity, not three. AI agents pick slightly different words each invocation; product copy drifts.",
|
|
57903
|
-
"product/ux-pattern-fragmentation": "Keep the per-category count tight: modal \u22643, toast \u22642, button \u22644, input \u22643, card \u22643. Pick the canonical one and alias the rest. `slopbrick patterns` reports the per-category count.",
|
|
57904
|
-
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
57905
|
-
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
57906
|
-
"ai/comment-ratio": "AI tools either skip comments (reductive models) or over-comment (expansive models). Match the corpus mean \xB1 2\u03C3 (Rahman et al. 2024, Bisztray et al. 2025).",
|
|
57907
|
-
"ai/whitespace-regularity": "Vary inter-token spacing (single spaces mostly, occasional alignment in tables). Uniform runs are an AI tell (Shi et al. DetectCodeGPT 2024).",
|
|
57908
|
-
"ai/text-like-ratio": "Move natural-language explanations to README files or doc comments. Inline prose in source code is hard to maintain (Yotkova 2026).",
|
|
57909
|
-
"ai/errors-near-eof": "Check whether the file was truncated by a token limit. Unbalanced delimiters near EOF suggest the model ran out of output budget (Yotkova 2026).",
|
|
57910
|
-
"ai/any-density": "Replace `any` with `unknown`, `Record<string, unknown>`, or a domain type. The `: any` annotation propagates type-errors and defeats TS safety (Lee, Hassan, Hindle MSR 2026).",
|
|
57911
|
-
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
57912
|
-
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
57913
|
-
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
57914
|
-
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
57915
|
-
// reclassified post-v7 calibration in v0.14.5d)
|
|
57916
|
-
"ai/tailwind-color-overuse": "If most utility classes are bg-violet-500, text-violet-600, ring-violet-400 \u2014 the project is on the AI-default palette. Audit and replace with the project's design tokens.",
|
|
57917
|
-
"ai/default-react-stack": "Every new file is a Vite + React + Tailwind + Zustand + React Hook Form clone. Verify the project actually needs each piece before adding it.",
|
|
57918
|
-
"ai/library-reinvention": "Re-implementing zustand, react-hook-form, or date-fns inline (custom event emitters, useState reducers, manual date math) is a sign of LLM completion-mode code. Use the libraries the project already depends on.",
|
|
57919
|
-
"ai/state-default-overuse": "Wrapping every component in useState + useEffect for transient UI state is the React tutorial default. Real production code uses refs, uncontrolled inputs, or the project's state lib.",
|
|
57920
|
-
"ai/fetch-default-overuse": "Calling fetch() inline in components instead of going through the project's data-fetching layer (react-query, swr, or your own client) bypasses the cache, error boundary, and abort handling.",
|
|
57921
|
-
"ai/console-debug-storm": "5+ console.log calls in a single file is debug-by-print-statement, the LLM training-data default. Remove before commit; use the project's logger or a real debugger.",
|
|
57922
|
-
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
57923
|
-
"db/sql-concat": 'Never build SQL with template-literal interpolation \u2014 `db.query(`SELECT \u2026 WHERE id = ${id}`)` is a SQL injection vector. Use parameterized queries (`db.query("\u2026 WHERE id = $1", [id])`) or your ORM query builder.',
|
|
57924
|
-
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
57925
|
-
"docs/stale-package-reference": "Update the doc to reference an installed package, or add the package to package.json. Copy-pasted install commands from a previous project are the #1 doc-drift failure mode.",
|
|
57926
|
-
"docs/stale-function-reference": "Rename the doc reference to match a current export, or add a wrapper export. Stale function callouts in tutorials cost readers 10+ minutes of debugging.",
|
|
57927
|
-
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
|
|
57928
|
-
// v0.18.9 — Rust-specific rules (tree-sitter-backed)
|
|
57929
|
-
"rust/unused-pub-fn": "Remove the function or call it. Rust's compiler doesn't warn on `pub fn` references unless `#![warn(dead_code)]` is set at the crate root. AI agents leave these behind during iterative refactors.",
|
|
57930
|
-
"rust/unwrap-in-production": "Replace `.unwrap()` / `.expect()` with `?` (early-return on Err), `.map_err(...)` for error conversion, or an explicit `match`. They panic in production; reserve them for `#[cfg(test)]` or `#[test]` scopes.",
|
|
57931
|
-
"rust/todo-macro": "Implement the function body or remove the stub. `todo!()` / `unimplemented!()` both expand to `panic!()`, so a hit at runtime crashes the program. Move them into a `#[cfg(test)] mod tests` block to suppress.",
|
|
57932
|
-
"rust/stringly-typed": 'Replace the `String` / `&str` parameter with the typed enum that already exists in the file. Stringly-typed APIs lose type information at the boundary \u2014 typos (`"Click"` vs `"click"`) only fail at runtime.',
|
|
57933
|
-
// v0.19.0 — TypeScript-specific rules
|
|
57934
|
-
"ts/enum-vs-as-const": "Replace `enum Foo { A, B }` with `const Foo = { A: 'A', B: 'B' } as const` (or `const Foo = ['A', 'B'] as const`). Modern TS style guides (Google, TS-eslint) prefer `as const` because enums have surprising runtime semantics.",
|
|
57935
|
-
"ts/import-type-misuse": "Use `import type { X } from '...'` instead of `import { type X } from '...'`. The split form is more common in real codebases and makes the type-only intent unambiguous.",
|
|
57936
|
-
"ts/never-vs-unknown": "The `never` return type means 'this function never returns'. Reserve it for functions that always throw, always loop, or always exit. For 'impossible' branches, use a concrete type (`void`, `Error`, `unknown`) and an exhaustive check.",
|
|
57937
|
-
"ts/excessive-type-assertion": "More than 3 `as` assertions in one function is a strong signal that the type is wrong, not the code. Fix the type definition (or use a type guard) instead of bypassing the type system.",
|
|
57938
|
-
// v0.19.0 — Go-specific rules
|
|
57939
|
-
"go/struct-tag-inconsistency": 'Pick one tag style per struct. If most fields are `json:"foo"`, this field should be too. Real Go code maintains consistency within a struct (or within a package).',
|
|
57940
|
-
"go/nil-slice-vs-empty": "Either declare as `var x = []int{}` or assign with `make([]int, 0)`. The nil/empty inconsistency is an AI signal \u2014 real code picks one form and sticks with it.",
|
|
57941
|
-
// v0.19.0 — Duplication detector
|
|
57942
|
-
"dup/identical-block": "Refactor to a shared helper. This is a Type-1 clone (byte-for-byte identical after normalization). Common in AI-generated code that copy-pastes from training data.",
|
|
57943
|
-
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
57944
|
-
"dup/near-duplicate": "Refactor to a shared helper. Type-2 near-duplicate: same identifiers + structure, different whitespace/comments. AI-iterated code regenerates the same function with cosmetic edits. (v0.23.0 \u2014 MinHash + LSH, k=5, thr 0.7.)",
|
|
57945
|
-
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
57946
|
-
"dup/structural-clone": "Refactor to a shared helper. Type-3 clone: same shape as another file after identifier canonicalization, with renames and/or added/removed statements. AI agents and copy-paste both produce this. Extract the shared part. (v0.24.0.)",
|
|
57947
|
-
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
57948
|
-
// v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
|
|
57949
|
-
// v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
|
|
57950
|
-
"java/sql-string-concat": "Use a PreparedStatement (JDBC), setParameter (jOOQ), or an ORM (Hibernate, MyBatis with #{}). String concat into SQL is the canonical SQL-injection pattern. OWASP A03:2021. (v0.30.0 \u2014 DORMANT, ratio 0.59.)",
|
|
57951
|
-
"java/thread-sleep-in-loop": "Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the polling anti-pattern \u2014 ties up Tomcat/Jetty/Netty threads. (v0.30.0 \u2014 DORMANT, era-confounded ratio 0.97.)",
|
|
57952
|
-
// v0.35.0 — content-based detection (CoCoNUTS-inspired)
|
|
57953
|
-
"java/suspicious-implementation": "Function name claims validate/encrypt/hash/sanitize but body is empty, returns null/true, or returns the input. This is a content mismatch \u2014 the function's claimed behavior doesn't match its actual behavior. OWASP A04:2021.",
|
|
57954
|
-
// v0.35.1 — Raidar-inspired content-based detection
|
|
57955
|
-
"java/lost-stack-trace": 'catch block throws a new exception without the original cause \u2014 stack trace is lost. Pass the original exception as the second arg to the new exception\'s constructor: `throw new XxxException("msg", e)`.',
|
|
57956
|
-
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
57957
|
-
"swift/force-unwrap": "Replace with the safe form: `as?` + guard/if let, `try?`, or `guard let x = optional else { return }`. `!` crashes unconditionally in release. (v0.24.0 \u2014 DORMANT.)",
|
|
57958
|
-
"swift/print-debug": "Replace with `Logger(subsystem:..., category:...).info(...)` (os.log). `print` writes to stdout with no level and no redaction. (v0.24.0 \u2014 DORMANT.)",
|
|
57959
|
-
"swift/fatal-error-thrown": "Replace with `return` of a typed default (e.g. `nil`, `[]()`) or throw a typed `enum MyError: Error`. `fatalError` survives release builds. (v0.24.0 \u2014 DORMANT.)",
|
|
57960
|
-
"swift/implicitly-unwrapped-optional": "Declare as `var name: Type?` and unwrap with `guard let`. IUOs exist for Obj-C bridging / IBOutlets but using them everywhere is an AI signal. (v0.24.0 \u2014 DORMANT.)",
|
|
57961
|
-
"swift/strong-self-capture": "Capture with `[weak self]` (or `[unowned self]` if guaranteed non-nil): `[weak self] in self?.foo = bar`. Strong self capture creates a retain cycle. (v0.24.0 \u2014 DORMANT.)",
|
|
57962
|
-
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
57963
|
-
"cpp/raw-new-delete": "Replace with `auto p = std::make_unique<T>(...)`. Smart pointers delete on scope exit, never leak on early return / exception, never double-free. (v0.24.0 \u2014 DORMANT.)",
|
|
57964
|
-
"cpp/c-style-cast": "Use the named cast: `static_cast<int>(x)`, `reinterpret_cast<...>(p)`, `const_cast<...>(ref)`. C-style casts silently pick static / reinterpret / const \u2014 impossible to grep. (v0.24.0 \u2014 DORMANT.)",
|
|
57965
|
-
"cpp/printf-debug": "Replace with `spdlog::info(...)`, `LOG(INFO) << ...` (glog), or `ABSL_LOG(INFO) << ...`. `printf` / `std::cout` have no levels, no redaction, no sink routing. (v0.24.0 \u2014 DORMANT.)",
|
|
57966
|
-
"cpp/magic-numbers": "Replace with `constexpr int MAX = 1024;` (or `static constexpr`). Named constants live next to their value, can be searched, force the reader to mean what they say. (v0.24.0 \u2014 DORMANT.)"
|
|
57967
|
-
};
|
|
57968
|
-
|
|
57969
|
-
// src/cli/commands/explain.ts
|
|
58998
|
+
init_data();
|
|
57970
58999
|
function registerExplain(program) {
|
|
57971
|
-
program.command("explain
|
|
59000
|
+
program.command("explain [ruleId]").description("Print rationale, pattern, and remediation for a single rule. With no ruleId, lists the rules.").action((ruleId) => {
|
|
59001
|
+
if (!ruleId) {
|
|
59002
|
+
logger.warn("Missing ruleId. Run `slopbrick rules` to see available rules, then `slopbrick explain <ruleId>`.");
|
|
59003
|
+
process.exit(2);
|
|
59004
|
+
}
|
|
57972
59005
|
const result = explainRule(ruleId, builtinRules, RULE_HINTS);
|
|
57973
59006
|
logger.info(formatExplain(result));
|
|
57974
59007
|
if ("error" in result) process.exit(2);
|
|
@@ -57976,7 +59009,7 @@ function registerExplain(program) {
|
|
|
57976
59009
|
}
|
|
57977
59010
|
|
|
57978
59011
|
// src/cli/commands/calibration.ts
|
|
57979
|
-
var
|
|
59012
|
+
var import_node_path39 = require("path");
|
|
57980
59013
|
var import_node_url = require("url");
|
|
57981
59014
|
|
|
57982
59015
|
// src/rules/signal-strength.json
|
|
@@ -61569,7 +62602,7 @@ var signal_strength_default2 = {
|
|
|
61569
62602
|
};
|
|
61570
62603
|
|
|
61571
62604
|
// src/cli/commands/calibration.ts
|
|
61572
|
-
var __dirname = (0,
|
|
62605
|
+
var __dirname = (0, import_node_path39.dirname)((0, import_node_url.fileURLToPath)(__importMetaUrl));
|
|
61573
62606
|
function loadData() {
|
|
61574
62607
|
return signal_strength_default2;
|
|
61575
62608
|
}
|
|
@@ -61690,13 +62723,13 @@ function registerCalibration(program) {
|
|
|
61690
62723
|
}
|
|
61691
62724
|
|
|
61692
62725
|
// src/cli/commands/install.ts
|
|
61693
|
-
var
|
|
62726
|
+
var import_node_path41 = require("path");
|
|
61694
62727
|
init_logger();
|
|
61695
62728
|
init_git();
|
|
61696
62729
|
|
|
61697
62730
|
// src/cli/installer.ts
|
|
61698
|
-
var
|
|
61699
|
-
var
|
|
62731
|
+
var import_node_fs33 = require("fs");
|
|
62732
|
+
var import_node_path40 = require("path");
|
|
61700
62733
|
var BEGIN_SENTINEL = "# slopbrick-hook-begin";
|
|
61701
62734
|
var END_SENTINEL = "# slopbrick-hook-end";
|
|
61702
62735
|
var SENTINEL_BLOCK = `${BEGIN_SENTINEL}
|
|
@@ -61704,14 +62737,14 @@ npx slopbrick --staged
|
|
|
61704
62737
|
${END_SENTINEL}
|
|
61705
62738
|
`;
|
|
61706
62739
|
function hookPath(gitRoot) {
|
|
61707
|
-
const huskyDir = (0,
|
|
61708
|
-
if ((0,
|
|
61709
|
-
return (0,
|
|
62740
|
+
const huskyDir = (0, import_node_path40.join)(gitRoot, ".husky");
|
|
62741
|
+
if ((0, import_node_fs33.existsSync)(huskyDir)) {
|
|
62742
|
+
return (0, import_node_path40.join)(huskyDir, "pre-commit");
|
|
61710
62743
|
}
|
|
61711
|
-
return (0,
|
|
62744
|
+
return (0, import_node_path40.join)(gitRoot, ".git", "hooks", "pre-commit");
|
|
61712
62745
|
}
|
|
61713
62746
|
function readHookContent(path) {
|
|
61714
|
-
return (0,
|
|
62747
|
+
return (0, import_node_fs33.readFileSync)(path, "utf8");
|
|
61715
62748
|
}
|
|
61716
62749
|
function sentinelsPresent(content) {
|
|
61717
62750
|
const lines = content.split(/\r?\n/);
|
|
@@ -61733,7 +62766,7 @@ function replaceSentinelBlock(content) {
|
|
|
61733
62766
|
}
|
|
61734
62767
|
function installHook(gitRoot) {
|
|
61735
62768
|
const path = hookPath(gitRoot);
|
|
61736
|
-
if ((0,
|
|
62769
|
+
if ((0, import_node_fs33.existsSync)(path)) {
|
|
61737
62770
|
const content = readHookContent(path);
|
|
61738
62771
|
const { begin, end } = sentinelsPresent(content);
|
|
61739
62772
|
if (begin && end) {
|
|
@@ -61745,9 +62778,9 @@ function installHook(gitRoot) {
|
|
|
61745
62778
|
exitCode: 0
|
|
61746
62779
|
};
|
|
61747
62780
|
}
|
|
61748
|
-
(0,
|
|
62781
|
+
(0, import_node_fs33.writeFileSync)(path, replaced.endsWith("\n") ? replaced : `${replaced}
|
|
61749
62782
|
`);
|
|
61750
|
-
(0,
|
|
62783
|
+
(0, import_node_fs33.chmodSync)(path, 493);
|
|
61751
62784
|
return {
|
|
61752
62785
|
ok: true,
|
|
61753
62786
|
message: "Replaced pre-commit hook block",
|
|
@@ -61765,17 +62798,17 @@ function installHook(gitRoot) {
|
|
|
61765
62798
|
}
|
|
61766
62799
|
const normalized = content.length > 0 && !content.endsWith("\n") ? `${content}
|
|
61767
62800
|
` : content;
|
|
61768
|
-
(0,
|
|
61769
|
-
(0,
|
|
62801
|
+
(0, import_node_fs33.writeFileSync)(path, `${normalized}${SENTINEL_BLOCK}`);
|
|
62802
|
+
(0, import_node_fs33.chmodSync)(path, 493);
|
|
61770
62803
|
return {
|
|
61771
62804
|
ok: true,
|
|
61772
62805
|
message: "Installed pre-commit hook",
|
|
61773
62806
|
exitCode: 0
|
|
61774
62807
|
};
|
|
61775
62808
|
}
|
|
61776
|
-
(0,
|
|
61777
|
-
(0,
|
|
61778
|
-
(0,
|
|
62809
|
+
(0, import_node_fs33.mkdirSync)((0, import_node_path40.dirname)(path), { recursive: true });
|
|
62810
|
+
(0, import_node_fs33.writeFileSync)(path, SENTINEL_BLOCK, { mode: 493 });
|
|
62811
|
+
(0, import_node_fs33.chmodSync)(path, 493);
|
|
61779
62812
|
return {
|
|
61780
62813
|
ok: true,
|
|
61781
62814
|
message: "Installed pre-commit hook",
|
|
@@ -61784,7 +62817,7 @@ function installHook(gitRoot) {
|
|
|
61784
62817
|
}
|
|
61785
62818
|
function uninstallHook(gitRoot) {
|
|
61786
62819
|
const path = hookPath(gitRoot);
|
|
61787
|
-
if (!(0,
|
|
62820
|
+
if (!(0, import_node_fs33.existsSync)(path)) {
|
|
61788
62821
|
return {
|
|
61789
62822
|
ok: true,
|
|
61790
62823
|
message: "Hook not installed",
|
|
@@ -61820,7 +62853,7 @@ function uninstallHook(gitRoot) {
|
|
|
61820
62853
|
}
|
|
61821
62854
|
const remaining = [...lines.slice(0, start), ...lines.slice(end + 1)];
|
|
61822
62855
|
const result = remaining.join("\n");
|
|
61823
|
-
(0,
|
|
62856
|
+
(0, import_node_fs33.writeFileSync)(path, result.endsWith("\n") ? result : `${result}
|
|
61824
62857
|
`);
|
|
61825
62858
|
return {
|
|
61826
62859
|
ok: true,
|
|
@@ -61833,7 +62866,7 @@ function uninstallHook(gitRoot) {
|
|
|
61833
62866
|
function registerInstall(program) {
|
|
61834
62867
|
program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
61835
62868
|
const options = command.optsWithGlobals();
|
|
61836
|
-
const cwd = (0,
|
|
62869
|
+
const cwd = (0, import_node_path41.resolve)(options.workspace ?? process.cwd());
|
|
61837
62870
|
const root = getGitRoot(cwd);
|
|
61838
62871
|
if (!root) {
|
|
61839
62872
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -61848,13 +62881,13 @@ function registerInstall(program) {
|
|
|
61848
62881
|
}
|
|
61849
62882
|
|
|
61850
62883
|
// src/cli/commands/uninstall.ts
|
|
61851
|
-
var
|
|
62884
|
+
var import_node_path42 = require("path");
|
|
61852
62885
|
init_logger();
|
|
61853
62886
|
init_git();
|
|
61854
62887
|
function registerUninstall(program) {
|
|
61855
62888
|
program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
61856
62889
|
const options = command.optsWithGlobals();
|
|
61857
|
-
const cwd = (0,
|
|
62890
|
+
const cwd = (0, import_node_path42.resolve)(options.workspace ?? process.cwd());
|
|
61858
62891
|
const root = getGitRoot(cwd);
|
|
61859
62892
|
if (!root) {
|
|
61860
62893
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -61877,8 +62910,8 @@ init_config2();
|
|
|
61877
62910
|
init_header();
|
|
61878
62911
|
|
|
61879
62912
|
// src/mcp/tools.ts
|
|
61880
|
-
var
|
|
61881
|
-
var
|
|
62913
|
+
var import_node_fs34 = require("fs");
|
|
62914
|
+
var import_node_path43 = require("path");
|
|
61882
62915
|
init_worker();
|
|
61883
62916
|
init_patterns();
|
|
61884
62917
|
init_architecture_score();
|
|
@@ -61887,7 +62920,7 @@ init_structure_md();
|
|
|
61887
62920
|
var TOOL_DEFINITIONS = [
|
|
61888
62921
|
{
|
|
61889
62922
|
name: "slop_scan_file",
|
|
61890
|
-
description:
|
|
62923
|
+
description: 'Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, category, severity, line, column, message, advice), a composite AI-likelihood score (probability + confidenceTier), and a componentCount. The composite score is the Bayesian log-likelihood ratio of all rules that fired, NOT a per-file "Slop Index" \u2014 for project-level scores use slop_suggest.',
|
|
61891
62924
|
inputSchema: {
|
|
61892
62925
|
type: "object",
|
|
61893
62926
|
properties: {
|
|
@@ -61903,7 +62936,7 @@ var TOOL_DEFINITIONS = [
|
|
|
61903
62936
|
},
|
|
61904
62937
|
{
|
|
61905
62938
|
name: "slop_explain_rule",
|
|
61906
|
-
description: "Return metadata for a single rule
|
|
62939
|
+
description: "Return metadata for a single rule: ruleId, category, severity, aiSpecific, a brief rationale string, and a whereToLook path into src/rules/. The actual fix-steps live in the rule source (whereToLook) rather than in this response \u2014 read that file to understand the rule before auto-applying --fix.",
|
|
61907
62940
|
inputSchema: {
|
|
61908
62941
|
type: "object",
|
|
61909
62942
|
properties: {
|
|
@@ -61924,7 +62957,7 @@ var TOOL_DEFINITIONS = [
|
|
|
61924
62957
|
},
|
|
61925
62958
|
{
|
|
61926
62959
|
name: "slop_suggest",
|
|
61927
|
-
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate),
|
|
62960
|
+
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), the declared stack, and (when .slopbrick/health.json exists) a Bayesian composite AI-likelihood score. Call this BEFORE writing new code so the agent reuses existing patterns instead of duplicating them. For per-issue details or per-file hot-spots, use slop_scan_file on each candidate path.",
|
|
61928
62961
|
inputSchema: {
|
|
61929
62962
|
type: "object",
|
|
61930
62963
|
properties: {
|
|
@@ -61956,7 +62989,7 @@ var TOOL_DEFINITIONS = [
|
|
|
61956
62989
|
// call slop_suggest and read repositoryHealth /
|
|
61957
62990
|
// architectureConsistency / businessLogicCoherence.
|
|
61958
62991
|
name: "slop_check_constitution",
|
|
61959
|
-
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns
|
|
62992
|
+
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns the file path, total import + violation counts, the parsed imports, the list of violations (each with import, category, and reason), and a conventionSource indicating whether the constitution was declared, detected, or absent. Use this on a newly-written or modified file before suggesting a PR.",
|
|
61960
62993
|
inputSchema: {
|
|
61961
62994
|
type: "object",
|
|
61962
62995
|
properties: {
|
|
@@ -61973,7 +63006,7 @@ var TOOL_DEFINITIONS = [
|
|
|
61973
63006
|
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
61974
63007
|
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
61975
63008
|
name: "slop_find_similar",
|
|
61976
|
-
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns
|
|
63009
|
+
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns up to `limit` matches (default 10) with name, file, line, fingerprint (sha256 over signature), hooks, props, params, and similarity in [0, 1].",
|
|
61977
63010
|
inputSchema: {
|
|
61978
63011
|
type: "object",
|
|
61979
63012
|
properties: {
|
|
@@ -62124,10 +63157,10 @@ async function runSuggest(args, ctx, options = {}) {
|
|
|
62124
63157
|
function runCheckConstitution(args, ctx) {
|
|
62125
63158
|
const path = args.path;
|
|
62126
63159
|
if (!path) return toolError("Missing required argument: path");
|
|
62127
|
-
const absPath = (0,
|
|
63160
|
+
const absPath = (0, import_node_path43.resolve)(ctx.cwd, path);
|
|
62128
63161
|
let source;
|
|
62129
63162
|
try {
|
|
62130
|
-
source = (0,
|
|
63163
|
+
source = (0, import_node_fs34.readFileSync)(absPath, "utf-8");
|
|
62131
63164
|
} catch (err) {
|
|
62132
63165
|
return toolError(
|
|
62133
63166
|
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -62204,13 +63237,6 @@ async function runFindSimilar(args, ctx) {
|
|
|
62204
63237
|
}
|
|
62205
63238
|
}
|
|
62206
63239
|
async function handleToolCall(toolName, args, ctx) {
|
|
62207
|
-
const deprecation = getDeprecation(toolName);
|
|
62208
|
-
const deprecationNotice = deprecation ? {
|
|
62209
|
-
tool: toolName,
|
|
62210
|
-
replacedBy: deprecation.replacedBy,
|
|
62211
|
-
removedIn: deprecation.removedIn ?? "next major",
|
|
62212
|
-
reason: deprecation.reason
|
|
62213
|
-
} : void 0;
|
|
62214
63240
|
switch (toolName) {
|
|
62215
63241
|
case "slop_scan_file":
|
|
62216
63242
|
return runScanFile(args, ctx);
|
|
@@ -62310,7 +63336,7 @@ function errorResponse(id, code, message, data2) {
|
|
|
62310
63336
|
return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
|
|
62311
63337
|
}
|
|
62312
63338
|
async function runMcpServer(input, output, cwd) {
|
|
62313
|
-
return new Promise((
|
|
63339
|
+
return new Promise((resolve43) => {
|
|
62314
63340
|
let buffer = "";
|
|
62315
63341
|
input.setEncoding("utf-8");
|
|
62316
63342
|
input.on("data", (chunk) => {
|
|
@@ -62348,8 +63374,8 @@ async function runMcpServer(input, output, cwd) {
|
|
|
62348
63374
|
nlIdx = buffer.indexOf("\n");
|
|
62349
63375
|
}
|
|
62350
63376
|
});
|
|
62351
|
-
input.on("end", () =>
|
|
62352
|
-
input.on("close", () =>
|
|
63377
|
+
input.on("end", () => resolve43());
|
|
63378
|
+
input.on("close", () => resolve43());
|
|
62353
63379
|
});
|
|
62354
63380
|
}
|
|
62355
63381
|
|
|
@@ -62372,7 +63398,7 @@ function registerDoctor(program) {
|
|
|
62372
63398
|
}
|
|
62373
63399
|
|
|
62374
63400
|
// src/cli/commands/watch.ts
|
|
62375
|
-
var
|
|
63401
|
+
var import_node_path44 = require("path");
|
|
62376
63402
|
init_watch();
|
|
62377
63403
|
function registerWatch(program, scanAction) {
|
|
62378
63404
|
program.command("watch").description("re-run scan on every file change. Flags new violations as you write. The LockBrick prevention loop entry.").action(async (_cmdOptions, command) => {
|
|
@@ -62381,19 +63407,19 @@ function registerWatch(program, scanAction) {
|
|
|
62381
63407
|
...rawGlobals,
|
|
62382
63408
|
noIncrease: rawGlobals.increase === false
|
|
62383
63409
|
};
|
|
62384
|
-
const cwd = (0,
|
|
63410
|
+
const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
|
|
62385
63411
|
await scanAction([], options, command);
|
|
62386
63412
|
await watchProject(options, cwd, []);
|
|
62387
63413
|
});
|
|
62388
63414
|
}
|
|
62389
63415
|
|
|
62390
63416
|
// src/cli/commands/lock.ts
|
|
62391
|
-
var
|
|
63417
|
+
var import_node_path45 = require("path");
|
|
62392
63418
|
init_logger();
|
|
62393
63419
|
function registerLock(program) {
|
|
62394
63420
|
program.command("lock").description("install a Git pre-commit hook that runs `slopbrick scan --staged` on every commit. The LockBrick prevention loop: block AI-introduced slop from ever reaching the repo.").option("--uninstall", "remove the pre-commit hook instead of installing it").option("--husky", "force-install under .husky/pre-commit (Husky v9). Default auto-detects via .husky/ dir.").option("--workspace <path>", "workspace directory", process.cwd()).action(
|
|
62395
63421
|
(cmdOptions) => {
|
|
62396
|
-
const cwd = (0,
|
|
63422
|
+
const cwd = (0, import_node_path45.resolve)(cmdOptions.workspace ?? process.cwd());
|
|
62397
63423
|
if (cmdOptions.uninstall) {
|
|
62398
63424
|
const result2 = uninstallHook(cwd);
|
|
62399
63425
|
logger.info(result2.message);
|
|
@@ -62414,11 +63440,11 @@ function registerLock(program) {
|
|
|
62414
63440
|
}
|
|
62415
63441
|
|
|
62416
63442
|
// src/cli/commands/ci.ts
|
|
62417
|
-
var
|
|
63443
|
+
var import_node_path46 = require("path");
|
|
62418
63444
|
init_logger();
|
|
62419
63445
|
init_dist();
|
|
62420
63446
|
function registerCi(program, scanAction) {
|
|
62421
|
-
program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if
|
|
63447
|
+
program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if aiSlopScore exceeds this number (lower = cleaner since v0.21)", parseCount).option("--max-new-issues <n>", "exit 1 if new issues (vs .slop-audit-cache.json) exceed this number", parseCount).option("--strict-constitution", "exit 1 on any constitution violation").option("--format <pretty|json>", "output format", "json").action(
|
|
62422
63448
|
async (cmdOptions, command) => {
|
|
62423
63449
|
const globals = command.optsWithGlobals();
|
|
62424
63450
|
const options = {
|
|
@@ -62429,7 +63455,7 @@ function registerCi(program, scanAction) {
|
|
|
62429
63455
|
// scan only changed files
|
|
62430
63456
|
format: cmdOptions.format ?? "json"
|
|
62431
63457
|
};
|
|
62432
|
-
const cwd = (0,
|
|
63458
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
62433
63459
|
await scanAction([], options, command);
|
|
62434
63460
|
const health = loadHealth(cwd);
|
|
62435
63461
|
if (!health) {
|
|
@@ -62457,12 +63483,12 @@ function registerCi(program, scanAction) {
|
|
|
62457
63483
|
}
|
|
62458
63484
|
|
|
62459
63485
|
// src/cli/commands/memory.ts
|
|
62460
|
-
var
|
|
63486
|
+
var import_node_path47 = require("path");
|
|
62461
63487
|
init_logger();
|
|
62462
63488
|
function registerMemory(program) {
|
|
62463
63489
|
program.command("memory").description("show or regenerate .slopbrick/structure.md (the agent-readable repository summary) without re-scanning").option("--show", "print the current .slopbrick/structure.md to stdout (default if no flag is passed)").option("--regenerate", "re-render structure.md from the existing inventory.json + constitution.json (no scan)").option("--workspace <path>", "workspace directory", process.cwd()).action(
|
|
62464
63490
|
async (cmdOptions) => {
|
|
62465
|
-
const cwd = (0,
|
|
63491
|
+
const cwd = (0, import_node_path47.resolve)(cmdOptions.workspace ?? process.cwd());
|
|
62466
63492
|
const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
|
|
62467
63493
|
const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
62468
63494
|
if (cmdOptions.regenerate) {
|
|
@@ -62492,49 +63518,49 @@ function registerMemory(program) {
|
|
|
62492
63518
|
}
|
|
62493
63519
|
|
|
62494
63520
|
// src/cli/commands/migrate.ts
|
|
62495
|
-
var
|
|
63521
|
+
var import_node_path49 = require("path");
|
|
62496
63522
|
init_logger();
|
|
62497
63523
|
|
|
62498
63524
|
// src/cli/migrate.ts
|
|
62499
|
-
var
|
|
62500
|
-
var
|
|
63525
|
+
var import_node_fs35 = require("fs");
|
|
63526
|
+
var import_node_path48 = require("path");
|
|
62501
63527
|
init_logger();
|
|
62502
63528
|
function planMigration(workspaceDir) {
|
|
62503
63529
|
const moves = [];
|
|
62504
63530
|
const rewrites = [];
|
|
62505
63531
|
const gitignoreEdits = [];
|
|
62506
|
-
const oldDir = (0,
|
|
62507
|
-
const newDir = (0,
|
|
62508
|
-
if ((0,
|
|
63532
|
+
const oldDir = (0, import_node_path48.join)(workspaceDir, ".slop-audit");
|
|
63533
|
+
const newDir = (0, import_node_path48.join)(workspaceDir, ".slopbrick");
|
|
63534
|
+
if ((0, import_node_fs35.existsSync)(oldDir)) {
|
|
62509
63535
|
moves.push({ from: oldDir, to: newDir, kind: "dir" });
|
|
62510
63536
|
rewrites.push({
|
|
62511
|
-
path: (0,
|
|
63537
|
+
path: (0, import_node_path48.join)(newDir, "inventory.json"),
|
|
62512
63538
|
field: "version",
|
|
62513
63539
|
from: '"1"',
|
|
62514
63540
|
to: '"2"'
|
|
62515
63541
|
});
|
|
62516
63542
|
rewrites.push({
|
|
62517
|
-
path: (0,
|
|
63543
|
+
path: (0, import_node_path48.join)(newDir, "constitution.json"),
|
|
62518
63544
|
field: "version",
|
|
62519
63545
|
from: '"1"',
|
|
62520
63546
|
to: '"2"'
|
|
62521
63547
|
});
|
|
62522
63548
|
}
|
|
62523
|
-
const oldCache = (0,
|
|
62524
|
-
const newCache = (0,
|
|
62525
|
-
if ((0,
|
|
63549
|
+
const oldCache = (0, import_node_path48.join)(workspaceDir, ".slop-audit-cache.json");
|
|
63550
|
+
const newCache = (0, import_node_path48.join)(workspaceDir, ".slopbrick-cache.json");
|
|
63551
|
+
if ((0, import_node_fs35.existsSync)(oldCache)) {
|
|
62526
63552
|
moves.push({ from: oldCache, to: newCache, kind: "file" });
|
|
62527
63553
|
}
|
|
62528
63554
|
for (const ext of ["mjs", "cjs", "js"]) {
|
|
62529
|
-
const oldCfg = (0,
|
|
62530
|
-
const newCfg = (0,
|
|
62531
|
-
if ((0,
|
|
63555
|
+
const oldCfg = (0, import_node_path48.join)(workspaceDir, `slop-audit.config.${ext}`);
|
|
63556
|
+
const newCfg = (0, import_node_path48.join)(workspaceDir, `slopbrick.config.${ext}`);
|
|
63557
|
+
if ((0, import_node_fs35.existsSync)(oldCfg)) {
|
|
62532
63558
|
moves.push({ from: oldCfg, to: newCfg, kind: "config" });
|
|
62533
63559
|
}
|
|
62534
63560
|
}
|
|
62535
|
-
const gi = (0,
|
|
62536
|
-
if ((0,
|
|
62537
|
-
const content = (0,
|
|
63561
|
+
const gi = (0, import_node_path48.join)(workspaceDir, ".gitignore");
|
|
63562
|
+
if ((0, import_node_fs35.existsSync)(gi)) {
|
|
63563
|
+
const content = (0, import_node_fs35.readFileSync)(gi, "utf-8");
|
|
62538
63564
|
if (content.includes(".slop-audit/")) {
|
|
62539
63565
|
gitignoreEdits.push({
|
|
62540
63566
|
path: gi,
|
|
@@ -62553,28 +63579,28 @@ function planMigration(workspaceDir) {
|
|
|
62553
63579
|
return { moves, rewrites, gitignoreEdits };
|
|
62554
63580
|
}
|
|
62555
63581
|
function isAlreadyMigrated(workspaceDir) {
|
|
62556
|
-
return (0,
|
|
63582
|
+
return (0, import_node_fs35.existsSync)((0, import_node_path48.join)(workspaceDir, ".slopbrick")) && !(0, import_node_fs35.existsSync)((0, import_node_path48.join)(workspaceDir, ".slop-audit"));
|
|
62557
63583
|
}
|
|
62558
63584
|
function applyMigration(plan, options = {}) {
|
|
62559
63585
|
if (options.dryRun) return;
|
|
62560
63586
|
for (const m of plan.moves) {
|
|
62561
|
-
(0,
|
|
63587
|
+
(0, import_node_fs35.renameSync)(m.from, m.to);
|
|
62562
63588
|
}
|
|
62563
63589
|
for (const r of plan.rewrites) {
|
|
62564
|
-
if (!(0,
|
|
62565
|
-
const content = (0,
|
|
63590
|
+
if (!(0, import_node_fs35.existsSync)(r.path)) continue;
|
|
63591
|
+
const content = (0, import_node_fs35.readFileSync)(r.path, "utf-8");
|
|
62566
63592
|
const next = content.replace(`"version": ${r.from}`, `"version": ${r.to}`);
|
|
62567
|
-
(0,
|
|
63593
|
+
(0, import_node_fs35.writeFileSync)(r.path, next);
|
|
62568
63594
|
}
|
|
62569
63595
|
for (const g of plan.gitignoreEdits) {
|
|
62570
|
-
const content = (0,
|
|
63596
|
+
const content = (0, import_node_fs35.readFileSync)(g.path, "utf-8");
|
|
62571
63597
|
const next = content.replaceAll(g.from, g.to);
|
|
62572
|
-
(0,
|
|
63598
|
+
(0, import_node_fs35.writeFileSync)(g.path, next);
|
|
62573
63599
|
}
|
|
62574
63600
|
}
|
|
62575
63601
|
function runMigrate(options) {
|
|
62576
63602
|
const { workspace, dryRun = false, force = false } = options;
|
|
62577
|
-
if (!(0,
|
|
63603
|
+
if (!(0, import_node_fs35.existsSync)(workspace)) {
|
|
62578
63604
|
return {
|
|
62579
63605
|
ok: false,
|
|
62580
63606
|
alreadyMigrated: false,
|
|
@@ -62584,9 +63610,9 @@ function runMigrate(options) {
|
|
|
62584
63610
|
};
|
|
62585
63611
|
}
|
|
62586
63612
|
const alreadyMigrated = isAlreadyMigrated(workspace);
|
|
62587
|
-
const newDir = (0,
|
|
62588
|
-
const oldDir = (0,
|
|
62589
|
-
if ((0,
|
|
63613
|
+
const newDir = (0, import_node_path48.join)(workspace, ".slopbrick");
|
|
63614
|
+
const oldDir = (0, import_node_path48.join)(workspace, ".slop-audit");
|
|
63615
|
+
if ((0, import_node_fs35.existsSync)(newDir) && (0, import_node_fs35.existsSync)(oldDir) && !force) {
|
|
62590
63616
|
return {
|
|
62591
63617
|
ok: false,
|
|
62592
63618
|
alreadyMigrated: false,
|
|
@@ -62662,7 +63688,7 @@ function registerMigrate(program) {
|
|
|
62662
63688
|
(cmdOptions, command) => {
|
|
62663
63689
|
const globals = command.optsWithGlobals();
|
|
62664
63690
|
const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
|
|
62665
|
-
const cwd = (0,
|
|
63691
|
+
const cwd = (0, import_node_path49.resolve)(cmdOptions.workspace ?? process.cwd());
|
|
62666
63692
|
const result = runMigrate({
|
|
62667
63693
|
workspace: cwd,
|
|
62668
63694
|
dryRun: cmdOptions.dryRun,
|
|
@@ -62758,7 +63784,25 @@ function registerRules(program) {
|
|
|
62758
63784
|
const sev = r.severity.padEnd(8);
|
|
62759
63785
|
const tag = r.aiSpecific ? "[AI]" : " ";
|
|
62760
63786
|
lines.push(` ${sev} ${tag} ${r.id}`);
|
|
62761
|
-
if (r.description)
|
|
63787
|
+
if (r.description) {
|
|
63788
|
+
const cols = process.stdout.isTTY ? process.stdout.columns ?? 100 : 100;
|
|
63789
|
+
const indent = " ";
|
|
63790
|
+
const firstCol = indent.length + 1;
|
|
63791
|
+
const maxWidth = Math.max(40, cols - firstCol);
|
|
63792
|
+
const words = r.description.split(" ");
|
|
63793
|
+
let line = "";
|
|
63794
|
+
for (const word of words) {
|
|
63795
|
+
if (line === "") {
|
|
63796
|
+
line = word;
|
|
63797
|
+
} else if ((line + " " + word).length > maxWidth) {
|
|
63798
|
+
lines.push(indent + line);
|
|
63799
|
+
line = word;
|
|
63800
|
+
} else {
|
|
63801
|
+
line = line + " " + word;
|
|
63802
|
+
}
|
|
63803
|
+
}
|
|
63804
|
+
if (line) lines.push(indent + line);
|
|
63805
|
+
}
|
|
62762
63806
|
}
|
|
62763
63807
|
}
|
|
62764
63808
|
logger.info(lines.join("\n"));
|
|
@@ -62766,19 +63810,19 @@ function registerRules(program) {
|
|
|
62766
63810
|
}
|
|
62767
63811
|
|
|
62768
63812
|
// src/cli/commands/validate-config.ts
|
|
62769
|
-
var
|
|
62770
|
-
var
|
|
63813
|
+
var import_node_fs36 = require("fs");
|
|
63814
|
+
var import_node_path50 = require("path");
|
|
62771
63815
|
init_logger();
|
|
62772
63816
|
init_validation();
|
|
62773
63817
|
function registerValidateConfig(program) {
|
|
62774
63818
|
program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
|
|
62775
|
-
const path = configPath ? (0,
|
|
62776
|
-
if (!(0,
|
|
63819
|
+
const path = configPath ? (0, import_node_path50.resolve)(configPath) : (0, import_node_path50.resolve)(process.cwd(), "slopbrick.config.mjs");
|
|
63820
|
+
if (!(0, import_node_fs36.existsSync)(path)) {
|
|
62777
63821
|
logger.error(`Error: config file not found: ${path}`);
|
|
62778
63822
|
process.exit(2);
|
|
62779
63823
|
}
|
|
62780
63824
|
try {
|
|
62781
|
-
const mod = (0,
|
|
63825
|
+
const mod = (0, import_node_path50.extname)(path) === ".cjs" ? require(path) : await import(path);
|
|
62782
63826
|
const userConfig = mod.default ?? mod;
|
|
62783
63827
|
const result = validateConfig(userConfig);
|
|
62784
63828
|
if (result.errors.length === 0) {
|
|
@@ -62998,8 +64042,8 @@ ${formatMarkdown(result.report)}`);
|
|
|
62998
64042
|
}
|
|
62999
64043
|
|
|
63000
64044
|
// src/cli/commands/calibrate.ts
|
|
63001
|
-
var
|
|
63002
|
-
var
|
|
64045
|
+
var import_node_fs39 = require("fs");
|
|
64046
|
+
var import_node_path54 = require("path");
|
|
63003
64047
|
init_logger();
|
|
63004
64048
|
|
|
63005
64049
|
// src/research/providers/openai.ts
|
|
@@ -63055,8 +64099,8 @@ function createProvider(config) {
|
|
|
63055
64099
|
}
|
|
63056
64100
|
|
|
63057
64101
|
// src/research/generator.ts
|
|
63058
|
-
var
|
|
63059
|
-
var
|
|
64102
|
+
var import_node_fs37 = require("fs");
|
|
64103
|
+
var import_node_path51 = require("path");
|
|
63060
64104
|
|
|
63061
64105
|
// src/research/prompts.ts
|
|
63062
64106
|
var DEFAULT_PROMPT_TEMPLATES = [
|
|
@@ -63119,14 +64163,14 @@ async function generateSamples(options) {
|
|
|
63119
64163
|
}
|
|
63120
64164
|
const samples = [];
|
|
63121
64165
|
const ext = extForFramework(framework);
|
|
63122
|
-
const dir = (0,
|
|
63123
|
-
(0,
|
|
64166
|
+
const dir = (0, import_node_path51.join)(outputDir, framework, componentType);
|
|
64167
|
+
(0, import_node_fs37.mkdirSync)(dir, { recursive: true });
|
|
63124
64168
|
for (let i = 1; i <= count; i += 1) {
|
|
63125
64169
|
const raw = await provider.generateSample(renderPrompt(template), { temperature });
|
|
63126
64170
|
const code = extractCodeFromMarkdown(raw);
|
|
63127
64171
|
const fileName = `sample-${i}${ext}`;
|
|
63128
|
-
const filePath = (0,
|
|
63129
|
-
(0,
|
|
64172
|
+
const filePath = (0, import_node_path51.join)(dir, fileName);
|
|
64173
|
+
(0, import_node_fs37.writeFileSync)(filePath, code, "utf8");
|
|
63130
64174
|
const sample = {
|
|
63131
64175
|
filePath,
|
|
63132
64176
|
framework,
|
|
@@ -63137,8 +64181,8 @@ async function generateSamples(options) {
|
|
|
63137
64181
|
};
|
|
63138
64182
|
samples.push(sample);
|
|
63139
64183
|
}
|
|
63140
|
-
const metadataPath = (0,
|
|
63141
|
-
(0,
|
|
64184
|
+
const metadataPath = (0, import_node_path51.join)(dir, "metadata.json");
|
|
64185
|
+
(0, import_node_fs37.writeFileSync)(metadataPath, JSON.stringify(samples, null, 2), "utf8");
|
|
63142
64186
|
return samples;
|
|
63143
64187
|
}
|
|
63144
64188
|
|
|
@@ -63344,46 +64388,46 @@ function slugify(value) {
|
|
|
63344
64388
|
|
|
63345
64389
|
// src/research/calibrator.ts
|
|
63346
64390
|
var import_node_child_process2 = require("child_process");
|
|
63347
|
-
var
|
|
63348
|
-
var
|
|
64391
|
+
var import_node_fs38 = require("fs");
|
|
64392
|
+
var import_node_path53 = require("path");
|
|
63349
64393
|
|
|
63350
64394
|
// src/corpus-paths.ts
|
|
63351
|
-
var
|
|
64395
|
+
var import_node_path52 = require("path");
|
|
63352
64396
|
var CORPUS_ROOT = process.env["SLOPBRICK_CORPUS_DIR"] ?? "/Users/cheng/corpus-expansion";
|
|
63353
|
-
var POSITIVE_DIR = (0,
|
|
63354
|
-
var NEGATIVE_DIR = (0,
|
|
63355
|
-
var FILELISTS_DIR = (0,
|
|
64397
|
+
var POSITIVE_DIR = (0, import_node_path52.join)(CORPUS_ROOT, "positive");
|
|
64398
|
+
var NEGATIVE_DIR = (0, import_node_path52.join)(CORPUS_ROOT, "negative");
|
|
64399
|
+
var FILELISTS_DIR = (0, import_node_path52.join)(CORPUS_ROOT, "filelists");
|
|
63356
64400
|
|
|
63357
64401
|
// src/research/calibrator.ts
|
|
63358
64402
|
var DEFAULT_POSITIVE = POSITIVE_DIR;
|
|
63359
64403
|
var DEFAULT_NEGATIVE = NEGATIVE_DIR;
|
|
63360
64404
|
function buildFileList(dir, extensions) {
|
|
63361
|
-
const tmpList = (0,
|
|
64405
|
+
const tmpList = (0, import_node_path53.join)("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
|
|
63362
64406
|
const expr = extensions.map((e) => `-name '*.${e}'`).join(" -o ");
|
|
63363
64407
|
(0, import_node_child_process2.execFileSync)("bash", ["-c", `find ${dir} -maxdepth 8 -type f \\( ${expr} \\) -print0 | xargs -0 realpath > ${tmpList}`]);
|
|
63364
|
-
const out = (0,
|
|
64408
|
+
const out = (0, import_node_fs38.readFileSync)(tmpList, "utf8");
|
|
63365
64409
|
(0, import_node_child_process2.execFileSync)("rm", ["-f", tmpList]);
|
|
63366
64410
|
return out.trim().split("\n").filter(Boolean);
|
|
63367
64411
|
}
|
|
63368
64412
|
function runScan2(fileListPath) {
|
|
63369
|
-
const files = (0,
|
|
64413
|
+
const files = (0, import_node_fs38.readFileSync)(fileListPath, "utf8").trim().split("\n").filter(Boolean);
|
|
63370
64414
|
const CHUNK = 600;
|
|
63371
64415
|
const ruleFires = /* @__PURE__ */ new Map();
|
|
63372
64416
|
const uniqueFilesPerRule = /* @__PURE__ */ new Map();
|
|
63373
64417
|
let fileCount = 0;
|
|
63374
|
-
const tmpOut = (0,
|
|
64418
|
+
const tmpOut = (0, import_node_path53.join)("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
63375
64419
|
for (let i = 0; i < files.length; i += CHUNK) {
|
|
63376
64420
|
const chunk = files.slice(i, i + CHUNK);
|
|
63377
64421
|
try {
|
|
63378
64422
|
(0, import_node_child_process2.execFileSync)(
|
|
63379
64423
|
"node",
|
|
63380
|
-
[(0,
|
|
64424
|
+
[(0, import_node_path53.join)(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
|
|
63381
64425
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
63382
64426
|
);
|
|
63383
64427
|
} catch {
|
|
63384
64428
|
}
|
|
63385
|
-
if (!(0,
|
|
63386
|
-
const report = JSON.parse((0,
|
|
64429
|
+
if (!(0, import_node_fs38.existsSync)(tmpOut)) continue;
|
|
64430
|
+
const report = JSON.parse((0, import_node_fs38.readFileSync)(tmpOut, "utf8"));
|
|
63387
64431
|
fileCount += report.fileCount;
|
|
63388
64432
|
for (const issue of report.issues) {
|
|
63389
64433
|
ruleFires.set(issue.ruleId, (ruleFires.get(issue.ruleId) ?? 0) + 1);
|
|
@@ -63411,16 +64455,16 @@ function classify(posFiles, negFiles) {
|
|
|
63411
64455
|
async function calibrate(cwd, options = {}) {
|
|
63412
64456
|
const positiveDir = options.positiveDir ?? DEFAULT_POSITIVE;
|
|
63413
64457
|
const negativeDir = options.negativeDir ?? DEFAULT_NEGATIVE;
|
|
63414
|
-
if (!(0,
|
|
63415
|
-
if (!(0,
|
|
64458
|
+
if (!(0, import_node_fs38.existsSync)(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
|
|
64459
|
+
if (!(0, import_node_fs38.existsSync)(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
|
|
63416
64460
|
const positiveFiles = buildFileList(positiveDir, ["tsx", "ts", "jsx", "js"]);
|
|
63417
64461
|
const negativeFiles = buildFileList(negativeDir, ["tsx", "ts"]);
|
|
63418
64462
|
const posSample = options.positiveLimit ? positiveFiles.slice(0, options.positiveLimit) : positiveFiles;
|
|
63419
64463
|
const negSample = options.negativeLimit ? negativeFiles.slice(0, options.negativeLimit) : negativeFiles;
|
|
63420
|
-
const posListPath = (0,
|
|
63421
|
-
const negListPath = (0,
|
|
63422
|
-
(0,
|
|
63423
|
-
(0,
|
|
64464
|
+
const posListPath = (0, import_node_path53.join)("/tmp", `cal-pos-${Date.now()}.txt`);
|
|
64465
|
+
const negListPath = (0, import_node_path53.join)("/tmp", `cal-neg-${Date.now()}.txt`);
|
|
64466
|
+
(0, import_node_fs38.writeFileSync)(posListPath, posSample.join("\n"));
|
|
64467
|
+
(0, import_node_fs38.writeFileSync)(negListPath, negSample.join("\n"));
|
|
63424
64468
|
const builtins = await Promise.resolve().then(() => (init_builtins(), builtins_exports));
|
|
63425
64469
|
const builtinRules2 = builtins.builtinRules ?? [];
|
|
63426
64470
|
const metaById = /* @__PURE__ */ new Map();
|
|
@@ -63534,9 +64578,9 @@ function registerCalibrate(program) {
|
|
|
63534
64578
|
positiveLimit: cmdOptions.positiveLimit,
|
|
63535
64579
|
negativeLimit: cmdOptions.negativeLimit
|
|
63536
64580
|
});
|
|
63537
|
-
const outputPath = cmdOptions.output ? (0,
|
|
63538
|
-
(0,
|
|
63539
|
-
(0,
|
|
64581
|
+
const outputPath = cmdOptions.output ? (0, import_node_path54.resolve)(cwd, cmdOptions.output) : (0, import_node_path54.resolve)(cwd, "corpus", "calibration-empirical.md");
|
|
64582
|
+
(0, import_node_fs39.mkdirSync)((0, import_node_path54.dirname)(outputPath), { recursive: true });
|
|
64583
|
+
(0, import_node_fs39.writeFileSync)(outputPath, reportToMarkdown(report), "utf8");
|
|
63540
64584
|
logger.info(
|
|
63541
64585
|
"Calibrated " + report.rules.length + " rules across " + report.positiveFileCount + " positive + " + report.negativeFileCount + " negative files."
|
|
63542
64586
|
);
|
|
@@ -63603,19 +64647,20 @@ function trendToText(report) {
|
|
|
63603
64647
|
}
|
|
63604
64648
|
const slopValues = report.points.map((p) => p.slopIndex);
|
|
63605
64649
|
const spark = sparkline(slopValues);
|
|
63606
|
-
lines.push("Slop
|
|
64650
|
+
lines.push("AI Slop Score trend (last " + report.points.length + " scans, " + report.totalScans + " total; lower = cleaner since v0.21):");
|
|
63607
64651
|
lines.push("");
|
|
63608
64652
|
lines.push(" " + spark + " " + slopValues[0].toFixed(1) + " -> " + slopValues[slopValues.length - 1].toFixed(1));
|
|
63609
64653
|
lines.push("");
|
|
63610
64654
|
if (report.first && report.latest) {
|
|
63611
64655
|
const direction = report.delta < 0 ? "improved" : report.delta > 0 ? "regressed" : "unchanged";
|
|
63612
64656
|
const arrow = report.delta < 0 ? "v" : report.delta > 0 ? "^" : "=";
|
|
63613
|
-
lines.push(" " + arrow + " Slop
|
|
64657
|
+
lines.push(" " + arrow + " AI Slop Score " + direction + " by " + Math.abs(report.delta).toFixed(1) + " points (lower = cleaner since v0.21)");
|
|
63614
64658
|
lines.push(" " + report.first.timestamp + " -> " + report.latest.timestamp);
|
|
63615
64659
|
}
|
|
63616
64660
|
lines.push("");
|
|
63617
64661
|
lines.push("Recent scans:");
|
|
63618
|
-
lines.push(" Timestamp Slop
|
|
64662
|
+
lines.push(" Timestamp AI Slop Health Framework Issues");
|
|
64663
|
+
lines.push(" (lower=cleaner)");
|
|
63619
64664
|
lines.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500");
|
|
63620
64665
|
for (const p of report.points.slice(-10)) {
|
|
63621
64666
|
const ts = p.timestamp.slice(0, 19).replace("T", " ");
|
|
@@ -63627,14 +64672,14 @@ function trendToText(report) {
|
|
|
63627
64672
|
}
|
|
63628
64673
|
function trendToMarkdown(report) {
|
|
63629
64674
|
const lines = [];
|
|
63630
|
-
lines.push("# Slop
|
|
64675
|
+
lines.push("# AI Slop Score Trend (lower = cleaner since v0.21)");
|
|
63631
64676
|
lines.push("");
|
|
63632
64677
|
if (report.points.length === 0) {
|
|
63633
64678
|
lines.push("No telemetry data yet. Run `slopbrick scan` to start tracking.");
|
|
63634
64679
|
return lines.join("\n");
|
|
63635
64680
|
}
|
|
63636
64681
|
const slopValues = report.points.map((p) => p.slopIndex);
|
|
63637
|
-
lines.push("Slop
|
|
64682
|
+
lines.push("AI Slop Score sparkline (last " + report.points.length + " scans; lower = cleaner since v0.21):");
|
|
63638
64683
|
lines.push("");
|
|
63639
64684
|
lines.push("```");
|
|
63640
64685
|
lines.push(sparkline(slopValues));
|
|
@@ -63642,10 +64687,10 @@ function trendToMarkdown(report) {
|
|
|
63642
64687
|
lines.push("");
|
|
63643
64688
|
if (report.first && report.latest) {
|
|
63644
64689
|
const direction = report.delta < 0 ? "improved" : report.delta > 0 ? "regressed" : "unchanged";
|
|
63645
|
-
lines.push("Slop
|
|
64690
|
+
lines.push("AI Slop Score " + direction + " by **" + Math.abs(report.delta).toFixed(1) + " points** between first and latest scan (lower = cleaner since v0.21).");
|
|
63646
64691
|
}
|
|
63647
64692
|
lines.push("");
|
|
63648
|
-
lines.push("| Timestamp | Slop
|
|
64693
|
+
lines.push("| Timestamp | AI Slop Score (lower = cleaner) | Health | Framework | Issues |");
|
|
63649
64694
|
lines.push("|-----------|-----------:|-------:|-----------|-------:|");
|
|
63650
64695
|
for (const p of report.points) {
|
|
63651
64696
|
lines.push("| " + p.timestamp + " | " + p.slopIndex.toFixed(1) + " | " + p.assemblyHealth.toFixed(1) + " | " + p.framework + " | " + p.totalIssues + " |");
|
|
@@ -63655,7 +64700,7 @@ function trendToMarkdown(report) {
|
|
|
63655
64700
|
|
|
63656
64701
|
// src/cli/commands/trend.ts
|
|
63657
64702
|
function registerTrend(program) {
|
|
63658
|
-
program.command("trend").description("Slop
|
|
64703
|
+
program.command("trend").description("AI Slop Score over time, from .slopbrick/flywheel/scans.jsonl (lower = cleaner since v0.21)").option("--max-points <n>", "how many recent scans to plot", parseCount, 30).option("--render <kind>", "output rendering: text | markdown", "text").action((cmdOptions) => {
|
|
63659
64704
|
try {
|
|
63660
64705
|
const cwd = process.cwd();
|
|
63661
64706
|
const report = buildTrend(cwd, cmdOptions.maxPoints);
|
|
@@ -63663,7 +64708,14 @@ function registerTrend(program) {
|
|
|
63663
64708
|
logger.info(out);
|
|
63664
64709
|
if (report.delta > 1) {
|
|
63665
64710
|
logger.info("");
|
|
63666
|
-
logger.info(
|
|
64711
|
+
logger.info(
|
|
64712
|
+
// v0.42.0: the legacy 'Slop Index increased' message implied higher
|
|
64713
|
+
// = worse, which is the v0.15-v0.20.1 inverted reading. Since v0.21,
|
|
64714
|
+
// AI Slop Score is lower-is-better; an INCREASE is worse, but
|
|
64715
|
+
// the warning should say "AI Slop Score got worse" or just
|
|
64716
|
+
// "delta went up" so users don't have to remember the direction.
|
|
64717
|
+
`Warning: AI Slop Score got worse by ${report.delta.toFixed(1)} points over this period (lower = cleaner since v0.21).`
|
|
64718
|
+
);
|
|
63667
64719
|
}
|
|
63668
64720
|
} catch (error) {
|
|
63669
64721
|
logger.error(error instanceof Error ? error.message : String(error));
|
|
@@ -63673,13 +64725,13 @@ function registerTrend(program) {
|
|
|
63673
64725
|
}
|
|
63674
64726
|
|
|
63675
64727
|
// src/cli/commands/drift.ts
|
|
63676
|
-
var
|
|
64728
|
+
var import_node_path56 = require("path");
|
|
63677
64729
|
init_logger();
|
|
63678
64730
|
init_scan2();
|
|
63679
64731
|
|
|
63680
64732
|
// src/cli/drift.ts
|
|
63681
|
-
var
|
|
63682
|
-
var
|
|
64733
|
+
var import_node_fs40 = require("fs");
|
|
64734
|
+
var import_node_path55 = require("path");
|
|
63683
64735
|
init_discover();
|
|
63684
64736
|
init_patterns();
|
|
63685
64737
|
init_telemetry();
|
|
@@ -63693,7 +64745,7 @@ async function runDrift(cwd, config, options = {}) {
|
|
|
63693
64745
|
for (const absPath of limited) {
|
|
63694
64746
|
let source;
|
|
63695
64747
|
try {
|
|
63696
|
-
source = (0,
|
|
64748
|
+
source = (0, import_node_fs40.readFileSync)(absPath, "utf-8");
|
|
63697
64749
|
} catch {
|
|
63698
64750
|
continue;
|
|
63699
64751
|
}
|
|
@@ -63704,7 +64756,7 @@ async function runDrift(cwd, config, options = {}) {
|
|
|
63704
64756
|
byCategory[v.category] = (byCategory[v.category] ?? 0) + 1;
|
|
63705
64757
|
byFile.push({
|
|
63706
64758
|
file: absPath,
|
|
63707
|
-
relPath: (0,
|
|
64759
|
+
relPath: (0, import_node_path55.relative)(cwd, absPath),
|
|
63708
64760
|
category: v.category,
|
|
63709
64761
|
import: v.import,
|
|
63710
64762
|
declared: v.declared,
|
|
@@ -63980,7 +65032,7 @@ function registerDrift(program) {
|
|
|
63980
65032
|
const options = command.optsWithGlobals();
|
|
63981
65033
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
63982
65034
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
63983
|
-
const cwd = (0,
|
|
65035
|
+
const cwd = (0, import_node_path56.resolve)(options.workspace ?? process.cwd());
|
|
63984
65036
|
if (cmdOptions.temporalSince !== void 0) {
|
|
63985
65037
|
const temporalSince = cmdOptions.temporalSince.trim();
|
|
63986
65038
|
if (temporalSince.length === 0) {
|
|
@@ -64006,13 +65058,13 @@ function registerDrift(program) {
|
|
|
64006
65058
|
}
|
|
64007
65059
|
|
|
64008
65060
|
// src/cli/commands/pr.ts
|
|
64009
|
-
var
|
|
65061
|
+
var import_node_path58 = require("path");
|
|
64010
65062
|
init_logger();
|
|
64011
65063
|
init_scan2();
|
|
64012
65064
|
|
|
64013
65065
|
// src/cli/pr.ts
|
|
64014
|
-
var
|
|
64015
|
-
var
|
|
65066
|
+
var import_node_fs41 = require("fs");
|
|
65067
|
+
var import_node_path57 = require("path");
|
|
64016
65068
|
var import_node_child_process3 = require("child_process");
|
|
64017
65069
|
var import_node_util2 = require("util");
|
|
64018
65070
|
var import_minimatch3 = require("minimatch");
|
|
@@ -64058,10 +65110,10 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
|
|
|
64058
65110
|
if (gitFiles.length === 0) return [];
|
|
64059
65111
|
const sourceFiles = [];
|
|
64060
65112
|
for (const relOrAbs of gitFiles) {
|
|
64061
|
-
const abs = (0,
|
|
64062
|
-
const ext = (0,
|
|
65113
|
+
const abs = (0, import_node_path57.resolve)(cwd, relOrAbs);
|
|
65114
|
+
const ext = (0, import_node_path57.extname)(abs).toLowerCase();
|
|
64063
65115
|
if (!PR_EXTENSIONS.has(ext)) continue;
|
|
64064
|
-
const rel = (0,
|
|
65116
|
+
const rel = (0, import_node_path57.relative)(cwd, abs).split("\\").join("/");
|
|
64065
65117
|
if (config.include.length > 0 && !config.include.some((pattern) => (0, import_minimatch3.minimatch)(rel, pattern))) {
|
|
64066
65118
|
continue;
|
|
64067
65119
|
}
|
|
@@ -64089,7 +65141,7 @@ async function runPrScan(cwd, config, options = {}) {
|
|
|
64089
65141
|
for (const absPath of candidates) {
|
|
64090
65142
|
let source;
|
|
64091
65143
|
try {
|
|
64092
|
-
source = (0,
|
|
65144
|
+
source = (0, import_node_fs41.readFileSync)(absPath, "utf-8");
|
|
64093
65145
|
} catch {
|
|
64094
65146
|
continue;
|
|
64095
65147
|
}
|
|
@@ -64132,7 +65184,7 @@ async function runPrScan(cwd, config, options = {}) {
|
|
|
64132
65184
|
totalScore += fileScore;
|
|
64133
65185
|
files.push({
|
|
64134
65186
|
file: absPath,
|
|
64135
|
-
relPath: (0,
|
|
65187
|
+
relPath: (0, import_node_path57.relative)(cwd, absPath).split("\\").join("/"),
|
|
64136
65188
|
score: fileScore,
|
|
64137
65189
|
issueCount: issues.length,
|
|
64138
65190
|
constitutionViolationCount: constitutionViolations.length,
|
|
@@ -64264,20 +65316,135 @@ function formatPrMarkdown(result) {
|
|
|
64264
65316
|
function prExitCode(result) {
|
|
64265
65317
|
return result.totalScore > result.threshold ? 1 : 0;
|
|
64266
65318
|
}
|
|
65319
|
+
function parseUnifiedDiffPaths(diff) {
|
|
65320
|
+
const paths = [];
|
|
65321
|
+
const seen = /* @__PURE__ */ new Set();
|
|
65322
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
65323
|
+
const m = /^\+\+\+\s+(?:b\/)?(\S.*)$/.exec(line);
|
|
65324
|
+
if (!m) continue;
|
|
65325
|
+
const path = m[1];
|
|
65326
|
+
if (path === "/dev/null" || path === "dev/null") continue;
|
|
65327
|
+
if (seen.has(path)) continue;
|
|
65328
|
+
seen.add(path);
|
|
65329
|
+
paths.push(path);
|
|
65330
|
+
}
|
|
65331
|
+
return paths;
|
|
65332
|
+
}
|
|
65333
|
+
async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
|
|
65334
|
+
const threshold = options.threshold ?? config.prScoreThreshold ?? 20;
|
|
65335
|
+
const maxFiles = options.maxFiles ?? 500;
|
|
65336
|
+
const { existsSync: existsSync31, readFileSync: readFileSync41 } = await import("fs");
|
|
65337
|
+
const { isAbsolute: isAbsolute4, resolve: resolvePath } = await import("path");
|
|
65338
|
+
if (!existsSync31(diffPath)) {
|
|
65339
|
+
throw new Error(`Diff file not found: ${diffPath}`);
|
|
65340
|
+
}
|
|
65341
|
+
const diffAbs = isAbsolute4(diffPath) ? diffPath : resolvePath(cwd, diffPath);
|
|
65342
|
+
const diff = readFileSync41(diffAbs, "utf-8");
|
|
65343
|
+
const changedPaths = parseUnifiedDiffPaths(diff);
|
|
65344
|
+
if (changedPaths.length === 0) {
|
|
65345
|
+
return emptyPrResult(threshold);
|
|
65346
|
+
}
|
|
65347
|
+
return scoreChangedPaths(cwd, config, changedPaths, maxFiles, threshold);
|
|
65348
|
+
}
|
|
65349
|
+
async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
|
|
65350
|
+
const { existsSync: existsSync31, statSync: statSync8 } = await import("fs");
|
|
65351
|
+
const { resolve: resolvePath } = await import("path");
|
|
65352
|
+
const { readFile: readFile4 } = await import("fs/promises");
|
|
65353
|
+
const { scanFile: scanFile2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
|
|
65354
|
+
const limited = paths.slice(0, maxFiles);
|
|
65355
|
+
const files = [];
|
|
65356
|
+
let totalScore = 0;
|
|
65357
|
+
const byCategory = {};
|
|
65358
|
+
const bySeverity = { low: 0, medium: 0, high: 0 };
|
|
65359
|
+
let constitutionViolations = 0;
|
|
65360
|
+
for (const rel of limited) {
|
|
65361
|
+
const abs = resolvePath(cwd, rel);
|
|
65362
|
+
if (!existsSync31(abs) || !statSync8(abs).isFile()) continue;
|
|
65363
|
+
let source;
|
|
65364
|
+
try {
|
|
65365
|
+
source = await readFile4(abs, "utf-8");
|
|
65366
|
+
} catch {
|
|
65367
|
+
continue;
|
|
65368
|
+
}
|
|
65369
|
+
let result;
|
|
65370
|
+
try {
|
|
65371
|
+
result = await scanFile2(abs, config, void 0, cwd);
|
|
65372
|
+
} catch {
|
|
65373
|
+
continue;
|
|
65374
|
+
}
|
|
65375
|
+
if (result.parseError) continue;
|
|
65376
|
+
const issues = result.issues;
|
|
65377
|
+
let slopPoints = 0;
|
|
65378
|
+
const issueEntries = [];
|
|
65379
|
+
for (const issue of issues) {
|
|
65380
|
+
slopPoints += SEVERITY_WEIGHTS[issue.severity] ?? 0;
|
|
65381
|
+
byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
|
|
65382
|
+
bySeverity[issue.severity] = (bySeverity[issue.severity] ?? 0) + 1;
|
|
65383
|
+
issueEntries.push({
|
|
65384
|
+
ruleId: issue.ruleId,
|
|
65385
|
+
severity: issue.severity,
|
|
65386
|
+
line: issue.line,
|
|
65387
|
+
category: issue.category,
|
|
65388
|
+
message: issue.message
|
|
65389
|
+
});
|
|
65390
|
+
}
|
|
65391
|
+
totalScore += slopPoints;
|
|
65392
|
+
files.push({
|
|
65393
|
+
file: abs,
|
|
65394
|
+
relPath: rel,
|
|
65395
|
+
score: slopPoints,
|
|
65396
|
+
issueCount: issueEntries.length,
|
|
65397
|
+
constitutionViolationCount: 0,
|
|
65398
|
+
slopPoints,
|
|
65399
|
+
issues: issueEntries,
|
|
65400
|
+
constitutionViolations: []
|
|
65401
|
+
});
|
|
65402
|
+
}
|
|
65403
|
+
return {
|
|
65404
|
+
base: "diff",
|
|
65405
|
+
head: "diff",
|
|
65406
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65407
|
+
passed: totalScore <= threshold,
|
|
65408
|
+
threshold,
|
|
65409
|
+
totalScore,
|
|
65410
|
+
filesChanged: files.length,
|
|
65411
|
+
byCategory,
|
|
65412
|
+
bySeverity,
|
|
65413
|
+
files
|
|
65414
|
+
};
|
|
65415
|
+
}
|
|
65416
|
+
function emptyPrResult(threshold) {
|
|
65417
|
+
return {
|
|
65418
|
+
base: "diff",
|
|
65419
|
+
head: "diff",
|
|
65420
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65421
|
+
passed: true,
|
|
65422
|
+
threshold,
|
|
65423
|
+
totalScore: 0,
|
|
65424
|
+
filesChanged: 0,
|
|
65425
|
+
byCategory: {},
|
|
65426
|
+
bySeverity: { low: 0, medium: 0, high: 0 },
|
|
65427
|
+
files: []
|
|
65428
|
+
};
|
|
65429
|
+
}
|
|
64267
65430
|
|
|
64268
65431
|
// src/cli/commands/pr.ts
|
|
64269
65432
|
function registerPr(program) {
|
|
64270
65433
|
program.command("pr").description(
|
|
64271
65434
|
"Score the slop cost of the current PR. Compares --base (default: main) to --head (default: HEAD), scans only changed source files, returns a single weighted score. Exits 1 when score > --threshold (default 20)."
|
|
64272
|
-
).option("--base <ref>", "base git ref to diff from", "main").option("--head <ref>", "head git ref to diff to", "HEAD").option("--format <text|json|markdown>", "output format", "text").option("--threshold <n>", "score threshold (overrides config.prScoreThreshold)", parseThreshold).option("--max-files <n>", "cap on files scanned", parseCount, 500).action(
|
|
65435
|
+
).option("--base <ref>", "base git ref to diff from", "main").option("--head <ref>", "head git ref to diff to", "HEAD").option("--format <text|json|markdown>", "output format", "text").option("--threshold <n>", "score threshold (overrides config.prScoreThreshold)", parseThreshold).option("--max-files <n>", "cap on files scanned", parseCount, 500).option("--diff <file>", "unified-diff file (overrides --base/--head; meant for CI/Action use)").action(
|
|
64273
65436
|
async (cmdOptions, command) => {
|
|
64274
65437
|
try {
|
|
64275
65438
|
const options = command.optsWithGlobals();
|
|
64276
|
-
const cwd = (0,
|
|
65439
|
+
const cwd = (0, import_node_path58.resolve)(options.workspace ?? process.cwd());
|
|
64277
65440
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64278
65441
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64279
65442
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64280
|
-
const result = await
|
|
65443
|
+
const result = cmdOptions.diff ? await runPrScanFromDiffFile(cwd, config, cmdOptions.diff, {
|
|
65444
|
+
format,
|
|
65445
|
+
threshold: cmdOptions.threshold,
|
|
65446
|
+
maxFiles: cmdOptions.maxFiles
|
|
65447
|
+
}) : await runPrScan(cwd, config, {
|
|
64281
65448
|
base: cmdOptions.base,
|
|
64282
65449
|
head: cmdOptions.head,
|
|
64283
65450
|
format,
|
|
@@ -64295,7 +65462,7 @@ function registerPr(program) {
|
|
|
64295
65462
|
}
|
|
64296
65463
|
|
|
64297
65464
|
// src/cli/commands/security.ts
|
|
64298
|
-
var
|
|
65465
|
+
var import_node_path59 = require("path");
|
|
64299
65466
|
init_logger();
|
|
64300
65467
|
init_scan2();
|
|
64301
65468
|
init_ai_security_risk();
|
|
@@ -64308,7 +65475,7 @@ function registerSecurity(program) {
|
|
|
64308
65475
|
const options = command.optsWithGlobals();
|
|
64309
65476
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64310
65477
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64311
|
-
const cwd = (0,
|
|
65478
|
+
const cwd = (0, import_node_path59.resolve)(options.workspace ?? process.cwd());
|
|
64312
65479
|
const { report } = await runScan({ ...options, workspace: cwd });
|
|
64313
65480
|
const securityIssues = report.issues.filter((i) => i.category === "security");
|
|
64314
65481
|
const { risk, findings } = computeAiSecurityRisk(securityIssues);
|
|
@@ -64354,12 +65521,12 @@ function registerSecurity(program) {
|
|
|
64354
65521
|
}
|
|
64355
65522
|
|
|
64356
65523
|
// src/cli/commands/test.ts
|
|
64357
|
-
var
|
|
65524
|
+
var import_node_path61 = require("path");
|
|
64358
65525
|
init_logger();
|
|
64359
65526
|
init_scan2();
|
|
64360
65527
|
|
|
64361
65528
|
// src/cli/test.ts
|
|
64362
|
-
var
|
|
65529
|
+
var import_node_path60 = require("path");
|
|
64363
65530
|
init_scan2();
|
|
64364
65531
|
init_test_quality();
|
|
64365
65532
|
init_logger();
|
|
@@ -64370,7 +65537,7 @@ var TEST_INCLUDE_GLOBS = [
|
|
|
64370
65537
|
"**/__tests__/**/*.{ts,tsx,js,jsx}",
|
|
64371
65538
|
"**/__fixtures__/**/*.{ts,tsx,js,jsx,json}"
|
|
64372
65539
|
];
|
|
64373
|
-
async function runTestScan(cwd,
|
|
65540
|
+
async function runTestScan(cwd, _config, options = {}) {
|
|
64374
65541
|
const include = TEST_INCLUDE_GLOBS;
|
|
64375
65542
|
const cliOptions = {
|
|
64376
65543
|
include,
|
|
@@ -64430,7 +65597,7 @@ function formatTestReport(result, opts = {}) {
|
|
|
64430
65597
|
const sortedFiles = [...byFile.keys()].sort();
|
|
64431
65598
|
for (const file of sortedFiles) {
|
|
64432
65599
|
const issues = byFile.get(file) ?? [];
|
|
64433
|
-
const rel = file.startsWith((0,
|
|
65600
|
+
const rel = file.startsWith((0, import_node_path60.resolve)(process.cwd())) ? file : file;
|
|
64434
65601
|
lines.push("");
|
|
64435
65602
|
lines.push(` ${rel}`);
|
|
64436
65603
|
for (const issue of issues.slice(0, 20)) {
|
|
@@ -64459,7 +65626,7 @@ function registerTest(program) {
|
|
|
64459
65626
|
const options = command.optsWithGlobals();
|
|
64460
65627
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64461
65628
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64462
|
-
const cwd = (0,
|
|
65629
|
+
const cwd = (0, import_node_path61.resolve)(options.workspace ?? process.cwd());
|
|
64463
65630
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64464
65631
|
const { result } = await runTestScan(cwd, config, { strict: options.strict });
|
|
64465
65632
|
logger.info(formatTestReport(result, { json: format === "json" }));
|
|
@@ -64473,7 +65640,7 @@ function registerTest(program) {
|
|
|
64473
65640
|
}
|
|
64474
65641
|
|
|
64475
65642
|
// src/cli/commands/architecture.ts
|
|
64476
|
-
var
|
|
65643
|
+
var import_node_path62 = require("path");
|
|
64477
65644
|
init_logger();
|
|
64478
65645
|
init_scan2();
|
|
64479
65646
|
init_architecture_score();
|
|
@@ -64486,7 +65653,7 @@ function registerArchitecture(program) {
|
|
|
64486
65653
|
const options = command.optsWithGlobals();
|
|
64487
65654
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64488
65655
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64489
|
-
const cwd = (0,
|
|
65656
|
+
const cwd = (0, import_node_path62.resolve)(options.workspace ?? process.cwd());
|
|
64490
65657
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64491
65658
|
const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
|
|
64492
65659
|
const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
|
|
@@ -64501,13 +65668,13 @@ function registerArchitecture(program) {
|
|
|
64501
65668
|
}
|
|
64502
65669
|
|
|
64503
65670
|
// src/cli/commands/business-logic.ts
|
|
64504
|
-
var
|
|
65671
|
+
var import_node_path64 = require("path");
|
|
64505
65672
|
init_logger();
|
|
64506
65673
|
init_scan2();
|
|
64507
65674
|
|
|
64508
65675
|
// src/cli/business-logic.ts
|
|
64509
|
-
var
|
|
64510
|
-
var
|
|
65676
|
+
var import_node_fs42 = require("fs");
|
|
65677
|
+
var import_node_path63 = require("path");
|
|
64511
65678
|
init_discover();
|
|
64512
65679
|
init_business_logic();
|
|
64513
65680
|
async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
@@ -64519,7 +65686,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
|
64519
65686
|
for (const absPath of limited) {
|
|
64520
65687
|
let source;
|
|
64521
65688
|
try {
|
|
64522
|
-
source = (0,
|
|
65689
|
+
source = (0, import_node_fs42.readFileSync)(absPath, "utf-8");
|
|
64523
65690
|
} catch {
|
|
64524
65691
|
continue;
|
|
64525
65692
|
}
|
|
@@ -64528,7 +65695,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
|
64528
65695
|
for (const issue of fileIssues) {
|
|
64529
65696
|
issues.push({
|
|
64530
65697
|
...issue,
|
|
64531
|
-
filePath: (0,
|
|
65698
|
+
filePath: (0, import_node_path63.relative)(cwd, absPath) || absPath
|
|
64532
65699
|
});
|
|
64533
65700
|
}
|
|
64534
65701
|
}
|
|
@@ -64631,7 +65798,7 @@ function registerBusinessLogic(program) {
|
|
|
64631
65798
|
const options = command.optsWithGlobals();
|
|
64632
65799
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64633
65800
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64634
|
-
const cwd = (0,
|
|
65801
|
+
const cwd = (0, import_node_path64.resolve)(options.workspace ?? process.cwd());
|
|
64635
65802
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64636
65803
|
const result = await runBusinessLogicScan(cwd, config, {
|
|
64637
65804
|
maxFiles: cmdOptions.maxFiles
|
|
@@ -64647,7 +65814,7 @@ function registerBusinessLogic(program) {
|
|
|
64647
65814
|
}
|
|
64648
65815
|
|
|
64649
65816
|
// src/cli/commands/maintenance-cost.ts
|
|
64650
|
-
var
|
|
65817
|
+
var import_node_path65 = require("path");
|
|
64651
65818
|
init_logger();
|
|
64652
65819
|
init_scan2();
|
|
64653
65820
|
|
|
@@ -64773,7 +65940,7 @@ function registerMaintenanceCost(program) {
|
|
|
64773
65940
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64774
65941
|
const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
|
|
64775
65942
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64776
|
-
const cwd = (0,
|
|
65943
|
+
const cwd = (0, import_node_path65.resolve)(options.workspace ?? process.cwd());
|
|
64777
65944
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64778
65945
|
const result = await runMaintenanceCostScan(cwd, config, {
|
|
64779
65946
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -64790,7 +65957,7 @@ function registerMaintenanceCost(program) {
|
|
|
64790
65957
|
}
|
|
64791
65958
|
|
|
64792
65959
|
// src/cli/commands/docs.ts
|
|
64793
|
-
var
|
|
65960
|
+
var import_node_path66 = require("path");
|
|
64794
65961
|
init_logger();
|
|
64795
65962
|
init_scan2();
|
|
64796
65963
|
|
|
@@ -64917,7 +66084,7 @@ function registerDocs(program) {
|
|
|
64917
66084
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64918
66085
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64919
66086
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64920
|
-
const cwd = (0,
|
|
66087
|
+
const cwd = (0, import_node_path66.resolve)(options.workspace ?? process.cwd());
|
|
64921
66088
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64922
66089
|
const result = await runDocsScan(cwd, config, {
|
|
64923
66090
|
maxDocFiles: cmdOptions.maxFiles,
|
|
@@ -64939,7 +66106,7 @@ function registerDocs(program) {
|
|
|
64939
66106
|
}
|
|
64940
66107
|
|
|
64941
66108
|
// src/cli/commands/db.ts
|
|
64942
|
-
var
|
|
66109
|
+
var import_node_path67 = require("path");
|
|
64943
66110
|
init_logger();
|
|
64944
66111
|
init_scan2();
|
|
64945
66112
|
|
|
@@ -65060,7 +66227,7 @@ function registerDb(program) {
|
|
|
65060
66227
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
65061
66228
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
65062
66229
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
65063
|
-
const cwd = (0,
|
|
66230
|
+
const cwd = (0, import_node_path67.resolve)(options.workspace ?? process.cwd());
|
|
65064
66231
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
65065
66232
|
const result = await runDbScan(cwd, config, {
|
|
65066
66233
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -65082,13 +66249,13 @@ function registerDb(program) {
|
|
|
65082
66249
|
}
|
|
65083
66250
|
|
|
65084
66251
|
// src/cli/commands/patterns.ts
|
|
65085
|
-
var
|
|
66252
|
+
var import_node_path69 = require("path");
|
|
65086
66253
|
init_logger();
|
|
65087
66254
|
init_scan2();
|
|
65088
66255
|
|
|
65089
66256
|
// src/engine/patterns.ts
|
|
65090
|
-
var
|
|
65091
|
-
var
|
|
66257
|
+
var import_node_fs43 = require("fs");
|
|
66258
|
+
var import_node_path68 = require("path");
|
|
65092
66259
|
init_patterns();
|
|
65093
66260
|
init_discover();
|
|
65094
66261
|
var PATTERN_CATEGORIES = [
|
|
@@ -65123,7 +66290,7 @@ var TOAST_NAME_RE = /^(?:[A-Z][a-zA-Z0-9]+)?(Toast|Notification|Snackbar|Alert|B
|
|
|
65123
66290
|
var CARD_NAME_RE = /^(?:[A-Z][a-zA-Z0-9]+)?(Card|Tile|Chip|Badge)$/;
|
|
65124
66291
|
var API_PATH_RE2 = /(?:^|\/)(?:lib\/api|services|api-client|clients)\//;
|
|
65125
66292
|
function baseName(filePath) {
|
|
65126
|
-
return (0,
|
|
66293
|
+
return (0, import_node_path68.basename)(filePath).replace(/\.(tsx|ts|jsx|js|vue|svelte|astro)$/i, "");
|
|
65127
66294
|
}
|
|
65128
66295
|
function makeStats(patterns) {
|
|
65129
66296
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -65182,7 +66349,7 @@ function detectApiFromFiles(files, cwd) {
|
|
|
65182
66349
|
const out = [];
|
|
65183
66350
|
for (const f of files) {
|
|
65184
66351
|
if (API_PATH_RE2.test(f)) {
|
|
65185
|
-
out.push((0,
|
|
66352
|
+
out.push((0, import_node_path68.relative)(cwd, f).split("\\").join("/"));
|
|
65186
66353
|
}
|
|
65187
66354
|
}
|
|
65188
66355
|
return out;
|
|
@@ -65192,7 +66359,7 @@ function detectFormsFromFiles(files) {
|
|
|
65192
66359
|
for (const f of files) {
|
|
65193
66360
|
let source;
|
|
65194
66361
|
try {
|
|
65195
|
-
source = (0,
|
|
66362
|
+
source = (0, import_node_fs43.readFileSync)(f, "utf-8");
|
|
65196
66363
|
} catch {
|
|
65197
66364
|
continue;
|
|
65198
66365
|
}
|
|
@@ -65319,7 +66486,7 @@ function formatText2(report) {
|
|
|
65319
66486
|
const label = CATEGORY_LABELS2[cat].padEnd(20);
|
|
65320
66487
|
const count = String(stats.count).padStart(2);
|
|
65321
66488
|
const baseline = String(stats.baseline).padStart(1);
|
|
65322
|
-
const excess = stats.excess > 0 ? `\u26A0 +${stats.excess} over` : "\u2713
|
|
66489
|
+
const excess = stats.excess > 0 ? `\u26A0 +${stats.excess} over` : stats.count === 0 ? "\u2713 none" : "\u2713 within baseline";
|
|
65323
66490
|
lines.push(` ${label} ${count} implementations (baseline ${baseline}) ${excess}`);
|
|
65324
66491
|
}
|
|
65325
66492
|
if (report.doNotCreate.length > 0) {
|
|
@@ -65377,7 +66544,7 @@ function formatMarkdown3(report) {
|
|
|
65377
66544
|
for (const cat of PATTERN_CATEGORIES) {
|
|
65378
66545
|
const stats = report.byCategory[cat];
|
|
65379
66546
|
const label = CATEGORY_LABELS2[cat];
|
|
65380
|
-
const excess = stats.excess > 0 ? `+${stats.excess}` : "\u2014";
|
|
66547
|
+
const excess = stats.excess > 0 ? `+${stats.excess}` : stats.count === 0 ? "\u2014" : "\xB7";
|
|
65381
66548
|
lines.push(`| ${label} | ${stats.count} | ${stats.baseline} | ${excess} |`);
|
|
65382
66549
|
}
|
|
65383
66550
|
if (report.doNotCreate.length > 0) {
|
|
@@ -65413,7 +66580,7 @@ function registerPatterns(program) {
|
|
|
65413
66580
|
const options = command.optsWithGlobals();
|
|
65414
66581
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
65415
66582
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
65416
|
-
const cwd = (0,
|
|
66583
|
+
const cwd = (0, import_node_path69.resolve)(options.workspace ?? process.cwd());
|
|
65417
66584
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
65418
66585
|
const result = await runPatternsScan(cwd, config, {
|
|
65419
66586
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -65430,8 +66597,8 @@ function registerPatterns(program) {
|
|
|
65430
66597
|
}
|
|
65431
66598
|
|
|
65432
66599
|
// src/cli/commands/research.ts
|
|
65433
|
-
var
|
|
65434
|
-
var
|
|
66600
|
+
var import_node_fs44 = require("fs");
|
|
66601
|
+
var import_node_path70 = require("path");
|
|
65435
66602
|
init_logger();
|
|
65436
66603
|
init_config2();
|
|
65437
66604
|
function registerResearch(program) {
|
|
@@ -65448,24 +66615,24 @@ function registerResearch(program) {
|
|
|
65448
66615
|
framework: cmdOptions.framework,
|
|
65449
66616
|
componentType: cmdOptions.componentType,
|
|
65450
66617
|
provider,
|
|
65451
|
-
outputDir: (0,
|
|
66618
|
+
outputDir: (0, import_node_path70.resolve)(cmdOptions.outputDir),
|
|
65452
66619
|
temperature: cmdOptions.temperature
|
|
65453
66620
|
});
|
|
65454
66621
|
logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
|
|
65455
66622
|
});
|
|
65456
66623
|
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) => {
|
|
65457
66624
|
try {
|
|
65458
|
-
const metadataPath = (0,
|
|
65459
|
-
if (!(0,
|
|
66625
|
+
const metadataPath = (0, import_node_path70.resolve)(cmdOptions.inputDir, "metadata.json");
|
|
66626
|
+
if (!(0, import_node_fs44.existsSync)(metadataPath)) {
|
|
65460
66627
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
65461
66628
|
process.exit(2);
|
|
65462
66629
|
}
|
|
65463
|
-
const samples = JSON.parse((0,
|
|
66630
|
+
const samples = JSON.parse((0, import_node_fs44.readFileSync)(metadataPath, "utf8"));
|
|
65464
66631
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
65465
66632
|
const analysis = await analyzeSamples(samples, config);
|
|
65466
|
-
const outputPath = (0,
|
|
65467
|
-
(0,
|
|
65468
|
-
(0,
|
|
66633
|
+
const outputPath = (0, import_node_path70.resolve)(cmdOptions.output);
|
|
66634
|
+
(0, import_node_fs44.mkdirSync)((0, import_node_path70.dirname)(outputPath), { recursive: true });
|
|
66635
|
+
(0, import_node_fs44.writeFileSync)(outputPath, JSON.stringify(analysis, null, 2), "utf8");
|
|
65469
66636
|
logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
|
|
65470
66637
|
logger.info(`Wrote analysis to ${outputPath}`);
|
|
65471
66638
|
} catch (error) {
|
|
@@ -65475,12 +66642,12 @@ function registerResearch(program) {
|
|
|
65475
66642
|
});
|
|
65476
66643
|
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) => {
|
|
65477
66644
|
try {
|
|
65478
|
-
const metadataPath = (0,
|
|
65479
|
-
if (!(0,
|
|
66645
|
+
const metadataPath = (0, import_node_path70.resolve)(cmdOptions.inputDir, "metadata.json");
|
|
66646
|
+
if (!(0, import_node_fs44.existsSync)(metadataPath)) {
|
|
65480
66647
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
65481
66648
|
process.exit(2);
|
|
65482
66649
|
}
|
|
65483
|
-
const samples = JSON.parse((0,
|
|
66650
|
+
const samples = JSON.parse((0, import_node_fs44.readFileSync)(metadataPath, "utf8"));
|
|
65484
66651
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
65485
66652
|
const analysis = await analyzeSamples(samples, config);
|
|
65486
66653
|
const extraction = extractAndCluster(analysis.samples, {
|
|
@@ -65490,8 +66657,8 @@ function registerResearch(program) {
|
|
|
65490
66657
|
const candidates = clustersToCandidates(extraction.clusters, {
|
|
65491
66658
|
minFrequency: cmdOptions.minFrequency
|
|
65492
66659
|
});
|
|
65493
|
-
const outputPath = (0,
|
|
65494
|
-
(0,
|
|
66660
|
+
const outputPath = (0, import_node_path70.resolve)(cmdOptions.output);
|
|
66661
|
+
(0, import_node_fs44.mkdirSync)((0, import_node_path70.dirname)(outputPath), { recursive: true });
|
|
65495
66662
|
const payload = {
|
|
65496
66663
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65497
66664
|
sampleCount: analysis.summary.total,
|
|
@@ -65499,7 +66666,7 @@ function registerResearch(program) {
|
|
|
65499
66666
|
fingerprintCount: extraction.total,
|
|
65500
66667
|
candidates
|
|
65501
66668
|
};
|
|
65502
|
-
(0,
|
|
66669
|
+
(0, import_node_fs44.writeFileSync)(outputPath, JSON.stringify(payload, null, 2), "utf8");
|
|
65503
66670
|
logger.info(`Extracted ${extraction.total} fingerprints across ${analysis.summary.total} samples`);
|
|
65504
66671
|
logger.info(`Wrote ${candidates.length} candidate rule(s) to ${outputPath}`);
|
|
65505
66672
|
} catch (error) {
|
|
@@ -65510,8 +66677,8 @@ function registerResearch(program) {
|
|
|
65510
66677
|
}
|
|
65511
66678
|
|
|
65512
66679
|
// src/cli/commands/init.ts
|
|
65513
|
-
var
|
|
65514
|
-
var
|
|
66680
|
+
var import_node_fs45 = require("fs");
|
|
66681
|
+
var import_node_path72 = require("path");
|
|
65515
66682
|
init_logger();
|
|
65516
66683
|
init_builtins();
|
|
65517
66684
|
init_scan2();
|
|
@@ -65521,145 +66688,9 @@ init_git();
|
|
|
65521
66688
|
init_cache();
|
|
65522
66689
|
|
|
65523
66690
|
// src/snippet/targets.ts
|
|
65524
|
-
var
|
|
65525
|
-
|
|
65526
|
-
|
|
65527
|
-
function aiSpecificRules(rules) {
|
|
65528
|
-
return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
|
|
65529
|
-
}
|
|
65530
|
-
function categorizeRules(rules) {
|
|
65531
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
65532
|
-
for (const r of rules) {
|
|
65533
|
-
const list = byCategory.get(r.category) ?? [];
|
|
65534
|
-
list.push(r);
|
|
65535
|
-
byCategory.set(r.category, list);
|
|
65536
|
-
}
|
|
65537
|
-
for (const list of byCategory.values()) {
|
|
65538
|
-
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
65539
|
-
}
|
|
65540
|
-
return byCategory;
|
|
65541
|
-
}
|
|
65542
|
-
function baseContent(rules, toolLabel, intro) {
|
|
65543
|
-
const lines = [];
|
|
65544
|
-
lines.push("# " + toolLabel);
|
|
65545
|
-
lines.push("");
|
|
65546
|
-
lines.push(intro);
|
|
65547
|
-
lines.push("");
|
|
65548
|
-
const ai = aiSpecificRules(rules);
|
|
65549
|
-
const byCategory = categorizeRules(ai);
|
|
65550
|
-
lines.push("## Category-level directives");
|
|
65551
|
-
lines.push("");
|
|
65552
|
-
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
65553
|
-
lines.push("- **" + category + "**: " + directive);
|
|
65554
|
-
}
|
|
65555
|
-
lines.push("");
|
|
65556
|
-
lines.push("## Per-rule directives");
|
|
65557
|
-
lines.push("");
|
|
65558
|
-
for (const [category, list] of byCategory) {
|
|
65559
|
-
lines.push("### " + category);
|
|
65560
|
-
lines.push("");
|
|
65561
|
-
for (const rule of list) {
|
|
65562
|
-
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
65563
|
-
lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
|
|
65564
|
-
}
|
|
65565
|
-
lines.push("");
|
|
65566
|
-
}
|
|
65567
|
-
return lines.join("\n");
|
|
65568
|
-
}
|
|
65569
|
-
|
|
65570
|
-
// src/snippet/generators.ts
|
|
65571
|
-
function generateCursorSnippet(rules) {
|
|
65572
|
-
const lines = [];
|
|
65573
|
-
lines.push("# slopbrick cursor rules");
|
|
65574
|
-
lines.push("");
|
|
65575
|
-
lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
|
|
65576
|
-
lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
|
|
65577
|
-
lines.push("");
|
|
65578
|
-
lines.push("## Category-level directives");
|
|
65579
|
-
lines.push("");
|
|
65580
|
-
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
65581
|
-
lines.push("- **" + category + "**: " + directive);
|
|
65582
|
-
}
|
|
65583
|
-
lines.push("");
|
|
65584
|
-
lines.push("## Per-rule directives");
|
|
65585
|
-
lines.push("");
|
|
65586
|
-
const ai = aiSpecificRules(rules);
|
|
65587
|
-
const byCategory = categorizeRules(ai);
|
|
65588
|
-
for (const [category, list] of byCategory) {
|
|
65589
|
-
lines.push("### " + category);
|
|
65590
|
-
lines.push("");
|
|
65591
|
-
for (const rule of list) {
|
|
65592
|
-
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
65593
|
-
lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
|
|
65594
|
-
}
|
|
65595
|
-
lines.push("");
|
|
65596
|
-
}
|
|
65597
|
-
lines.push("## Verification");
|
|
65598
|
-
lines.push("");
|
|
65599
|
-
lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
|
|
65600
|
-
lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
|
|
65601
|
-
lines.push("");
|
|
65602
|
-
return lines.join("\n");
|
|
65603
|
-
}
|
|
65604
|
-
function generateAgentsMdSnippet(rules) {
|
|
65605
|
-
return baseContent(
|
|
65606
|
-
rules,
|
|
65607
|
-
"AGENTS.md",
|
|
65608
|
-
"Generated by `slopbrick init --agents-md`. Read these directives when writing or modifying code in this project. AGENTS.md is the de facto standard read by OpenAI Codex, opencode, Pi, Cline, Continue, Gemini, Claude Code (fallback), and others."
|
|
65609
|
-
) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n";
|
|
65610
|
-
}
|
|
65611
|
-
function generateClaudeMdSnippet(rules) {
|
|
65612
|
-
return baseContent(
|
|
65613
|
-
rules,
|
|
65614
|
-
"CLAUDE.md",
|
|
65615
|
-
"Generated by `slopbrick init --claude-md`. Read this file at the start of every Claude Code session in this project. CLAUDE.md takes precedence over AGENTS.md for Claude Code."
|
|
65616
|
-
) + "## Verification\n\nRun `slopbrick scan` after edits. If MCP is configured (`slopbrick mcp`), use `slop_scan_file` proactively to check before reporting code as done.\n";
|
|
65617
|
-
}
|
|
65618
|
-
function generateAiderSnippet(rules) {
|
|
65619
|
-
return baseContent(
|
|
65620
|
-
rules,
|
|
65621
|
-
"CONVENTIONS.md",
|
|
65622
|
-
"Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
|
|
65623
|
-
);
|
|
65624
|
-
}
|
|
65625
|
-
function generateWindsurfSnippet(rules) {
|
|
65626
|
-
return baseContent(
|
|
65627
|
-
rules,
|
|
65628
|
-
".windsurfrules",
|
|
65629
|
-
"Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
|
|
65630
|
-
);
|
|
65631
|
-
}
|
|
65632
|
-
function generateClineRules(rules) {
|
|
65633
|
-
return baseContent(
|
|
65634
|
-
rules,
|
|
65635
|
-
"AGENTS.md",
|
|
65636
|
-
"Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
|
|
65637
|
-
);
|
|
65638
|
-
}
|
|
65639
|
-
function generateCursorrulesLegacySnippet(rules) {
|
|
65640
|
-
return baseContent(
|
|
65641
|
-
rules,
|
|
65642
|
-
".cursorrules",
|
|
65643
|
-
"Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
|
|
65644
|
-
);
|
|
65645
|
-
}
|
|
65646
|
-
function generateGeminiSnippet(rules) {
|
|
65647
|
-
return baseContent(
|
|
65648
|
-
rules,
|
|
65649
|
-
"GEMINI.md",
|
|
65650
|
-
"Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
|
|
65651
|
-
);
|
|
65652
|
-
}
|
|
65653
|
-
function generateCopilotSnippet(rules) {
|
|
65654
|
-
return baseContent(
|
|
65655
|
-
rules,
|
|
65656
|
-
"copilot-instructions.md",
|
|
65657
|
-
"Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
|
|
65658
|
-
);
|
|
65659
|
-
}
|
|
65660
|
-
|
|
65661
|
-
// src/snippet/targets.ts
|
|
65662
|
-
var SNIPPET_TARGETS = [
|
|
66691
|
+
var import_node_path71 = require("path");
|
|
66692
|
+
init_generators();
|
|
66693
|
+
var SNIPPET_TARGETS2 = [
|
|
65663
66694
|
{
|
|
65664
66695
|
flag: "cursor",
|
|
65665
66696
|
cliName: "--cursor",
|
|
@@ -65752,7 +66783,7 @@ var SNIPPET_TARGETS = [
|
|
|
65752
66783
|
}
|
|
65753
66784
|
];
|
|
65754
66785
|
function resolveTargetPath(target) {
|
|
65755
|
-
return target.isFolder ? (0,
|
|
66786
|
+
return target.isFolder ? (0, import_node_path71.join)(target.path, target.filename) : target.path;
|
|
65756
66787
|
}
|
|
65757
66788
|
function renderMatrix() {
|
|
65758
66789
|
const lines = [];
|
|
@@ -65760,7 +66791,7 @@ function renderMatrix() {
|
|
|
65760
66791
|
lines.push("");
|
|
65761
66792
|
lines.push("| Flag | File | Agent |");
|
|
65762
66793
|
lines.push("|------|------|-------|");
|
|
65763
|
-
for (const t of
|
|
66794
|
+
for (const t of SNIPPET_TARGETS2) {
|
|
65764
66795
|
lines.push("| `" + t.cliName + "` | `" + resolveTargetPath(t) + "` | " + t.agent + " |");
|
|
65765
66796
|
}
|
|
65766
66797
|
return lines.join("\n");
|
|
@@ -65774,13 +66805,13 @@ function registerInit(program) {
|
|
|
65774
66805
|
logger.info(renderMatrix());
|
|
65775
66806
|
process.exit(0);
|
|
65776
66807
|
}
|
|
65777
|
-
const cwd = (0,
|
|
65778
|
-
const configPath = (0,
|
|
66808
|
+
const cwd = (0, import_node_path72.resolve)(options.workspace ?? process.cwd());
|
|
66809
|
+
const configPath = (0, import_node_path72.join)(cwd, "slopbrick.config.mjs");
|
|
65779
66810
|
const detected = detectStack(cwd);
|
|
65780
66811
|
const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
|
|
65781
66812
|
const proposed = serializeConfig(fallbackConfig);
|
|
65782
|
-
if ((0,
|
|
65783
|
-
const current = (0,
|
|
66813
|
+
if ((0, import_node_fs45.existsSync)(configPath) && !cmdOptions.yes) {
|
|
66814
|
+
const current = (0, import_node_fs45.readFileSync)(configPath, "utf8");
|
|
65784
66815
|
logger.error(`A config file already exists at ${configPath}.`);
|
|
65785
66816
|
logger.error("To overwrite it with defaults, run `slopbrick init --yes`.");
|
|
65786
66817
|
logger.error("");
|
|
@@ -65801,7 +66832,7 @@ function registerInit(program) {
|
|
|
65801
66832
|
config = buildInitConfig(detected, answers);
|
|
65802
66833
|
usedWizard = true;
|
|
65803
66834
|
}
|
|
65804
|
-
(0,
|
|
66835
|
+
(0, import_node_fs45.writeFileSync)(configPath, serializeConfig(config));
|
|
65805
66836
|
appendGitignore(cwd);
|
|
65806
66837
|
const refresh = await refreshRegistrySnapshot(cwd);
|
|
65807
66838
|
if (!refresh.ok) {
|
|
@@ -65825,27 +66856,27 @@ function registerInit(program) {
|
|
|
65825
66856
|
}
|
|
65826
66857
|
logger.info(refresh.message);
|
|
65827
66858
|
}
|
|
65828
|
-
const targetsToWrite =
|
|
66859
|
+
const targetsToWrite = SNIPPET_TARGETS2.filter((t) => {
|
|
65829
66860
|
if (cmdOptions.all) return true;
|
|
65830
66861
|
const opts = cmdOptions;
|
|
65831
66862
|
return Boolean(opts[t.flag]);
|
|
65832
66863
|
});
|
|
65833
66864
|
for (const target of targetsToWrite) {
|
|
65834
|
-
const snippetPath = (0,
|
|
65835
|
-
(0,
|
|
66865
|
+
const snippetPath = (0, import_node_path72.join)(cwd, resolveTargetPath(target));
|
|
66866
|
+
(0, import_node_fs45.mkdirSync)((0, import_node_path72.dirname)(snippetPath), { recursive: true });
|
|
65836
66867
|
const generated = target.generator(builtinRules);
|
|
65837
|
-
if (!target.isFolder && (0,
|
|
65838
|
-
const existing = (0,
|
|
66868
|
+
if (!target.isFolder && (0, import_node_fs45.existsSync)(snippetPath)) {
|
|
66869
|
+
const existing = (0, import_node_fs45.readFileSync)(snippetPath, "utf8");
|
|
65839
66870
|
if (existing.includes("<!-- slopbrick:begin -->")) {
|
|
65840
66871
|
const updated = existing.replace(
|
|
65841
66872
|
/<!-- slopbrick:begin -->[\s\S]*?<!-- slopbrick:end -->/,
|
|
65842
66873
|
"<!-- slopbrick:begin -->\n" + generated + "<!-- slopbrick:end -->"
|
|
65843
66874
|
);
|
|
65844
|
-
(0,
|
|
66875
|
+
(0, import_node_fs45.writeFileSync)(snippetPath, updated, "utf8");
|
|
65845
66876
|
if (!options.quiet) logger.info(`Updated ${snippetPath}`);
|
|
65846
66877
|
continue;
|
|
65847
66878
|
}
|
|
65848
|
-
(0,
|
|
66879
|
+
(0, import_node_fs45.writeFileSync)(
|
|
65849
66880
|
snippetPath,
|
|
65850
66881
|
existing + (existing.endsWith("\n") ? "\n" : "\n\n") + generated,
|
|
65851
66882
|
"utf8"
|
|
@@ -65853,7 +66884,7 @@ function registerInit(program) {
|
|
|
65853
66884
|
if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
|
|
65854
66885
|
continue;
|
|
65855
66886
|
}
|
|
65856
|
-
(0,
|
|
66887
|
+
(0, import_node_fs45.writeFileSync)(snippetPath, generated, "utf8");
|
|
65857
66888
|
if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
|
|
65858
66889
|
}
|
|
65859
66890
|
if (options.baseline) {
|
|
@@ -65871,14 +66902,14 @@ function registerInit(program) {
|
|
|
65871
66902
|
}
|
|
65872
66903
|
|
|
65873
66904
|
// src/cli/commands/flywheel.ts
|
|
65874
|
-
var
|
|
65875
|
-
var
|
|
66905
|
+
var import_node_fs47 = require("fs");
|
|
66906
|
+
var import_node_path74 = require("path");
|
|
65876
66907
|
init_logger();
|
|
65877
66908
|
init_telemetry();
|
|
65878
66909
|
|
|
65879
66910
|
// src/report/flywheel.ts
|
|
65880
|
-
var
|
|
65881
|
-
var
|
|
66911
|
+
var import_node_fs46 = require("fs");
|
|
66912
|
+
var import_node_path73 = require("path");
|
|
65882
66913
|
function average(values) {
|
|
65883
66914
|
if (values.length === 0) return 0;
|
|
65884
66915
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
@@ -65938,8 +66969,9 @@ function formatFlywheel(summary, options = {}) {
|
|
|
65938
66969
|
if (summary.firstRunAt && summary.latestRunAt) {
|
|
65939
66970
|
lines.push(` Window: ${summary.firstRunAt} \u2192 ${summary.latestRunAt}`);
|
|
65940
66971
|
}
|
|
65941
|
-
|
|
65942
|
-
lines.push(`
|
|
66972
|
+
const directionNote = " (lower = cleaner since v0.21.0)";
|
|
66973
|
+
lines.push(` Average AI Slop Score: ${summary.averageSlopIndex.toFixed(2)}`);
|
|
66974
|
+
lines.push(` Latest AI Slop Score: ${summary.latestSlopIndex.toFixed(2)}${directionNote}`);
|
|
65943
66975
|
lines.push(` Average assembly health: ${summary.averageAssemblyHealth.toFixed(2)}`);
|
|
65944
66976
|
lines.push(` Latest assembly health: ${summary.latestAssemblyHealth.toFixed(2)}`);
|
|
65945
66977
|
if (summary.topViolations.length > 0) {
|
|
@@ -65968,7 +67000,7 @@ function formatFlywheel(summary, options = {}) {
|
|
|
65968
67000
|
function registerFlywheel(program) {
|
|
65969
67001
|
program.command("flywheel").description("summarize aggregated scan telemetry").option("--format <pretty|json>", "output format", "pretty").option("--export <path>", "write summary as JSON to <path>").action(async (cmdOptions, command) => {
|
|
65970
67002
|
const options = command.optsWithGlobals();
|
|
65971
|
-
const cwd = (0,
|
|
67003
|
+
const cwd = (0, import_node_path74.resolve)(options.workspace ?? process.cwd());
|
|
65972
67004
|
const payloads = readTelemetry(cwd);
|
|
65973
67005
|
if (payloads.length === 0) {
|
|
65974
67006
|
logger.info("No flywheel telemetry found. Run a scan first.");
|
|
@@ -65976,9 +67008,9 @@ function registerFlywheel(program) {
|
|
|
65976
67008
|
}
|
|
65977
67009
|
const summary = summarizeTelemetry(payloads);
|
|
65978
67010
|
if (cmdOptions.export) {
|
|
65979
|
-
const exportPath = (0,
|
|
65980
|
-
(0,
|
|
65981
|
-
(0,
|
|
67011
|
+
const exportPath = (0, import_node_path74.resolve)(cmdOptions.export);
|
|
67012
|
+
(0, import_node_fs47.mkdirSync)((0, import_node_path74.dirname)(exportPath), { recursive: true });
|
|
67013
|
+
(0, import_node_fs47.writeFileSync)(exportPath, JSON.stringify(summary, null, 2), "utf-8");
|
|
65982
67014
|
logger.info(`Wrote flywheel summary to ${exportPath}`);
|
|
65983
67015
|
process.exit(0);
|
|
65984
67016
|
}
|
|
@@ -66005,8 +67037,8 @@ init_dist2();
|
|
|
66005
67037
|
init_memory_io();
|
|
66006
67038
|
|
|
66007
67039
|
// src/fix/focus-ring.ts
|
|
66008
|
-
var
|
|
66009
|
-
var
|
|
67040
|
+
var import_node_fs48 = require("fs");
|
|
67041
|
+
var import_node_fs49 = require("fs");
|
|
66010
67042
|
var ANCHOR_START = "/* @slopbrick:v1.0.0:fix:focus-ring */";
|
|
66011
67043
|
var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
|
|
66012
67044
|
:focus-visible {
|
|
@@ -66015,15 +67047,15 @@ var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
|
|
|
66015
67047
|
}
|
|
66016
67048
|
/* @slopbrick:v1.0.0:fix:focus-ring-end */`;
|
|
66017
67049
|
function applyFocusRingFix(targetFile) {
|
|
66018
|
-
if (!(0,
|
|
67050
|
+
if (!(0, import_node_fs48.existsSync)(targetFile)) {
|
|
66019
67051
|
return { applied: false, reason: "missing-global-css-target" };
|
|
66020
67052
|
}
|
|
66021
|
-
const content = (0,
|
|
67053
|
+
const content = (0, import_node_fs49.readFileSync)(targetFile, "utf-8");
|
|
66022
67054
|
if (content.includes(ANCHOR_START)) {
|
|
66023
67055
|
return { applied: false, reason: "already-present" };
|
|
66024
67056
|
}
|
|
66025
67057
|
const separator = content.endsWith("\n") ? "" : "\n";
|
|
66026
|
-
(0,
|
|
67058
|
+
(0, import_node_fs49.writeFileSync)(targetFile, `${content}${separator}${CSS_BLOCK}
|
|
66027
67059
|
`);
|
|
66028
67060
|
return { applied: true };
|
|
66029
67061
|
}
|
|
@@ -66032,21 +67064,21 @@ function applyFocusRingFix(targetFile) {
|
|
|
66032
67064
|
init_layout_token();
|
|
66033
67065
|
|
|
66034
67066
|
// src/fix/use-client.ts
|
|
66035
|
-
var
|
|
67067
|
+
var import_node_fs50 = require("fs");
|
|
66036
67068
|
function applyUseClientFix(filePath) {
|
|
66037
|
-
const content = (0,
|
|
67069
|
+
const content = (0, import_node_fs50.readFileSync)(filePath, "utf-8");
|
|
66038
67070
|
const trimmed = content.trimStart();
|
|
66039
67071
|
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) {
|
|
66040
67072
|
return { applied: false, reason: "already-present" };
|
|
66041
67073
|
}
|
|
66042
|
-
(0,
|
|
67074
|
+
(0, import_node_fs50.writeFileSync)(filePath, `"use client";
|
|
66043
67075
|
|
|
66044
67076
|
${content}`);
|
|
66045
67077
|
return { applied: true };
|
|
66046
67078
|
}
|
|
66047
67079
|
|
|
66048
67080
|
// src/fix/visual-codemod.ts
|
|
66049
|
-
var
|
|
67081
|
+
var import_node_fs51 = require("fs");
|
|
66050
67082
|
|
|
66051
67083
|
// src/fix/visual-codemods/tailwind.ts
|
|
66052
67084
|
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;
|
|
@@ -66255,7 +67287,7 @@ var ALL_CODEMODS = [
|
|
|
66255
67287
|
{ name: "aria-attr-typo", fn: applyAriaAttrTypoCodemod }
|
|
66256
67288
|
];
|
|
66257
67289
|
function applyVisualCodemods(filePath) {
|
|
66258
|
-
const original = (0,
|
|
67290
|
+
const original = (0, import_node_fs51.readFileSync)(filePath, "utf-8");
|
|
66259
67291
|
let content = original;
|
|
66260
67292
|
const reasons = [];
|
|
66261
67293
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -66271,7 +67303,7 @@ function applyVisualCodemods(filePath) {
|
|
|
66271
67303
|
}
|
|
66272
67304
|
}
|
|
66273
67305
|
if (content !== original) {
|
|
66274
|
-
(0,
|
|
67306
|
+
(0, import_node_fs51.writeFileSync)(filePath, content);
|
|
66275
67307
|
}
|
|
66276
67308
|
return {
|
|
66277
67309
|
filePath,
|
|
@@ -66286,7 +67318,7 @@ function applyVisualCodemods(filePath) {
|
|
|
66286
67318
|
function collectAllFixes2(issue) {
|
|
66287
67319
|
return [...issue.fix ? [issue.fix] : [], ...issue.fixes ?? []];
|
|
66288
67320
|
}
|
|
66289
|
-
async function applyFixes(report,
|
|
67321
|
+
async function applyFixes(report, _config, scannedFiles) {
|
|
66290
67322
|
const byFile = /* @__PURE__ */ new Map();
|
|
66291
67323
|
for (const issue of report.issues) {
|
|
66292
67324
|
const fixes = collectAllFixes2(issue);
|
|
@@ -66347,7 +67379,6 @@ async function applyFixes(report, config, scannedFiles) {
|
|
|
66347
67379
|
const result = applyLayoutTokenFix(filePath, group.replaces);
|
|
66348
67380
|
const total = group.replaceApps.length;
|
|
66349
67381
|
const appliedCount = Math.min(result.applied, total);
|
|
66350
|
-
const skippedCount = total - appliedCount;
|
|
66351
67382
|
applied.push(...group.replaceApps.slice(0, appliedCount));
|
|
66352
67383
|
skipped.push(...group.replaceApps.slice(appliedCount));
|
|
66353
67384
|
} catch (err) {
|
|
@@ -66542,6 +67573,40 @@ function formatGroupedHelp(cmd) {
|
|
|
66542
67573
|
}
|
|
66543
67574
|
}
|
|
66544
67575
|
lines.push("");
|
|
67576
|
+
lines.push(
|
|
67577
|
+
"Commands (run `<command> --help` for per-command details):\n" + [
|
|
67578
|
+
" scan score a workspace, with severity gating and report views",
|
|
67579
|
+
" pr score a PR (or a unified-diff file via `--diff <file>`)",
|
|
67580
|
+
" drift detect constitution / forbidden-package violations",
|
|
67581
|
+
" test test quality sub-score (CI gate)",
|
|
67582
|
+
" docs documentation freshness sub-score",
|
|
67583
|
+
" security AI security-risk sub-score",
|
|
67584
|
+
" rules list all built-in rules with categories and severities",
|
|
67585
|
+
" explain show rationale, pattern, and remediation for a single rule",
|
|
67586
|
+
" suggest print per-issue remediation advice",
|
|
67587
|
+
" badge print a shields.io slop-index badge",
|
|
67588
|
+
" doctor check setup, config, and environment for common problems",
|
|
67589
|
+
" init create a slopbrick config file and AGENTS.md/CLAUDE.md snippets",
|
|
67590
|
+
" flywheel summarize aggregated scan telemetry",
|
|
67591
|
+
" composite empirical-composite rule discover / enable (Sprint 3 \xA73b)",
|
|
67592
|
+
" calibrate empirical precision/recall/F1 calibration from held-out corpora",
|
|
67593
|
+
" patterns pattern-fragmentation sub-score",
|
|
67594
|
+
" business-logic business-logic coherence sub-score",
|
|
67595
|
+
" maintenance-cost AI maintenance-cost sub-score",
|
|
67596
|
+
" architecture architecture-consistency sub-score",
|
|
67597
|
+
" memory show patterns detected for `slop_suggest`",
|
|
67598
|
+
" lock install a git pre-commit hook (idempotent)",
|
|
67599
|
+
" migrate v0.10.x slop-audit/ \u2192 v0.11.0+ .slopbrick/ migration",
|
|
67600
|
+
" validate-config validate slopbrick.config.mjs",
|
|
67601
|
+
" tokens merge a design-tokens.json layout values into the allowlist",
|
|
67602
|
+
" report re-render a saved JSON report (from --json <path>)",
|
|
67603
|
+
" install / uninstall manage the git pre-commit hook",
|
|
67604
|
+
" mcp run the MCP server (JSON-RPC 2.0 over stdio)",
|
|
67605
|
+
" watch / --watch re-run scan on file change",
|
|
67606
|
+
" trend sparkline of the last n runs"
|
|
67607
|
+
].join("\n")
|
|
67608
|
+
);
|
|
67609
|
+
lines.push("");
|
|
66545
67610
|
lines.push(
|
|
66546
67611
|
"Use `--help-flat` for the standard un-grouped list. See `https://usebrick.dev/docs/scan-options` for full docs."
|
|
66547
67612
|
);
|
|
@@ -66553,12 +67618,12 @@ var http = __toESM(require("http"), 1);
|
|
|
66553
67618
|
var https = __toESM(require("https"), 1);
|
|
66554
67619
|
var import_node_url2 = require("url");
|
|
66555
67620
|
var sendBeacon = (url, payload, timeoutMs) => {
|
|
66556
|
-
return new Promise((
|
|
67621
|
+
return new Promise((resolve43) => {
|
|
66557
67622
|
let parsed;
|
|
66558
67623
|
try {
|
|
66559
67624
|
parsed = new import_node_url2.URL(url);
|
|
66560
67625
|
} catch {
|
|
66561
|
-
|
|
67626
|
+
resolve43();
|
|
66562
67627
|
return;
|
|
66563
67628
|
}
|
|
66564
67629
|
const isHttps = parsed.protocol === "https:";
|
|
@@ -66579,7 +67644,7 @@ var sendBeacon = (url, payload, timeoutMs) => {
|
|
|
66579
67644
|
const settle = () => {
|
|
66580
67645
|
if (settled) return;
|
|
66581
67646
|
settled = true;
|
|
66582
|
-
|
|
67647
|
+
resolve43();
|
|
66583
67648
|
};
|
|
66584
67649
|
let req;
|
|
66585
67650
|
try {
|
|
@@ -66659,7 +67724,7 @@ process.on("uncaughtException", (err) => {
|
|
|
66659
67724
|
});
|
|
66660
67725
|
async function runCli({ start }) {
|
|
66661
67726
|
try {
|
|
66662
|
-
const program = new import_commander3.Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if
|
|
67727
|
+
const program = new import_commander3.Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if AI Slop Score increased since last run (lower = cleaner since v0.21)").option("--refresh-snippets", "rewrite the managed slopbrick block in AGENTS.md and CLAUDE.md after a scan").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
|
|
66663
67728
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
66664
67729
|
setExitOverride(program);
|
|
66665
67730
|
registerInit(program);
|
|
@@ -66668,6 +67733,7 @@ async function runCli({ start }) {
|
|
|
66668
67733
|
registerBadge(program);
|
|
66669
67734
|
registerSuggest(program);
|
|
66670
67735
|
registerExplain(program);
|
|
67736
|
+
registerComposite(program);
|
|
66671
67737
|
registerCalibration(program);
|
|
66672
67738
|
registerFlywheel(program);
|
|
66673
67739
|
const scanAction = async (paths, _options, command) => {
|
|
@@ -66686,7 +67752,7 @@ async function runCli({ start }) {
|
|
|
66686
67752
|
logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
|
|
66687
67753
|
process.exit(2);
|
|
66688
67754
|
}
|
|
66689
|
-
const cwd = (0,
|
|
67755
|
+
const cwd = (0, import_node_path75.resolve)(options.workspace ?? process.cwd());
|
|
66690
67756
|
if (options.trend !== void 0) {
|
|
66691
67757
|
const runs = await readRuns(cwd, fsMemoryIO);
|
|
66692
67758
|
if (runs.length === 0) {
|
|
@@ -66734,25 +67800,25 @@ async function runCli({ start }) {
|
|
|
66734
67800
|
});
|
|
66735
67801
|
}
|
|
66736
67802
|
if (options.baseline) {
|
|
66737
|
-
const cwd2 = (0,
|
|
67803
|
+
const cwd2 = (0, import_node_path75.resolve)(options.workspace ?? process.cwd());
|
|
66738
67804
|
const configHash = hashConfig(config);
|
|
66739
67805
|
const gitHead = await getGitHead(cwd2) ?? "unknown";
|
|
66740
67806
|
const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
|
|
66741
67807
|
saveBaseline(cwd2, cache);
|
|
66742
|
-
if (!options.quiet) {
|
|
67808
|
+
if (!options.quiet && !machineReadableStdout) {
|
|
66743
67809
|
logger.info(`Saved baseline to ${baselinePath(cwd2)}`);
|
|
66744
67810
|
}
|
|
66745
67811
|
}
|
|
66746
67812
|
if (options.tighten && baseline) {
|
|
66747
67813
|
saveBaseline(cwd, baseline);
|
|
66748
|
-
if (!options.quiet) {
|
|
67814
|
+
if (!options.quiet && !machineReadableStdout) {
|
|
66749
67815
|
logger.info(`Tightened baseline saved (revision ${baseline.baseline_revision}).`);
|
|
66750
67816
|
}
|
|
66751
67817
|
}
|
|
66752
|
-
if (options.doctor && !options.quiet) {
|
|
67818
|
+
if (options.doctor && !options.quiet && !machineReadableStdout) {
|
|
66753
67819
|
logger.info(`Doctor: bootstrap ${totalElapsed - scanElapsed}ms, scan ${scanElapsed}ms`);
|
|
66754
67820
|
}
|
|
66755
|
-
if (report.baseline && !options.quiet) {
|
|
67821
|
+
if (report.baseline && !options.quiet && !machineReadableStdout) {
|
|
66756
67822
|
logger.info(baselineStatusMessage(report.baseline));
|
|
66757
67823
|
}
|
|
66758
67824
|
if (options.fix) {
|
|
@@ -66766,7 +67832,7 @@ async function runCli({ start }) {
|
|
|
66766
67832
|
}
|
|
66767
67833
|
const scannedFiles = results.map((r) => r.filePath);
|
|
66768
67834
|
const fixResults = await applyFixes(report, config, scannedFiles);
|
|
66769
|
-
const {
|
|
67835
|
+
const { hasErrors } = printFixSummary(fixResults, options.quiet ?? false);
|
|
66770
67836
|
if (!options.showFixesDiff && !options.quiet) {
|
|
66771
67837
|
logger.info("Tip: pass --show-fixes-diff to see the unified diff of proposed changes.");
|
|
66772
67838
|
}
|
|
@@ -66806,8 +67872,18 @@ async function runCli({ start }) {
|
|
|
66806
67872
|
if (options.staged && stagedGatingResult.reason) {
|
|
66807
67873
|
logger.error(`Gating failure: ${stagedGatingResult.reason}`);
|
|
66808
67874
|
} else {
|
|
66809
|
-
const
|
|
66810
|
-
|
|
67875
|
+
const failedNames = failedThresholds(report, config);
|
|
67876
|
+
const detail = failedNames.map((name) => {
|
|
67877
|
+
if (name === "meanSlop") return `meanSlop (score ${report.aiSlopScore} > ${config.thresholds.meanSlop})`;
|
|
67878
|
+
if (name === "p90Slop") return `p90Slop (score ${report.p90Score} > ${config.thresholds.p90Slop})`;
|
|
67879
|
+
if (name === "individualSlopThreshold") return `individualSlopThreshold (peak ${report.peakScore} > ${config.thresholds.individualSlopThreshold})`;
|
|
67880
|
+
const cat = name.replace(/^category:/, "");
|
|
67881
|
+
const limit = config.thresholds.categoryThresholds?.[cat];
|
|
67882
|
+
const score = report.categoryScores[cat];
|
|
67883
|
+
return `${name} (score ${score} > ${limit})`;
|
|
67884
|
+
}).join(", ");
|
|
67885
|
+
const failed = failedNames.length;
|
|
67886
|
+
logger.error(`${failed} threshold${failed === 1 ? "" : "s"} failed: ${detail}`);
|
|
66811
67887
|
}
|
|
66812
67888
|
}
|
|
66813
67889
|
if (!options.quiet && !machineReadableStdout) {
|
|
@@ -66905,6 +67981,7 @@ init_render();
|
|
|
66905
67981
|
directoryScore,
|
|
66906
67982
|
extractSignatures,
|
|
66907
67983
|
failedThresholdCount,
|
|
67984
|
+
failedThresholds,
|
|
66908
67985
|
filterByDisabledDirectives,
|
|
66909
67986
|
filterIssues,
|
|
66910
67987
|
findSimilarFunctions,
|