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.js
CHANGED
|
@@ -19,7 +19,7 @@ var VERSION;
|
|
|
19
19
|
var init_header = __esm({
|
|
20
20
|
"src/types/_header.ts"() {
|
|
21
21
|
"use strict";
|
|
22
|
-
VERSION = "0.
|
|
22
|
+
VERSION = "0.43.0";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -321,8 +321,16 @@ var init_defaults = __esm({
|
|
|
321
321
|
// rule definitions are meta-code
|
|
322
322
|
"**/snippet/**",
|
|
323
323
|
// RULE_HINTS examples are intentional bad code
|
|
324
|
-
"**/tests/**"
|
|
324
|
+
"**/tests/**",
|
|
325
325
|
// all test files (fixtures + unit + integration)
|
|
326
|
+
// v0.42.0 (post-self-scan cleanup): language visitors are
|
|
327
|
+
// intentional clones — see the §3 v0.42.0 release notes for
|
|
328
|
+
// the design rationale. `dup/identical-block` fires 100+
|
|
329
|
+
// times during self-scan because each visitor shares the
|
|
330
|
+
// same service-suffix arrays and import-walking preamble.
|
|
331
|
+
// Suppress these from the self-scan noise floor; they
|
|
332
|
+
// remain on for user codebases.
|
|
333
|
+
"**/src/engine/visitors/**"
|
|
326
334
|
]
|
|
327
335
|
}
|
|
328
336
|
};
|
|
@@ -25984,7 +25992,7 @@ var init_comment_ratio = __esm({
|
|
|
25984
25992
|
message: `Comment-line ratio is ${direction} (${ratio.toFixed(2)} vs corpus mean ${mean3}). Reductive LLMs (GPT-3.5, Claude 3 Haiku) skip comments; expansive LLMs (Claude Haiku 4.5, GPT-OSS) over-comment when prompted for "well-commented code".`,
|
|
25985
25993
|
line: 1,
|
|
25986
25994
|
column: 1,
|
|
25987
|
-
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "
|
|
25995
|
+
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "High comment ratio \u2014 consider whether the comments are intentional (api docs, type defs, tutorials). For functional code, trim comments that just restate the next line. Keep comments that explain WHY, not WHAT."
|
|
25988
25996
|
}
|
|
25989
25997
|
];
|
|
25990
25998
|
return issues;
|
|
@@ -26834,7 +26842,6 @@ function buildTrigramLM(samples, alpha = 1) {
|
|
|
26834
26842
|
logProb(context, t) {
|
|
26835
26843
|
const ctxKey = context.slice(-2).join("|");
|
|
26836
26844
|
const triKey = `${ctxKey}|${t}`;
|
|
26837
|
-
const biKey = context.slice(-1).join("|") + "|" + t;
|
|
26838
26845
|
const triCount = tri.get(triKey) ?? 0;
|
|
26839
26846
|
const biCount = bi.get(ctxKey) ?? 0;
|
|
26840
26847
|
const V = uni.size;
|
|
@@ -28842,7 +28849,7 @@ var init_stale_function_reference = __esm({
|
|
|
28842
28849
|
]);
|
|
28843
28850
|
SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
28844
28851
|
SOURCE_ROOTS = ["src", "lib", "app", "components"];
|
|
28845
|
-
CAP =
|
|
28852
|
+
CAP = 4e3;
|
|
28846
28853
|
staleFunctionReferenceRule = createRule({
|
|
28847
28854
|
id: "docs/stale-function-reference",
|
|
28848
28855
|
category: "docs",
|
|
@@ -28858,6 +28865,7 @@ var init_stale_function_reference = __esm({
|
|
|
28858
28865
|
if (!source) return issues;
|
|
28859
28866
|
for (const span of extractInlineCodeSpans(source)) {
|
|
28860
28867
|
const text = span.text;
|
|
28868
|
+
if (span.inBlockComment || span.inComment) continue;
|
|
28861
28869
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
|
|
28862
28870
|
if (text.length < 3) continue;
|
|
28863
28871
|
if (RESERVED.has(text.toLowerCase())) continue;
|
|
@@ -29028,6 +29036,7 @@ var init_stale_package_reference = __esm({
|
|
|
29028
29036
|
if (!source) return issues;
|
|
29029
29037
|
const spans = extractInlineCodeSpans(source);
|
|
29030
29038
|
for (const span of spans) {
|
|
29039
|
+
if (span.inComment) continue;
|
|
29031
29040
|
const lineStart = source.lastIndexOf("\n", span.index) + 1;
|
|
29032
29041
|
const lineEnd = source.indexOf("\n", span.index);
|
|
29033
29042
|
const line = source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd);
|
|
@@ -29082,7 +29091,8 @@ __export(doc_freshness_exports, {
|
|
|
29082
29091
|
extractExports: () => extractExports,
|
|
29083
29092
|
extractFencedCodeBlocks: () => extractFencedCodeBlocks,
|
|
29084
29093
|
extractInlineCodeSpans: () => extractInlineCodeSpans,
|
|
29085
|
-
extractMarkdownLinks: () => extractMarkdownLinks
|
|
29094
|
+
extractMarkdownLinks: () => extractMarkdownLinks,
|
|
29095
|
+
findCommentLineRanges: () => findCommentLineRanges
|
|
29086
29096
|
});
|
|
29087
29097
|
import { readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
|
|
29088
29098
|
import { join as join6, dirname as dirname2, relative } from "path";
|
|
@@ -29090,6 +29100,8 @@ import { globby } from "globby";
|
|
|
29090
29100
|
function extractInlineCodeSpans(source) {
|
|
29091
29101
|
const hits = [];
|
|
29092
29102
|
const re = /`([^`\n]+?)`/g;
|
|
29103
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
29104
|
+
const commentLines = findCommentLineRanges(source);
|
|
29093
29105
|
let m;
|
|
29094
29106
|
while ((m = re.exec(source)) !== null) {
|
|
29095
29107
|
const text = m[1] ?? "";
|
|
@@ -29097,7 +29109,14 @@ function extractInlineCodeSpans(source) {
|
|
|
29097
29109
|
const line = upTo.split("\n").length;
|
|
29098
29110
|
const lastNl = upTo.lastIndexOf("\n");
|
|
29099
29111
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
29100
|
-
hits.push({
|
|
29112
|
+
hits.push({
|
|
29113
|
+
text,
|
|
29114
|
+
line,
|
|
29115
|
+
column,
|
|
29116
|
+
index: m.index,
|
|
29117
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
29118
|
+
inComment: commentLines.has(line)
|
|
29119
|
+
});
|
|
29101
29120
|
}
|
|
29102
29121
|
return hits;
|
|
29103
29122
|
}
|
|
@@ -29136,6 +29155,8 @@ function extractFencedCodeBlocks(source) {
|
|
|
29136
29155
|
function extractMarkdownLinks(source) {
|
|
29137
29156
|
const hits = [];
|
|
29138
29157
|
const re = /(?<!\!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
29158
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
29159
|
+
const commentLines = findCommentLineRanges(source);
|
|
29139
29160
|
let m;
|
|
29140
29161
|
while ((m = re.exec(source)) !== null) {
|
|
29141
29162
|
const target = m[2] ?? "";
|
|
@@ -29143,10 +29164,55 @@ function extractMarkdownLinks(source) {
|
|
|
29143
29164
|
const line = upTo.split("\n").length;
|
|
29144
29165
|
const lastNl = upTo.lastIndexOf("\n");
|
|
29145
29166
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
29146
|
-
hits.push({
|
|
29167
|
+
hits.push({
|
|
29168
|
+
target,
|
|
29169
|
+
line,
|
|
29170
|
+
column,
|
|
29171
|
+
index: m.index,
|
|
29172
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
29173
|
+
inComment: commentLines.has(line)
|
|
29174
|
+
});
|
|
29147
29175
|
}
|
|
29148
29176
|
return hits;
|
|
29149
29177
|
}
|
|
29178
|
+
function findBlockCommentRanges(source) {
|
|
29179
|
+
const ranges = [];
|
|
29180
|
+
const stack = [];
|
|
29181
|
+
let i = 0;
|
|
29182
|
+
while (i < source.length) {
|
|
29183
|
+
if (source[i] === "/" && source[i + 1] === "/") {
|
|
29184
|
+
const eol = source.indexOf("\n", i);
|
|
29185
|
+
i = eol === -1 ? source.length : eol + 1;
|
|
29186
|
+
continue;
|
|
29187
|
+
}
|
|
29188
|
+
if (source[i] === "/" && source[i + 1] === "*") {
|
|
29189
|
+
stack.push(i);
|
|
29190
|
+
i += 2;
|
|
29191
|
+
continue;
|
|
29192
|
+
}
|
|
29193
|
+
if (source[i] === "*" && source[i + 1] === "/" && stack.length > 0) {
|
|
29194
|
+
const start = stack.pop();
|
|
29195
|
+
ranges.push([start, i + 2]);
|
|
29196
|
+
i += 2;
|
|
29197
|
+
continue;
|
|
29198
|
+
}
|
|
29199
|
+
i += 1;
|
|
29200
|
+
}
|
|
29201
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
29202
|
+
return ranges;
|
|
29203
|
+
}
|
|
29204
|
+
function isInBlockComment(ranges, pos) {
|
|
29205
|
+
let lo = 0;
|
|
29206
|
+
let hi = ranges.length - 1;
|
|
29207
|
+
while (lo <= hi) {
|
|
29208
|
+
const mid = lo + hi >>> 1;
|
|
29209
|
+
const [start, end] = ranges[mid];
|
|
29210
|
+
if (pos < start) hi = mid - 1;
|
|
29211
|
+
else if (pos >= end) lo = mid + 1;
|
|
29212
|
+
else return true;
|
|
29213
|
+
}
|
|
29214
|
+
return false;
|
|
29215
|
+
}
|
|
29150
29216
|
function declaredPackages(cwd) {
|
|
29151
29217
|
const out = /* @__PURE__ */ new Set();
|
|
29152
29218
|
const pkgPath = join6(cwd, "package.json");
|
|
@@ -29301,6 +29367,58 @@ function docDriftFromFreshness(score) {
|
|
|
29301
29367
|
if (score >= DOC_FRESHNESS_THRESHOLDS.high) return "high";
|
|
29302
29368
|
return "critical";
|
|
29303
29369
|
}
|
|
29370
|
+
function findCommentLineRanges(source) {
|
|
29371
|
+
const out = /* @__PURE__ */ new Set();
|
|
29372
|
+
const stack = [];
|
|
29373
|
+
let inLineComment = false;
|
|
29374
|
+
const lines = source.split("\n");
|
|
29375
|
+
for (let i = 0; i < lines.length; i++) {
|
|
29376
|
+
const line = lines[i];
|
|
29377
|
+
const lineNo = i + 1;
|
|
29378
|
+
let j = 0;
|
|
29379
|
+
while (j < line.length) {
|
|
29380
|
+
const c = line[j];
|
|
29381
|
+
const next = line[j + 1];
|
|
29382
|
+
if (!stack.length && c === "/" && next === "/") {
|
|
29383
|
+
inLineComment = true;
|
|
29384
|
+
out.add(lineNo);
|
|
29385
|
+
break;
|
|
29386
|
+
}
|
|
29387
|
+
if (c === "/" && next === "*") {
|
|
29388
|
+
stack.push(lineNo);
|
|
29389
|
+
out.add(lineNo);
|
|
29390
|
+
j += 2;
|
|
29391
|
+
continue;
|
|
29392
|
+
}
|
|
29393
|
+
if (c === "*" && next === "/" && stack.length > 0) {
|
|
29394
|
+
stack.pop();
|
|
29395
|
+
j += 2;
|
|
29396
|
+
continue;
|
|
29397
|
+
}
|
|
29398
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
29399
|
+
const quote = c;
|
|
29400
|
+
j += 1;
|
|
29401
|
+
while (j < line.length) {
|
|
29402
|
+
if (line[j] === "\\") {
|
|
29403
|
+
j += 2;
|
|
29404
|
+
continue;
|
|
29405
|
+
}
|
|
29406
|
+
if (line[j] === quote) {
|
|
29407
|
+
j += 1;
|
|
29408
|
+
break;
|
|
29409
|
+
}
|
|
29410
|
+
j += 1;
|
|
29411
|
+
}
|
|
29412
|
+
continue;
|
|
29413
|
+
}
|
|
29414
|
+
j += 1;
|
|
29415
|
+
}
|
|
29416
|
+
if (stack.length > 0) {
|
|
29417
|
+
out.add(lineNo);
|
|
29418
|
+
}
|
|
29419
|
+
}
|
|
29420
|
+
return out;
|
|
29421
|
+
}
|
|
29304
29422
|
var DOC_RULE_WEIGHTS, DOC_FRESHNESS_THRESHOLDS;
|
|
29305
29423
|
var init_doc_freshness = __esm({
|
|
29306
29424
|
"src/engine/doc-freshness.ts"() {
|
|
@@ -29324,6 +29442,9 @@ var init_doc_freshness = __esm({
|
|
|
29324
29442
|
// src/rules/docs/broken-link.ts
|
|
29325
29443
|
import { existsSync as existsSync6 } from "fs";
|
|
29326
29444
|
import { dirname as dirname3, join as join7, resolve as resolve4 } from "path";
|
|
29445
|
+
function looksLikeRegexSyntax(target) {
|
|
29446
|
+
return /\[[^\]]*\]/.test(target) || /^\s*[?:=!]/.test(target) || /\\(?:s|d|w|S|D|W|t|n|r|[^\\])/.test(target) || /[{}|]/.test(target);
|
|
29447
|
+
}
|
|
29327
29448
|
var brokenLinkRule;
|
|
29328
29449
|
var init_broken_link = __esm({
|
|
29329
29450
|
"src/rules/docs/broken-link.ts"() {
|
|
@@ -29352,6 +29473,8 @@ var init_broken_link = __esm({
|
|
|
29352
29473
|
if (target.startsWith("#")) continue;
|
|
29353
29474
|
if (target.startsWith("//")) continue;
|
|
29354
29475
|
if (target.startsWith("/")) continue;
|
|
29476
|
+
if (link.inBlockComment) continue;
|
|
29477
|
+
if (looksLikeRegexSyntax(target)) continue;
|
|
29355
29478
|
const filePart = target.split("#")[0] ?? target;
|
|
29356
29479
|
if (filePart === "") continue;
|
|
29357
29480
|
const resolved = join7(docDir, filePart);
|
|
@@ -38526,6 +38649,29 @@ var init_stringly_typed = __esm({
|
|
|
38526
38649
|
}
|
|
38527
38650
|
});
|
|
38528
38651
|
|
|
38652
|
+
// src/engine/parser-shared.ts
|
|
38653
|
+
function parseTreeSitterSource(parserResult, source) {
|
|
38654
|
+
if (!parserResult.ok) return null;
|
|
38655
|
+
if (!source || source.trim() === "") return null;
|
|
38656
|
+
try {
|
|
38657
|
+
const rawTree = parserResult.parser.parse(source);
|
|
38658
|
+
if (!rawTree) return null;
|
|
38659
|
+
const tree = rawTree;
|
|
38660
|
+
if (!tree || !tree.rootNode) return null;
|
|
38661
|
+
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
38662
|
+
return null;
|
|
38663
|
+
}
|
|
38664
|
+
return tree;
|
|
38665
|
+
} catch {
|
|
38666
|
+
return null;
|
|
38667
|
+
}
|
|
38668
|
+
}
|
|
38669
|
+
var init_parser_shared = __esm({
|
|
38670
|
+
"src/engine/parser-shared.ts"() {
|
|
38671
|
+
"use strict";
|
|
38672
|
+
}
|
|
38673
|
+
});
|
|
38674
|
+
|
|
38529
38675
|
// src/engine/parser-rust.ts
|
|
38530
38676
|
import Parser from "tree-sitter";
|
|
38531
38677
|
import Rust from "tree-sitter-rust";
|
|
@@ -38551,21 +38697,7 @@ function effectiveParser() {
|
|
|
38551
38697
|
return getRustParser();
|
|
38552
38698
|
}
|
|
38553
38699
|
function parseRust(source) {
|
|
38554
|
-
|
|
38555
|
-
if (!result.ok) return null;
|
|
38556
|
-
if (!source || source.trim() === "") return null;
|
|
38557
|
-
try {
|
|
38558
|
-
const rawTree = result.parser.parse(source);
|
|
38559
|
-
if (!rawTree) return null;
|
|
38560
|
-
const tree = rawTree;
|
|
38561
|
-
if (!tree || !tree.rootNode) return null;
|
|
38562
|
-
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
38563
|
-
return null;
|
|
38564
|
-
}
|
|
38565
|
-
return tree;
|
|
38566
|
-
} catch {
|
|
38567
|
-
return null;
|
|
38568
|
-
}
|
|
38700
|
+
return parseTreeSitterSource(effectiveParser(), source);
|
|
38569
38701
|
}
|
|
38570
38702
|
function isRustParserAvailable() {
|
|
38571
38703
|
const result = getRustParser();
|
|
@@ -38575,6 +38707,7 @@ var cachedParser, rustLanguage, loadError, forcedFailure;
|
|
|
38575
38707
|
var init_parser_rust = __esm({
|
|
38576
38708
|
"src/engine/parser-rust.ts"() {
|
|
38577
38709
|
"use strict";
|
|
38710
|
+
init_parser_shared();
|
|
38578
38711
|
cachedParser = null;
|
|
38579
38712
|
rustLanguage = null;
|
|
38580
38713
|
loadError = null;
|
|
@@ -43682,8 +43815,6 @@ function mergeTemplateClassNames(filePath, source, facts, templateClassNames) {
|
|
|
43682
43815
|
}
|
|
43683
43816
|
function extractFacts(filePath, ast, source, supportsRsc = true, framework = "react", config) {
|
|
43684
43817
|
const lineOffsets = buildLineOffsets2(source);
|
|
43685
|
-
const isTypeScript = /\.tsx?$/i.test(filePath);
|
|
43686
|
-
const NON_JSX_FRAMEWORKS = /* @__PURE__ */ new Set(["vue", "svelte", "astro"]);
|
|
43687
43818
|
const facts = {
|
|
43688
43819
|
filePath,
|
|
43689
43820
|
components: [],
|
|
@@ -45622,7 +45753,7 @@ function formatDeltaSuffix(report) {
|
|
|
45622
45753
|
}
|
|
45623
45754
|
function formatVerdict(report) {
|
|
45624
45755
|
const score = report.aiSlopScore;
|
|
45625
|
-
const band =
|
|
45756
|
+
const band = slopScoreBand(score);
|
|
45626
45757
|
const ranked = rankByImpact(report.issues);
|
|
45627
45758
|
const topRule = ranked[0];
|
|
45628
45759
|
const topFile = report.topOffenders?.[0];
|
|
@@ -45705,7 +45836,8 @@ function formatNextStep(report) {
|
|
|
45705
45836
|
` \u2192 \`slopbrick scan --baseline\` to accept today's scores as the new floor`
|
|
45706
45837
|
)
|
|
45707
45838
|
);
|
|
45708
|
-
|
|
45839
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
45840
|
+
if (report.aiSlopScore > meanSlop) {
|
|
45709
45841
|
lines.push(
|
|
45710
45842
|
chalk.dim(
|
|
45711
45843
|
` \u2192 \`slopbrick scan --why-failing\` for the top 5 issues dragging the score down`
|
|
@@ -45730,17 +45862,18 @@ function formatWhyFailing(report) {
|
|
|
45730
45862
|
return chalk.green("Nothing is failing the threshold. Score is clean.");
|
|
45731
45863
|
}
|
|
45732
45864
|
const headline = report.aiSlopScore;
|
|
45733
|
-
const
|
|
45734
|
-
const
|
|
45865
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
45866
|
+
const status = headline <= meanSlop ? "PASS" : "FAIL";
|
|
45867
|
+
const colorize = headline <= meanSlop ? chalk.green : chalk.red;
|
|
45868
|
+
const qualifier = headline <= meanSlop ? `below ${meanSlop}` : `above ${meanSlop}`;
|
|
45735
45869
|
const lines = [];
|
|
45736
|
-
lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014
|
|
45870
|
+
lines.push(colorize.bold(`Headline score: ${headline.toFixed(0)}/100 (${status} \u2014 ${qualifier})`));
|
|
45737
45871
|
lines.push("");
|
|
45738
45872
|
lines.push(chalk.bold("Top 5 rules dragging the score down:"));
|
|
45739
45873
|
for (let i = 0; i < ranked.length; i++) {
|
|
45740
45874
|
const [ruleId, info] = ranked[i];
|
|
45741
45875
|
const fileCount = info.files.size;
|
|
45742
45876
|
const fileHint = fileCount > 0 ? ` across ${fileCount} file${fileCount === 1 ? "" : "s"}` : "";
|
|
45743
|
-
const catShort = CATEGORY_GLOSSARY[info.count > 0 ? "" : ""]?.short;
|
|
45744
45877
|
const sampleIssue = report.issues.find((i2) => i2.ruleId === ruleId);
|
|
45745
45878
|
const cat = sampleIssue ? CATEGORY_GLOSSARY[sampleIssue.category]?.short : null;
|
|
45746
45879
|
const catHint = cat ? chalk.dim(` [${cat}]`) : "";
|
|
@@ -45800,7 +45933,7 @@ function formatCompositeScore(report) {
|
|
|
45800
45933
|
lines.push("");
|
|
45801
45934
|
lines.push(
|
|
45802
45935
|
chalk.dim(
|
|
45803
|
-
`Repository Coherence: ${coherenceValue} / 100 [${cohBand.label.toUpperCase()}] \u2014 higher = better \xB7 measures internal consistency. This is a secondary view; the Slop
|
|
45936
|
+
`Repository Coherence: ${coherenceValue} / 100 [${cohBand.label.toUpperCase()}] \u2014 higher = better \xB7 measures internal consistency. This is a secondary view; the AI Slop Score above is the gate.`
|
|
45804
45937
|
)
|
|
45805
45938
|
);
|
|
45806
45939
|
}
|
|
@@ -45930,13 +46063,12 @@ function formatDriftSection(report) {
|
|
|
45930
46063
|
return lines.join("\n");
|
|
45931
46064
|
}
|
|
45932
46065
|
function formatThresholds(report) {
|
|
45933
|
-
const
|
|
46066
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
45934
46067
|
const headline = report.aiSlopScore;
|
|
45935
|
-
const passed = headline
|
|
45936
|
-
const valueText = `${headline.toFixed(1)} \
|
|
46068
|
+
const passed = headline <= meanSlop;
|
|
46069
|
+
const valueText = `${headline.toFixed(1)} \u2264 ${meanSlop}`.padStart(12, " ");
|
|
45937
46070
|
const status = passed ? chalk.green("pass") : chalk.red("fail");
|
|
45938
46071
|
const result = ["Threshold (CI gate)", ` AI Slop Score ${valueText} ${status}`];
|
|
45939
|
-
void limit;
|
|
45940
46072
|
return result;
|
|
45941
46073
|
}
|
|
45942
46074
|
function formatTopComponents(components, topOffenders) {
|
|
@@ -46014,9 +46146,10 @@ function formatPretty(report) {
|
|
|
46014
46146
|
}
|
|
46015
46147
|
return sections.join("\n\n");
|
|
46016
46148
|
}
|
|
46017
|
-
function formatScoringExplainer(
|
|
46149
|
+
function formatScoringExplainer(report) {
|
|
46150
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
46018
46151
|
return chalk.dim(
|
|
46019
|
-
|
|
46152
|
+
`Four orthogonal scores (all 0-100, **lower AI Slop Score = cleaner**): AI Slop Score (AI-slop signatures; the CI gate, AI Slop Score \u2264 ${meanSlop} passes), Engineering Hygiene (issues per category across arch/logic/layout/component/test; higher = cleaner), Security (AI-flagged security risks, inverted from risk level; higher = cleaner), Repository Health (composite: 0.4*AI Slop Score + 0.3*Engineering Hygiene + 0.2*Security + 0.1*Test Quality). Only AI Slop Score gates CI; the others are informational. Default-off rules (INVERTED/NOISY/DORMANT) are suppressed from the scores automatically.`
|
|
46020
46153
|
);
|
|
46021
46154
|
}
|
|
46022
46155
|
function formatWhyFailingReport(report) {
|
|
@@ -46034,7 +46167,7 @@ function formatBriefReport(report) {
|
|
|
46034
46167
|
];
|
|
46035
46168
|
const deltaSuffix = formatDeltaSuffix(report);
|
|
46036
46169
|
scoreLines.forEach(({ label, field, value }, idx) => {
|
|
46037
|
-
const band = scoreBand(value);
|
|
46170
|
+
const band = field === "aiSlopScore" ? slopScoreBand(value) : scoreBand(value);
|
|
46038
46171
|
const paddedLabel = label.padEnd(20, " ");
|
|
46039
46172
|
const valueStr = value.toFixed(0).padStart(3, " ");
|
|
46040
46173
|
const delta = idx === 0 ? deltaSuffix : "";
|
|
@@ -46042,11 +46175,12 @@ function formatBriefReport(report) {
|
|
|
46042
46175
|
` ${paddedLabel} ${band.color(valueStr)} ${chalk.dim(band.label)} ${chalk.dim.italic(`(${field})`)}${delta}`
|
|
46043
46176
|
);
|
|
46044
46177
|
});
|
|
46045
|
-
const
|
|
46178
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
46179
|
+
const passed = report.aiSlopScore <= meanSlop;
|
|
46046
46180
|
lines.push("");
|
|
46047
46181
|
lines.push(
|
|
46048
46182
|
chalk.dim(
|
|
46049
|
-
` CI gate: AI Slop Score <=
|
|
46183
|
+
` CI gate: AI Slop Score <= ${meanSlop} -> ${passed ? chalk.green("pass") : chalk.red("fail")}`
|
|
46050
46184
|
)
|
|
46051
46185
|
);
|
|
46052
46186
|
const suppressed = report.defaultOffSuppressedCount ?? 0;
|
|
@@ -46192,19 +46326,22 @@ function thresholdExceeded(report, config) {
|
|
|
46192
46326
|
return categoryThresholdBreached(report, config.thresholds.categoryThresholds);
|
|
46193
46327
|
}
|
|
46194
46328
|
function failedThresholdCount(report, config) {
|
|
46195
|
-
|
|
46196
|
-
|
|
46197
|
-
|
|
46198
|
-
|
|
46329
|
+
return failedThresholds(report, config).length;
|
|
46330
|
+
}
|
|
46331
|
+
function failedThresholds(report, config) {
|
|
46332
|
+
const failed = [];
|
|
46333
|
+
if (report.aiSlopScore > config.thresholds.meanSlop) failed.push("meanSlop");
|
|
46334
|
+
if (report.p90Score > config.thresholds.p90Slop) failed.push("p90Slop");
|
|
46335
|
+
if (report.peakScore > config.thresholds.individualSlopThreshold) failed.push("individualSlopThreshold");
|
|
46199
46336
|
const cat = config.thresholds.categoryThresholds;
|
|
46200
46337
|
if (cat) {
|
|
46201
46338
|
for (const [category, limit] of Object.entries(cat)) {
|
|
46202
46339
|
if (limit === void 0) continue;
|
|
46203
46340
|
const score = report.categoryScores[category];
|
|
46204
|
-
if (score !== void 0 && score > limit)
|
|
46341
|
+
if (score !== void 0 && score > limit) failed.push(`category:${category}`);
|
|
46205
46342
|
}
|
|
46206
46343
|
}
|
|
46207
|
-
return
|
|
46344
|
+
return failed;
|
|
46208
46345
|
}
|
|
46209
46346
|
function categoryThresholdBreached(report, categoryThresholds) {
|
|
46210
46347
|
if (!categoryThresholds) return false;
|
|
@@ -46862,11 +46999,11 @@ var init_pool = __esm({
|
|
|
46862
46999
|
for (let i = 0; i < this.threadCount; i++) {
|
|
46863
47000
|
spawnWorker();
|
|
46864
47001
|
}
|
|
46865
|
-
return new Promise((
|
|
47002
|
+
return new Promise((resolve43) => {
|
|
46866
47003
|
const check = setInterval(() => {
|
|
46867
47004
|
if (resolved) {
|
|
46868
47005
|
clearInterval(check);
|
|
46869
|
-
|
|
47006
|
+
resolve43(results);
|
|
46870
47007
|
}
|
|
46871
47008
|
}, 10);
|
|
46872
47009
|
});
|
|
@@ -46883,23 +47020,16 @@ var init_registry = __esm({
|
|
|
46883
47020
|
init_builtins();
|
|
46884
47021
|
RuleRegistry = class {
|
|
46885
47022
|
rules = /* @__PURE__ */ new Map();
|
|
46886
|
-
|
|
46887
|
-
|
|
46888
|
-
|
|
46889
|
-
|
|
46890
|
-
|
|
46891
|
-
|
|
46892
|
-
|
|
46893
|
-
|
|
46894
|
-
|
|
46895
|
-
|
|
46896
|
-
create: factory.create,
|
|
46897
|
-
analyze: factory.analyze
|
|
46898
|
-
};
|
|
46899
|
-
this.rules.set(ruleOrId, rule);
|
|
46900
|
-
} else {
|
|
46901
|
-
this.rules.set(ruleOrId.id, ruleOrId);
|
|
46902
|
-
}
|
|
47023
|
+
/**
|
|
47024
|
+
* Register a rule. The 2-arg overload `register(id, factory)` was
|
|
47025
|
+
* deleted in v0.42.0 (architecture review F6): no callers used it,
|
|
47026
|
+
* every `Rule` carries its own `id`, and the per-id registration
|
|
47027
|
+
* was ergonomically redundant with the 1-arg form (`register(rule)`).
|
|
47028
|
+
* External consumers that needed `register-by-id` semantics can
|
|
47029
|
+
* construct a `Rule` directly and pass it in.
|
|
47030
|
+
*/
|
|
47031
|
+
register(rule) {
|
|
47032
|
+
this.rules.set(rule.id, rule);
|
|
46903
47033
|
}
|
|
46904
47034
|
loadBuiltins(onlyRuleId) {
|
|
46905
47035
|
for (const rule of builtinRules) {
|
|
@@ -46928,6 +47058,15 @@ var init_registry = __esm({
|
|
|
46928
47058
|
all() {
|
|
46929
47059
|
return Array.from(this.rules.values());
|
|
46930
47060
|
}
|
|
47061
|
+
/** v0.42.0 (§3b.5): look up a registered rule by id.
|
|
47062
|
+
* Returns undefined if not registered. */
|
|
47063
|
+
get(id) {
|
|
47064
|
+
return this.rules.get(id);
|
|
47065
|
+
}
|
|
47066
|
+
/** v0.42.0 (§3b.5): check whether a rule id is registered. */
|
|
47067
|
+
has(id) {
|
|
47068
|
+
return this.rules.has(id);
|
|
47069
|
+
}
|
|
46931
47070
|
createContexts(config, filePath, cwd, hotspotIssues = []) {
|
|
46932
47071
|
const context = {
|
|
46933
47072
|
config,
|
|
@@ -50579,6 +50718,10 @@ var init_signal_strength2 = __esm({
|
|
|
50579
50718
|
});
|
|
50580
50719
|
|
|
50581
50720
|
// src/engine/worker.ts
|
|
50721
|
+
var worker_exports = {};
|
|
50722
|
+
__export(worker_exports, {
|
|
50723
|
+
scanFile: () => scanFile
|
|
50724
|
+
});
|
|
50582
50725
|
import { isMainThread, parentPort, workerData } from "worker_threads";
|
|
50583
50726
|
import { extname as extname5 } from "path";
|
|
50584
50727
|
import { join as join14, relative as relative5, sep as sep2 } from "path";
|
|
@@ -50659,6 +50802,36 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
|
50659
50802
|
const issues = applyRuleOverrides(rawIssues, config.rules);
|
|
50660
50803
|
const triggeredRuleIds = Array.from(new Set(issues.map((i) => i.ruleId)));
|
|
50661
50804
|
const compScore = compositeScore(triggeredRuleIds, loadSignalStrength());
|
|
50805
|
+
const fireSet = new Set(triggeredRuleIds);
|
|
50806
|
+
const factsWithFireSet = Object.assign(facts, {
|
|
50807
|
+
compositeFireSet: fireSet
|
|
50808
|
+
});
|
|
50809
|
+
for (const { rule } of rules) {
|
|
50810
|
+
if (!rule.id.startsWith("composite/")) continue;
|
|
50811
|
+
try {
|
|
50812
|
+
const compositeIssues = rule.analyze(
|
|
50813
|
+
rule.create({
|
|
50814
|
+
config,
|
|
50815
|
+
filePath,
|
|
50816
|
+
cwd,
|
|
50817
|
+
framework: config.framework,
|
|
50818
|
+
uiLibraries: config.uiLibraries,
|
|
50819
|
+
hasTailwind: config.hasTailwind,
|
|
50820
|
+
supportsRsc: config.supportsRsc,
|
|
50821
|
+
hotspotIssues: []
|
|
50822
|
+
}),
|
|
50823
|
+
factsWithFireSet
|
|
50824
|
+
);
|
|
50825
|
+
if (compositeIssues.length > 0) {
|
|
50826
|
+
issues.push(...compositeIssues);
|
|
50827
|
+
for (const ci of compositeIssues) fireSet.add(ci.ruleId);
|
|
50828
|
+
}
|
|
50829
|
+
} catch (err) {
|
|
50830
|
+
if (process.env.SLOP_AUDIT_DEBUG === "1") {
|
|
50831
|
+
console.error(`[worker] composite ${rule.id} threw: ${err.message}`);
|
|
50832
|
+
}
|
|
50833
|
+
}
|
|
50834
|
+
}
|
|
50662
50835
|
const gapValues = collectGapValues(facts);
|
|
50663
50836
|
const styleSources = collectStyleSources(facts);
|
|
50664
50837
|
const elementTags = facts.v2.jsx.elements.map((e) => e.tag);
|
|
@@ -50834,9 +51007,22 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
|
|
|
50834
51007
|
}
|
|
50835
51008
|
}
|
|
50836
51009
|
if (currentTopFiles.length > 0 && runs.length >= CONSECUTIVE_THRESHOLD) {
|
|
51010
|
+
const latestRun = runs[runs.length - 1];
|
|
51011
|
+
const isNoiseTier = (id) => {
|
|
51012
|
+
const rule = ruleById.get(id);
|
|
51013
|
+
if (!rule) return false;
|
|
51014
|
+
if (rule.defaultOff === true) return true;
|
|
51015
|
+
if (rule.aiSpecific && (rule.severity === "medium" || rule.severity === "low")) return true;
|
|
51016
|
+
return false;
|
|
51017
|
+
};
|
|
51018
|
+
const onRulesInLatestTop = latestRun.topOffenseIds.filter(
|
|
51019
|
+
(id) => !isNoiseTier(id)
|
|
51020
|
+
);
|
|
51021
|
+
const skipChronicOnDefaultOffNoise = onRulesInLatestTop.length === 0;
|
|
50837
51022
|
for (const file of currentTopFiles) {
|
|
50838
51023
|
const consecutive = countConsecutiveTopFileAppearances(file.hash, recentTopHashes);
|
|
50839
51024
|
if (consecutive >= CONSECUTIVE_THRESHOLD) {
|
|
51025
|
+
if (skipChronicOnDefaultOffNoise) continue;
|
|
50840
51026
|
hotspotIssues.push({
|
|
50841
51027
|
ruleId: "component/chronic-offender",
|
|
50842
51028
|
category: "component",
|
|
@@ -50846,7 +51032,7 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
|
|
|
50846
51032
|
filePath: file.filePath,
|
|
50847
51033
|
line: 1,
|
|
50848
51034
|
column: 1,
|
|
50849
|
-
advice: "
|
|
51035
|
+
advice: "Run `slopbrick scan --baseline` to accept current scores as the new floor, or refactor the file to reduce its issue count."
|
|
50850
51036
|
});
|
|
50851
51037
|
}
|
|
50852
51038
|
}
|
|
@@ -51534,14 +51720,14 @@ var init_tokens = __esm({
|
|
|
51534
51720
|
|
|
51535
51721
|
// src/cli/memory-io.ts
|
|
51536
51722
|
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, access as access2 } from "fs/promises";
|
|
51537
|
-
import { dirname as
|
|
51723
|
+
import { dirname as dirname9 } from "path";
|
|
51538
51724
|
var fsMemoryIO;
|
|
51539
51725
|
var init_memory_io = __esm({
|
|
51540
51726
|
"src/cli/memory-io.ts"() {
|
|
51541
51727
|
"use strict";
|
|
51542
51728
|
fsMemoryIO = {
|
|
51543
51729
|
read: (path) => readFile3(path, "utf-8").catch(() => null),
|
|
51544
|
-
write: (path, content) => mkdir2(
|
|
51730
|
+
write: (path, content) => mkdir2(dirname9(path), { recursive: true }).then(
|
|
51545
51731
|
() => writeFile2(path, content, "utf-8")
|
|
51546
51732
|
),
|
|
51547
51733
|
exists: (path) => access2(path).then(
|
|
@@ -53177,16 +53363,16 @@ function axisSlopIndex(slopIndex) {
|
|
|
53177
53363
|
if (slopIndex === void 0 || Number.isNaN(slopIndex)) {
|
|
53178
53364
|
return {
|
|
53179
53365
|
axis: "slopIndex",
|
|
53180
|
-
label: "Slop
|
|
53366
|
+
label: "AI Slop Score (raw)",
|
|
53181
53367
|
health: 70,
|
|
53182
53368
|
source: "default (not measured) \u2014 neutral 70"
|
|
53183
53369
|
};
|
|
53184
53370
|
}
|
|
53185
53371
|
return {
|
|
53186
53372
|
axis: "slopIndex",
|
|
53187
|
-
label: "Slop
|
|
53373
|
+
label: "AI Slop Score (raw)",
|
|
53188
53374
|
health: clamp100(100 - slopIndex),
|
|
53189
|
-
source: `100 -
|
|
53375
|
+
source: `100 - aiSlopScore (${slopIndex.toFixed(1)}) \u2192 inverted to cleanliness`
|
|
53190
53376
|
};
|
|
53191
53377
|
}
|
|
53192
53378
|
function axisArchitecture(architectureConsistency) {
|
|
@@ -54133,6 +54319,16 @@ function assembleScanReport(input) {
|
|
|
54133
54319
|
enrichment
|
|
54134
54320
|
} = input;
|
|
54135
54321
|
const prSlopScore = computePrSlopScore(diffRef, allIssues);
|
|
54322
|
+
const thresholdReport = {
|
|
54323
|
+
aiSlopScore: aggregated.aiSlopScore,
|
|
54324
|
+
p90Score: aggregated.p90Score,
|
|
54325
|
+
peakScore: aggregated.peakScore,
|
|
54326
|
+
categoryScores: aggregated.categoryScores
|
|
54327
|
+
};
|
|
54328
|
+
const failedThresholds_ = failedThresholds(
|
|
54329
|
+
thresholdReport,
|
|
54330
|
+
config
|
|
54331
|
+
);
|
|
54136
54332
|
return {
|
|
54137
54333
|
version: VERSION,
|
|
54138
54334
|
generatedAt,
|
|
@@ -54188,6 +54384,11 @@ function assembleScanReport(input) {
|
|
|
54188
54384
|
parseErrors: parseErrors.length > 0 ? parseErrors : void 0,
|
|
54189
54385
|
baseline: baselineMeta,
|
|
54190
54386
|
thresholds: config.thresholds,
|
|
54387
|
+
// v0.42.0 (user-review fix): expose which gates tripped so CI
|
|
54388
|
+
// consumers can read the JSON output instead of grepping stderr.
|
|
54389
|
+
// Optional field: absent when no thresholds tripped. Computed from
|
|
54390
|
+
// the report's own scores so the JSON is self-describing.
|
|
54391
|
+
failedThresholds: failedThresholds_,
|
|
54191
54392
|
topOffenders: topOffenders.length > 0 ? topOffenders : void 0,
|
|
54192
54393
|
coherence: enrichment.coherence,
|
|
54193
54394
|
coherenceBreakdown: enrichment.coherenceBreakdown,
|
|
@@ -54204,6 +54405,7 @@ var PR_SLOP_WEIGHTS;
|
|
|
54204
54405
|
var init_assembleScanReport = __esm({
|
|
54205
54406
|
"src/cli/report/assembleScanReport.ts"() {
|
|
54206
54407
|
"use strict";
|
|
54408
|
+
init_threshold();
|
|
54207
54409
|
init_types();
|
|
54208
54410
|
PR_SLOP_WEIGHTS = { high: 10, medium: 5, low: 1 };
|
|
54209
54411
|
}
|
|
@@ -54221,7 +54423,7 @@ import {
|
|
|
54221
54423
|
statSync as statSync5
|
|
54222
54424
|
} from "fs";
|
|
54223
54425
|
import { createHash as createHash10 } from "crypto";
|
|
54224
|
-
import { dirname as
|
|
54426
|
+
import { dirname as dirname10, join as join17, relative as relative9 } from "path";
|
|
54225
54427
|
function telemetryPath2(cwd) {
|
|
54226
54428
|
return join17(cwd, TELEMETRY_DIR, TELEMETRY_FILE2);
|
|
54227
54429
|
}
|
|
@@ -54298,7 +54500,7 @@ function rotateTelemetry(cwd) {
|
|
|
54298
54500
|
if (stats.size < MAX_TELEMETRY_BYTES) {
|
|
54299
54501
|
return;
|
|
54300
54502
|
}
|
|
54301
|
-
const dir =
|
|
54503
|
+
const dir = dirname10(path);
|
|
54302
54504
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
54303
54505
|
renameSync3(path, join17(dir, `scans-${timestamp}.jsonl`));
|
|
54304
54506
|
const rotated = readdirSync5(dir).filter(isTelemetryFile).map((name) => ({ name, mtime: statSync5(join17(dir, name)).mtimeMs })).sort((a, b) => a.mtime - b.mtime);
|
|
@@ -54360,7 +54562,7 @@ function recordTelemetry(cwd, report, results, config, inventory) {
|
|
|
54360
54562
|
...inventory ? { inventory: buildInventorySummary(inventory) } : {}
|
|
54361
54563
|
};
|
|
54362
54564
|
const path = telemetryPath2(cwd);
|
|
54363
|
-
const dir =
|
|
54565
|
+
const dir = dirname10(path);
|
|
54364
54566
|
if (!existsSync15(dir)) {
|
|
54365
54567
|
mkdirSync5(dir, { recursive: true });
|
|
54366
54568
|
}
|
|
@@ -54388,7 +54590,7 @@ __export(structure_md_exports, {
|
|
|
54388
54590
|
writeStructureMarkdown: () => writeStructureMarkdown
|
|
54389
54591
|
});
|
|
54390
54592
|
import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync20, writeFileSync as writeFileSync6 } from "fs";
|
|
54391
|
-
import { dirname as
|
|
54593
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
54392
54594
|
function formatDuration(ms) {
|
|
54393
54595
|
if (!Number.isFinite(ms) || ms < 0) return "unknown";
|
|
54394
54596
|
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
@@ -54529,29 +54731,29 @@ function renderStructureMarkdown(inventory, constitution) {
|
|
|
54529
54731
|
return lines.join("\n");
|
|
54530
54732
|
}
|
|
54531
54733
|
async function writeStructureMarkdown(workspaceDir, md) {
|
|
54532
|
-
await new Promise((
|
|
54734
|
+
await new Promise((resolve43, reject) => {
|
|
54533
54735
|
try {
|
|
54534
54736
|
const path = join18(workspaceDir, STRUCTURE_MD_FILE);
|
|
54535
|
-
mkdirSync6(
|
|
54737
|
+
mkdirSync6(dirname11(path), { recursive: true });
|
|
54536
54738
|
writeFileSync6(path, md, "utf-8");
|
|
54537
|
-
|
|
54739
|
+
resolve43();
|
|
54538
54740
|
} catch (err) {
|
|
54539
54741
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
54540
54742
|
}
|
|
54541
54743
|
});
|
|
54542
54744
|
}
|
|
54543
54745
|
async function readStructureMarkdown(workspaceDir) {
|
|
54544
|
-
return new Promise((
|
|
54746
|
+
return new Promise((resolve43) => {
|
|
54545
54747
|
try {
|
|
54546
54748
|
const path = join18(workspaceDir, STRUCTURE_MD_FILE);
|
|
54547
54749
|
if (!existsSync16(path)) {
|
|
54548
|
-
|
|
54750
|
+
resolve43(null);
|
|
54549
54751
|
return;
|
|
54550
54752
|
}
|
|
54551
54753
|
const content = readFileSync20(path, "utf-8");
|
|
54552
|
-
|
|
54754
|
+
resolve43(content);
|
|
54553
54755
|
} catch {
|
|
54554
|
-
|
|
54756
|
+
resolve43(null);
|
|
54555
54757
|
}
|
|
54556
54758
|
});
|
|
54557
54759
|
}
|
|
@@ -54599,9 +54801,391 @@ var init_structure_md = __esm({
|
|
|
54599
54801
|
}
|
|
54600
54802
|
});
|
|
54601
54803
|
|
|
54804
|
+
// src/snippet/data.ts
|
|
54805
|
+
var CATEGORY_DIRECTIVES, RULE_HINTS;
|
|
54806
|
+
var init_data = __esm({
|
|
54807
|
+
"src/snippet/data.ts"() {
|
|
54808
|
+
"use strict";
|
|
54809
|
+
CATEGORY_DIRECTIVES = {
|
|
54810
|
+
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.',
|
|
54811
|
+
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.",
|
|
54812
|
+
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.',
|
|
54813
|
+
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".',
|
|
54814
|
+
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.",
|
|
54815
|
+
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
54816
|
+
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].",
|
|
54817
|
+
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
54818
|
+
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.",
|
|
54819
|
+
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'."
|
|
54820
|
+
};
|
|
54821
|
+
RULE_HINTS = {
|
|
54822
|
+
// v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
|
|
54823
|
+
// remaining key matches a rule in src/rules/builtins.ts. No orphans.
|
|
54824
|
+
// (v0.16.0 previously moved 35 out-of-scope orphan hints to
|
|
54825
|
+
// docs/research/backlog-rule-hints.md
|
|
54826
|
+
// for future implementers to paste back when the corresponding rules ship.)
|
|
54827
|
+
"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.",
|
|
54828
|
+
"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.",
|
|
54829
|
+
"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.",
|
|
54830
|
+
"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.",
|
|
54831
|
+
"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.",
|
|
54832
|
+
"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.",
|
|
54833
|
+
"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.',
|
|
54834
|
+
"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.",
|
|
54835
|
+
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
54836
|
+
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
54837
|
+
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
54838
|
+
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
54839
|
+
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
54840
|
+
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
54841
|
+
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
54842
|
+
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
54843
|
+
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
54844
|
+
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
54845
|
+
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
54846
|
+
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
54847
|
+
"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.",
|
|
54848
|
+
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
54849
|
+
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
54850
|
+
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
54851
|
+
"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.",
|
|
54852
|
+
"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.",
|
|
54853
|
+
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
54854
|
+
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
54855
|
+
"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.)",
|
|
54856
|
+
"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.)",
|
|
54857
|
+
"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.)",
|
|
54858
|
+
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
54859
|
+
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
54860
|
+
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
54861
|
+
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
54862
|
+
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
54863
|
+
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
54864
|
+
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
54865
|
+
"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.",
|
|
54866
|
+
"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.",
|
|
54867
|
+
"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.",
|
|
54868
|
+
"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.",
|
|
54869
|
+
"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.",
|
|
54870
|
+
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
54871
|
+
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
54872
|
+
"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.',
|
|
54873
|
+
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
54874
|
+
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
54875
|
+
"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.",
|
|
54876
|
+
"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.",
|
|
54877
|
+
"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.",
|
|
54878
|
+
"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%.",
|
|
54879
|
+
"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.",
|
|
54880
|
+
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
54881
|
+
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
54882
|
+
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
54883
|
+
"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.",
|
|
54884
|
+
"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`).",
|
|
54885
|
+
"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.",
|
|
54886
|
+
"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.",
|
|
54887
|
+
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
54888
|
+
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
54889
|
+
"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).",
|
|
54890
|
+
"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).",
|
|
54891
|
+
"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).",
|
|
54892
|
+
"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).",
|
|
54893
|
+
"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).",
|
|
54894
|
+
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
54895
|
+
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
54896
|
+
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
54897
|
+
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
54898
|
+
// reclassified post-v7 calibration in v0.14.5d)
|
|
54899
|
+
"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.",
|
|
54900
|
+
"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.",
|
|
54901
|
+
"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.",
|
|
54902
|
+
"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.",
|
|
54903
|
+
"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.",
|
|
54904
|
+
"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.",
|
|
54905
|
+
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
54906
|
+
"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.',
|
|
54907
|
+
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
54908
|
+
"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.",
|
|
54909
|
+
"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.",
|
|
54910
|
+
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
|
|
54911
|
+
// v0.18.9 — Rust-specific rules (tree-sitter-backed)
|
|
54912
|
+
"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.",
|
|
54913
|
+
"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.",
|
|
54914
|
+
"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.",
|
|
54915
|
+
"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.',
|
|
54916
|
+
// v0.19.0 — TypeScript-specific rules
|
|
54917
|
+
"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.",
|
|
54918
|
+
"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.",
|
|
54919
|
+
"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.",
|
|
54920
|
+
"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.",
|
|
54921
|
+
// v0.19.0 — Go-specific rules
|
|
54922
|
+
"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).',
|
|
54923
|
+
"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.",
|
|
54924
|
+
// v0.19.0 — Duplication detector
|
|
54925
|
+
"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.",
|
|
54926
|
+
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
54927
|
+
"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.)",
|
|
54928
|
+
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
54929
|
+
"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.)",
|
|
54930
|
+
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
54931
|
+
// v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
|
|
54932
|
+
// v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
|
|
54933
|
+
"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.)",
|
|
54934
|
+
"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.)",
|
|
54935
|
+
// v0.35.0 — content-based detection (CoCoNUTS-inspired)
|
|
54936
|
+
"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.",
|
|
54937
|
+
// v0.35.1 — Raidar-inspired content-based detection
|
|
54938
|
+
"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)`.',
|
|
54939
|
+
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
54940
|
+
"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.)",
|
|
54941
|
+
"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.)",
|
|
54942
|
+
"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.)",
|
|
54943
|
+
"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.)",
|
|
54944
|
+
"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.)",
|
|
54945
|
+
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
54946
|
+
"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.)",
|
|
54947
|
+
"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.)",
|
|
54948
|
+
"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.)",
|
|
54949
|
+
"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.)"
|
|
54950
|
+
};
|
|
54951
|
+
}
|
|
54952
|
+
});
|
|
54953
|
+
|
|
54954
|
+
// src/snippet/generators.ts
|
|
54955
|
+
function wrapWithMarkers(level, body) {
|
|
54956
|
+
const head = level === 1 ? "<!-- slopbrick-managed-section -->\n" : "";
|
|
54957
|
+
return `${head}${MARKER_BEGIN}
|
|
54958
|
+
${body}
|
|
54959
|
+
${MARKER_END}
|
|
54960
|
+
`;
|
|
54961
|
+
}
|
|
54962
|
+
function generateCursorSnippet(rules) {
|
|
54963
|
+
const lines = [];
|
|
54964
|
+
lines.push("# slopbrick cursor rules");
|
|
54965
|
+
lines.push("");
|
|
54966
|
+
lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
|
|
54967
|
+
lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
|
|
54968
|
+
lines.push("");
|
|
54969
|
+
lines.push("## Category-level directives");
|
|
54970
|
+
lines.push("");
|
|
54971
|
+
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
54972
|
+
lines.push("- **" + category + "**: " + directive);
|
|
54973
|
+
}
|
|
54974
|
+
lines.push("");
|
|
54975
|
+
lines.push("## Per-rule directives");
|
|
54976
|
+
lines.push("");
|
|
54977
|
+
const ai = aiSpecificRules(rules);
|
|
54978
|
+
const byCategory = categorizeRules(ai);
|
|
54979
|
+
for (const [category, list] of byCategory) {
|
|
54980
|
+
lines.push("### " + category);
|
|
54981
|
+
lines.push("");
|
|
54982
|
+
for (const rule of list) {
|
|
54983
|
+
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
54984
|
+
lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
|
|
54985
|
+
}
|
|
54986
|
+
lines.push("");
|
|
54987
|
+
}
|
|
54988
|
+
lines.push("## Verification");
|
|
54989
|
+
lines.push("");
|
|
54990
|
+
lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
|
|
54991
|
+
lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
|
|
54992
|
+
lines.push("");
|
|
54993
|
+
return lines.join("\n");
|
|
54994
|
+
}
|
|
54995
|
+
function generateAgentsMdSnippet(rules) {
|
|
54996
|
+
return wrapWithMarkers(
|
|
54997
|
+
1,
|
|
54998
|
+
baseContent(
|
|
54999
|
+
rules,
|
|
55000
|
+
"AGENTS.md",
|
|
55001
|
+
"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."
|
|
55002
|
+
) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n"
|
|
55003
|
+
);
|
|
55004
|
+
}
|
|
55005
|
+
function generateClaudeMdSnippet(rules) {
|
|
55006
|
+
return wrapWithMarkers(
|
|
55007
|
+
1,
|
|
55008
|
+
baseContent(
|
|
55009
|
+
rules,
|
|
55010
|
+
"CLAUDE.md",
|
|
55011
|
+
"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."
|
|
55012
|
+
) + "## 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"
|
|
55013
|
+
);
|
|
55014
|
+
}
|
|
55015
|
+
function generateAiderSnippet(rules) {
|
|
55016
|
+
return baseContent(
|
|
55017
|
+
rules,
|
|
55018
|
+
"CONVENTIONS.md",
|
|
55019
|
+
"Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
|
|
55020
|
+
);
|
|
55021
|
+
}
|
|
55022
|
+
function generateWindsurfSnippet(rules) {
|
|
55023
|
+
return baseContent(
|
|
55024
|
+
rules,
|
|
55025
|
+
".windsurfrules",
|
|
55026
|
+
"Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
|
|
55027
|
+
);
|
|
55028
|
+
}
|
|
55029
|
+
function generateClineRules(rules) {
|
|
55030
|
+
return baseContent(
|
|
55031
|
+
rules,
|
|
55032
|
+
"AGENTS.md",
|
|
55033
|
+
"Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
|
|
55034
|
+
);
|
|
55035
|
+
}
|
|
55036
|
+
function generateCursorrulesLegacySnippet(rules) {
|
|
55037
|
+
return baseContent(
|
|
55038
|
+
rules,
|
|
55039
|
+
".cursorrules",
|
|
55040
|
+
"Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
|
|
55041
|
+
);
|
|
55042
|
+
}
|
|
55043
|
+
function generateGeminiSnippet(rules) {
|
|
55044
|
+
return baseContent(
|
|
55045
|
+
rules,
|
|
55046
|
+
"GEMINI.md",
|
|
55047
|
+
"Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
|
|
55048
|
+
);
|
|
55049
|
+
}
|
|
55050
|
+
function generateCopilotSnippet(rules) {
|
|
55051
|
+
return baseContent(
|
|
55052
|
+
rules,
|
|
55053
|
+
"copilot-instructions.md",
|
|
55054
|
+
"Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
|
|
55055
|
+
);
|
|
55056
|
+
}
|
|
55057
|
+
var MARKER_BEGIN, MARKER_END;
|
|
55058
|
+
var init_generators = __esm({
|
|
55059
|
+
"src/snippet/generators.ts"() {
|
|
55060
|
+
"use strict";
|
|
55061
|
+
init_data();
|
|
55062
|
+
init_render2();
|
|
55063
|
+
MARKER_BEGIN = "<!-- slopbrick:begin:v3 -->";
|
|
55064
|
+
MARKER_END = "<!-- slopbrick:end:v3 -->";
|
|
55065
|
+
}
|
|
55066
|
+
});
|
|
55067
|
+
|
|
55068
|
+
// src/snippet/render.ts
|
|
55069
|
+
function aiSpecificRules(rules) {
|
|
55070
|
+
return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
|
|
55071
|
+
}
|
|
55072
|
+
function categorizeRules(rules) {
|
|
55073
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
55074
|
+
for (const r of rules) {
|
|
55075
|
+
const list = byCategory.get(r.category) ?? [];
|
|
55076
|
+
list.push(r);
|
|
55077
|
+
byCategory.set(r.category, list);
|
|
55078
|
+
}
|
|
55079
|
+
for (const list of byCategory.values()) {
|
|
55080
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
55081
|
+
}
|
|
55082
|
+
return byCategory;
|
|
55083
|
+
}
|
|
55084
|
+
function baseContent(rules, toolLabel, intro) {
|
|
55085
|
+
const lines = [];
|
|
55086
|
+
lines.push("# " + toolLabel);
|
|
55087
|
+
lines.push("");
|
|
55088
|
+
lines.push(intro);
|
|
55089
|
+
lines.push("");
|
|
55090
|
+
const ai = aiSpecificRules(rules);
|
|
55091
|
+
const byCategory = categorizeRules(ai);
|
|
55092
|
+
lines.push("## Category-level directives");
|
|
55093
|
+
lines.push("");
|
|
55094
|
+
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
55095
|
+
lines.push("- **" + category + "**: " + directive);
|
|
55096
|
+
}
|
|
55097
|
+
lines.push("");
|
|
55098
|
+
lines.push("## Per-rule directives");
|
|
55099
|
+
lines.push("");
|
|
55100
|
+
for (const [category, list] of byCategory) {
|
|
55101
|
+
lines.push("### " + category);
|
|
55102
|
+
lines.push("");
|
|
55103
|
+
for (const rule of list) {
|
|
55104
|
+
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
55105
|
+
lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
|
|
55106
|
+
}
|
|
55107
|
+
lines.push("");
|
|
55108
|
+
}
|
|
55109
|
+
return lines.join("\n");
|
|
55110
|
+
}
|
|
55111
|
+
function safeRewrite(existing, freshBlockBody) {
|
|
55112
|
+
const beginIdx = existing.indexOf(MARKER_BEGIN);
|
|
55113
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
55114
|
+
if (beginIdx === -1 && endIdx === -1) {
|
|
55115
|
+
return { content: existing, rewritten: false, failClosed: true, mismatched: false };
|
|
55116
|
+
}
|
|
55117
|
+
if (beginIdx === -1 || endIdx === -1) {
|
|
55118
|
+
return { content: existing, rewritten: false, failClosed: false, mismatched: true };
|
|
55119
|
+
}
|
|
55120
|
+
if (endIdx <= beginIdx) {
|
|
55121
|
+
return { content: existing, rewritten: false, failClosed: false, mismatched: true };
|
|
55122
|
+
}
|
|
55123
|
+
const before = existing.slice(0, beginIdx + MARKER_BEGIN.length);
|
|
55124
|
+
const after = existing.slice(endIdx);
|
|
55125
|
+
const next = `${before}
|
|
55126
|
+
${freshBlockBody}
|
|
55127
|
+
${after}`;
|
|
55128
|
+
return { content: next, rewritten: true, failClosed: false, mismatched: false };
|
|
55129
|
+
}
|
|
55130
|
+
var init_render2 = __esm({
|
|
55131
|
+
"src/snippet/render.ts"() {
|
|
55132
|
+
"use strict";
|
|
55133
|
+
init_data();
|
|
55134
|
+
init_generators();
|
|
55135
|
+
}
|
|
55136
|
+
});
|
|
55137
|
+
|
|
55138
|
+
// src/snippet/refresh.ts
|
|
55139
|
+
var refresh_exports = {};
|
|
55140
|
+
__export(refresh_exports, {
|
|
55141
|
+
SNIPPET_TARGETS: () => SNIPPET_TARGETS,
|
|
55142
|
+
refreshSnippets: () => refreshSnippets
|
|
55143
|
+
});
|
|
55144
|
+
import { existsSync as existsSync17, readFileSync as readFileSync21, writeFileSync as writeFileSync7 } from "fs";
|
|
55145
|
+
import { join as join19 } from "path";
|
|
55146
|
+
function refreshSnippets(cwd, rules, targets = [...SNIPPET_TARGETS]) {
|
|
55147
|
+
const result = { rewritten: [], failClosed: [], mismatched: [], absent: [] };
|
|
55148
|
+
for (const filename of targets) {
|
|
55149
|
+
const path = join19(cwd, filename);
|
|
55150
|
+
if (!existsSync17(path)) {
|
|
55151
|
+
result.absent.push(filename);
|
|
55152
|
+
continue;
|
|
55153
|
+
}
|
|
55154
|
+
const existing = readFileSync21(path, "utf-8");
|
|
55155
|
+
const agentName = filename.replace(/\.md$/i, "").toLowerCase();
|
|
55156
|
+
const generator = agentName === "claude" ? generateAgentsMdSnippet : generateAgentsMdSnippet;
|
|
55157
|
+
const newBlock = generator([...rules]).trim();
|
|
55158
|
+
const outcome = safeRewrite(existing, newBlock);
|
|
55159
|
+
if (outcome.rewritten) {
|
|
55160
|
+
const tmp = `${path}.tmp`;
|
|
55161
|
+
writeFileSync7(tmp, outcome.content, "utf-8");
|
|
55162
|
+
const fs = __require("fs");
|
|
55163
|
+
fs.renameSync(tmp, path);
|
|
55164
|
+
result.rewritten.push(filename);
|
|
55165
|
+
} else if (outcome.failClosed) {
|
|
55166
|
+
result.failClosed.push(filename);
|
|
55167
|
+
} else if (outcome.mismatched) {
|
|
55168
|
+
result.mismatched.push(filename);
|
|
55169
|
+
}
|
|
55170
|
+
}
|
|
55171
|
+
return result;
|
|
55172
|
+
}
|
|
55173
|
+
var SNIPPET_TARGETS;
|
|
55174
|
+
var init_refresh = __esm({
|
|
55175
|
+
"src/snippet/refresh.ts"() {
|
|
55176
|
+
"use strict";
|
|
55177
|
+
init_render2();
|
|
55178
|
+
init_generators();
|
|
55179
|
+
SNIPPET_TARGETS = [
|
|
55180
|
+
"AGENTS.md",
|
|
55181
|
+
"CLAUDE.md"
|
|
55182
|
+
];
|
|
55183
|
+
}
|
|
55184
|
+
});
|
|
55185
|
+
|
|
54602
55186
|
// src/cli/report/persistRun.ts
|
|
54603
|
-
import { existsSync as
|
|
54604
|
-
import { join as
|
|
55187
|
+
import { existsSync as existsSync18, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
55188
|
+
import { join as join20, relative as relative10 } from "path";
|
|
54605
55189
|
async function persistRun(input) {
|
|
54606
55190
|
const {
|
|
54607
55191
|
cwd,
|
|
@@ -54683,10 +55267,10 @@ async function persistRun(input) {
|
|
|
54683
55267
|
report.research = state.research;
|
|
54684
55268
|
}
|
|
54685
55269
|
if (flywheelOutput.suggestions.length > 0) {
|
|
54686
|
-
const suggestionsDir =
|
|
54687
|
-
if (!
|
|
54688
|
-
|
|
54689
|
-
|
|
55270
|
+
const suggestionsDir = join20(cwd, ".slopbrick", "flywheel");
|
|
55271
|
+
if (!existsSync18(suggestionsDir)) mkdirSync7(suggestionsDir, { recursive: true });
|
|
55272
|
+
writeFileSync8(
|
|
55273
|
+
join20(suggestionsDir, "rule-suggestions.json"),
|
|
54690
55274
|
JSON.stringify(flywheelOutput.suggestions, null, 2)
|
|
54691
55275
|
);
|
|
54692
55276
|
}
|
|
@@ -54731,6 +55315,21 @@ async function persistRun(input) {
|
|
|
54731
55315
|
}
|
|
54732
55316
|
}
|
|
54733
55317
|
recordTelemetry(cwd, report, results, config, patternInventory);
|
|
55318
|
+
if (input.autoRefreshSnippets) {
|
|
55319
|
+
try {
|
|
55320
|
+
const { refreshSnippets: refreshSnippets2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
|
|
55321
|
+
const outcome = refreshSnippets2(cwd, registry.getRules());
|
|
55322
|
+
if (outcome.rewritten.length > 0 && !options.quiet && !machineReadableStdout) {
|
|
55323
|
+
logger.info(
|
|
55324
|
+
`Snippet refresh: rewrote ${outcome.rewritten.length} file(s) (${outcome.rewritten.join(", ")}).`
|
|
55325
|
+
);
|
|
55326
|
+
}
|
|
55327
|
+
} catch (err) {
|
|
55328
|
+
if (options.quiet !== true && !machineReadableStdout) {
|
|
55329
|
+
logger.warn(`snippet refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
55330
|
+
}
|
|
55331
|
+
}
|
|
55332
|
+
}
|
|
54734
55333
|
}
|
|
54735
55334
|
var init_persistRun = __esm({
|
|
54736
55335
|
"src/cli/report/persistRun.ts"() {
|
|
@@ -54951,7 +55550,7 @@ var init_json = __esm({
|
|
|
54951
55550
|
|
|
54952
55551
|
// src/report/sarif.ts
|
|
54953
55552
|
import { createHash as createHash11 } from "crypto";
|
|
54954
|
-
import { readFileSync as
|
|
55553
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
54955
55554
|
import { basename as basename3, isAbsolute as isAbsolute3, relative as relative12, resolve as resolve9 } from "path";
|
|
54956
55555
|
function buildArtifactUri(filePath, cwd) {
|
|
54957
55556
|
if (!filePath) {
|
|
@@ -55058,7 +55657,7 @@ function buildResultFromIssue(issue, cwd, fileContentCache) {
|
|
|
55058
55657
|
let content = fileContentCache.get(sourcePath);
|
|
55059
55658
|
if (content === void 0) {
|
|
55060
55659
|
try {
|
|
55061
|
-
content =
|
|
55660
|
+
content = readFileSync22(sourcePath, "utf-8");
|
|
55062
55661
|
fileContentCache.set(sourcePath, content);
|
|
55063
55662
|
} catch {
|
|
55064
55663
|
fileContentCache.set(sourcePath, "");
|
|
@@ -55306,11 +55905,13 @@ function extractNamedScores(report) {
|
|
|
55306
55905
|
};
|
|
55307
55906
|
}
|
|
55308
55907
|
function renderThresholds(report) {
|
|
55309
|
-
const
|
|
55310
|
-
const
|
|
55311
|
-
const
|
|
55312
|
-
const
|
|
55313
|
-
const
|
|
55908
|
+
const meanSlop = report.thresholds?.meanSlop ?? 30;
|
|
55909
|
+
const aiAvailable = typeof report.aiSlopScore === "number";
|
|
55910
|
+
const aiPassed = aiAvailable && report.aiSlopScore <= meanSlop;
|
|
55911
|
+
const rh = report.repositoryHealth;
|
|
55912
|
+
const rhAvailable = typeof rh === "number";
|
|
55913
|
+
const rhLimit = 70;
|
|
55914
|
+
const rhPassed = rhAvailable && rh >= rhLimit;
|
|
55314
55915
|
return `
|
|
55315
55916
|
<section class="thresholds-section">
|
|
55316
55917
|
<h2>Thresholds</h2>
|
|
@@ -55324,9 +55925,14 @@ function renderThresholds(report) {
|
|
|
55324
55925
|
</thead>
|
|
55325
55926
|
<tbody>
|
|
55326
55927
|
<tr>
|
|
55327
|
-
<td>
|
|
55328
|
-
<td>${
|
|
55329
|
-
<td><span class="status-badge ${thresholdStatusClass(
|
|
55928
|
+
<td>AI Slop Score (CI gate)</td>
|
|
55929
|
+
<td>${aiAvailable ? `${report.aiSlopScore.toFixed(1)} / ${meanSlop}` : "\u2013"}</td>
|
|
55930
|
+
<td><span class="status-badge ${thresholdStatusClass(!aiPassed)}">${aiAvailable ? aiPassed ? "pass" : "fail" : "n/a"}</span></td>
|
|
55931
|
+
</tr>
|
|
55932
|
+
<tr>
|
|
55933
|
+
<td>Repository Health (informational)</td>
|
|
55934
|
+
<td>${rhAvailable ? `${rh.toFixed(1)} / ${rhLimit}` : "\u2013"}</td>
|
|
55935
|
+
<td><span class="status-badge ${thresholdStatusClass(!rhPassed)}">${rhAvailable ? rhPassed ? "pass" : "fail" : "n/a"}</span></td>
|
|
55330
55936
|
</tr>
|
|
55331
55937
|
</tbody>
|
|
55332
55938
|
</table>
|
|
@@ -56284,7 +56890,7 @@ var init_advice = __esm({
|
|
|
56284
56890
|
});
|
|
56285
56891
|
|
|
56286
56892
|
// src/fix/layout-token.ts
|
|
56287
|
-
import { readFileSync as
|
|
56893
|
+
import { readFileSync as readFileSync23, writeFileSync as writeFileSync9 } from "fs";
|
|
56288
56894
|
function escapeRegExp(value) {
|
|
56289
56895
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
56290
56896
|
}
|
|
@@ -56321,10 +56927,10 @@ function applyReplaceFixes(content, fixes) {
|
|
|
56321
56927
|
return { content, applied, skipped, reasons };
|
|
56322
56928
|
}
|
|
56323
56929
|
function applyLayoutTokenFix(filePath, fixes) {
|
|
56324
|
-
let content =
|
|
56930
|
+
let content = readFileSync23(filePath, "utf-8");
|
|
56325
56931
|
const { content: patched, applied, skipped, reasons } = applyReplaceFixes(content, fixes);
|
|
56326
56932
|
if (patched !== content) {
|
|
56327
|
-
|
|
56933
|
+
writeFileSync9(filePath, patched);
|
|
56328
56934
|
}
|
|
56329
56935
|
return { applied, skipped, reasons };
|
|
56330
56936
|
}
|
|
@@ -56335,7 +56941,7 @@ var init_layout_token = __esm({
|
|
|
56335
56941
|
});
|
|
56336
56942
|
|
|
56337
56943
|
// src/report/unified-diff.ts
|
|
56338
|
-
import { existsSync as
|
|
56944
|
+
import { existsSync as existsSync19, readFileSync as readFileSync24 } from "fs";
|
|
56339
56945
|
import { relative as relative13 } from "path";
|
|
56340
56946
|
function collectAllFixes(issue) {
|
|
56341
56947
|
return [...issue.fix ? [issue.fix] : [], ...issue.fixes ?? []];
|
|
@@ -56412,8 +57018,8 @@ function formatUnifiedDiff(report, cwd) {
|
|
|
56412
57018
|
const parts = [];
|
|
56413
57019
|
let hasHunk = false;
|
|
56414
57020
|
for (const [filePath, fixes] of byFile) {
|
|
56415
|
-
if (!
|
|
56416
|
-
const original =
|
|
57021
|
+
if (!existsSync19(filePath)) continue;
|
|
57022
|
+
const original = readFileSync24(filePath, "utf-8");
|
|
56417
57023
|
const patched = applyFixesToString(original, fixes);
|
|
56418
57024
|
if (patched === original) continue;
|
|
56419
57025
|
if (!hasHunk) {
|
|
@@ -56519,7 +57125,7 @@ var init_heatmap = __esm({
|
|
|
56519
57125
|
|
|
56520
57126
|
// src/cli/report/renderOutput.ts
|
|
56521
57127
|
import { resolve as resolve10 } from "path";
|
|
56522
|
-
import { writeFileSync as
|
|
57128
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
56523
57129
|
function renderOutput(report, options, cwd) {
|
|
56524
57130
|
if (options.format && !VALID_FORMATS.has(options.format)) {
|
|
56525
57131
|
process.stderr.write(
|
|
@@ -56553,7 +57159,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56553
57159
|
if (options.html) {
|
|
56554
57160
|
const html = formatHtml(report);
|
|
56555
57161
|
if (typeof options.html === "string") {
|
|
56556
|
-
|
|
57162
|
+
writeFileSync10(resolve10(options.html), html);
|
|
56557
57163
|
if (!options.quiet) {
|
|
56558
57164
|
logger.info(`Wrote HTML report to ${options.html}`);
|
|
56559
57165
|
}
|
|
@@ -56565,7 +57171,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56565
57171
|
if (options.json) {
|
|
56566
57172
|
const json = formatJson(report);
|
|
56567
57173
|
if (typeof options.json === "string") {
|
|
56568
|
-
|
|
57174
|
+
writeFileSync10(resolve10(options.json), json);
|
|
56569
57175
|
if (!options.quiet) {
|
|
56570
57176
|
logger.info(`Wrote JSON report to ${options.json}`);
|
|
56571
57177
|
}
|
|
@@ -56796,7 +57402,7 @@ var init_watch = __esm({
|
|
|
56796
57402
|
});
|
|
56797
57403
|
|
|
56798
57404
|
// src/cli/scan.ts
|
|
56799
|
-
import { existsSync as
|
|
57405
|
+
import { existsSync as existsSync20, statSync as statSync7 } from "fs";
|
|
56800
57406
|
import { resolve as resolve12, relative as relative15, extname as extname7, sep as sep3 } from "path";
|
|
56801
57407
|
import { randomUUID } from "crypto";
|
|
56802
57408
|
async function runScan(options, explicitPaths) {
|
|
@@ -56804,7 +57410,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56804
57410
|
const startTime = Date.now();
|
|
56805
57411
|
const scanId = randomUUID();
|
|
56806
57412
|
const cwd = resolve12(options.workspace ?? process.cwd());
|
|
56807
|
-
if (!
|
|
57413
|
+
if (!existsSync20(cwd)) {
|
|
56808
57414
|
throw new Error(`Workspace not found: ${cwd}`);
|
|
56809
57415
|
}
|
|
56810
57416
|
const cwdStat = statSync7(cwd);
|
|
@@ -56849,7 +57455,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56849
57455
|
const resolved = explicitPaths.map((p) => resolve12(cwd, p));
|
|
56850
57456
|
const expanded = [];
|
|
56851
57457
|
for (const p of resolved) {
|
|
56852
|
-
if (
|
|
57458
|
+
if (existsSync20(p) && statSync7(p).isDirectory()) {
|
|
56853
57459
|
const found = await globby4(`${p}/**/*`, { absolute: true, onlyFiles: true });
|
|
56854
57460
|
for (const f of found) {
|
|
56855
57461
|
if (!ALL_SOURCE_EXTENSIONS.has(extname7(f).toLowerCase())) continue;
|
|
@@ -56887,12 +57493,16 @@ async function runScan(options, explicitPaths) {
|
|
|
56887
57493
|
files = intersectFiles(files, since, cwd);
|
|
56888
57494
|
}
|
|
56889
57495
|
let incrementalSummary;
|
|
57496
|
+
let cachePath3;
|
|
57497
|
+
let existingCache;
|
|
57498
|
+
let unchanged = [];
|
|
56890
57499
|
if (options.incremental) {
|
|
56891
|
-
|
|
56892
|
-
|
|
56893
|
-
const
|
|
56894
|
-
files = toScan;
|
|
56895
|
-
|
|
57500
|
+
cachePath3 = options.cachePath ?? ".slopbrick-cache.json";
|
|
57501
|
+
existingCache = loadCache(cachePath3);
|
|
57502
|
+
const partition = partitionByCache(files, existingCache);
|
|
57503
|
+
files = partition.toScan;
|
|
57504
|
+
unchanged = partition.unchanged;
|
|
57505
|
+
incrementalSummary = { skipped: unchanged.length, rescanned: partition.toScan.length };
|
|
56896
57506
|
}
|
|
56897
57507
|
const machineReadableStdout = !!options.json || !!options.html || options.format === "json" || options.format === "sarif" || options.format === "html";
|
|
56898
57508
|
if (files.length === 0 && !options.quiet && !machineReadableStdout) {
|
|
@@ -56912,6 +57522,9 @@ async function runScan(options, explicitPaths) {
|
|
|
56912
57522
|
logger.warn(
|
|
56913
57523
|
`No source files matched in ${cwd}. Adjust --include or check your config at ${projectRoot}.`
|
|
56914
57524
|
);
|
|
57525
|
+
logger.warn(
|
|
57526
|
+
"Tip: run from a directory with source files, or pass --workspace <path> (monorepos usually want `--workspace packages/<name>`)."
|
|
57527
|
+
);
|
|
56915
57528
|
}
|
|
56916
57529
|
}
|
|
56917
57530
|
const configHash = hashConfig(config);
|
|
@@ -57003,7 +57616,7 @@ async function runScan(options, explicitPaths) {
|
|
|
57003
57616
|
const scannedPaths = new Set(results.map((result) => result.filePath));
|
|
57004
57617
|
for (const [filePath, cached] of Object.entries(baseline.scores)) {
|
|
57005
57618
|
if (scannedPaths.has(filePath)) continue;
|
|
57006
|
-
if (!
|
|
57619
|
+
if (!existsSync20(filePath)) continue;
|
|
57007
57620
|
scores.push({
|
|
57008
57621
|
filePath,
|
|
57009
57622
|
rawScore: 0,
|
|
@@ -57042,7 +57655,12 @@ async function runScan(options, explicitPaths) {
|
|
|
57042
57655
|
const { report, noIncreaseFailure } = await finalizeReport({
|
|
57043
57656
|
cwd,
|
|
57044
57657
|
config,
|
|
57045
|
-
options
|
|
57658
|
+
options: {
|
|
57659
|
+
...options,
|
|
57660
|
+
// v0.42.0 (§3a.4): forward the opt-in flag through to
|
|
57661
|
+
// `persistRun`, which calls `refreshSnippets` post-scan.
|
|
57662
|
+
autoRefreshSnippets: Boolean(options.autoRefreshSnippets) || Boolean(config.autoRefreshSnippets)
|
|
57663
|
+
},
|
|
57046
57664
|
results,
|
|
57047
57665
|
aggregated,
|
|
57048
57666
|
allIssues,
|
|
@@ -57056,6 +57674,38 @@ async function runScan(options, explicitPaths) {
|
|
|
57056
57674
|
telemetryEnabled,
|
|
57057
57675
|
machineReadableStdout
|
|
57058
57676
|
});
|
|
57677
|
+
if (options.incremental && cachePath3 !== void 0) {
|
|
57678
|
+
const cachedFiles = {};
|
|
57679
|
+
for (const r of results) {
|
|
57680
|
+
try {
|
|
57681
|
+
cachedFiles[r.filePath] = {
|
|
57682
|
+
hash: computeFileHash(r.filePath),
|
|
57683
|
+
issueCount: r.issues.length,
|
|
57684
|
+
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
57685
|
+
};
|
|
57686
|
+
} catch {
|
|
57687
|
+
}
|
|
57688
|
+
}
|
|
57689
|
+
for (const f of unchanged) {
|
|
57690
|
+
const cached = existingCache?.files[f];
|
|
57691
|
+
if (cached) {
|
|
57692
|
+
cachedFiles[f] = {
|
|
57693
|
+
hash: cached.hash,
|
|
57694
|
+
issueCount: cached.issueCount,
|
|
57695
|
+
lastScannedAt: cached.lastScannedAt
|
|
57696
|
+
};
|
|
57697
|
+
} else {
|
|
57698
|
+
}
|
|
57699
|
+
}
|
|
57700
|
+
try {
|
|
57701
|
+
saveCache(cachePath3, {
|
|
57702
|
+
version: VERSION,
|
|
57703
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
57704
|
+
files: cachedFiles
|
|
57705
|
+
});
|
|
57706
|
+
} catch {
|
|
57707
|
+
}
|
|
57708
|
+
}
|
|
57059
57709
|
return {
|
|
57060
57710
|
report,
|
|
57061
57711
|
scores,
|
|
@@ -57110,13 +57760,57 @@ var init_scan2 = __esm({
|
|
|
57110
57760
|
}
|
|
57111
57761
|
});
|
|
57112
57762
|
|
|
57763
|
+
// src/rules/composite-seed.ts
|
|
57764
|
+
var composite_seed_exports = {};
|
|
57765
|
+
__export(composite_seed_exports, {
|
|
57766
|
+
HAND_CURATED_SEED: () => HAND_CURATED_SEED,
|
|
57767
|
+
maybeAppendSeed: () => maybeAppendSeed
|
|
57768
|
+
});
|
|
57769
|
+
function maybeAppendSeed(entries) {
|
|
57770
|
+
if (entries.length === 0) {
|
|
57771
|
+
return [...HAND_CURATED_SEED];
|
|
57772
|
+
}
|
|
57773
|
+
return entries;
|
|
57774
|
+
}
|
|
57775
|
+
var HAND_CURATED_SEED;
|
|
57776
|
+
var init_composite_seed = __esm({
|
|
57777
|
+
"src/rules/composite-seed.ts"() {
|
|
57778
|
+
"use strict";
|
|
57779
|
+
HAND_CURATED_SEED = [
|
|
57780
|
+
{
|
|
57781
|
+
id: "composite/ai-fingerprint-cluster",
|
|
57782
|
+
ruleIds: ["ai/compression-profile", "ai/comment-ratio", "ai/segment-surprisal-cv"],
|
|
57783
|
+
minMatch: 2,
|
|
57784
|
+
severity: "medium",
|
|
57785
|
+
defaultOff: true,
|
|
57786
|
+
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.",
|
|
57787
|
+
calibration: {
|
|
57788
|
+
recall: 0,
|
|
57789
|
+
FP: 0,
|
|
57790
|
+
precision: 0,
|
|
57791
|
+
F1: 0,
|
|
57792
|
+
nFiles: 0
|
|
57793
|
+
},
|
|
57794
|
+
provenance: {
|
|
57795
|
+
seed: "hand-curated-by-brief",
|
|
57796
|
+
discoveredAt: "2026-07-05T00:00:00.000Z",
|
|
57797
|
+
nFiles: 0,
|
|
57798
|
+
members: 3,
|
|
57799
|
+
npmi: 0,
|
|
57800
|
+
fisherP: 1
|
|
57801
|
+
}
|
|
57802
|
+
}
|
|
57803
|
+
];
|
|
57804
|
+
}
|
|
57805
|
+
});
|
|
57806
|
+
|
|
57113
57807
|
// src/index.ts
|
|
57114
57808
|
init_types();
|
|
57115
57809
|
init_config2();
|
|
57116
57810
|
init_dist2();
|
|
57117
57811
|
|
|
57118
57812
|
// src/cli/program.ts
|
|
57119
|
-
import { resolve as
|
|
57813
|
+
import { resolve as resolve42 } from "path";
|
|
57120
57814
|
import { Command } from "commander";
|
|
57121
57815
|
|
|
57122
57816
|
// src/cli/options.ts
|
|
@@ -57164,13 +57858,15 @@ init_discover();
|
|
|
57164
57858
|
init_git();
|
|
57165
57859
|
init_cache();
|
|
57166
57860
|
init_logger();
|
|
57167
|
-
|
|
57168
|
-
import
|
|
57861
|
+
init_types();
|
|
57862
|
+
import chalk2 from "chalk";
|
|
57863
|
+
import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync9, rmSync as rmSync2 } from "fs";
|
|
57864
|
+
import { join as join23, dirname as dirname13 } from "path";
|
|
57169
57865
|
import { createInterface } from "readline";
|
|
57170
57866
|
|
|
57171
57867
|
// src/rules/registry-loader.ts
|
|
57172
|
-
import { existsSync as
|
|
57173
|
-
import { dirname as
|
|
57868
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync8, readFileSync as readFileSync25, writeFileSync as writeFileSync11 } from "fs";
|
|
57869
|
+
import { dirname as dirname12, join as join21 } from "path";
|
|
57174
57870
|
|
|
57175
57871
|
// src/data/shadcn-registry.json
|
|
57176
57872
|
var shadcn_registry_default = {
|
|
@@ -57304,11 +58000,11 @@ var shadcn_registry_default = {
|
|
|
57304
58000
|
var REGISTRY_URL = "https://ui.shadcn.com/registry.json";
|
|
57305
58001
|
var BUNDLED_REGISTRY_VERSION = shadcn_registry_default.version;
|
|
57306
58002
|
function cachePath2(cwd) {
|
|
57307
|
-
return
|
|
58003
|
+
return join21(cwd, ".slopbrick", "cache", "registry-snapshot.json");
|
|
57308
58004
|
}
|
|
57309
58005
|
function ensureCacheDir(cwd) {
|
|
57310
|
-
const dir =
|
|
57311
|
-
if (!
|
|
58006
|
+
const dir = dirname12(cachePath2(cwd));
|
|
58007
|
+
if (!existsSync21(dir)) {
|
|
57312
58008
|
mkdirSync8(dir, { recursive: true });
|
|
57313
58009
|
}
|
|
57314
58010
|
}
|
|
@@ -57321,9 +58017,9 @@ function isValidSnapshot(value) {
|
|
|
57321
58017
|
}
|
|
57322
58018
|
function isRegistryFresh(cwd) {
|
|
57323
58019
|
const cached = cachePath2(cwd);
|
|
57324
|
-
if (!
|
|
58020
|
+
if (!existsSync21(cached)) return false;
|
|
57325
58021
|
try {
|
|
57326
|
-
const parsed = JSON.parse(
|
|
58022
|
+
const parsed = JSON.parse(readFileSync25(cached, "utf8"));
|
|
57327
58023
|
if (!isValidSnapshot(parsed)) return false;
|
|
57328
58024
|
return parsed.version === BUNDLED_REGISTRY_VERSION;
|
|
57329
58025
|
} catch {
|
|
@@ -57334,19 +58030,24 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
|
|
|
57334
58030
|
try {
|
|
57335
58031
|
const cacheFile = cachePath2(cwd);
|
|
57336
58032
|
ensureCacheDir(cwd);
|
|
57337
|
-
|
|
58033
|
+
writeFileSync11(cacheFile, JSON.stringify(shadcn_registry_default, null, 2), "utf-8");
|
|
57338
58034
|
} catch {
|
|
57339
58035
|
}
|
|
57340
58036
|
return {
|
|
57341
58037
|
snapshot: shadcn_registry_default,
|
|
57342
58038
|
ok: true,
|
|
57343
58039
|
fresh: false,
|
|
57344
|
-
|
|
58040
|
+
// v0.42.0 (user-review fix): the previous message said
|
|
58041
|
+
// "upstream URL returns 404" which is alarming without context —
|
|
58042
|
+
// upstream has been dead since v0.39.0, and the bundled snapshot
|
|
58043
|
+
// IS the snapshot in use. Replaced with a neutral message that
|
|
58044
|
+
// doesn't suggest "something is wrong".
|
|
58045
|
+
message: "Using bundled shadcn/ui snapshot (built-in; no network fetch)."
|
|
57345
58046
|
};
|
|
57346
58047
|
}
|
|
57347
58048
|
function copyBundledSnapshotToCache(cwd) {
|
|
57348
58049
|
ensureCacheDir(cwd);
|
|
57349
|
-
|
|
58050
|
+
writeFileSync11(cachePath2(cwd), JSON.stringify(shadcn_registry_default, null, 2));
|
|
57350
58051
|
}
|
|
57351
58052
|
|
|
57352
58053
|
// src/cli/init.ts
|
|
@@ -57375,17 +58076,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
|
|
|
57375
58076
|
var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
|
|
57376
58077
|
var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
|
|
57377
58078
|
function promptText(rl, question, detected) {
|
|
57378
|
-
return new Promise((
|
|
58079
|
+
return new Promise((resolve43) => {
|
|
57379
58080
|
const lines = [
|
|
57380
58081
|
`? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
|
|
57381
58082
|
];
|
|
57382
58083
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57383
58084
|
const trimmed = answer.trim();
|
|
57384
58085
|
if (trimmed === "") {
|
|
57385
|
-
|
|
58086
|
+
resolve43(void 0);
|
|
57386
58087
|
return;
|
|
57387
58088
|
}
|
|
57388
|
-
|
|
58089
|
+
resolve43(trimmed);
|
|
57389
58090
|
});
|
|
57390
58091
|
});
|
|
57391
58092
|
}
|
|
@@ -57393,7 +58094,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57393
58094
|
const defaultIndex = options.indexOf(defaultValue);
|
|
57394
58095
|
const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
|
|
57395
58096
|
const safeDefaultValue = options[safeDefaultIndex];
|
|
57396
|
-
return new Promise((
|
|
58097
|
+
return new Promise((resolve43) => {
|
|
57397
58098
|
function ask() {
|
|
57398
58099
|
const lines = [
|
|
57399
58100
|
`? ${question} (detected: ${safeDefaultValue}):`,
|
|
@@ -57403,7 +58104,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57403
58104
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57404
58105
|
const trimmed = answer.trim();
|
|
57405
58106
|
if (trimmed === "") {
|
|
57406
|
-
|
|
58107
|
+
resolve43(safeDefaultValue);
|
|
57407
58108
|
return;
|
|
57408
58109
|
}
|
|
57409
58110
|
const num = parseInt(trimmed, 10);
|
|
@@ -57412,7 +58113,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57412
58113
|
ask();
|
|
57413
58114
|
return;
|
|
57414
58115
|
}
|
|
57415
|
-
|
|
58116
|
+
resolve43(options[num - 1]);
|
|
57416
58117
|
});
|
|
57417
58118
|
}
|
|
57418
58119
|
ask();
|
|
@@ -57422,7 +58123,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57422
58123
|
const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
|
|
57423
58124
|
const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
|
|
57424
58125
|
const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
|
|
57425
|
-
return new Promise((
|
|
58126
|
+
return new Promise((resolve43) => {
|
|
57426
58127
|
function ask() {
|
|
57427
58128
|
const lines = [
|
|
57428
58129
|
`? ${question} (detected: ${defaultDisplay}):`,
|
|
@@ -57433,7 +58134,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57433
58134
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57434
58135
|
const trimmed = answer.trim();
|
|
57435
58136
|
if (trimmed === "") {
|
|
57436
|
-
|
|
58137
|
+
resolve43(defaultValue.length > 0 ? defaultValue : []);
|
|
57437
58138
|
return;
|
|
57438
58139
|
}
|
|
57439
58140
|
const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
|
|
@@ -57443,11 +58144,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57443
58144
|
return;
|
|
57444
58145
|
}
|
|
57445
58146
|
if (numbers.includes(1)) {
|
|
57446
|
-
|
|
58147
|
+
resolve43([]);
|
|
57447
58148
|
return;
|
|
57448
58149
|
}
|
|
57449
58150
|
const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
|
|
57450
|
-
|
|
58151
|
+
resolve43(selected);
|
|
57451
58152
|
});
|
|
57452
58153
|
}
|
|
57453
58154
|
ask();
|
|
@@ -57512,16 +58213,23 @@ async function runDoctor(cwd) {
|
|
|
57512
58213
|
lines.push("slopbrick doctor\n");
|
|
57513
58214
|
lines.push("Checking your setup:\n");
|
|
57514
58215
|
const nodeMajor = parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
57515
|
-
if (nodeMajor >= 20) ok(`Node ${process.versions.node}`);
|
|
58216
|
+
if (nodeMajor >= 20) ok(`Node ${process.versions.node}, slopbrick ${VERSION}`);
|
|
57516
58217
|
else fail(`Node ${process.versions.node} \u2014 slopbrick needs Node 20 or newer. Run: nvm install 20`);
|
|
57517
58218
|
lines.push(`
|
|
57518
58219
|
Working in: ${cwd}`);
|
|
57519
58220
|
try {
|
|
58221
|
+
const configPath = resolveConfigPath(cwd);
|
|
57520
58222
|
const config = await loadConfig(cwd);
|
|
57521
|
-
|
|
57522
|
-
|
|
57523
|
-
|
|
57524
|
-
|
|
58223
|
+
if (configPath) {
|
|
58224
|
+
const ruleCount = Object.keys(config.rules ?? {}).length;
|
|
58225
|
+
lines.push(`
|
|
58226
|
+
\u2713 Config loaded from: ${configPath}`);
|
|
58227
|
+
lines.push(` ${ruleCount} rule${ruleCount === 1 ? "" : "s"} configured in your config file.`);
|
|
58228
|
+
} else {
|
|
58229
|
+
lines.push(`
|
|
58230
|
+
\u2713 Config: using built-in defaults (no config file found).`);
|
|
58231
|
+
lines.push(chalk2.dim(` Run \`slopbrick init\` to create ${cwd.endsWith("/") ? cwd : cwd + "/"}slopbrick.config.mjs and tune rule severity/rules to your project.`));
|
|
58232
|
+
}
|
|
57525
58233
|
} catch (error) {
|
|
57526
58234
|
fail(`Could not read your config: ${error.message}`);
|
|
57527
58235
|
exitCode = 3;
|
|
@@ -57535,9 +58243,9 @@ async function runDoctor(cwd) {
|
|
|
57535
58243
|
}
|
|
57536
58244
|
try {
|
|
57537
58245
|
const { parseFile: tryParse } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
57538
|
-
const testFile =
|
|
57539
|
-
mkdirSync9(
|
|
57540
|
-
|
|
58246
|
+
const testFile = join23(cwd, ".slopbrick", ".doctor-test.ts");
|
|
58247
|
+
mkdirSync9(dirname13(testFile), { recursive: true });
|
|
58248
|
+
writeFileSync12(testFile, "export const x = 1;\n");
|
|
57541
58249
|
await tryParse(testFile);
|
|
57542
58250
|
rmSync2(testFile, { force: true });
|
|
57543
58251
|
ok("Parser is working.");
|
|
@@ -57578,6 +58286,7 @@ async function runDoctor(cwd) {
|
|
|
57578
58286
|
ok(`Found ${scannable.length} source files to scan.`);
|
|
57579
58287
|
} else {
|
|
57580
58288
|
warn("No source files (.ts/.tsx/.js/.jsx/.vue/.svelte/.astro/.mdx) found in this directory.");
|
|
58289
|
+
lines.push(chalk2.dim(" Run from a directory with source files, or pass --workspace <path>. Monorepos usually want `--workspace packages/<name>`."));
|
|
57581
58290
|
}
|
|
57582
58291
|
} catch {
|
|
57583
58292
|
}
|
|
@@ -57642,7 +58351,7 @@ function registerBadge(program) {
|
|
|
57642
58351
|
const health = loadHealth2(cwd);
|
|
57643
58352
|
if (health) {
|
|
57644
58353
|
const synthetic = {
|
|
57645
|
-
|
|
58354
|
+
aiSlopScore: health.aiSlopScore ?? 0
|
|
57646
58355
|
};
|
|
57647
58356
|
logger.info(formatBadge(synthetic));
|
|
57648
58357
|
process.exit(0);
|
|
@@ -57671,6 +58380,470 @@ function registerSuggest(program) {
|
|
|
57671
58380
|
});
|
|
57672
58381
|
}
|
|
57673
58382
|
|
|
58383
|
+
// src/cli/commands/composite.ts
|
|
58384
|
+
init_logger();
|
|
58385
|
+
init_telemetry();
|
|
58386
|
+
import { resolve as resolve15 } from "path";
|
|
58387
|
+
|
|
58388
|
+
// src/engine/cluster.ts
|
|
58389
|
+
import { createHash as createHash12 } from "crypto";
|
|
58390
|
+
var DEFAULT_CLUSTER_PARAMS = {
|
|
58391
|
+
minSupport: 0.05,
|
|
58392
|
+
// 5% of files
|
|
58393
|
+
minNPMI: 0.3,
|
|
58394
|
+
// Cohen's-Landis-and-Koch "fair" agreement
|
|
58395
|
+
fisherAlpha: 0.01,
|
|
58396
|
+
// Bonferroni-style floor for many-pair tests
|
|
58397
|
+
minClusterSize: 2
|
|
58398
|
+
};
|
|
58399
|
+
function adaptiveMinSupport(fileCount) {
|
|
58400
|
+
if (fileCount <= 0) return DEFAULT_CLUSTER_PARAMS.minSupport;
|
|
58401
|
+
return Math.max(DEFAULT_CLUSTER_PARAMS.minSupport, 5 / fileCount);
|
|
58402
|
+
}
|
|
58403
|
+
function computeRuleSupport(fireMatrix) {
|
|
58404
|
+
const support = /* @__PURE__ */ new Map();
|
|
58405
|
+
for (const ruleIds of fireMatrix.values()) {
|
|
58406
|
+
for (const ruleId of ruleIds) {
|
|
58407
|
+
support.set(ruleId, (support.get(ruleId) ?? 0) + 1);
|
|
58408
|
+
}
|
|
58409
|
+
}
|
|
58410
|
+
return support;
|
|
58411
|
+
}
|
|
58412
|
+
function prefilterBySupport(support, totalFiles, minSupport) {
|
|
58413
|
+
const kept = /* @__PURE__ */ new Set();
|
|
58414
|
+
const minCount = Math.max(1, Math.ceil(minSupport * totalFiles));
|
|
58415
|
+
for (const [ruleId, count] of support) {
|
|
58416
|
+
if (count >= minCount) kept.add(ruleId);
|
|
58417
|
+
}
|
|
58418
|
+
return kept;
|
|
58419
|
+
}
|
|
58420
|
+
function buildContingency(rA, rB, totalFiles) {
|
|
58421
|
+
let aAndB = 0;
|
|
58422
|
+
let aOnly = 0;
|
|
58423
|
+
let bOnly = 0;
|
|
58424
|
+
for (const file of rA) {
|
|
58425
|
+
if (rB.has(file)) aAndB++;
|
|
58426
|
+
else aOnly++;
|
|
58427
|
+
}
|
|
58428
|
+
for (const file of rB) {
|
|
58429
|
+
if (!rA.has(file)) bOnly++;
|
|
58430
|
+
}
|
|
58431
|
+
return {
|
|
58432
|
+
aAndB,
|
|
58433
|
+
aOnly,
|
|
58434
|
+
bOnly,
|
|
58435
|
+
neither: Math.max(0, totalFiles - aAndB - aOnly - bOnly)
|
|
58436
|
+
};
|
|
58437
|
+
}
|
|
58438
|
+
function fisherExactTwoSided(c) {
|
|
58439
|
+
const N = c.aAndB + c.aOnly + c.bOnly + c.neither;
|
|
58440
|
+
const K1 = c.aAndB + c.aOnly;
|
|
58441
|
+
const K2 = c.aAndB + c.bOnly;
|
|
58442
|
+
const n = K1;
|
|
58443
|
+
const kObs = c.aAndB;
|
|
58444
|
+
const logPmf = (k) => {
|
|
58445
|
+
if (k < Math.max(0, K1 + K2 - N) || k > Math.min(K1, K2)) return -Infinity;
|
|
58446
|
+
return lnChoose(K2, k) + lnChoose(N - K2, K1 - k) - lnChoose(N, K1);
|
|
58447
|
+
};
|
|
58448
|
+
const pObs = Math.exp(logPmf(kObs));
|
|
58449
|
+
const kMin = Math.max(0, K1 + K2 - N);
|
|
58450
|
+
const kMax = Math.min(K1, K2);
|
|
58451
|
+
let p = 0;
|
|
58452
|
+
for (let k = kMin; k <= kMax; k++) {
|
|
58453
|
+
const pk = Math.exp(logPmf(k));
|
|
58454
|
+
if (pk <= pObs + 1e-12) {
|
|
58455
|
+
p += pk;
|
|
58456
|
+
}
|
|
58457
|
+
}
|
|
58458
|
+
return Math.min(1, p);
|
|
58459
|
+
}
|
|
58460
|
+
function lnChoose(n, k) {
|
|
58461
|
+
if (k < 0 || k > n) return -Infinity;
|
|
58462
|
+
if (k === 0 || k === n) return 0;
|
|
58463
|
+
k = Math.min(k, n - k);
|
|
58464
|
+
let s = 0;
|
|
58465
|
+
for (let i = 1; i <= k; i++) {
|
|
58466
|
+
s += Math.log((n - k + i) / i);
|
|
58467
|
+
}
|
|
58468
|
+
return s;
|
|
58469
|
+
}
|
|
58470
|
+
function npmi(c, total) {
|
|
58471
|
+
if (total === 0 || c.aAndB === 0) return -1;
|
|
58472
|
+
const pXY = c.aAndB / total;
|
|
58473
|
+
const pX = (c.aAndB + c.aOnly) / total;
|
|
58474
|
+
const pY = (c.aAndB + c.bOnly) / total;
|
|
58475
|
+
if (pX === 0 || pY === 0 || pXY === 0) return -1;
|
|
58476
|
+
const pmi = Math.log2(pXY / (pX * pY));
|
|
58477
|
+
const norm = -Math.log2(pXY);
|
|
58478
|
+
return norm === 0 ? 1 : pmi / norm;
|
|
58479
|
+
}
|
|
58480
|
+
function computeEdges(fireMatrix, rules, total, params) {
|
|
58481
|
+
const ruleToFiles = /* @__PURE__ */ new Map();
|
|
58482
|
+
for (const ruleId of rules) ruleToFiles.set(ruleId, /* @__PURE__ */ new Set());
|
|
58483
|
+
for (const [file, fired] of fireMatrix) {
|
|
58484
|
+
for (const ruleId of fired) {
|
|
58485
|
+
if (rules.has(ruleId)) ruleToFiles.get(ruleId).add(file);
|
|
58486
|
+
}
|
|
58487
|
+
}
|
|
58488
|
+
const ruleList = Array.from(rules).sort();
|
|
58489
|
+
const edges = [];
|
|
58490
|
+
for (let i = 0; i < ruleList.length; i++) {
|
|
58491
|
+
for (let j = i + 1; j < ruleList.length; j++) {
|
|
58492
|
+
const a = ruleList[i];
|
|
58493
|
+
const b = ruleList[j];
|
|
58494
|
+
const rA = ruleToFiles.get(a);
|
|
58495
|
+
const rB = ruleToFiles.get(b);
|
|
58496
|
+
const c = buildContingency(rA, rB, total);
|
|
58497
|
+
const n = npmi(c, total);
|
|
58498
|
+
if (n < params.minNPMI) continue;
|
|
58499
|
+
const p = fisherExactTwoSided(c);
|
|
58500
|
+
if (p > params.fisherAlpha) continue;
|
|
58501
|
+
edges.push({ ruleIds: [a, b], weight: n, fisherP: p });
|
|
58502
|
+
}
|
|
58503
|
+
}
|
|
58504
|
+
return edges;
|
|
58505
|
+
}
|
|
58506
|
+
function singleLinkageCluster(edges, minClusterSize) {
|
|
58507
|
+
const parent = /* @__PURE__ */ new Map();
|
|
58508
|
+
const find = (x) => {
|
|
58509
|
+
if (!parent.has(x)) {
|
|
58510
|
+
parent.set(x, x);
|
|
58511
|
+
return x;
|
|
58512
|
+
}
|
|
58513
|
+
if (parent.get(x) === x) return x;
|
|
58514
|
+
const root = find(parent.get(x));
|
|
58515
|
+
parent.set(x, root);
|
|
58516
|
+
return root;
|
|
58517
|
+
};
|
|
58518
|
+
const union = (a, b) => {
|
|
58519
|
+
const ra = find(a);
|
|
58520
|
+
const rb = find(b);
|
|
58521
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
58522
|
+
};
|
|
58523
|
+
for (const e of edges) {
|
|
58524
|
+
union(e.ruleIds[0], e.ruleIds[1]);
|
|
58525
|
+
}
|
|
58526
|
+
const groups = /* @__PURE__ */ new Map();
|
|
58527
|
+
for (const k of parent.keys()) {
|
|
58528
|
+
const root = find(k);
|
|
58529
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
58530
|
+
groups.get(root).push(k);
|
|
58531
|
+
}
|
|
58532
|
+
const clusters = [];
|
|
58533
|
+
for (const members of groups.values()) {
|
|
58534
|
+
if (members.length < minClusterSize) continue;
|
|
58535
|
+
members.sort();
|
|
58536
|
+
clusters.push(members);
|
|
58537
|
+
}
|
|
58538
|
+
clusters.sort((a, b) => b.length - a.length || a[0].localeCompare(b[0]));
|
|
58539
|
+
return clusters;
|
|
58540
|
+
}
|
|
58541
|
+
function filesForComposite(fireMatrix, members, minMatch) {
|
|
58542
|
+
const fired = /* @__PURE__ */ new Set();
|
|
58543
|
+
for (const [file, ids] of fireMatrix) {
|
|
58544
|
+
let n = 0;
|
|
58545
|
+
for (const m of members) if (ids.has(m)) n++;
|
|
58546
|
+
if (n >= minMatch) fired.add(file);
|
|
58547
|
+
}
|
|
58548
|
+
return fired;
|
|
58549
|
+
}
|
|
58550
|
+
var DEFAULT_CALIBRATION = {
|
|
58551
|
+
// 1.5× is the documented floor.
|
|
58552
|
+
recallFpRatio: 1.5,
|
|
58553
|
+
// 0.05 absolute FP — the same `min` value most rules get via the
|
|
58554
|
+
// v0.18.x calibration contract.
|
|
58555
|
+
minFpFloor: 0.05
|
|
58556
|
+
};
|
|
58557
|
+
function calibrateCluster(cluster2, edges, fireMatrix, positiveFiles, totalFiles, thresholds = DEFAULT_CALIBRATION) {
|
|
58558
|
+
if (cluster2.length < 2) return null;
|
|
58559
|
+
const clusterSet = new Set(cluster2);
|
|
58560
|
+
const clusterEdges = edges.filter(
|
|
58561
|
+
(e) => clusterSet.has(e.ruleIds[0]) && clusterSet.has(e.ruleIds[1])
|
|
58562
|
+
);
|
|
58563
|
+
if (clusterEdges.length === 0) return null;
|
|
58564
|
+
const strongest = clusterEdges.reduce((a, b) => b.weight > a.weight ? b : a);
|
|
58565
|
+
let best = null;
|
|
58566
|
+
for (let k = 1; k <= cluster2.length; k++) {
|
|
58567
|
+
const firedFiles = filesForComposite(fireMatrix, cluster2, k);
|
|
58568
|
+
const nFiles = firedFiles.size;
|
|
58569
|
+
if (nFiles === 0) continue;
|
|
58570
|
+
let recall = 0;
|
|
58571
|
+
let fp = 0;
|
|
58572
|
+
let precision = 0;
|
|
58573
|
+
let canCalibrate = false;
|
|
58574
|
+
if (positiveFiles && positiveFiles.size > 0) {
|
|
58575
|
+
let tp = 0;
|
|
58576
|
+
let fpc = 0;
|
|
58577
|
+
for (const file of firedFiles) {
|
|
58578
|
+
if (positiveFiles.has(file)) tp++;
|
|
58579
|
+
else fpc++;
|
|
58580
|
+
}
|
|
58581
|
+
const totalPos = positiveFiles.size;
|
|
58582
|
+
const totalNeg = Math.max(1, totalFiles - totalPos);
|
|
58583
|
+
recall = totalPos > 0 ? tp / totalPos : 0;
|
|
58584
|
+
fp = fpc / totalNeg;
|
|
58585
|
+
precision = tp + fpc > 0 ? tp / (tp + fpc) : 0;
|
|
58586
|
+
canCalibrate = true;
|
|
58587
|
+
}
|
|
58588
|
+
const F1 = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0;
|
|
58589
|
+
if (canCalibrate) {
|
|
58590
|
+
if (recall / Math.max(fp, 1e-9) < thresholds.recallFpRatio) continue;
|
|
58591
|
+
if (fp < thresholds.minFpFloor && recall < 0.5) continue;
|
|
58592
|
+
}
|
|
58593
|
+
if (best === null || F1 > best.F1 + 1e-9 || Math.abs(F1 - best.F1) < 1e-9 && k > best.minMatch) {
|
|
58594
|
+
best = {
|
|
58595
|
+
minMatch: k,
|
|
58596
|
+
npmi: strongest.weight,
|
|
58597
|
+
fisherP: strongest.fisherP,
|
|
58598
|
+
recall,
|
|
58599
|
+
FP: fp,
|
|
58600
|
+
precision,
|
|
58601
|
+
F1,
|
|
58602
|
+
nFiles
|
|
58603
|
+
};
|
|
58604
|
+
}
|
|
58605
|
+
}
|
|
58606
|
+
return best;
|
|
58607
|
+
}
|
|
58608
|
+
function compositeId(members) {
|
|
58609
|
+
const sorted = [...members].sort();
|
|
58610
|
+
return "composite/" + createHash12("sha1").update(sorted.join("\n")).digest("hex").slice(0, 12);
|
|
58611
|
+
}
|
|
58612
|
+
function worstSeverity(members, memberSeverities) {
|
|
58613
|
+
let worst = "low";
|
|
58614
|
+
const order = { low: 0, medium: 1, high: 2 };
|
|
58615
|
+
for (const m of members) {
|
|
58616
|
+
const s = memberSeverities?.get(m);
|
|
58617
|
+
if (s && order[s] > order[worst]) worst = s;
|
|
58618
|
+
}
|
|
58619
|
+
return worst;
|
|
58620
|
+
}
|
|
58621
|
+
function describeComposite(members, minMatch) {
|
|
58622
|
+
const clusterPart = members.length <= 4 ? members.join("+") : `${members.slice(0, 3).join("+")}+${members.length - 3} more`;
|
|
58623
|
+
return `Composite rule: at least ${minMatch} of {${clusterPart}} fire on the same file.`;
|
|
58624
|
+
}
|
|
58625
|
+
function runClusterer(input) {
|
|
58626
|
+
const total = input.fireMatrix.size;
|
|
58627
|
+
const minSupport = input.params?.minSupport ?? adaptiveMinSupport(total);
|
|
58628
|
+
const params = {
|
|
58629
|
+
minSupport,
|
|
58630
|
+
minNPMI: input.params?.minNPMI ?? DEFAULT_CLUSTER_PARAMS.minNPMI,
|
|
58631
|
+
fisherAlpha: input.params?.fisherAlpha ?? DEFAULT_CLUSTER_PARAMS.fisherAlpha,
|
|
58632
|
+
minClusterSize: input.params?.minClusterSize ?? DEFAULT_CLUSTER_PARAMS.minClusterSize
|
|
58633
|
+
};
|
|
58634
|
+
const support = computeRuleSupport(input.fireMatrix);
|
|
58635
|
+
const rulesKept = prefilterBySupport(support, total, params.minSupport);
|
|
58636
|
+
const edges = computeEdges(input.fireMatrix, rulesKept, total, params);
|
|
58637
|
+
const clusters = singleLinkageCluster(edges, params.minClusterSize);
|
|
58638
|
+
const entries = [];
|
|
58639
|
+
const clusterMaxNPMI = /* @__PURE__ */ new Map();
|
|
58640
|
+
for (const cluster2 of clusters) {
|
|
58641
|
+
const cal = calibrateCluster(
|
|
58642
|
+
cluster2,
|
|
58643
|
+
edges,
|
|
58644
|
+
input.fireMatrix,
|
|
58645
|
+
input.positiveFiles,
|
|
58646
|
+
total
|
|
58647
|
+
);
|
|
58648
|
+
if (!cal) continue;
|
|
58649
|
+
const id = compositeId(cluster2);
|
|
58650
|
+
const severity = worstSeverity(cluster2, input.memberSeverities);
|
|
58651
|
+
entries.push({
|
|
58652
|
+
id,
|
|
58653
|
+
ruleIds: [...cluster2].sort(),
|
|
58654
|
+
minMatch: cal.minMatch,
|
|
58655
|
+
severity,
|
|
58656
|
+
defaultOff: true,
|
|
58657
|
+
description: describeComposite(cluster2, cal.minMatch),
|
|
58658
|
+
calibration: {
|
|
58659
|
+
recall: round3(cal.recall),
|
|
58660
|
+
FP: round3(cal.FP),
|
|
58661
|
+
precision: round3(cal.precision),
|
|
58662
|
+
F1: round3(cal.F1),
|
|
58663
|
+
nFiles: cal.nFiles
|
|
58664
|
+
},
|
|
58665
|
+
provenance: {
|
|
58666
|
+
seed: "auto-cluster",
|
|
58667
|
+
discoveredAt: input.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
58668
|
+
nFiles: total,
|
|
58669
|
+
members: cluster2.length,
|
|
58670
|
+
npmi: round3(cal.npmi),
|
|
58671
|
+
fisherP: round3(cal.fisherP)
|
|
58672
|
+
}
|
|
58673
|
+
});
|
|
58674
|
+
clusterMaxNPMI.set(id, cal.npmi);
|
|
58675
|
+
}
|
|
58676
|
+
entries.sort((a, b) => b.calibration.F1 - a.calibration.F1 || a.id.localeCompare(b.id));
|
|
58677
|
+
return { entries, supportCounts: support, clusterMaxNPMI, filesScanned: total, params };
|
|
58678
|
+
}
|
|
58679
|
+
function round3(n) {
|
|
58680
|
+
return Math.round(n * 1e3) / 1e3;
|
|
58681
|
+
}
|
|
58682
|
+
|
|
58683
|
+
// src/rules/composite-loader.ts
|
|
58684
|
+
import { existsSync as existsSync22, readFileSync as readFileSync26, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
58685
|
+
import { dirname as dirname14, join as join24 } from "path";
|
|
58686
|
+
var COMPOSITES_FILE = "composites.json";
|
|
58687
|
+
function compositesPath(cwd) {
|
|
58688
|
+
return join24(cwd, COMPOSITES_FILE);
|
|
58689
|
+
}
|
|
58690
|
+
function readComposites(cwd) {
|
|
58691
|
+
const path = compositesPath(cwd);
|
|
58692
|
+
if (!existsSync22(path)) return [];
|
|
58693
|
+
try {
|
|
58694
|
+
const raw = JSON.parse(readFileSync26(path, "utf-8"));
|
|
58695
|
+
if (!Array.isArray(raw)) return [];
|
|
58696
|
+
return raw.filter(isCompositeRuleEntry);
|
|
58697
|
+
} catch {
|
|
58698
|
+
return [];
|
|
58699
|
+
}
|
|
58700
|
+
}
|
|
58701
|
+
function isCompositeRuleEntry(value) {
|
|
58702
|
+
if (!value || typeof value !== "object") return false;
|
|
58703
|
+
const v = value;
|
|
58704
|
+
return typeof v.id === "string" && Array.isArray(v.ruleIds) && typeof v.minMatch === "number";
|
|
58705
|
+
}
|
|
58706
|
+
function writeComposites(cwd, entries) {
|
|
58707
|
+
const path = compositesPath(cwd);
|
|
58708
|
+
mkdirSync10(dirname14(path), { recursive: true });
|
|
58709
|
+
const tmp = `${path}.tmp`;
|
|
58710
|
+
writeFileSync13(tmp, JSON.stringify(entries, null, 2), "utf-8");
|
|
58711
|
+
const fs = __require("fs");
|
|
58712
|
+
fs.renameSync(tmp, path);
|
|
58713
|
+
}
|
|
58714
|
+
|
|
58715
|
+
// src/cli/commands/composite.ts
|
|
58716
|
+
function registerComposite(program) {
|
|
58717
|
+
const cmd = program.command("composite").description(
|
|
58718
|
+
"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."
|
|
58719
|
+
);
|
|
58720
|
+
cmd.command("discover").description(
|
|
58721
|
+
"run the empirical composite clusterer on .slopbrick/flywheel/scans.jsonl; writes composites.json"
|
|
58722
|
+
).action(async (_cmdOptions, command) => {
|
|
58723
|
+
const options = command.optsWithGlobals();
|
|
58724
|
+
const cwd = resolve15(options.workspace ?? process.cwd());
|
|
58725
|
+
const payloads = readTelemetry(cwd);
|
|
58726
|
+
if (payloads.length === 0) {
|
|
58727
|
+
logger.error(
|
|
58728
|
+
"`composite discover` needs at least 2 scans in .slopbrick/flywheel/scans.jsonl. Run `slopbrick scan` a few times first."
|
|
58729
|
+
);
|
|
58730
|
+
process.exit(1);
|
|
58731
|
+
}
|
|
58732
|
+
const fireMatrix = /* @__PURE__ */ new Map();
|
|
58733
|
+
for (const payload of payloads) {
|
|
58734
|
+
for (const f of payload.files) {
|
|
58735
|
+
const set = fireMatrix.get(f.hash) ?? /* @__PURE__ */ new Set();
|
|
58736
|
+
for (const id of f.ruleIds) set.add(id);
|
|
58737
|
+
if (set.size > 0) fireMatrix.set(f.hash, set);
|
|
58738
|
+
}
|
|
58739
|
+
}
|
|
58740
|
+
const out = runClusterer({
|
|
58741
|
+
fireMatrix,
|
|
58742
|
+
// No labeled positive set; let the clusterer surface for audit.
|
|
58743
|
+
positiveFiles: void 0,
|
|
58744
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
58745
|
+
});
|
|
58746
|
+
const finalEntries = out.entries.length === 0 ? [...(await Promise.resolve().then(() => (init_composite_seed(), composite_seed_exports))).HAND_CURATED_SEED] : out.entries;
|
|
58747
|
+
writeComposites(cwd, finalEntries);
|
|
58748
|
+
if (out.entries.length === 0) {
|
|
58749
|
+
logger.info("Composite discovery: no clusters passed the empirical gate.");
|
|
58750
|
+
logger.info(
|
|
58751
|
+
" (Try lowering --min-support, or run more scans to densify the ledger.)"
|
|
58752
|
+
);
|
|
58753
|
+
return;
|
|
58754
|
+
}
|
|
58755
|
+
logger.info(
|
|
58756
|
+
`Composite discovery: ${out.entries.length} candidate(s) written to composites.json`
|
|
58757
|
+
);
|
|
58758
|
+
const lines = [];
|
|
58759
|
+
for (const e of out.entries) {
|
|
58760
|
+
lines.push("");
|
|
58761
|
+
lines.push(
|
|
58762
|
+
` ${e.id} minMatch=${e.minMatch} F1=${e.calibration.F1} (${e.ruleIds.length} members)`
|
|
58763
|
+
);
|
|
58764
|
+
lines.push(` ${describeComposite(e.ruleIds, e.minMatch)}`);
|
|
58765
|
+
lines.push(
|
|
58766
|
+
` provenance: ${e.provenance.seed} npmi=${e.provenance.npmi} fisherP=${e.provenance.fisherP}`
|
|
58767
|
+
);
|
|
58768
|
+
}
|
|
58769
|
+
logger.info(lines.join("\n"));
|
|
58770
|
+
logger.info(
|
|
58771
|
+
"\nNext step: review each entry. Opt in with `slopbrick composite enable <id>`; opt out by removing the entry."
|
|
58772
|
+
);
|
|
58773
|
+
});
|
|
58774
|
+
cmd.command("enable <id>").description(
|
|
58775
|
+
"opt a specific composite into slopbrick.config.mjs#compositeRules (one composite per call)"
|
|
58776
|
+
).action(async (id, _cmdOptions, command) => {
|
|
58777
|
+
const options = command.optsWithGlobals();
|
|
58778
|
+
const cwd = resolve15(options.workspace ?? process.cwd());
|
|
58779
|
+
const composites = readComposites(cwd);
|
|
58780
|
+
const target = composites.find((c) => c.id === id);
|
|
58781
|
+
if (!target) {
|
|
58782
|
+
logger.error(
|
|
58783
|
+
`Composite "${id}" not found in composites.json. Run \`slopbrick composite discover\` first to populate the ledger.`
|
|
58784
|
+
);
|
|
58785
|
+
process.exit(1);
|
|
58786
|
+
}
|
|
58787
|
+
const cfgPath = resolve15(cwd, "slopbrick.config.mjs");
|
|
58788
|
+
const fs = __require("fs");
|
|
58789
|
+
if (!fs.existsSync(cfgPath)) {
|
|
58790
|
+
logger.error(`No slopbrick.config.mjs found at ${cfgPath}.`);
|
|
58791
|
+
process.exit(1);
|
|
58792
|
+
}
|
|
58793
|
+
const src = fs.readFileSync(cfgPath, "utf-8");
|
|
58794
|
+
const compositeSnippet = JSON.stringify(
|
|
58795
|
+
{
|
|
58796
|
+
id: target.id,
|
|
58797
|
+
category: "ai",
|
|
58798
|
+
severity: target.severity,
|
|
58799
|
+
aiSpecific: true,
|
|
58800
|
+
description: target.description,
|
|
58801
|
+
defaultOff: true,
|
|
58802
|
+
ruleIds: target.ruleIds,
|
|
58803
|
+
minMatch: target.minMatch,
|
|
58804
|
+
create: "() => ({})",
|
|
58805
|
+
analyze: "() => []"
|
|
58806
|
+
},
|
|
58807
|
+
null,
|
|
58808
|
+
2
|
|
58809
|
+
);
|
|
58810
|
+
if (src.includes(`id: '${target.id}'`) || src.includes(`id: "${target.id}"`)) {
|
|
58811
|
+
logger.info(`Composite "${id}" is already enabled in slopbrick.config.mjs (idempotent).`);
|
|
58812
|
+
return;
|
|
58813
|
+
}
|
|
58814
|
+
const updated = src.replace(
|
|
58815
|
+
/(export default\s*{[\s\S]*?)(}\s*;?\s*)$/,
|
|
58816
|
+
(_match, head, tail) => `${head} compositeRules: [
|
|
58817
|
+
${compositeSnippet}
|
|
58818
|
+
],
|
|
58819
|
+
${tail}`
|
|
58820
|
+
);
|
|
58821
|
+
fs.writeFileSync(cfgPath, updated, "utf-8");
|
|
58822
|
+
logger.info(`Composite "${id}" enabled.`);
|
|
58823
|
+
logger.info("Next scan will apply it; rerun `slopbrick scan` to see the effect.");
|
|
58824
|
+
});
|
|
58825
|
+
cmd.command("list").description("list composites currently registered (composites.json + config-declared)").action(async (_cmdOptions, command) => {
|
|
58826
|
+
const options = command.optsWithGlobals();
|
|
58827
|
+
const cwd = resolve15(options.workspace ?? process.cwd());
|
|
58828
|
+
const composites = readComposites(cwd);
|
|
58829
|
+
if (composites.length === 0) {
|
|
58830
|
+
logger.info("No composites registered. Run `slopbrick composite discover` first.");
|
|
58831
|
+
return;
|
|
58832
|
+
}
|
|
58833
|
+
const lines = ["Registered composites:"];
|
|
58834
|
+
for (const e of composites) {
|
|
58835
|
+
lines.push(
|
|
58836
|
+
`
|
|
58837
|
+
${e.id}
|
|
58838
|
+
${describeComposite(e.ruleIds, e.minMatch)}
|
|
58839
|
+
members=[${e.ruleIds.join(", ")}] minMatch=${e.minMatch}
|
|
58840
|
+
F1=${e.calibration.F1} recall=${e.calibration.recall} FP=${e.calibration.FP}`
|
|
58841
|
+
);
|
|
58842
|
+
}
|
|
58843
|
+
logger.info(lines.join("\n"));
|
|
58844
|
+
});
|
|
58845
|
+
}
|
|
58846
|
+
|
|
57674
58847
|
// src/cli/explain.ts
|
|
57675
58848
|
var RULES_BASE_URL2 = "https://github.com/usebrick/platform/blob/main/packages/slopbrick/src/rules";
|
|
57676
58849
|
function ruleIdToFilename2(ruleId) {
|
|
@@ -57720,154 +58893,13 @@ function formatExplain(result) {
|
|
|
57720
58893
|
// src/cli/commands/explain.ts
|
|
57721
58894
|
init_logger();
|
|
57722
58895
|
init_builtins();
|
|
57723
|
-
|
|
57724
|
-
// src/snippet/data.ts
|
|
57725
|
-
var CATEGORY_DIRECTIVES = {
|
|
57726
|
-
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.',
|
|
57727
|
-
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.",
|
|
57728
|
-
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.',
|
|
57729
|
-
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".',
|
|
57730
|
-
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.",
|
|
57731
|
-
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
57732
|
-
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].",
|
|
57733
|
-
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
57734
|
-
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.",
|
|
57735
|
-
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'."
|
|
57736
|
-
};
|
|
57737
|
-
var RULE_HINTS = {
|
|
57738
|
-
// v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
|
|
57739
|
-
// remaining key matches a rule in src/rules/builtins.ts. No orphans.
|
|
57740
|
-
// (v0.16.0 previously moved 35 out-of-scope orphan hints to
|
|
57741
|
-
// docs/research/backlog-rule-hints.md
|
|
57742
|
-
// for future implementers to paste back when the corresponding rules ship.)
|
|
57743
|
-
"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.",
|
|
57744
|
-
"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.",
|
|
57745
|
-
"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.",
|
|
57746
|
-
"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.",
|
|
57747
|
-
"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.",
|
|
57748
|
-
"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.",
|
|
57749
|
-
"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.',
|
|
57750
|
-
"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.",
|
|
57751
|
-
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
57752
|
-
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
57753
|
-
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
57754
|
-
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
57755
|
-
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
57756
|
-
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
57757
|
-
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
57758
|
-
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
57759
|
-
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
57760
|
-
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
57761
|
-
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
57762
|
-
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
57763
|
-
"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.",
|
|
57764
|
-
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
57765
|
-
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
57766
|
-
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
57767
|
-
"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.",
|
|
57768
|
-
"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.",
|
|
57769
|
-
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
57770
|
-
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
57771
|
-
"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.)",
|
|
57772
|
-
"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.)",
|
|
57773
|
-
"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.)",
|
|
57774
|
-
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
57775
|
-
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
57776
|
-
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
57777
|
-
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
57778
|
-
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
57779
|
-
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
57780
|
-
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
57781
|
-
"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.",
|
|
57782
|
-
"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.",
|
|
57783
|
-
"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.",
|
|
57784
|
-
"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.",
|
|
57785
|
-
"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.",
|
|
57786
|
-
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
57787
|
-
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
57788
|
-
"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.',
|
|
57789
|
-
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
57790
|
-
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
57791
|
-
"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.",
|
|
57792
|
-
"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.",
|
|
57793
|
-
"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.",
|
|
57794
|
-
"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%.",
|
|
57795
|
-
"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.",
|
|
57796
|
-
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
57797
|
-
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
57798
|
-
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
57799
|
-
"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.",
|
|
57800
|
-
"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`).",
|
|
57801
|
-
"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.",
|
|
57802
|
-
"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.",
|
|
57803
|
-
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
57804
|
-
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
57805
|
-
"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).",
|
|
57806
|
-
"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).",
|
|
57807
|
-
"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).",
|
|
57808
|
-
"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).",
|
|
57809
|
-
"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).",
|
|
57810
|
-
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
57811
|
-
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
57812
|
-
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
57813
|
-
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
57814
|
-
// reclassified post-v7 calibration in v0.14.5d)
|
|
57815
|
-
"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.",
|
|
57816
|
-
"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.",
|
|
57817
|
-
"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.",
|
|
57818
|
-
"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.",
|
|
57819
|
-
"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.",
|
|
57820
|
-
"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.",
|
|
57821
|
-
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
57822
|
-
"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.',
|
|
57823
|
-
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
57824
|
-
"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.",
|
|
57825
|
-
"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.",
|
|
57826
|
-
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
|
|
57827
|
-
// v0.18.9 — Rust-specific rules (tree-sitter-backed)
|
|
57828
|
-
"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.",
|
|
57829
|
-
"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.",
|
|
57830
|
-
"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.",
|
|
57831
|
-
"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.',
|
|
57832
|
-
// v0.19.0 — TypeScript-specific rules
|
|
57833
|
-
"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.",
|
|
57834
|
-
"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.",
|
|
57835
|
-
"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.",
|
|
57836
|
-
"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.",
|
|
57837
|
-
// v0.19.0 — Go-specific rules
|
|
57838
|
-
"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).',
|
|
57839
|
-
"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.",
|
|
57840
|
-
// v0.19.0 — Duplication detector
|
|
57841
|
-
"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.",
|
|
57842
|
-
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
57843
|
-
"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.)",
|
|
57844
|
-
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
57845
|
-
"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.)",
|
|
57846
|
-
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
57847
|
-
// v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
|
|
57848
|
-
// v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
|
|
57849
|
-
"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.)",
|
|
57850
|
-
"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.)",
|
|
57851
|
-
// v0.35.0 — content-based detection (CoCoNUTS-inspired)
|
|
57852
|
-
"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.",
|
|
57853
|
-
// v0.35.1 — Raidar-inspired content-based detection
|
|
57854
|
-
"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)`.',
|
|
57855
|
-
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
57856
|
-
"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.)",
|
|
57857
|
-
"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.)",
|
|
57858
|
-
"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.)",
|
|
57859
|
-
"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.)",
|
|
57860
|
-
"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.)",
|
|
57861
|
-
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
57862
|
-
"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.)",
|
|
57863
|
-
"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.)",
|
|
57864
|
-
"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.)",
|
|
57865
|
-
"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.)"
|
|
57866
|
-
};
|
|
57867
|
-
|
|
57868
|
-
// src/cli/commands/explain.ts
|
|
58896
|
+
init_data();
|
|
57869
58897
|
function registerExplain(program) {
|
|
57870
|
-
program.command("explain
|
|
58898
|
+
program.command("explain [ruleId]").description("Print rationale, pattern, and remediation for a single rule. With no ruleId, lists the rules.").action((ruleId) => {
|
|
58899
|
+
if (!ruleId) {
|
|
58900
|
+
logger.warn("Missing ruleId. Run `slopbrick rules` to see available rules, then `slopbrick explain <ruleId>`.");
|
|
58901
|
+
process.exit(2);
|
|
58902
|
+
}
|
|
57871
58903
|
const result = explainRule(ruleId, builtinRules, RULE_HINTS);
|
|
57872
58904
|
logger.info(formatExplain(result));
|
|
57873
58905
|
if ("error" in result) process.exit(2);
|
|
@@ -61591,17 +62623,17 @@ function registerCalibration(program) {
|
|
|
61591
62623
|
// src/cli/commands/install.ts
|
|
61592
62624
|
init_logger();
|
|
61593
62625
|
init_git();
|
|
61594
|
-
import { resolve as
|
|
62626
|
+
import { resolve as resolve16 } from "path";
|
|
61595
62627
|
|
|
61596
62628
|
// src/cli/installer.ts
|
|
61597
62629
|
import {
|
|
61598
62630
|
chmodSync,
|
|
61599
|
-
existsSync as
|
|
61600
|
-
mkdirSync as
|
|
61601
|
-
readFileSync as
|
|
61602
|
-
writeFileSync as
|
|
62631
|
+
existsSync as existsSync23,
|
|
62632
|
+
mkdirSync as mkdirSync11,
|
|
62633
|
+
readFileSync as readFileSync27,
|
|
62634
|
+
writeFileSync as writeFileSync14
|
|
61603
62635
|
} from "fs";
|
|
61604
|
-
import { dirname as dirname16, join as
|
|
62636
|
+
import { dirname as dirname16, join as join25 } from "path";
|
|
61605
62637
|
var BEGIN_SENTINEL = "# slopbrick-hook-begin";
|
|
61606
62638
|
var END_SENTINEL = "# slopbrick-hook-end";
|
|
61607
62639
|
var SENTINEL_BLOCK = `${BEGIN_SENTINEL}
|
|
@@ -61609,14 +62641,14 @@ npx slopbrick --staged
|
|
|
61609
62641
|
${END_SENTINEL}
|
|
61610
62642
|
`;
|
|
61611
62643
|
function hookPath(gitRoot) {
|
|
61612
|
-
const huskyDir =
|
|
61613
|
-
if (
|
|
61614
|
-
return
|
|
62644
|
+
const huskyDir = join25(gitRoot, ".husky");
|
|
62645
|
+
if (existsSync23(huskyDir)) {
|
|
62646
|
+
return join25(huskyDir, "pre-commit");
|
|
61615
62647
|
}
|
|
61616
|
-
return
|
|
62648
|
+
return join25(gitRoot, ".git", "hooks", "pre-commit");
|
|
61617
62649
|
}
|
|
61618
62650
|
function readHookContent(path) {
|
|
61619
|
-
return
|
|
62651
|
+
return readFileSync27(path, "utf8");
|
|
61620
62652
|
}
|
|
61621
62653
|
function sentinelsPresent(content) {
|
|
61622
62654
|
const lines = content.split(/\r?\n/);
|
|
@@ -61638,7 +62670,7 @@ function replaceSentinelBlock(content) {
|
|
|
61638
62670
|
}
|
|
61639
62671
|
function installHook(gitRoot) {
|
|
61640
62672
|
const path = hookPath(gitRoot);
|
|
61641
|
-
if (
|
|
62673
|
+
if (existsSync23(path)) {
|
|
61642
62674
|
const content = readHookContent(path);
|
|
61643
62675
|
const { begin, end } = sentinelsPresent(content);
|
|
61644
62676
|
if (begin && end) {
|
|
@@ -61650,7 +62682,7 @@ function installHook(gitRoot) {
|
|
|
61650
62682
|
exitCode: 0
|
|
61651
62683
|
};
|
|
61652
62684
|
}
|
|
61653
|
-
|
|
62685
|
+
writeFileSync14(path, replaced.endsWith("\n") ? replaced : `${replaced}
|
|
61654
62686
|
`);
|
|
61655
62687
|
chmodSync(path, 493);
|
|
61656
62688
|
return {
|
|
@@ -61670,7 +62702,7 @@ function installHook(gitRoot) {
|
|
|
61670
62702
|
}
|
|
61671
62703
|
const normalized = content.length > 0 && !content.endsWith("\n") ? `${content}
|
|
61672
62704
|
` : content;
|
|
61673
|
-
|
|
62705
|
+
writeFileSync14(path, `${normalized}${SENTINEL_BLOCK}`);
|
|
61674
62706
|
chmodSync(path, 493);
|
|
61675
62707
|
return {
|
|
61676
62708
|
ok: true,
|
|
@@ -61678,8 +62710,8 @@ function installHook(gitRoot) {
|
|
|
61678
62710
|
exitCode: 0
|
|
61679
62711
|
};
|
|
61680
62712
|
}
|
|
61681
|
-
|
|
61682
|
-
|
|
62713
|
+
mkdirSync11(dirname16(path), { recursive: true });
|
|
62714
|
+
writeFileSync14(path, SENTINEL_BLOCK, { mode: 493 });
|
|
61683
62715
|
chmodSync(path, 493);
|
|
61684
62716
|
return {
|
|
61685
62717
|
ok: true,
|
|
@@ -61689,7 +62721,7 @@ function installHook(gitRoot) {
|
|
|
61689
62721
|
}
|
|
61690
62722
|
function uninstallHook(gitRoot) {
|
|
61691
62723
|
const path = hookPath(gitRoot);
|
|
61692
|
-
if (!
|
|
62724
|
+
if (!existsSync23(path)) {
|
|
61693
62725
|
return {
|
|
61694
62726
|
ok: true,
|
|
61695
62727
|
message: "Hook not installed",
|
|
@@ -61725,7 +62757,7 @@ function uninstallHook(gitRoot) {
|
|
|
61725
62757
|
}
|
|
61726
62758
|
const remaining = [...lines.slice(0, start), ...lines.slice(end + 1)];
|
|
61727
62759
|
const result = remaining.join("\n");
|
|
61728
|
-
|
|
62760
|
+
writeFileSync14(path, result.endsWith("\n") ? result : `${result}
|
|
61729
62761
|
`);
|
|
61730
62762
|
return {
|
|
61731
62763
|
ok: true,
|
|
@@ -61738,7 +62770,7 @@ function uninstallHook(gitRoot) {
|
|
|
61738
62770
|
function registerInstall(program) {
|
|
61739
62771
|
program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
61740
62772
|
const options = command.optsWithGlobals();
|
|
61741
|
-
const cwd =
|
|
62773
|
+
const cwd = resolve16(options.workspace ?? process.cwd());
|
|
61742
62774
|
const root = getGitRoot(cwd);
|
|
61743
62775
|
if (!root) {
|
|
61744
62776
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -61755,11 +62787,11 @@ function registerInstall(program) {
|
|
|
61755
62787
|
// src/cli/commands/uninstall.ts
|
|
61756
62788
|
init_logger();
|
|
61757
62789
|
init_git();
|
|
61758
|
-
import { resolve as
|
|
62790
|
+
import { resolve as resolve17 } from "path";
|
|
61759
62791
|
function registerUninstall(program) {
|
|
61760
62792
|
program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
61761
62793
|
const options = command.optsWithGlobals();
|
|
61762
|
-
const cwd =
|
|
62794
|
+
const cwd = resolve17(options.workspace ?? process.cwd());
|
|
61763
62795
|
const root = getGitRoot(cwd);
|
|
61764
62796
|
if (!root) {
|
|
61765
62797
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -61787,12 +62819,12 @@ init_patterns();
|
|
|
61787
62819
|
init_architecture_score();
|
|
61788
62820
|
init_business_logic();
|
|
61789
62821
|
init_structure_md();
|
|
61790
|
-
import { readFileSync as
|
|
61791
|
-
import { resolve as
|
|
62822
|
+
import { readFileSync as readFileSync28 } from "fs";
|
|
62823
|
+
import { resolve as resolve18 } from "path";
|
|
61792
62824
|
var TOOL_DEFINITIONS = [
|
|
61793
62825
|
{
|
|
61794
62826
|
name: "slop_scan_file",
|
|
61795
|
-
description:
|
|
62827
|
+
description: 'Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, category, severity, line, column, message, advice), a composite AI-likelihood score (probability + confidenceTier), and a componentCount. The composite score is the Bayesian log-likelihood ratio of all rules that fired, NOT a per-file "Slop Index" \u2014 for project-level scores use slop_suggest.',
|
|
61796
62828
|
inputSchema: {
|
|
61797
62829
|
type: "object",
|
|
61798
62830
|
properties: {
|
|
@@ -61808,7 +62840,7 @@ var TOOL_DEFINITIONS = [
|
|
|
61808
62840
|
},
|
|
61809
62841
|
{
|
|
61810
62842
|
name: "slop_explain_rule",
|
|
61811
|
-
description: "Return metadata for a single rule
|
|
62843
|
+
description: "Return metadata for a single rule: ruleId, category, severity, aiSpecific, a brief rationale string, and a whereToLook path into src/rules/. The actual fix-steps live in the rule source (whereToLook) rather than in this response \u2014 read that file to understand the rule before auto-applying --fix.",
|
|
61812
62844
|
inputSchema: {
|
|
61813
62845
|
type: "object",
|
|
61814
62846
|
properties: {
|
|
@@ -61829,7 +62861,7 @@ var TOOL_DEFINITIONS = [
|
|
|
61829
62861
|
},
|
|
61830
62862
|
{
|
|
61831
62863
|
name: "slop_suggest",
|
|
61832
|
-
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate),
|
|
62864
|
+
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), the declared stack, and (when .slopbrick/health.json exists) a Bayesian composite AI-likelihood score. Call this BEFORE writing new code so the agent reuses existing patterns instead of duplicating them. For per-issue details or per-file hot-spots, use slop_scan_file on each candidate path.",
|
|
61833
62865
|
inputSchema: {
|
|
61834
62866
|
type: "object",
|
|
61835
62867
|
properties: {
|
|
@@ -61861,7 +62893,7 @@ var TOOL_DEFINITIONS = [
|
|
|
61861
62893
|
// call slop_suggest and read repositoryHealth /
|
|
61862
62894
|
// architectureConsistency / businessLogicCoherence.
|
|
61863
62895
|
name: "slop_check_constitution",
|
|
61864
|
-
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns
|
|
62896
|
+
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns the file path, total import + violation counts, the parsed imports, the list of violations (each with import, category, and reason), and a conventionSource indicating whether the constitution was declared, detected, or absent. Use this on a newly-written or modified file before suggesting a PR.",
|
|
61865
62897
|
inputSchema: {
|
|
61866
62898
|
type: "object",
|
|
61867
62899
|
properties: {
|
|
@@ -61878,7 +62910,7 @@ var TOOL_DEFINITIONS = [
|
|
|
61878
62910
|
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
61879
62911
|
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
61880
62912
|
name: "slop_find_similar",
|
|
61881
|
-
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns
|
|
62913
|
+
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns up to `limit` matches (default 10) with name, file, line, fingerprint (sha256 over signature), hooks, props, params, and similarity in [0, 1].",
|
|
61882
62914
|
inputSchema: {
|
|
61883
62915
|
type: "object",
|
|
61884
62916
|
properties: {
|
|
@@ -62029,10 +63061,10 @@ async function runSuggest(args, ctx, options = {}) {
|
|
|
62029
63061
|
function runCheckConstitution(args, ctx) {
|
|
62030
63062
|
const path = args.path;
|
|
62031
63063
|
if (!path) return toolError("Missing required argument: path");
|
|
62032
|
-
const absPath =
|
|
63064
|
+
const absPath = resolve18(ctx.cwd, path);
|
|
62033
63065
|
let source;
|
|
62034
63066
|
try {
|
|
62035
|
-
source =
|
|
63067
|
+
source = readFileSync28(absPath, "utf-8");
|
|
62036
63068
|
} catch (err) {
|
|
62037
63069
|
return toolError(
|
|
62038
63070
|
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -62109,13 +63141,6 @@ async function runFindSimilar(args, ctx) {
|
|
|
62109
63141
|
}
|
|
62110
63142
|
}
|
|
62111
63143
|
async function handleToolCall(toolName, args, ctx) {
|
|
62112
|
-
const deprecation = getDeprecation(toolName);
|
|
62113
|
-
const deprecationNotice = deprecation ? {
|
|
62114
|
-
tool: toolName,
|
|
62115
|
-
replacedBy: deprecation.replacedBy,
|
|
62116
|
-
removedIn: deprecation.removedIn ?? "next major",
|
|
62117
|
-
reason: deprecation.reason
|
|
62118
|
-
} : void 0;
|
|
62119
63144
|
switch (toolName) {
|
|
62120
63145
|
case "slop_scan_file":
|
|
62121
63146
|
return runScanFile(args, ctx);
|
|
@@ -62215,7 +63240,7 @@ function errorResponse(id, code, message, data2) {
|
|
|
62215
63240
|
return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
|
|
62216
63241
|
}
|
|
62217
63242
|
async function runMcpServer(input, output, cwd) {
|
|
62218
|
-
return new Promise((
|
|
63243
|
+
return new Promise((resolve43) => {
|
|
62219
63244
|
let buffer = "";
|
|
62220
63245
|
input.setEncoding("utf-8");
|
|
62221
63246
|
input.on("data", (chunk) => {
|
|
@@ -62253,8 +63278,8 @@ async function runMcpServer(input, output, cwd) {
|
|
|
62253
63278
|
nlIdx = buffer.indexOf("\n");
|
|
62254
63279
|
}
|
|
62255
63280
|
});
|
|
62256
|
-
input.on("end", () =>
|
|
62257
|
-
input.on("close", () =>
|
|
63281
|
+
input.on("end", () => resolve43());
|
|
63282
|
+
input.on("close", () => resolve43());
|
|
62258
63283
|
});
|
|
62259
63284
|
}
|
|
62260
63285
|
|
|
@@ -62278,7 +63303,7 @@ function registerDoctor(program) {
|
|
|
62278
63303
|
|
|
62279
63304
|
// src/cli/commands/watch.ts
|
|
62280
63305
|
init_watch();
|
|
62281
|
-
import { resolve as
|
|
63306
|
+
import { resolve as resolve19 } from "path";
|
|
62282
63307
|
function registerWatch(program, scanAction) {
|
|
62283
63308
|
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) => {
|
|
62284
63309
|
const rawGlobals = command.optsWithGlobals();
|
|
@@ -62286,7 +63311,7 @@ function registerWatch(program, scanAction) {
|
|
|
62286
63311
|
...rawGlobals,
|
|
62287
63312
|
noIncrease: rawGlobals.increase === false
|
|
62288
63313
|
};
|
|
62289
|
-
const cwd =
|
|
63314
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
62290
63315
|
await scanAction([], options, command);
|
|
62291
63316
|
await watchProject(options, cwd, []);
|
|
62292
63317
|
});
|
|
@@ -62294,11 +63319,11 @@ function registerWatch(program, scanAction) {
|
|
|
62294
63319
|
|
|
62295
63320
|
// src/cli/commands/lock.ts
|
|
62296
63321
|
init_logger();
|
|
62297
|
-
import { resolve as
|
|
63322
|
+
import { resolve as resolve20 } from "path";
|
|
62298
63323
|
function registerLock(program) {
|
|
62299
63324
|
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(
|
|
62300
63325
|
(cmdOptions) => {
|
|
62301
|
-
const cwd =
|
|
63326
|
+
const cwd = resolve20(cmdOptions.workspace ?? process.cwd());
|
|
62302
63327
|
if (cmdOptions.uninstall) {
|
|
62303
63328
|
const result2 = uninstallHook(cwd);
|
|
62304
63329
|
logger.info(result2.message);
|
|
@@ -62321,9 +63346,9 @@ function registerLock(program) {
|
|
|
62321
63346
|
// src/cli/commands/ci.ts
|
|
62322
63347
|
init_logger();
|
|
62323
63348
|
init_dist();
|
|
62324
|
-
import { resolve as
|
|
63349
|
+
import { resolve as resolve21 } from "path";
|
|
62325
63350
|
function registerCi(program, scanAction) {
|
|
62326
|
-
program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if
|
|
63351
|
+
program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if aiSlopScore exceeds this number (lower = cleaner since v0.21)", parseCount).option("--max-new-issues <n>", "exit 1 if new issues (vs .slop-audit-cache.json) exceed this number", parseCount).option("--strict-constitution", "exit 1 on any constitution violation").option("--format <pretty|json>", "output format", "json").action(
|
|
62327
63352
|
async (cmdOptions, command) => {
|
|
62328
63353
|
const globals = command.optsWithGlobals();
|
|
62329
63354
|
const options = {
|
|
@@ -62334,7 +63359,7 @@ function registerCi(program, scanAction) {
|
|
|
62334
63359
|
// scan only changed files
|
|
62335
63360
|
format: cmdOptions.format ?? "json"
|
|
62336
63361
|
};
|
|
62337
|
-
const cwd =
|
|
63362
|
+
const cwd = resolve21(options.workspace ?? process.cwd());
|
|
62338
63363
|
await scanAction([], options, command);
|
|
62339
63364
|
const health = loadHealth(cwd);
|
|
62340
63365
|
if (!health) {
|
|
@@ -62363,11 +63388,11 @@ function registerCi(program, scanAction) {
|
|
|
62363
63388
|
|
|
62364
63389
|
// src/cli/commands/memory.ts
|
|
62365
63390
|
init_logger();
|
|
62366
|
-
import { resolve as
|
|
63391
|
+
import { resolve as resolve22 } from "path";
|
|
62367
63392
|
function registerMemory(program) {
|
|
62368
63393
|
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(
|
|
62369
63394
|
async (cmdOptions) => {
|
|
62370
|
-
const cwd =
|
|
63395
|
+
const cwd = resolve22(cmdOptions.workspace ?? process.cwd());
|
|
62371
63396
|
const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
|
|
62372
63397
|
const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
62373
63398
|
if (cmdOptions.regenerate) {
|
|
@@ -62398,48 +63423,48 @@ function registerMemory(program) {
|
|
|
62398
63423
|
|
|
62399
63424
|
// src/cli/commands/migrate.ts
|
|
62400
63425
|
init_logger();
|
|
62401
|
-
import { resolve as
|
|
63426
|
+
import { resolve as resolve23 } from "path";
|
|
62402
63427
|
|
|
62403
63428
|
// src/cli/migrate.ts
|
|
62404
63429
|
init_logger();
|
|
62405
|
-
import { existsSync as
|
|
62406
|
-
import { join as
|
|
63430
|
+
import { existsSync as existsSync24, readFileSync as readFileSync29, renameSync as renameSync4, writeFileSync as writeFileSync15 } from "fs";
|
|
63431
|
+
import { join as join26 } from "path";
|
|
62407
63432
|
function planMigration(workspaceDir) {
|
|
62408
63433
|
const moves = [];
|
|
62409
63434
|
const rewrites = [];
|
|
62410
63435
|
const gitignoreEdits = [];
|
|
62411
|
-
const oldDir =
|
|
62412
|
-
const newDir =
|
|
62413
|
-
if (
|
|
63436
|
+
const oldDir = join26(workspaceDir, ".slop-audit");
|
|
63437
|
+
const newDir = join26(workspaceDir, ".slopbrick");
|
|
63438
|
+
if (existsSync24(oldDir)) {
|
|
62414
63439
|
moves.push({ from: oldDir, to: newDir, kind: "dir" });
|
|
62415
63440
|
rewrites.push({
|
|
62416
|
-
path:
|
|
63441
|
+
path: join26(newDir, "inventory.json"),
|
|
62417
63442
|
field: "version",
|
|
62418
63443
|
from: '"1"',
|
|
62419
63444
|
to: '"2"'
|
|
62420
63445
|
});
|
|
62421
63446
|
rewrites.push({
|
|
62422
|
-
path:
|
|
63447
|
+
path: join26(newDir, "constitution.json"),
|
|
62423
63448
|
field: "version",
|
|
62424
63449
|
from: '"1"',
|
|
62425
63450
|
to: '"2"'
|
|
62426
63451
|
});
|
|
62427
63452
|
}
|
|
62428
|
-
const oldCache =
|
|
62429
|
-
const newCache =
|
|
62430
|
-
if (
|
|
63453
|
+
const oldCache = join26(workspaceDir, ".slop-audit-cache.json");
|
|
63454
|
+
const newCache = join26(workspaceDir, ".slopbrick-cache.json");
|
|
63455
|
+
if (existsSync24(oldCache)) {
|
|
62431
63456
|
moves.push({ from: oldCache, to: newCache, kind: "file" });
|
|
62432
63457
|
}
|
|
62433
63458
|
for (const ext of ["mjs", "cjs", "js"]) {
|
|
62434
|
-
const oldCfg =
|
|
62435
|
-
const newCfg =
|
|
62436
|
-
if (
|
|
63459
|
+
const oldCfg = join26(workspaceDir, `slop-audit.config.${ext}`);
|
|
63460
|
+
const newCfg = join26(workspaceDir, `slopbrick.config.${ext}`);
|
|
63461
|
+
if (existsSync24(oldCfg)) {
|
|
62437
63462
|
moves.push({ from: oldCfg, to: newCfg, kind: "config" });
|
|
62438
63463
|
}
|
|
62439
63464
|
}
|
|
62440
|
-
const gi =
|
|
62441
|
-
if (
|
|
62442
|
-
const content =
|
|
63465
|
+
const gi = join26(workspaceDir, ".gitignore");
|
|
63466
|
+
if (existsSync24(gi)) {
|
|
63467
|
+
const content = readFileSync29(gi, "utf-8");
|
|
62443
63468
|
if (content.includes(".slop-audit/")) {
|
|
62444
63469
|
gitignoreEdits.push({
|
|
62445
63470
|
path: gi,
|
|
@@ -62458,7 +63483,7 @@ function planMigration(workspaceDir) {
|
|
|
62458
63483
|
return { moves, rewrites, gitignoreEdits };
|
|
62459
63484
|
}
|
|
62460
63485
|
function isAlreadyMigrated(workspaceDir) {
|
|
62461
|
-
return
|
|
63486
|
+
return existsSync24(join26(workspaceDir, ".slopbrick")) && !existsSync24(join26(workspaceDir, ".slop-audit"));
|
|
62462
63487
|
}
|
|
62463
63488
|
function applyMigration(plan, options = {}) {
|
|
62464
63489
|
if (options.dryRun) return;
|
|
@@ -62466,20 +63491,20 @@ function applyMigration(plan, options = {}) {
|
|
|
62466
63491
|
renameSync4(m.from, m.to);
|
|
62467
63492
|
}
|
|
62468
63493
|
for (const r of plan.rewrites) {
|
|
62469
|
-
if (!
|
|
62470
|
-
const content =
|
|
63494
|
+
if (!existsSync24(r.path)) continue;
|
|
63495
|
+
const content = readFileSync29(r.path, "utf-8");
|
|
62471
63496
|
const next = content.replace(`"version": ${r.from}`, `"version": ${r.to}`);
|
|
62472
|
-
|
|
63497
|
+
writeFileSync15(r.path, next);
|
|
62473
63498
|
}
|
|
62474
63499
|
for (const g of plan.gitignoreEdits) {
|
|
62475
|
-
const content =
|
|
63500
|
+
const content = readFileSync29(g.path, "utf-8");
|
|
62476
63501
|
const next = content.replaceAll(g.from, g.to);
|
|
62477
|
-
|
|
63502
|
+
writeFileSync15(g.path, next);
|
|
62478
63503
|
}
|
|
62479
63504
|
}
|
|
62480
63505
|
function runMigrate(options) {
|
|
62481
63506
|
const { workspace, dryRun = false, force = false } = options;
|
|
62482
|
-
if (!
|
|
63507
|
+
if (!existsSync24(workspace)) {
|
|
62483
63508
|
return {
|
|
62484
63509
|
ok: false,
|
|
62485
63510
|
alreadyMigrated: false,
|
|
@@ -62489,9 +63514,9 @@ function runMigrate(options) {
|
|
|
62489
63514
|
};
|
|
62490
63515
|
}
|
|
62491
63516
|
const alreadyMigrated = isAlreadyMigrated(workspace);
|
|
62492
|
-
const newDir =
|
|
62493
|
-
const oldDir =
|
|
62494
|
-
if (
|
|
63517
|
+
const newDir = join26(workspace, ".slopbrick");
|
|
63518
|
+
const oldDir = join26(workspace, ".slop-audit");
|
|
63519
|
+
if (existsSync24(newDir) && existsSync24(oldDir) && !force) {
|
|
62495
63520
|
return {
|
|
62496
63521
|
ok: false,
|
|
62497
63522
|
alreadyMigrated: false,
|
|
@@ -62567,7 +63592,7 @@ function registerMigrate(program) {
|
|
|
62567
63592
|
(cmdOptions, command) => {
|
|
62568
63593
|
const globals = command.optsWithGlobals();
|
|
62569
63594
|
const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
|
|
62570
|
-
const cwd =
|
|
63595
|
+
const cwd = resolve23(cmdOptions.workspace ?? process.cwd());
|
|
62571
63596
|
const result = runMigrate({
|
|
62572
63597
|
workspace: cwd,
|
|
62573
63598
|
dryRun: cmdOptions.dryRun,
|
|
@@ -62663,7 +63688,25 @@ function registerRules(program) {
|
|
|
62663
63688
|
const sev = r.severity.padEnd(8);
|
|
62664
63689
|
const tag = r.aiSpecific ? "[AI]" : " ";
|
|
62665
63690
|
lines.push(` ${sev} ${tag} ${r.id}`);
|
|
62666
|
-
if (r.description)
|
|
63691
|
+
if (r.description) {
|
|
63692
|
+
const cols = process.stdout.isTTY ? process.stdout.columns ?? 100 : 100;
|
|
63693
|
+
const indent = " ";
|
|
63694
|
+
const firstCol = indent.length + 1;
|
|
63695
|
+
const maxWidth = Math.max(40, cols - firstCol);
|
|
63696
|
+
const words = r.description.split(" ");
|
|
63697
|
+
let line = "";
|
|
63698
|
+
for (const word of words) {
|
|
63699
|
+
if (line === "") {
|
|
63700
|
+
line = word;
|
|
63701
|
+
} else if ((line + " " + word).length > maxWidth) {
|
|
63702
|
+
lines.push(indent + line);
|
|
63703
|
+
line = word;
|
|
63704
|
+
} else {
|
|
63705
|
+
line = line + " " + word;
|
|
63706
|
+
}
|
|
63707
|
+
}
|
|
63708
|
+
if (line) lines.push(indent + line);
|
|
63709
|
+
}
|
|
62667
63710
|
}
|
|
62668
63711
|
}
|
|
62669
63712
|
logger.info(lines.join("\n"));
|
|
@@ -62673,12 +63716,12 @@ function registerRules(program) {
|
|
|
62673
63716
|
// src/cli/commands/validate-config.ts
|
|
62674
63717
|
init_logger();
|
|
62675
63718
|
init_validation();
|
|
62676
|
-
import { existsSync as
|
|
62677
|
-
import { extname as extname8, resolve as
|
|
63719
|
+
import { existsSync as existsSync25 } from "fs";
|
|
63720
|
+
import { extname as extname8, resolve as resolve24 } from "path";
|
|
62678
63721
|
function registerValidateConfig(program) {
|
|
62679
63722
|
program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
|
|
62680
|
-
const path = configPath ?
|
|
62681
|
-
if (!
|
|
63723
|
+
const path = configPath ? resolve24(configPath) : resolve24(process.cwd(), "slopbrick.config.mjs");
|
|
63724
|
+
if (!existsSync25(path)) {
|
|
62682
63725
|
logger.error(`Error: config file not found: ${path}`);
|
|
62683
63726
|
process.exit(2);
|
|
62684
63727
|
}
|
|
@@ -62904,8 +63947,8 @@ ${formatMarkdown(result.report)}`);
|
|
|
62904
63947
|
|
|
62905
63948
|
// src/cli/commands/calibrate.ts
|
|
62906
63949
|
init_logger();
|
|
62907
|
-
import { mkdirSync as
|
|
62908
|
-
import { dirname as
|
|
63950
|
+
import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync18 } from "fs";
|
|
63951
|
+
import { dirname as dirname17, resolve as resolve26 } from "path";
|
|
62909
63952
|
|
|
62910
63953
|
// src/research/providers/openai.ts
|
|
62911
63954
|
var OpenAiProvider = class {
|
|
@@ -62960,8 +64003,8 @@ function createProvider(config) {
|
|
|
62960
64003
|
}
|
|
62961
64004
|
|
|
62962
64005
|
// src/research/generator.ts
|
|
62963
|
-
import { mkdirSync as
|
|
62964
|
-
import { join as
|
|
64006
|
+
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync16 } from "fs";
|
|
64007
|
+
import { join as join27 } from "path";
|
|
62965
64008
|
|
|
62966
64009
|
// src/research/prompts.ts
|
|
62967
64010
|
var DEFAULT_PROMPT_TEMPLATES = [
|
|
@@ -63024,14 +64067,14 @@ async function generateSamples(options) {
|
|
|
63024
64067
|
}
|
|
63025
64068
|
const samples = [];
|
|
63026
64069
|
const ext = extForFramework(framework);
|
|
63027
|
-
const dir =
|
|
63028
|
-
|
|
64070
|
+
const dir = join27(outputDir, framework, componentType);
|
|
64071
|
+
mkdirSync12(dir, { recursive: true });
|
|
63029
64072
|
for (let i = 1; i <= count; i += 1) {
|
|
63030
64073
|
const raw = await provider.generateSample(renderPrompt(template), { temperature });
|
|
63031
64074
|
const code = extractCodeFromMarkdown(raw);
|
|
63032
64075
|
const fileName = `sample-${i}${ext}`;
|
|
63033
|
-
const filePath =
|
|
63034
|
-
|
|
64076
|
+
const filePath = join27(dir, fileName);
|
|
64077
|
+
writeFileSync16(filePath, code, "utf8");
|
|
63035
64078
|
const sample = {
|
|
63036
64079
|
filePath,
|
|
63037
64080
|
framework,
|
|
@@ -63042,8 +64085,8 @@ async function generateSamples(options) {
|
|
|
63042
64085
|
};
|
|
63043
64086
|
samples.push(sample);
|
|
63044
64087
|
}
|
|
63045
|
-
const metadataPath =
|
|
63046
|
-
|
|
64088
|
+
const metadataPath = join27(dir, "metadata.json");
|
|
64089
|
+
writeFileSync16(metadataPath, JSON.stringify(samples, null, 2), "utf8");
|
|
63047
64090
|
return samples;
|
|
63048
64091
|
}
|
|
63049
64092
|
|
|
@@ -63249,46 +64292,46 @@ function slugify(value) {
|
|
|
63249
64292
|
|
|
63250
64293
|
// src/research/calibrator.ts
|
|
63251
64294
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
63252
|
-
import { existsSync as
|
|
63253
|
-
import { join as
|
|
64295
|
+
import { existsSync as existsSync26, readFileSync as readFileSync30, writeFileSync as writeFileSync17, mkdirSync as mkdirSync13 } from "fs";
|
|
64296
|
+
import { join as join29, resolve as resolve25 } from "path";
|
|
63254
64297
|
|
|
63255
64298
|
// src/corpus-paths.ts
|
|
63256
|
-
import { join as
|
|
64299
|
+
import { join as join28 } from "path";
|
|
63257
64300
|
var CORPUS_ROOT = process.env["SLOPBRICK_CORPUS_DIR"] ?? "/Users/cheng/corpus-expansion";
|
|
63258
|
-
var POSITIVE_DIR =
|
|
63259
|
-
var NEGATIVE_DIR =
|
|
63260
|
-
var FILELISTS_DIR =
|
|
64301
|
+
var POSITIVE_DIR = join28(CORPUS_ROOT, "positive");
|
|
64302
|
+
var NEGATIVE_DIR = join28(CORPUS_ROOT, "negative");
|
|
64303
|
+
var FILELISTS_DIR = join28(CORPUS_ROOT, "filelists");
|
|
63261
64304
|
|
|
63262
64305
|
// src/research/calibrator.ts
|
|
63263
64306
|
var DEFAULT_POSITIVE = POSITIVE_DIR;
|
|
63264
64307
|
var DEFAULT_NEGATIVE = NEGATIVE_DIR;
|
|
63265
64308
|
function buildFileList(dir, extensions) {
|
|
63266
|
-
const tmpList =
|
|
64309
|
+
const tmpList = join29("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
|
|
63267
64310
|
const expr = extensions.map((e) => `-name '*.${e}'`).join(" -o ");
|
|
63268
64311
|
execFileSync2("bash", ["-c", `find ${dir} -maxdepth 8 -type f \\( ${expr} \\) -print0 | xargs -0 realpath > ${tmpList}`]);
|
|
63269
|
-
const out =
|
|
64312
|
+
const out = readFileSync30(tmpList, "utf8");
|
|
63270
64313
|
execFileSync2("rm", ["-f", tmpList]);
|
|
63271
64314
|
return out.trim().split("\n").filter(Boolean);
|
|
63272
64315
|
}
|
|
63273
64316
|
function runScan2(fileListPath) {
|
|
63274
|
-
const files =
|
|
64317
|
+
const files = readFileSync30(fileListPath, "utf8").trim().split("\n").filter(Boolean);
|
|
63275
64318
|
const CHUNK = 600;
|
|
63276
64319
|
const ruleFires = /* @__PURE__ */ new Map();
|
|
63277
64320
|
const uniqueFilesPerRule = /* @__PURE__ */ new Map();
|
|
63278
64321
|
let fileCount = 0;
|
|
63279
|
-
const tmpOut =
|
|
64322
|
+
const tmpOut = join29("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
63280
64323
|
for (let i = 0; i < files.length; i += CHUNK) {
|
|
63281
64324
|
const chunk = files.slice(i, i + CHUNK);
|
|
63282
64325
|
try {
|
|
63283
64326
|
execFileSync2(
|
|
63284
64327
|
"node",
|
|
63285
|
-
[
|
|
64328
|
+
[join29(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
|
|
63286
64329
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
63287
64330
|
);
|
|
63288
64331
|
} catch {
|
|
63289
64332
|
}
|
|
63290
|
-
if (!
|
|
63291
|
-
const report = JSON.parse(
|
|
64333
|
+
if (!existsSync26(tmpOut)) continue;
|
|
64334
|
+
const report = JSON.parse(readFileSync30(tmpOut, "utf8"));
|
|
63292
64335
|
fileCount += report.fileCount;
|
|
63293
64336
|
for (const issue of report.issues) {
|
|
63294
64337
|
ruleFires.set(issue.ruleId, (ruleFires.get(issue.ruleId) ?? 0) + 1);
|
|
@@ -63316,16 +64359,16 @@ function classify(posFiles, negFiles) {
|
|
|
63316
64359
|
async function calibrate(cwd, options = {}) {
|
|
63317
64360
|
const positiveDir = options.positiveDir ?? DEFAULT_POSITIVE;
|
|
63318
64361
|
const negativeDir = options.negativeDir ?? DEFAULT_NEGATIVE;
|
|
63319
|
-
if (!
|
|
63320
|
-
if (!
|
|
64362
|
+
if (!existsSync26(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
|
|
64363
|
+
if (!existsSync26(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
|
|
63321
64364
|
const positiveFiles = buildFileList(positiveDir, ["tsx", "ts", "jsx", "js"]);
|
|
63322
64365
|
const negativeFiles = buildFileList(negativeDir, ["tsx", "ts"]);
|
|
63323
64366
|
const posSample = options.positiveLimit ? positiveFiles.slice(0, options.positiveLimit) : positiveFiles;
|
|
63324
64367
|
const negSample = options.negativeLimit ? negativeFiles.slice(0, options.negativeLimit) : negativeFiles;
|
|
63325
|
-
const posListPath =
|
|
63326
|
-
const negListPath =
|
|
63327
|
-
|
|
63328
|
-
|
|
64368
|
+
const posListPath = join29("/tmp", `cal-pos-${Date.now()}.txt`);
|
|
64369
|
+
const negListPath = join29("/tmp", `cal-neg-${Date.now()}.txt`);
|
|
64370
|
+
writeFileSync17(posListPath, posSample.join("\n"));
|
|
64371
|
+
writeFileSync17(negListPath, negSample.join("\n"));
|
|
63329
64372
|
const builtins = await Promise.resolve().then(() => (init_builtins(), builtins_exports));
|
|
63330
64373
|
const builtinRules2 = builtins.builtinRules ?? [];
|
|
63331
64374
|
const metaById = /* @__PURE__ */ new Map();
|
|
@@ -63439,9 +64482,9 @@ function registerCalibrate(program) {
|
|
|
63439
64482
|
positiveLimit: cmdOptions.positiveLimit,
|
|
63440
64483
|
negativeLimit: cmdOptions.negativeLimit
|
|
63441
64484
|
});
|
|
63442
|
-
const outputPath = cmdOptions.output ?
|
|
63443
|
-
|
|
63444
|
-
|
|
64485
|
+
const outputPath = cmdOptions.output ? resolve26(cwd, cmdOptions.output) : resolve26(cwd, "corpus", "calibration-empirical.md");
|
|
64486
|
+
mkdirSync14(dirname17(outputPath), { recursive: true });
|
|
64487
|
+
writeFileSync18(outputPath, reportToMarkdown(report), "utf8");
|
|
63445
64488
|
logger.info(
|
|
63446
64489
|
"Calibrated " + report.rules.length + " rules across " + report.positiveFileCount + " positive + " + report.negativeFileCount + " negative files."
|
|
63447
64490
|
);
|
|
@@ -63508,19 +64551,20 @@ function trendToText(report) {
|
|
|
63508
64551
|
}
|
|
63509
64552
|
const slopValues = report.points.map((p) => p.slopIndex);
|
|
63510
64553
|
const spark = sparkline(slopValues);
|
|
63511
|
-
lines.push("Slop
|
|
64554
|
+
lines.push("AI Slop Score trend (last " + report.points.length + " scans, " + report.totalScans + " total; lower = cleaner since v0.21):");
|
|
63512
64555
|
lines.push("");
|
|
63513
64556
|
lines.push(" " + spark + " " + slopValues[0].toFixed(1) + " -> " + slopValues[slopValues.length - 1].toFixed(1));
|
|
63514
64557
|
lines.push("");
|
|
63515
64558
|
if (report.first && report.latest) {
|
|
63516
64559
|
const direction = report.delta < 0 ? "improved" : report.delta > 0 ? "regressed" : "unchanged";
|
|
63517
64560
|
const arrow = report.delta < 0 ? "v" : report.delta > 0 ? "^" : "=";
|
|
63518
|
-
lines.push(" " + arrow + " Slop
|
|
64561
|
+
lines.push(" " + arrow + " AI Slop Score " + direction + " by " + Math.abs(report.delta).toFixed(1) + " points (lower = cleaner since v0.21)");
|
|
63519
64562
|
lines.push(" " + report.first.timestamp + " -> " + report.latest.timestamp);
|
|
63520
64563
|
}
|
|
63521
64564
|
lines.push("");
|
|
63522
64565
|
lines.push("Recent scans:");
|
|
63523
|
-
lines.push(" Timestamp Slop
|
|
64566
|
+
lines.push(" Timestamp AI Slop Health Framework Issues");
|
|
64567
|
+
lines.push(" (lower=cleaner)");
|
|
63524
64568
|
lines.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500");
|
|
63525
64569
|
for (const p of report.points.slice(-10)) {
|
|
63526
64570
|
const ts = p.timestamp.slice(0, 19).replace("T", " ");
|
|
@@ -63532,14 +64576,14 @@ function trendToText(report) {
|
|
|
63532
64576
|
}
|
|
63533
64577
|
function trendToMarkdown(report) {
|
|
63534
64578
|
const lines = [];
|
|
63535
|
-
lines.push("# Slop
|
|
64579
|
+
lines.push("# AI Slop Score Trend (lower = cleaner since v0.21)");
|
|
63536
64580
|
lines.push("");
|
|
63537
64581
|
if (report.points.length === 0) {
|
|
63538
64582
|
lines.push("No telemetry data yet. Run `slopbrick scan` to start tracking.");
|
|
63539
64583
|
return lines.join("\n");
|
|
63540
64584
|
}
|
|
63541
64585
|
const slopValues = report.points.map((p) => p.slopIndex);
|
|
63542
|
-
lines.push("Slop
|
|
64586
|
+
lines.push("AI Slop Score sparkline (last " + report.points.length + " scans; lower = cleaner since v0.21):");
|
|
63543
64587
|
lines.push("");
|
|
63544
64588
|
lines.push("```");
|
|
63545
64589
|
lines.push(sparkline(slopValues));
|
|
@@ -63547,10 +64591,10 @@ function trendToMarkdown(report) {
|
|
|
63547
64591
|
lines.push("");
|
|
63548
64592
|
if (report.first && report.latest) {
|
|
63549
64593
|
const direction = report.delta < 0 ? "improved" : report.delta > 0 ? "regressed" : "unchanged";
|
|
63550
|
-
lines.push("Slop
|
|
64594
|
+
lines.push("AI Slop Score " + direction + " by **" + Math.abs(report.delta).toFixed(1) + " points** between first and latest scan (lower = cleaner since v0.21).");
|
|
63551
64595
|
}
|
|
63552
64596
|
lines.push("");
|
|
63553
|
-
lines.push("| Timestamp | Slop
|
|
64597
|
+
lines.push("| Timestamp | AI Slop Score (lower = cleaner) | Health | Framework | Issues |");
|
|
63554
64598
|
lines.push("|-----------|-----------:|-------:|-----------|-------:|");
|
|
63555
64599
|
for (const p of report.points) {
|
|
63556
64600
|
lines.push("| " + p.timestamp + " | " + p.slopIndex.toFixed(1) + " | " + p.assemblyHealth.toFixed(1) + " | " + p.framework + " | " + p.totalIssues + " |");
|
|
@@ -63560,7 +64604,7 @@ function trendToMarkdown(report) {
|
|
|
63560
64604
|
|
|
63561
64605
|
// src/cli/commands/trend.ts
|
|
63562
64606
|
function registerTrend(program) {
|
|
63563
|
-
program.command("trend").description("Slop
|
|
64607
|
+
program.command("trend").description("AI Slop Score over time, from .slopbrick/flywheel/scans.jsonl (lower = cleaner since v0.21)").option("--max-points <n>", "how many recent scans to plot", parseCount, 30).option("--render <kind>", "output rendering: text | markdown", "text").action((cmdOptions) => {
|
|
63564
64608
|
try {
|
|
63565
64609
|
const cwd = process.cwd();
|
|
63566
64610
|
const report = buildTrend(cwd, cmdOptions.maxPoints);
|
|
@@ -63568,7 +64612,14 @@ function registerTrend(program) {
|
|
|
63568
64612
|
logger.info(out);
|
|
63569
64613
|
if (report.delta > 1) {
|
|
63570
64614
|
logger.info("");
|
|
63571
|
-
logger.info(
|
|
64615
|
+
logger.info(
|
|
64616
|
+
// v0.42.0: the legacy 'Slop Index increased' message implied higher
|
|
64617
|
+
// = worse, which is the v0.15-v0.20.1 inverted reading. Since v0.21,
|
|
64618
|
+
// AI Slop Score is lower-is-better; an INCREASE is worse, but
|
|
64619
|
+
// the warning should say "AI Slop Score got worse" or just
|
|
64620
|
+
// "delta went up" so users don't have to remember the direction.
|
|
64621
|
+
`Warning: AI Slop Score got worse by ${report.delta.toFixed(1)} points over this period (lower = cleaner since v0.21).`
|
|
64622
|
+
);
|
|
63572
64623
|
}
|
|
63573
64624
|
} catch (error) {
|
|
63574
64625
|
logger.error(error instanceof Error ? error.message : String(error));
|
|
@@ -63580,13 +64631,13 @@ function registerTrend(program) {
|
|
|
63580
64631
|
// src/cli/commands/drift.ts
|
|
63581
64632
|
init_logger();
|
|
63582
64633
|
init_scan2();
|
|
63583
|
-
import { resolve as
|
|
64634
|
+
import { resolve as resolve27 } from "path";
|
|
63584
64635
|
|
|
63585
64636
|
// src/cli/drift.ts
|
|
63586
64637
|
init_discover();
|
|
63587
64638
|
init_patterns();
|
|
63588
64639
|
init_telemetry();
|
|
63589
|
-
import { readFileSync as
|
|
64640
|
+
import { readFileSync as readFileSync31 } from "fs";
|
|
63590
64641
|
import { basename as basename4, relative as relative16 } from "path";
|
|
63591
64642
|
async function runDrift(cwd, config, options = {}) {
|
|
63592
64643
|
const maxFiles = options.maxFiles ?? 1e3;
|
|
@@ -63598,7 +64649,7 @@ async function runDrift(cwd, config, options = {}) {
|
|
|
63598
64649
|
for (const absPath of limited) {
|
|
63599
64650
|
let source;
|
|
63600
64651
|
try {
|
|
63601
|
-
source =
|
|
64652
|
+
source = readFileSync31(absPath, "utf-8");
|
|
63602
64653
|
} catch {
|
|
63603
64654
|
continue;
|
|
63604
64655
|
}
|
|
@@ -63885,7 +64936,7 @@ function registerDrift(program) {
|
|
|
63885
64936
|
const options = command.optsWithGlobals();
|
|
63886
64937
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
63887
64938
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
63888
|
-
const cwd =
|
|
64939
|
+
const cwd = resolve27(options.workspace ?? process.cwd());
|
|
63889
64940
|
if (cmdOptions.temporalSince !== void 0) {
|
|
63890
64941
|
const temporalSince = cmdOptions.temporalSince.trim();
|
|
63891
64942
|
if (temporalSince.length === 0) {
|
|
@@ -63913,7 +64964,7 @@ function registerDrift(program) {
|
|
|
63913
64964
|
// src/cli/commands/pr.ts
|
|
63914
64965
|
init_logger();
|
|
63915
64966
|
init_scan2();
|
|
63916
|
-
import { resolve as
|
|
64967
|
+
import { resolve as resolve29 } from "path";
|
|
63917
64968
|
|
|
63918
64969
|
// src/cli/pr.ts
|
|
63919
64970
|
init_git();
|
|
@@ -63921,8 +64972,8 @@ init_worker();
|
|
|
63921
64972
|
init_metrics();
|
|
63922
64973
|
init_patterns();
|
|
63923
64974
|
init_discover();
|
|
63924
|
-
import { readFileSync as
|
|
63925
|
-
import { extname as extname9, relative as relative17, resolve as
|
|
64975
|
+
import { readFileSync as readFileSync32 } from "fs";
|
|
64976
|
+
import { extname as extname9, relative as relative17, resolve as resolve28 } from "path";
|
|
63926
64977
|
import { execFile as execFileCb } from "child_process";
|
|
63927
64978
|
import { promisify as promisify2 } from "util";
|
|
63928
64979
|
import { minimatch as minimatch3 } from "minimatch";
|
|
@@ -63963,7 +65014,7 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
|
|
|
63963
65014
|
if (gitFiles.length === 0) return [];
|
|
63964
65015
|
const sourceFiles = [];
|
|
63965
65016
|
for (const relOrAbs of gitFiles) {
|
|
63966
|
-
const abs =
|
|
65017
|
+
const abs = resolve28(cwd, relOrAbs);
|
|
63967
65018
|
const ext = extname9(abs).toLowerCase();
|
|
63968
65019
|
if (!PR_EXTENSIONS.has(ext)) continue;
|
|
63969
65020
|
const rel = relative17(cwd, abs).split("\\").join("/");
|
|
@@ -63994,7 +65045,7 @@ async function runPrScan(cwd, config, options = {}) {
|
|
|
63994
65045
|
for (const absPath of candidates) {
|
|
63995
65046
|
let source;
|
|
63996
65047
|
try {
|
|
63997
|
-
source =
|
|
65048
|
+
source = readFileSync32(absPath, "utf-8");
|
|
63998
65049
|
} catch {
|
|
63999
65050
|
continue;
|
|
64000
65051
|
}
|
|
@@ -64169,20 +65220,135 @@ function formatPrMarkdown(result) {
|
|
|
64169
65220
|
function prExitCode(result) {
|
|
64170
65221
|
return result.totalScore > result.threshold ? 1 : 0;
|
|
64171
65222
|
}
|
|
65223
|
+
function parseUnifiedDiffPaths(diff) {
|
|
65224
|
+
const paths = [];
|
|
65225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
65226
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
65227
|
+
const m = /^\+\+\+\s+(?:b\/)?(\S.*)$/.exec(line);
|
|
65228
|
+
if (!m) continue;
|
|
65229
|
+
const path = m[1];
|
|
65230
|
+
if (path === "/dev/null" || path === "dev/null") continue;
|
|
65231
|
+
if (seen.has(path)) continue;
|
|
65232
|
+
seen.add(path);
|
|
65233
|
+
paths.push(path);
|
|
65234
|
+
}
|
|
65235
|
+
return paths;
|
|
65236
|
+
}
|
|
65237
|
+
async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
|
|
65238
|
+
const threshold = options.threshold ?? config.prScoreThreshold ?? 20;
|
|
65239
|
+
const maxFiles = options.maxFiles ?? 500;
|
|
65240
|
+
const { existsSync: existsSync31, readFileSync: readFileSync41 } = await import("fs");
|
|
65241
|
+
const { isAbsolute: isAbsolute4, resolve: resolvePath } = await import("path");
|
|
65242
|
+
if (!existsSync31(diffPath)) {
|
|
65243
|
+
throw new Error(`Diff file not found: ${diffPath}`);
|
|
65244
|
+
}
|
|
65245
|
+
const diffAbs = isAbsolute4(diffPath) ? diffPath : resolvePath(cwd, diffPath);
|
|
65246
|
+
const diff = readFileSync41(diffAbs, "utf-8");
|
|
65247
|
+
const changedPaths = parseUnifiedDiffPaths(diff);
|
|
65248
|
+
if (changedPaths.length === 0) {
|
|
65249
|
+
return emptyPrResult(threshold);
|
|
65250
|
+
}
|
|
65251
|
+
return scoreChangedPaths(cwd, config, changedPaths, maxFiles, threshold);
|
|
65252
|
+
}
|
|
65253
|
+
async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
|
|
65254
|
+
const { existsSync: existsSync31, statSync: statSync8 } = await import("fs");
|
|
65255
|
+
const { resolve: resolvePath } = await import("path");
|
|
65256
|
+
const { readFile: readFile4 } = await import("fs/promises");
|
|
65257
|
+
const { scanFile: scanFile2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
|
|
65258
|
+
const limited = paths.slice(0, maxFiles);
|
|
65259
|
+
const files = [];
|
|
65260
|
+
let totalScore = 0;
|
|
65261
|
+
const byCategory = {};
|
|
65262
|
+
const bySeverity = { low: 0, medium: 0, high: 0 };
|
|
65263
|
+
let constitutionViolations = 0;
|
|
65264
|
+
for (const rel of limited) {
|
|
65265
|
+
const abs = resolvePath(cwd, rel);
|
|
65266
|
+
if (!existsSync31(abs) || !statSync8(abs).isFile()) continue;
|
|
65267
|
+
let source;
|
|
65268
|
+
try {
|
|
65269
|
+
source = await readFile4(abs, "utf-8");
|
|
65270
|
+
} catch {
|
|
65271
|
+
continue;
|
|
65272
|
+
}
|
|
65273
|
+
let result;
|
|
65274
|
+
try {
|
|
65275
|
+
result = await scanFile2(abs, config, void 0, cwd);
|
|
65276
|
+
} catch {
|
|
65277
|
+
continue;
|
|
65278
|
+
}
|
|
65279
|
+
if (result.parseError) continue;
|
|
65280
|
+
const issues = result.issues;
|
|
65281
|
+
let slopPoints = 0;
|
|
65282
|
+
const issueEntries = [];
|
|
65283
|
+
for (const issue of issues) {
|
|
65284
|
+
slopPoints += SEVERITY_WEIGHTS[issue.severity] ?? 0;
|
|
65285
|
+
byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
|
|
65286
|
+
bySeverity[issue.severity] = (bySeverity[issue.severity] ?? 0) + 1;
|
|
65287
|
+
issueEntries.push({
|
|
65288
|
+
ruleId: issue.ruleId,
|
|
65289
|
+
severity: issue.severity,
|
|
65290
|
+
line: issue.line,
|
|
65291
|
+
category: issue.category,
|
|
65292
|
+
message: issue.message
|
|
65293
|
+
});
|
|
65294
|
+
}
|
|
65295
|
+
totalScore += slopPoints;
|
|
65296
|
+
files.push({
|
|
65297
|
+
file: abs,
|
|
65298
|
+
relPath: rel,
|
|
65299
|
+
score: slopPoints,
|
|
65300
|
+
issueCount: issueEntries.length,
|
|
65301
|
+
constitutionViolationCount: 0,
|
|
65302
|
+
slopPoints,
|
|
65303
|
+
issues: issueEntries,
|
|
65304
|
+
constitutionViolations: []
|
|
65305
|
+
});
|
|
65306
|
+
}
|
|
65307
|
+
return {
|
|
65308
|
+
base: "diff",
|
|
65309
|
+
head: "diff",
|
|
65310
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65311
|
+
passed: totalScore <= threshold,
|
|
65312
|
+
threshold,
|
|
65313
|
+
totalScore,
|
|
65314
|
+
filesChanged: files.length,
|
|
65315
|
+
byCategory,
|
|
65316
|
+
bySeverity,
|
|
65317
|
+
files
|
|
65318
|
+
};
|
|
65319
|
+
}
|
|
65320
|
+
function emptyPrResult(threshold) {
|
|
65321
|
+
return {
|
|
65322
|
+
base: "diff",
|
|
65323
|
+
head: "diff",
|
|
65324
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65325
|
+
passed: true,
|
|
65326
|
+
threshold,
|
|
65327
|
+
totalScore: 0,
|
|
65328
|
+
filesChanged: 0,
|
|
65329
|
+
byCategory: {},
|
|
65330
|
+
bySeverity: { low: 0, medium: 0, high: 0 },
|
|
65331
|
+
files: []
|
|
65332
|
+
};
|
|
65333
|
+
}
|
|
64172
65334
|
|
|
64173
65335
|
// src/cli/commands/pr.ts
|
|
64174
65336
|
function registerPr(program) {
|
|
64175
65337
|
program.command("pr").description(
|
|
64176
65338
|
"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)."
|
|
64177
|
-
).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(
|
|
65339
|
+
).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(
|
|
64178
65340
|
async (cmdOptions, command) => {
|
|
64179
65341
|
try {
|
|
64180
65342
|
const options = command.optsWithGlobals();
|
|
64181
|
-
const cwd =
|
|
65343
|
+
const cwd = resolve29(options.workspace ?? process.cwd());
|
|
64182
65344
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64183
65345
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64184
65346
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64185
|
-
const result = await
|
|
65347
|
+
const result = cmdOptions.diff ? await runPrScanFromDiffFile(cwd, config, cmdOptions.diff, {
|
|
65348
|
+
format,
|
|
65349
|
+
threshold: cmdOptions.threshold,
|
|
65350
|
+
maxFiles: cmdOptions.maxFiles
|
|
65351
|
+
}) : await runPrScan(cwd, config, {
|
|
64186
65352
|
base: cmdOptions.base,
|
|
64187
65353
|
head: cmdOptions.head,
|
|
64188
65354
|
format,
|
|
@@ -64203,7 +65369,7 @@ function registerPr(program) {
|
|
|
64203
65369
|
init_logger();
|
|
64204
65370
|
init_scan2();
|
|
64205
65371
|
init_ai_security_risk();
|
|
64206
|
-
import { resolve as
|
|
65372
|
+
import { resolve as resolve30 } from "path";
|
|
64207
65373
|
function registerSecurity(program) {
|
|
64208
65374
|
program.command("security").description(
|
|
64209
65375
|
"AI Security Risk \u2014 categorical severity for security findings disproportionately introduced by AI-generated code"
|
|
@@ -64213,7 +65379,7 @@ function registerSecurity(program) {
|
|
|
64213
65379
|
const options = command.optsWithGlobals();
|
|
64214
65380
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64215
65381
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64216
|
-
const cwd =
|
|
65382
|
+
const cwd = resolve30(options.workspace ?? process.cwd());
|
|
64217
65383
|
const { report } = await runScan({ ...options, workspace: cwd });
|
|
64218
65384
|
const securityIssues = report.issues.filter((i) => i.category === "security");
|
|
64219
65385
|
const { risk, findings } = computeAiSecurityRisk(securityIssues);
|
|
@@ -64261,13 +65427,13 @@ function registerSecurity(program) {
|
|
|
64261
65427
|
// src/cli/commands/test.ts
|
|
64262
65428
|
init_logger();
|
|
64263
65429
|
init_scan2();
|
|
64264
|
-
import { resolve as
|
|
65430
|
+
import { resolve as resolve32 } from "path";
|
|
64265
65431
|
|
|
64266
65432
|
// src/cli/test.ts
|
|
64267
65433
|
init_scan2();
|
|
64268
65434
|
init_test_quality();
|
|
64269
65435
|
init_logger();
|
|
64270
|
-
import { resolve as
|
|
65436
|
+
import { resolve as resolve31 } from "path";
|
|
64271
65437
|
var TEST_INCLUDE_GLOBS = [
|
|
64272
65438
|
"**/*.test.{ts,tsx,js,jsx}",
|
|
64273
65439
|
"**/*.spec.{ts,tsx,js,jsx}",
|
|
@@ -64275,7 +65441,7 @@ var TEST_INCLUDE_GLOBS = [
|
|
|
64275
65441
|
"**/__tests__/**/*.{ts,tsx,js,jsx}",
|
|
64276
65442
|
"**/__fixtures__/**/*.{ts,tsx,js,jsx,json}"
|
|
64277
65443
|
];
|
|
64278
|
-
async function runTestScan(cwd,
|
|
65444
|
+
async function runTestScan(cwd, _config, options = {}) {
|
|
64279
65445
|
const include = TEST_INCLUDE_GLOBS;
|
|
64280
65446
|
const cliOptions = {
|
|
64281
65447
|
include,
|
|
@@ -64335,7 +65501,7 @@ function formatTestReport(result, opts = {}) {
|
|
|
64335
65501
|
const sortedFiles = [...byFile.keys()].sort();
|
|
64336
65502
|
for (const file of sortedFiles) {
|
|
64337
65503
|
const issues = byFile.get(file) ?? [];
|
|
64338
|
-
const rel = file.startsWith(
|
|
65504
|
+
const rel = file.startsWith(resolve31(process.cwd())) ? file : file;
|
|
64339
65505
|
lines.push("");
|
|
64340
65506
|
lines.push(` ${rel}`);
|
|
64341
65507
|
for (const issue of issues.slice(0, 20)) {
|
|
@@ -64364,7 +65530,7 @@ function registerTest(program) {
|
|
|
64364
65530
|
const options = command.optsWithGlobals();
|
|
64365
65531
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64366
65532
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64367
|
-
const cwd =
|
|
65533
|
+
const cwd = resolve32(options.workspace ?? process.cwd());
|
|
64368
65534
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64369
65535
|
const { result } = await runTestScan(cwd, config, { strict: options.strict });
|
|
64370
65536
|
logger.info(formatTestReport(result, { json: format === "json" }));
|
|
@@ -64381,7 +65547,7 @@ function registerTest(program) {
|
|
|
64381
65547
|
init_logger();
|
|
64382
65548
|
init_scan2();
|
|
64383
65549
|
init_architecture_score();
|
|
64384
|
-
import { resolve as
|
|
65550
|
+
import { resolve as resolve33 } from "path";
|
|
64385
65551
|
function registerArchitecture(program) {
|
|
64386
65552
|
program.command("architecture").description(
|
|
64387
65553
|
"compute the Architecture Consistency Score (0-100) \u2014 one number for cross-file pattern duplication"
|
|
@@ -64391,7 +65557,7 @@ function registerArchitecture(program) {
|
|
|
64391
65557
|
const options = command.optsWithGlobals();
|
|
64392
65558
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64393
65559
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64394
|
-
const cwd =
|
|
65560
|
+
const cwd = resolve33(options.workspace ?? process.cwd());
|
|
64395
65561
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64396
65562
|
const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
|
|
64397
65563
|
const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
|
|
@@ -64408,12 +65574,12 @@ function registerArchitecture(program) {
|
|
|
64408
65574
|
// src/cli/commands/business-logic.ts
|
|
64409
65575
|
init_logger();
|
|
64410
65576
|
init_scan2();
|
|
64411
|
-
import { resolve as
|
|
65577
|
+
import { resolve as resolve34 } from "path";
|
|
64412
65578
|
|
|
64413
65579
|
// src/cli/business-logic.ts
|
|
64414
65580
|
init_discover();
|
|
64415
65581
|
init_business_logic();
|
|
64416
|
-
import { readFileSync as
|
|
65582
|
+
import { readFileSync as readFileSync33 } from "fs";
|
|
64417
65583
|
import { relative as relative18 } from "path";
|
|
64418
65584
|
async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
64419
65585
|
const maxFiles = options.maxFiles ?? 500;
|
|
@@ -64424,7 +65590,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
|
64424
65590
|
for (const absPath of limited) {
|
|
64425
65591
|
let source;
|
|
64426
65592
|
try {
|
|
64427
|
-
source =
|
|
65593
|
+
source = readFileSync33(absPath, "utf-8");
|
|
64428
65594
|
} catch {
|
|
64429
65595
|
continue;
|
|
64430
65596
|
}
|
|
@@ -64536,7 +65702,7 @@ function registerBusinessLogic(program) {
|
|
|
64536
65702
|
const options = command.optsWithGlobals();
|
|
64537
65703
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64538
65704
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64539
|
-
const cwd =
|
|
65705
|
+
const cwd = resolve34(options.workspace ?? process.cwd());
|
|
64540
65706
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64541
65707
|
const result = await runBusinessLogicScan(cwd, config, {
|
|
64542
65708
|
maxFiles: cmdOptions.maxFiles
|
|
@@ -64554,7 +65720,7 @@ function registerBusinessLogic(program) {
|
|
|
64554
65720
|
// src/cli/commands/maintenance-cost.ts
|
|
64555
65721
|
init_logger();
|
|
64556
65722
|
init_scan2();
|
|
64557
|
-
import { resolve as
|
|
65723
|
+
import { resolve as resolve35 } from "path";
|
|
64558
65724
|
|
|
64559
65725
|
// src/cli/maintenance-cost.ts
|
|
64560
65726
|
init_scan2();
|
|
@@ -64678,7 +65844,7 @@ function registerMaintenanceCost(program) {
|
|
|
64678
65844
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64679
65845
|
const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
|
|
64680
65846
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64681
|
-
const cwd =
|
|
65847
|
+
const cwd = resolve35(options.workspace ?? process.cwd());
|
|
64682
65848
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64683
65849
|
const result = await runMaintenanceCostScan(cwd, config, {
|
|
64684
65850
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -64697,7 +65863,7 @@ function registerMaintenanceCost(program) {
|
|
|
64697
65863
|
// src/cli/commands/docs.ts
|
|
64698
65864
|
init_logger();
|
|
64699
65865
|
init_scan2();
|
|
64700
|
-
import { resolve as
|
|
65866
|
+
import { resolve as resolve36 } from "path";
|
|
64701
65867
|
|
|
64702
65868
|
// src/cli/docs.ts
|
|
64703
65869
|
init_scan2();
|
|
@@ -64822,7 +65988,7 @@ function registerDocs(program) {
|
|
|
64822
65988
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64823
65989
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64824
65990
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64825
|
-
const cwd =
|
|
65991
|
+
const cwd = resolve36(options.workspace ?? process.cwd());
|
|
64826
65992
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64827
65993
|
const result = await runDocsScan(cwd, config, {
|
|
64828
65994
|
maxDocFiles: cmdOptions.maxFiles,
|
|
@@ -64846,7 +66012,7 @@ function registerDocs(program) {
|
|
|
64846
66012
|
// src/cli/commands/db.ts
|
|
64847
66013
|
init_logger();
|
|
64848
66014
|
init_scan2();
|
|
64849
|
-
import { resolve as
|
|
66015
|
+
import { resolve as resolve37 } from "path";
|
|
64850
66016
|
|
|
64851
66017
|
// src/cli/db.ts
|
|
64852
66018
|
init_scan2();
|
|
@@ -64965,7 +66131,7 @@ function registerDb(program) {
|
|
|
64965
66131
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64966
66132
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64967
66133
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64968
|
-
const cwd =
|
|
66134
|
+
const cwd = resolve37(options.workspace ?? process.cwd());
|
|
64969
66135
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64970
66136
|
const result = await runDbScan(cwd, config, {
|
|
64971
66137
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -64989,12 +66155,12 @@ function registerDb(program) {
|
|
|
64989
66155
|
// src/cli/commands/patterns.ts
|
|
64990
66156
|
init_logger();
|
|
64991
66157
|
init_scan2();
|
|
64992
|
-
import { resolve as
|
|
66158
|
+
import { resolve as resolve38 } from "path";
|
|
64993
66159
|
|
|
64994
66160
|
// src/engine/patterns.ts
|
|
64995
66161
|
init_patterns();
|
|
64996
66162
|
init_discover();
|
|
64997
|
-
import { readFileSync as
|
|
66163
|
+
import { readFileSync as readFileSync34 } from "fs";
|
|
64998
66164
|
import { basename as basename5, relative as relative19 } from "path";
|
|
64999
66165
|
var PATTERN_CATEGORIES = [
|
|
65000
66166
|
"modal",
|
|
@@ -65097,7 +66263,7 @@ function detectFormsFromFiles(files) {
|
|
|
65097
66263
|
for (const f of files) {
|
|
65098
66264
|
let source;
|
|
65099
66265
|
try {
|
|
65100
|
-
source =
|
|
66266
|
+
source = readFileSync34(f, "utf-8");
|
|
65101
66267
|
} catch {
|
|
65102
66268
|
continue;
|
|
65103
66269
|
}
|
|
@@ -65224,7 +66390,7 @@ function formatText2(report) {
|
|
|
65224
66390
|
const label = CATEGORY_LABELS2[cat].padEnd(20);
|
|
65225
66391
|
const count = String(stats.count).padStart(2);
|
|
65226
66392
|
const baseline = String(stats.baseline).padStart(1);
|
|
65227
|
-
const excess = stats.excess > 0 ? `\u26A0 +${stats.excess} over` : "\u2713
|
|
66393
|
+
const excess = stats.excess > 0 ? `\u26A0 +${stats.excess} over` : stats.count === 0 ? "\u2713 none" : "\u2713 within baseline";
|
|
65228
66394
|
lines.push(` ${label} ${count} implementations (baseline ${baseline}) ${excess}`);
|
|
65229
66395
|
}
|
|
65230
66396
|
if (report.doNotCreate.length > 0) {
|
|
@@ -65282,7 +66448,7 @@ function formatMarkdown3(report) {
|
|
|
65282
66448
|
for (const cat of PATTERN_CATEGORIES) {
|
|
65283
66449
|
const stats = report.byCategory[cat];
|
|
65284
66450
|
const label = CATEGORY_LABELS2[cat];
|
|
65285
|
-
const excess = stats.excess > 0 ? `+${stats.excess}` : "\u2014";
|
|
66451
|
+
const excess = stats.excess > 0 ? `+${stats.excess}` : stats.count === 0 ? "\u2014" : "\xB7";
|
|
65286
66452
|
lines.push(`| ${label} | ${stats.count} | ${stats.baseline} | ${excess} |`);
|
|
65287
66453
|
}
|
|
65288
66454
|
if (report.doNotCreate.length > 0) {
|
|
@@ -65318,7 +66484,7 @@ function registerPatterns(program) {
|
|
|
65318
66484
|
const options = command.optsWithGlobals();
|
|
65319
66485
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
65320
66486
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
65321
|
-
const cwd =
|
|
66487
|
+
const cwd = resolve38(options.workspace ?? process.cwd());
|
|
65322
66488
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
65323
66489
|
const result = await runPatternsScan(cwd, config, {
|
|
65324
66490
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -65337,8 +66503,8 @@ function registerPatterns(program) {
|
|
|
65337
66503
|
// src/cli/commands/research.ts
|
|
65338
66504
|
init_logger();
|
|
65339
66505
|
init_config2();
|
|
65340
|
-
import { existsSync as
|
|
65341
|
-
import { dirname as
|
|
66506
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync35, writeFileSync as writeFileSync19 } from "fs";
|
|
66507
|
+
import { dirname as dirname18, resolve as resolve39 } from "path";
|
|
65342
66508
|
function registerResearch(program) {
|
|
65343
66509
|
const research = program.command("research").description("research commands for the AI UI learning loop");
|
|
65344
66510
|
research.command("generate").description("generate synthetic UI samples").requiredOption("--count <n>", "number of samples", parseCount).requiredOption("--framework <name>", "target framework").requiredOption("--component-type <type>", "component type").requiredOption("--provider <name>", "AI provider (openai)").option("--api-key <key>", "API key for provider").option("--model <name>", "model name").option("--temperature <n>", "sampling temperature", parseFloat, 0.7).option("--output-dir <path>", "output directory", ".slopbrick/corpus/generated").action(async (cmdOptions) => {
|
|
@@ -65353,24 +66519,24 @@ function registerResearch(program) {
|
|
|
65353
66519
|
framework: cmdOptions.framework,
|
|
65354
66520
|
componentType: cmdOptions.componentType,
|
|
65355
66521
|
provider,
|
|
65356
|
-
outputDir:
|
|
66522
|
+
outputDir: resolve39(cmdOptions.outputDir),
|
|
65357
66523
|
temperature: cmdOptions.temperature
|
|
65358
66524
|
});
|
|
65359
66525
|
logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
|
|
65360
66526
|
});
|
|
65361
66527
|
research.command("analyze").description("analyze generated samples and report coverage").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "analysis output path", ".slopbrick/flywheel/analysis.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").action(async (cmdOptions) => {
|
|
65362
66528
|
try {
|
|
65363
|
-
const metadataPath =
|
|
65364
|
-
if (!
|
|
66529
|
+
const metadataPath = resolve39(cmdOptions.inputDir, "metadata.json");
|
|
66530
|
+
if (!existsSync27(metadataPath)) {
|
|
65365
66531
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
65366
66532
|
process.exit(2);
|
|
65367
66533
|
}
|
|
65368
|
-
const samples = JSON.parse(
|
|
66534
|
+
const samples = JSON.parse(readFileSync35(metadataPath, "utf8"));
|
|
65369
66535
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
65370
66536
|
const analysis = await analyzeSamples(samples, config);
|
|
65371
|
-
const outputPath =
|
|
65372
|
-
|
|
65373
|
-
|
|
66537
|
+
const outputPath = resolve39(cmdOptions.output);
|
|
66538
|
+
mkdirSync15(dirname18(outputPath), { recursive: true });
|
|
66539
|
+
writeFileSync19(outputPath, JSON.stringify(analysis, null, 2), "utf8");
|
|
65374
66540
|
logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
|
|
65375
66541
|
logger.info(`Wrote analysis to ${outputPath}`);
|
|
65376
66542
|
} catch (error) {
|
|
@@ -65380,12 +66546,12 @@ function registerResearch(program) {
|
|
|
65380
66546
|
});
|
|
65381
66547
|
research.command("candidates").description("extract patterns from generated samples and emit candidate rules").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "output path", ".slopbrick/flywheel/rule-candidates.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").option("--min-frequency <n>", "minimum cluster frequency", parseCount, 2).option("--include-covered", "include samples already covered by AI-specific rules").action(async (cmdOptions) => {
|
|
65382
66548
|
try {
|
|
65383
|
-
const metadataPath =
|
|
65384
|
-
if (!
|
|
66549
|
+
const metadataPath = resolve39(cmdOptions.inputDir, "metadata.json");
|
|
66550
|
+
if (!existsSync27(metadataPath)) {
|
|
65385
66551
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
65386
66552
|
process.exit(2);
|
|
65387
66553
|
}
|
|
65388
|
-
const samples = JSON.parse(
|
|
66554
|
+
const samples = JSON.parse(readFileSync35(metadataPath, "utf8"));
|
|
65389
66555
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
65390
66556
|
const analysis = await analyzeSamples(samples, config);
|
|
65391
66557
|
const extraction = extractAndCluster(analysis.samples, {
|
|
@@ -65395,8 +66561,8 @@ function registerResearch(program) {
|
|
|
65395
66561
|
const candidates = clustersToCandidates(extraction.clusters, {
|
|
65396
66562
|
minFrequency: cmdOptions.minFrequency
|
|
65397
66563
|
});
|
|
65398
|
-
const outputPath =
|
|
65399
|
-
|
|
66564
|
+
const outputPath = resolve39(cmdOptions.output);
|
|
66565
|
+
mkdirSync15(dirname18(outputPath), { recursive: true });
|
|
65400
66566
|
const payload = {
|
|
65401
66567
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65402
66568
|
sampleCount: analysis.summary.total,
|
|
@@ -65404,7 +66570,7 @@ function registerResearch(program) {
|
|
|
65404
66570
|
fingerprintCount: extraction.total,
|
|
65405
66571
|
candidates
|
|
65406
66572
|
};
|
|
65407
|
-
|
|
66573
|
+
writeFileSync19(outputPath, JSON.stringify(payload, null, 2), "utf8");
|
|
65408
66574
|
logger.info(`Extracted ${extraction.total} fingerprints across ${analysis.summary.total} samples`);
|
|
65409
66575
|
logger.info(`Wrote ${candidates.length} candidate rule(s) to ${outputPath}`);
|
|
65410
66576
|
} catch (error) {
|
|
@@ -65417,8 +66583,8 @@ function registerResearch(program) {
|
|
|
65417
66583
|
// src/cli/commands/init.ts
|
|
65418
66584
|
init_logger();
|
|
65419
66585
|
init_builtins();
|
|
65420
|
-
import { existsSync as
|
|
65421
|
-
import { dirname as
|
|
66586
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync36, writeFileSync as writeFileSync20 } from "fs";
|
|
66587
|
+
import { dirname as dirname19, join as join31, resolve as resolve40 } from "path";
|
|
65422
66588
|
init_scan2();
|
|
65423
66589
|
init_config2();
|
|
65424
66590
|
init_threshold();
|
|
@@ -65426,145 +66592,9 @@ init_git();
|
|
|
65426
66592
|
init_cache();
|
|
65427
66593
|
|
|
65428
66594
|
// src/snippet/targets.ts
|
|
65429
|
-
|
|
65430
|
-
|
|
65431
|
-
|
|
65432
|
-
function aiSpecificRules(rules) {
|
|
65433
|
-
return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
|
|
65434
|
-
}
|
|
65435
|
-
function categorizeRules(rules) {
|
|
65436
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
65437
|
-
for (const r of rules) {
|
|
65438
|
-
const list = byCategory.get(r.category) ?? [];
|
|
65439
|
-
list.push(r);
|
|
65440
|
-
byCategory.set(r.category, list);
|
|
65441
|
-
}
|
|
65442
|
-
for (const list of byCategory.values()) {
|
|
65443
|
-
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
65444
|
-
}
|
|
65445
|
-
return byCategory;
|
|
65446
|
-
}
|
|
65447
|
-
function baseContent(rules, toolLabel, intro) {
|
|
65448
|
-
const lines = [];
|
|
65449
|
-
lines.push("# " + toolLabel);
|
|
65450
|
-
lines.push("");
|
|
65451
|
-
lines.push(intro);
|
|
65452
|
-
lines.push("");
|
|
65453
|
-
const ai = aiSpecificRules(rules);
|
|
65454
|
-
const byCategory = categorizeRules(ai);
|
|
65455
|
-
lines.push("## Category-level directives");
|
|
65456
|
-
lines.push("");
|
|
65457
|
-
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
65458
|
-
lines.push("- **" + category + "**: " + directive);
|
|
65459
|
-
}
|
|
65460
|
-
lines.push("");
|
|
65461
|
-
lines.push("## Per-rule directives");
|
|
65462
|
-
lines.push("");
|
|
65463
|
-
for (const [category, list] of byCategory) {
|
|
65464
|
-
lines.push("### " + category);
|
|
65465
|
-
lines.push("");
|
|
65466
|
-
for (const rule of list) {
|
|
65467
|
-
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
65468
|
-
lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
|
|
65469
|
-
}
|
|
65470
|
-
lines.push("");
|
|
65471
|
-
}
|
|
65472
|
-
return lines.join("\n");
|
|
65473
|
-
}
|
|
65474
|
-
|
|
65475
|
-
// src/snippet/generators.ts
|
|
65476
|
-
function generateCursorSnippet(rules) {
|
|
65477
|
-
const lines = [];
|
|
65478
|
-
lines.push("# slopbrick cursor rules");
|
|
65479
|
-
lines.push("");
|
|
65480
|
-
lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
|
|
65481
|
-
lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
|
|
65482
|
-
lines.push("");
|
|
65483
|
-
lines.push("## Category-level directives");
|
|
65484
|
-
lines.push("");
|
|
65485
|
-
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
65486
|
-
lines.push("- **" + category + "**: " + directive);
|
|
65487
|
-
}
|
|
65488
|
-
lines.push("");
|
|
65489
|
-
lines.push("## Per-rule directives");
|
|
65490
|
-
lines.push("");
|
|
65491
|
-
const ai = aiSpecificRules(rules);
|
|
65492
|
-
const byCategory = categorizeRules(ai);
|
|
65493
|
-
for (const [category, list] of byCategory) {
|
|
65494
|
-
lines.push("### " + category);
|
|
65495
|
-
lines.push("");
|
|
65496
|
-
for (const rule of list) {
|
|
65497
|
-
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
65498
|
-
lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
|
|
65499
|
-
}
|
|
65500
|
-
lines.push("");
|
|
65501
|
-
}
|
|
65502
|
-
lines.push("## Verification");
|
|
65503
|
-
lines.push("");
|
|
65504
|
-
lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
|
|
65505
|
-
lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
|
|
65506
|
-
lines.push("");
|
|
65507
|
-
return lines.join("\n");
|
|
65508
|
-
}
|
|
65509
|
-
function generateAgentsMdSnippet(rules) {
|
|
65510
|
-
return baseContent(
|
|
65511
|
-
rules,
|
|
65512
|
-
"AGENTS.md",
|
|
65513
|
-
"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."
|
|
65514
|
-
) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n";
|
|
65515
|
-
}
|
|
65516
|
-
function generateClaudeMdSnippet(rules) {
|
|
65517
|
-
return baseContent(
|
|
65518
|
-
rules,
|
|
65519
|
-
"CLAUDE.md",
|
|
65520
|
-
"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."
|
|
65521
|
-
) + "## 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";
|
|
65522
|
-
}
|
|
65523
|
-
function generateAiderSnippet(rules) {
|
|
65524
|
-
return baseContent(
|
|
65525
|
-
rules,
|
|
65526
|
-
"CONVENTIONS.md",
|
|
65527
|
-
"Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
|
|
65528
|
-
);
|
|
65529
|
-
}
|
|
65530
|
-
function generateWindsurfSnippet(rules) {
|
|
65531
|
-
return baseContent(
|
|
65532
|
-
rules,
|
|
65533
|
-
".windsurfrules",
|
|
65534
|
-
"Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
|
|
65535
|
-
);
|
|
65536
|
-
}
|
|
65537
|
-
function generateClineRules(rules) {
|
|
65538
|
-
return baseContent(
|
|
65539
|
-
rules,
|
|
65540
|
-
"AGENTS.md",
|
|
65541
|
-
"Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
|
|
65542
|
-
);
|
|
65543
|
-
}
|
|
65544
|
-
function generateCursorrulesLegacySnippet(rules) {
|
|
65545
|
-
return baseContent(
|
|
65546
|
-
rules,
|
|
65547
|
-
".cursorrules",
|
|
65548
|
-
"Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
|
|
65549
|
-
);
|
|
65550
|
-
}
|
|
65551
|
-
function generateGeminiSnippet(rules) {
|
|
65552
|
-
return baseContent(
|
|
65553
|
-
rules,
|
|
65554
|
-
"GEMINI.md",
|
|
65555
|
-
"Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
|
|
65556
|
-
);
|
|
65557
|
-
}
|
|
65558
|
-
function generateCopilotSnippet(rules) {
|
|
65559
|
-
return baseContent(
|
|
65560
|
-
rules,
|
|
65561
|
-
"copilot-instructions.md",
|
|
65562
|
-
"Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
|
|
65563
|
-
);
|
|
65564
|
-
}
|
|
65565
|
-
|
|
65566
|
-
// src/snippet/targets.ts
|
|
65567
|
-
var SNIPPET_TARGETS = [
|
|
66595
|
+
init_generators();
|
|
66596
|
+
import { join as join30 } from "path";
|
|
66597
|
+
var SNIPPET_TARGETS2 = [
|
|
65568
66598
|
{
|
|
65569
66599
|
flag: "cursor",
|
|
65570
66600
|
cliName: "--cursor",
|
|
@@ -65657,7 +66687,7 @@ var SNIPPET_TARGETS = [
|
|
|
65657
66687
|
}
|
|
65658
66688
|
];
|
|
65659
66689
|
function resolveTargetPath(target) {
|
|
65660
|
-
return target.isFolder ?
|
|
66690
|
+
return target.isFolder ? join30(target.path, target.filename) : target.path;
|
|
65661
66691
|
}
|
|
65662
66692
|
function renderMatrix() {
|
|
65663
66693
|
const lines = [];
|
|
@@ -65665,7 +66695,7 @@ function renderMatrix() {
|
|
|
65665
66695
|
lines.push("");
|
|
65666
66696
|
lines.push("| Flag | File | Agent |");
|
|
65667
66697
|
lines.push("|------|------|-------|");
|
|
65668
|
-
for (const t of
|
|
66698
|
+
for (const t of SNIPPET_TARGETS2) {
|
|
65669
66699
|
lines.push("| `" + t.cliName + "` | `" + resolveTargetPath(t) + "` | " + t.agent + " |");
|
|
65670
66700
|
}
|
|
65671
66701
|
return lines.join("\n");
|
|
@@ -65679,13 +66709,13 @@ function registerInit(program) {
|
|
|
65679
66709
|
logger.info(renderMatrix());
|
|
65680
66710
|
process.exit(0);
|
|
65681
66711
|
}
|
|
65682
|
-
const cwd =
|
|
65683
|
-
const configPath =
|
|
66712
|
+
const cwd = resolve40(options.workspace ?? process.cwd());
|
|
66713
|
+
const configPath = join31(cwd, "slopbrick.config.mjs");
|
|
65684
66714
|
const detected = detectStack(cwd);
|
|
65685
66715
|
const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
|
|
65686
66716
|
const proposed = serializeConfig(fallbackConfig);
|
|
65687
|
-
if (
|
|
65688
|
-
const current =
|
|
66717
|
+
if (existsSync28(configPath) && !cmdOptions.yes) {
|
|
66718
|
+
const current = readFileSync36(configPath, "utf8");
|
|
65689
66719
|
logger.error(`A config file already exists at ${configPath}.`);
|
|
65690
66720
|
logger.error("To overwrite it with defaults, run `slopbrick init --yes`.");
|
|
65691
66721
|
logger.error("");
|
|
@@ -65706,7 +66736,7 @@ function registerInit(program) {
|
|
|
65706
66736
|
config = buildInitConfig(detected, answers);
|
|
65707
66737
|
usedWizard = true;
|
|
65708
66738
|
}
|
|
65709
|
-
|
|
66739
|
+
writeFileSync20(configPath, serializeConfig(config));
|
|
65710
66740
|
appendGitignore(cwd);
|
|
65711
66741
|
const refresh = await refreshRegistrySnapshot(cwd);
|
|
65712
66742
|
if (!refresh.ok) {
|
|
@@ -65730,27 +66760,27 @@ function registerInit(program) {
|
|
|
65730
66760
|
}
|
|
65731
66761
|
logger.info(refresh.message);
|
|
65732
66762
|
}
|
|
65733
|
-
const targetsToWrite =
|
|
66763
|
+
const targetsToWrite = SNIPPET_TARGETS2.filter((t) => {
|
|
65734
66764
|
if (cmdOptions.all) return true;
|
|
65735
66765
|
const opts = cmdOptions;
|
|
65736
66766
|
return Boolean(opts[t.flag]);
|
|
65737
66767
|
});
|
|
65738
66768
|
for (const target of targetsToWrite) {
|
|
65739
|
-
const snippetPath =
|
|
65740
|
-
|
|
66769
|
+
const snippetPath = join31(cwd, resolveTargetPath(target));
|
|
66770
|
+
mkdirSync16(dirname19(snippetPath), { recursive: true });
|
|
65741
66771
|
const generated = target.generator(builtinRules);
|
|
65742
|
-
if (!target.isFolder &&
|
|
65743
|
-
const existing =
|
|
66772
|
+
if (!target.isFolder && existsSync28(snippetPath)) {
|
|
66773
|
+
const existing = readFileSync36(snippetPath, "utf8");
|
|
65744
66774
|
if (existing.includes("<!-- slopbrick:begin -->")) {
|
|
65745
66775
|
const updated = existing.replace(
|
|
65746
66776
|
/<!-- slopbrick:begin -->[\s\S]*?<!-- slopbrick:end -->/,
|
|
65747
66777
|
"<!-- slopbrick:begin -->\n" + generated + "<!-- slopbrick:end -->"
|
|
65748
66778
|
);
|
|
65749
|
-
|
|
66779
|
+
writeFileSync20(snippetPath, updated, "utf8");
|
|
65750
66780
|
if (!options.quiet) logger.info(`Updated ${snippetPath}`);
|
|
65751
66781
|
continue;
|
|
65752
66782
|
}
|
|
65753
|
-
|
|
66783
|
+
writeFileSync20(
|
|
65754
66784
|
snippetPath,
|
|
65755
66785
|
existing + (existing.endsWith("\n") ? "\n" : "\n\n") + generated,
|
|
65756
66786
|
"utf8"
|
|
@@ -65758,7 +66788,7 @@ function registerInit(program) {
|
|
|
65758
66788
|
if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
|
|
65759
66789
|
continue;
|
|
65760
66790
|
}
|
|
65761
|
-
|
|
66791
|
+
writeFileSync20(snippetPath, generated, "utf8");
|
|
65762
66792
|
if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
|
|
65763
66793
|
}
|
|
65764
66794
|
if (options.baseline) {
|
|
@@ -65778,12 +66808,12 @@ function registerInit(program) {
|
|
|
65778
66808
|
// src/cli/commands/flywheel.ts
|
|
65779
66809
|
init_logger();
|
|
65780
66810
|
init_telemetry();
|
|
65781
|
-
import { mkdirSync as
|
|
65782
|
-
import { dirname as
|
|
66811
|
+
import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync21 } from "fs";
|
|
66812
|
+
import { dirname as dirname20, resolve as resolve41 } from "path";
|
|
65783
66813
|
|
|
65784
66814
|
// src/report/flywheel.ts
|
|
65785
|
-
import { existsSync as
|
|
65786
|
-
import { join as
|
|
66815
|
+
import { existsSync as existsSync29, readFileSync as readFileSync37 } from "fs";
|
|
66816
|
+
import { join as join32 } from "path";
|
|
65787
66817
|
function average(values) {
|
|
65788
66818
|
if (values.length === 0) return 0;
|
|
65789
66819
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
@@ -65843,8 +66873,9 @@ function formatFlywheel(summary, options = {}) {
|
|
|
65843
66873
|
if (summary.firstRunAt && summary.latestRunAt) {
|
|
65844
66874
|
lines.push(` Window: ${summary.firstRunAt} \u2192 ${summary.latestRunAt}`);
|
|
65845
66875
|
}
|
|
65846
|
-
|
|
65847
|
-
lines.push(`
|
|
66876
|
+
const directionNote = " (lower = cleaner since v0.21.0)";
|
|
66877
|
+
lines.push(` Average AI Slop Score: ${summary.averageSlopIndex.toFixed(2)}`);
|
|
66878
|
+
lines.push(` Latest AI Slop Score: ${summary.latestSlopIndex.toFixed(2)}${directionNote}`);
|
|
65848
66879
|
lines.push(` Average assembly health: ${summary.averageAssemblyHealth.toFixed(2)}`);
|
|
65849
66880
|
lines.push(` Latest assembly health: ${summary.latestAssemblyHealth.toFixed(2)}`);
|
|
65850
66881
|
if (summary.topViolations.length > 0) {
|
|
@@ -65873,7 +66904,7 @@ function formatFlywheel(summary, options = {}) {
|
|
|
65873
66904
|
function registerFlywheel(program) {
|
|
65874
66905
|
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) => {
|
|
65875
66906
|
const options = command.optsWithGlobals();
|
|
65876
|
-
const cwd =
|
|
66907
|
+
const cwd = resolve41(options.workspace ?? process.cwd());
|
|
65877
66908
|
const payloads = readTelemetry(cwd);
|
|
65878
66909
|
if (payloads.length === 0) {
|
|
65879
66910
|
logger.info("No flywheel telemetry found. Run a scan first.");
|
|
@@ -65881,9 +66912,9 @@ function registerFlywheel(program) {
|
|
|
65881
66912
|
}
|
|
65882
66913
|
const summary = summarizeTelemetry(payloads);
|
|
65883
66914
|
if (cmdOptions.export) {
|
|
65884
|
-
const exportPath =
|
|
65885
|
-
|
|
65886
|
-
|
|
66915
|
+
const exportPath = resolve41(cmdOptions.export);
|
|
66916
|
+
mkdirSync17(dirname20(exportPath), { recursive: true });
|
|
66917
|
+
writeFileSync21(exportPath, JSON.stringify(summary, null, 2), "utf-8");
|
|
65887
66918
|
logger.info(`Wrote flywheel summary to ${exportPath}`);
|
|
65888
66919
|
process.exit(0);
|
|
65889
66920
|
}
|
|
@@ -65910,8 +66941,8 @@ init_dist2();
|
|
|
65910
66941
|
init_memory_io();
|
|
65911
66942
|
|
|
65912
66943
|
// src/fix/focus-ring.ts
|
|
65913
|
-
import { existsSync as
|
|
65914
|
-
import { readFileSync as
|
|
66944
|
+
import { existsSync as existsSync30 } from "fs";
|
|
66945
|
+
import { readFileSync as readFileSync38, writeFileSync as writeFileSync22 } from "fs";
|
|
65915
66946
|
var ANCHOR_START = "/* @slopbrick:v1.0.0:fix:focus-ring */";
|
|
65916
66947
|
var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
|
|
65917
66948
|
:focus-visible {
|
|
@@ -65920,15 +66951,15 @@ var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
|
|
|
65920
66951
|
}
|
|
65921
66952
|
/* @slopbrick:v1.0.0:fix:focus-ring-end */`;
|
|
65922
66953
|
function applyFocusRingFix(targetFile) {
|
|
65923
|
-
if (!
|
|
66954
|
+
if (!existsSync30(targetFile)) {
|
|
65924
66955
|
return { applied: false, reason: "missing-global-css-target" };
|
|
65925
66956
|
}
|
|
65926
|
-
const content =
|
|
66957
|
+
const content = readFileSync38(targetFile, "utf-8");
|
|
65927
66958
|
if (content.includes(ANCHOR_START)) {
|
|
65928
66959
|
return { applied: false, reason: "already-present" };
|
|
65929
66960
|
}
|
|
65930
66961
|
const separator = content.endsWith("\n") ? "" : "\n";
|
|
65931
|
-
|
|
66962
|
+
writeFileSync22(targetFile, `${content}${separator}${CSS_BLOCK}
|
|
65932
66963
|
`);
|
|
65933
66964
|
return { applied: true };
|
|
65934
66965
|
}
|
|
@@ -65937,21 +66968,21 @@ function applyFocusRingFix(targetFile) {
|
|
|
65937
66968
|
init_layout_token();
|
|
65938
66969
|
|
|
65939
66970
|
// src/fix/use-client.ts
|
|
65940
|
-
import { readFileSync as
|
|
66971
|
+
import { readFileSync as readFileSync39, writeFileSync as writeFileSync23 } from "fs";
|
|
65941
66972
|
function applyUseClientFix(filePath) {
|
|
65942
|
-
const content =
|
|
66973
|
+
const content = readFileSync39(filePath, "utf-8");
|
|
65943
66974
|
const trimmed = content.trimStart();
|
|
65944
66975
|
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) {
|
|
65945
66976
|
return { applied: false, reason: "already-present" };
|
|
65946
66977
|
}
|
|
65947
|
-
|
|
66978
|
+
writeFileSync23(filePath, `"use client";
|
|
65948
66979
|
|
|
65949
66980
|
${content}`);
|
|
65950
66981
|
return { applied: true };
|
|
65951
66982
|
}
|
|
65952
66983
|
|
|
65953
66984
|
// src/fix/visual-codemod.ts
|
|
65954
|
-
import { readFileSync as
|
|
66985
|
+
import { readFileSync as readFileSync40, writeFileSync as writeFileSync24 } from "fs";
|
|
65955
66986
|
|
|
65956
66987
|
// src/fix/visual-codemods/tailwind.ts
|
|
65957
66988
|
var ARBITRARY_ESCAPE_RE = /\b(p|m|px|py|pt|pb|pl|pr|mx|my|mt|mb|ml|mr|gap|space-[xy]|w|h|min-w|max-w|min-h|max-h|text-\w+|leading-\w+|rounded|border)-?\[(-?\d+(?:\.\d+)?)(px|rem|em|%|vh|vw)?\]/g;
|
|
@@ -66160,7 +67191,7 @@ var ALL_CODEMODS = [
|
|
|
66160
67191
|
{ name: "aria-attr-typo", fn: applyAriaAttrTypoCodemod }
|
|
66161
67192
|
];
|
|
66162
67193
|
function applyVisualCodemods(filePath) {
|
|
66163
|
-
const original =
|
|
67194
|
+
const original = readFileSync40(filePath, "utf-8");
|
|
66164
67195
|
let content = original;
|
|
66165
67196
|
const reasons = [];
|
|
66166
67197
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -66176,7 +67207,7 @@ function applyVisualCodemods(filePath) {
|
|
|
66176
67207
|
}
|
|
66177
67208
|
}
|
|
66178
67209
|
if (content !== original) {
|
|
66179
|
-
|
|
67210
|
+
writeFileSync24(filePath, content);
|
|
66180
67211
|
}
|
|
66181
67212
|
return {
|
|
66182
67213
|
filePath,
|
|
@@ -66191,7 +67222,7 @@ function applyVisualCodemods(filePath) {
|
|
|
66191
67222
|
function collectAllFixes2(issue) {
|
|
66192
67223
|
return [...issue.fix ? [issue.fix] : [], ...issue.fixes ?? []];
|
|
66193
67224
|
}
|
|
66194
|
-
async function applyFixes(report,
|
|
67225
|
+
async function applyFixes(report, _config, scannedFiles) {
|
|
66195
67226
|
const byFile = /* @__PURE__ */ new Map();
|
|
66196
67227
|
for (const issue of report.issues) {
|
|
66197
67228
|
const fixes = collectAllFixes2(issue);
|
|
@@ -66252,7 +67283,6 @@ async function applyFixes(report, config, scannedFiles) {
|
|
|
66252
67283
|
const result = applyLayoutTokenFix(filePath, group.replaces);
|
|
66253
67284
|
const total = group.replaceApps.length;
|
|
66254
67285
|
const appliedCount = Math.min(result.applied, total);
|
|
66255
|
-
const skippedCount = total - appliedCount;
|
|
66256
67286
|
applied.push(...group.replaceApps.slice(0, appliedCount));
|
|
66257
67287
|
skipped.push(...group.replaceApps.slice(appliedCount));
|
|
66258
67288
|
} catch (err) {
|
|
@@ -66447,6 +67477,40 @@ function formatGroupedHelp(cmd) {
|
|
|
66447
67477
|
}
|
|
66448
67478
|
}
|
|
66449
67479
|
lines.push("");
|
|
67480
|
+
lines.push(
|
|
67481
|
+
"Commands (run `<command> --help` for per-command details):\n" + [
|
|
67482
|
+
" scan score a workspace, with severity gating and report views",
|
|
67483
|
+
" pr score a PR (or a unified-diff file via `--diff <file>`)",
|
|
67484
|
+
" drift detect constitution / forbidden-package violations",
|
|
67485
|
+
" test test quality sub-score (CI gate)",
|
|
67486
|
+
" docs documentation freshness sub-score",
|
|
67487
|
+
" security AI security-risk sub-score",
|
|
67488
|
+
" rules list all built-in rules with categories and severities",
|
|
67489
|
+
" explain show rationale, pattern, and remediation for a single rule",
|
|
67490
|
+
" suggest print per-issue remediation advice",
|
|
67491
|
+
" badge print a shields.io slop-index badge",
|
|
67492
|
+
" doctor check setup, config, and environment for common problems",
|
|
67493
|
+
" init create a slopbrick config file and AGENTS.md/CLAUDE.md snippets",
|
|
67494
|
+
" flywheel summarize aggregated scan telemetry",
|
|
67495
|
+
" composite empirical-composite rule discover / enable (Sprint 3 \xA73b)",
|
|
67496
|
+
" calibrate empirical precision/recall/F1 calibration from held-out corpora",
|
|
67497
|
+
" patterns pattern-fragmentation sub-score",
|
|
67498
|
+
" business-logic business-logic coherence sub-score",
|
|
67499
|
+
" maintenance-cost AI maintenance-cost sub-score",
|
|
67500
|
+
" architecture architecture-consistency sub-score",
|
|
67501
|
+
" memory show patterns detected for `slop_suggest`",
|
|
67502
|
+
" lock install a git pre-commit hook (idempotent)",
|
|
67503
|
+
" migrate v0.10.x slop-audit/ \u2192 v0.11.0+ .slopbrick/ migration",
|
|
67504
|
+
" validate-config validate slopbrick.config.mjs",
|
|
67505
|
+
" tokens merge a design-tokens.json layout values into the allowlist",
|
|
67506
|
+
" report re-render a saved JSON report (from --json <path>)",
|
|
67507
|
+
" install / uninstall manage the git pre-commit hook",
|
|
67508
|
+
" mcp run the MCP server (JSON-RPC 2.0 over stdio)",
|
|
67509
|
+
" watch / --watch re-run scan on file change",
|
|
67510
|
+
" trend sparkline of the last n runs"
|
|
67511
|
+
].join("\n")
|
|
67512
|
+
);
|
|
67513
|
+
lines.push("");
|
|
66450
67514
|
lines.push(
|
|
66451
67515
|
"Use `--help-flat` for the standard un-grouped list. See `https://usebrick.dev/docs/scan-options` for full docs."
|
|
66452
67516
|
);
|
|
@@ -66458,12 +67522,12 @@ import * as http from "http";
|
|
|
66458
67522
|
import * as https from "https";
|
|
66459
67523
|
import { URL as URL2 } from "url";
|
|
66460
67524
|
var sendBeacon = (url, payload, timeoutMs) => {
|
|
66461
|
-
return new Promise((
|
|
67525
|
+
return new Promise((resolve43) => {
|
|
66462
67526
|
let parsed;
|
|
66463
67527
|
try {
|
|
66464
67528
|
parsed = new URL2(url);
|
|
66465
67529
|
} catch {
|
|
66466
|
-
|
|
67530
|
+
resolve43();
|
|
66467
67531
|
return;
|
|
66468
67532
|
}
|
|
66469
67533
|
const isHttps = parsed.protocol === "https:";
|
|
@@ -66484,7 +67548,7 @@ var sendBeacon = (url, payload, timeoutMs) => {
|
|
|
66484
67548
|
const settle = () => {
|
|
66485
67549
|
if (settled) return;
|
|
66486
67550
|
settled = true;
|
|
66487
|
-
|
|
67551
|
+
resolve43();
|
|
66488
67552
|
};
|
|
66489
67553
|
let req;
|
|
66490
67554
|
try {
|
|
@@ -66564,7 +67628,7 @@ process.on("uncaughtException", (err) => {
|
|
|
66564
67628
|
});
|
|
66565
67629
|
async function runCli({ start }) {
|
|
66566
67630
|
try {
|
|
66567
|
-
const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if
|
|
67631
|
+
const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if AI Slop Score increased since last run (lower = cleaner since v0.21)").option("--refresh-snippets", "rewrite the managed slopbrick block in AGENTS.md and CLAUDE.md after a scan").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
|
|
66568
67632
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
66569
67633
|
setExitOverride(program);
|
|
66570
67634
|
registerInit(program);
|
|
@@ -66573,6 +67637,7 @@ async function runCli({ start }) {
|
|
|
66573
67637
|
registerBadge(program);
|
|
66574
67638
|
registerSuggest(program);
|
|
66575
67639
|
registerExplain(program);
|
|
67640
|
+
registerComposite(program);
|
|
66576
67641
|
registerCalibration(program);
|
|
66577
67642
|
registerFlywheel(program);
|
|
66578
67643
|
const scanAction = async (paths, _options, command) => {
|
|
@@ -66591,7 +67656,7 @@ async function runCli({ start }) {
|
|
|
66591
67656
|
logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
|
|
66592
67657
|
process.exit(2);
|
|
66593
67658
|
}
|
|
66594
|
-
const cwd =
|
|
67659
|
+
const cwd = resolve42(options.workspace ?? process.cwd());
|
|
66595
67660
|
if (options.trend !== void 0) {
|
|
66596
67661
|
const runs = await readRuns(cwd, fsMemoryIO);
|
|
66597
67662
|
if (runs.length === 0) {
|
|
@@ -66639,25 +67704,25 @@ async function runCli({ start }) {
|
|
|
66639
67704
|
});
|
|
66640
67705
|
}
|
|
66641
67706
|
if (options.baseline) {
|
|
66642
|
-
const cwd2 =
|
|
67707
|
+
const cwd2 = resolve42(options.workspace ?? process.cwd());
|
|
66643
67708
|
const configHash = hashConfig(config);
|
|
66644
67709
|
const gitHead = await getGitHead(cwd2) ?? "unknown";
|
|
66645
67710
|
const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
|
|
66646
67711
|
saveBaseline(cwd2, cache);
|
|
66647
|
-
if (!options.quiet) {
|
|
67712
|
+
if (!options.quiet && !machineReadableStdout) {
|
|
66648
67713
|
logger.info(`Saved baseline to ${baselinePath(cwd2)}`);
|
|
66649
67714
|
}
|
|
66650
67715
|
}
|
|
66651
67716
|
if (options.tighten && baseline) {
|
|
66652
67717
|
saveBaseline(cwd, baseline);
|
|
66653
|
-
if (!options.quiet) {
|
|
67718
|
+
if (!options.quiet && !machineReadableStdout) {
|
|
66654
67719
|
logger.info(`Tightened baseline saved (revision ${baseline.baseline_revision}).`);
|
|
66655
67720
|
}
|
|
66656
67721
|
}
|
|
66657
|
-
if (options.doctor && !options.quiet) {
|
|
67722
|
+
if (options.doctor && !options.quiet && !machineReadableStdout) {
|
|
66658
67723
|
logger.info(`Doctor: bootstrap ${totalElapsed - scanElapsed}ms, scan ${scanElapsed}ms`);
|
|
66659
67724
|
}
|
|
66660
|
-
if (report.baseline && !options.quiet) {
|
|
67725
|
+
if (report.baseline && !options.quiet && !machineReadableStdout) {
|
|
66661
67726
|
logger.info(baselineStatusMessage(report.baseline));
|
|
66662
67727
|
}
|
|
66663
67728
|
if (options.fix) {
|
|
@@ -66671,7 +67736,7 @@ async function runCli({ start }) {
|
|
|
66671
67736
|
}
|
|
66672
67737
|
const scannedFiles = results.map((r) => r.filePath);
|
|
66673
67738
|
const fixResults = await applyFixes(report, config, scannedFiles);
|
|
66674
|
-
const {
|
|
67739
|
+
const { hasErrors } = printFixSummary(fixResults, options.quiet ?? false);
|
|
66675
67740
|
if (!options.showFixesDiff && !options.quiet) {
|
|
66676
67741
|
logger.info("Tip: pass --show-fixes-diff to see the unified diff of proposed changes.");
|
|
66677
67742
|
}
|
|
@@ -66711,8 +67776,18 @@ async function runCli({ start }) {
|
|
|
66711
67776
|
if (options.staged && stagedGatingResult.reason) {
|
|
66712
67777
|
logger.error(`Gating failure: ${stagedGatingResult.reason}`);
|
|
66713
67778
|
} else {
|
|
66714
|
-
const
|
|
66715
|
-
|
|
67779
|
+
const failedNames = failedThresholds(report, config);
|
|
67780
|
+
const detail = failedNames.map((name) => {
|
|
67781
|
+
if (name === "meanSlop") return `meanSlop (score ${report.aiSlopScore} > ${config.thresholds.meanSlop})`;
|
|
67782
|
+
if (name === "p90Slop") return `p90Slop (score ${report.p90Score} > ${config.thresholds.p90Slop})`;
|
|
67783
|
+
if (name === "individualSlopThreshold") return `individualSlopThreshold (peak ${report.peakScore} > ${config.thresholds.individualSlopThreshold})`;
|
|
67784
|
+
const cat = name.replace(/^category:/, "");
|
|
67785
|
+
const limit = config.thresholds.categoryThresholds?.[cat];
|
|
67786
|
+
const score = report.categoryScores[cat];
|
|
67787
|
+
return `${name} (score ${score} > ${limit})`;
|
|
67788
|
+
}).join(", ");
|
|
67789
|
+
const failed = failedNames.length;
|
|
67790
|
+
logger.error(`${failed} threshold${failed === 1 ? "" : "s"} failed: ${detail}`);
|
|
66716
67791
|
}
|
|
66717
67792
|
}
|
|
66718
67793
|
if (!options.quiet && !machineReadableStdout) {
|
|
@@ -66809,6 +67884,7 @@ export {
|
|
|
66809
67884
|
directoryScore,
|
|
66810
67885
|
extractSignatures,
|
|
66811
67886
|
failedThresholdCount,
|
|
67887
|
+
failedThresholds,
|
|
66812
67888
|
filterByDisabledDirectives,
|
|
66813
67889
|
filterIssues,
|
|
66814
67890
|
findSimilarFunctions,
|