slopbrick 0.17.3 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/worker.cjs +16 -9
- package/dist/engine/worker.js +14 -7
- package/dist/index.cjs +418 -358
- package/dist/index.js +371 -311
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30235,7 +30235,7 @@ var init_math_element_uniformity = __esm({
|
|
|
30235
30235
|
if (!anchor) anchor = { line: el.line, column: el.column };
|
|
30236
30236
|
}
|
|
30237
30237
|
}
|
|
30238
|
-
const values = [counts.button, counts.input, counts.select].filter((v) => v > 0);
|
|
30238
|
+
const values = [counts.button, counts.input, counts.select].filter((v) => typeof v === "number" && v > 0);
|
|
30239
30239
|
if (values.length < 2) return issues;
|
|
30240
30240
|
const max = Math.max(...values);
|
|
30241
30241
|
const min = Math.min(...values);
|
|
@@ -30293,7 +30293,7 @@ var init_math_grid_uniformity = __esm({
|
|
|
30293
30293
|
const { h, vocab, total } = shannonEntropy(counts);
|
|
30294
30294
|
if (total < 4) return issues;
|
|
30295
30295
|
if (h > 1) return issues;
|
|
30296
|
-
const anchor = facts.v2 ? flatClassNames(facts.v2)[0] : { line: 1, column: 1 };
|
|
30296
|
+
const anchor = facts.v2 ? flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 } : { line: 1, column: 1 };
|
|
30297
30297
|
issues.push({
|
|
30298
30298
|
ruleId: "layout/math-grid-uniformity",
|
|
30299
30299
|
category: "layout",
|
|
@@ -34728,59 +34728,77 @@ __export(dist_exports, {
|
|
|
34728
34728
|
});
|
|
34729
34729
|
import { writeFileSync, renameSync, existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync4, mkdirSync } from "fs";
|
|
34730
34730
|
import { join as join9, dirname as dirname5 } from "path";
|
|
34731
|
+
function isRecord(v) {
|
|
34732
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34733
|
+
}
|
|
34734
|
+
function isStringArray(v) {
|
|
34735
|
+
return Array.isArray(v) && v.every((i) => typeof i === "string");
|
|
34736
|
+
}
|
|
34737
|
+
function isNumber(v) {
|
|
34738
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
34739
|
+
}
|
|
34731
34740
|
function isStructurePattern(value) {
|
|
34732
|
-
if (
|
|
34733
|
-
|
|
34734
|
-
return typeof v.category === "string" && typeof v.name === "string" && Array.isArray(v.imports) && v.imports.every((i) => typeof i === "string") && typeof v.fileCount === "number";
|
|
34741
|
+
if (!isRecord(value)) return false;
|
|
34742
|
+
return typeof value.category === "string" && typeof value.name === "string" && isStringArray(value.imports) && isNumber(value.fileCount);
|
|
34735
34743
|
}
|
|
34736
34744
|
function isComponentFingerprint(value) {
|
|
34737
|
-
if (
|
|
34738
|
-
|
|
34739
|
-
|
|
34745
|
+
if (!isRecord(value)) return false;
|
|
34746
|
+
return typeof value.name === "string" && isStringArray(value.files) && typeof value.fingerprint === "string" && isStringArray(value.hooks) && isStringArray(value.props) && isNumber(value.line) && isNumber(value.endLine);
|
|
34747
|
+
}
|
|
34748
|
+
function isVersion3(value) {
|
|
34749
|
+
return value === STRUCTURE_SCHEMA_VERSION;
|
|
34740
34750
|
}
|
|
34741
34751
|
function isInventoryFile(value) {
|
|
34742
|
-
if (
|
|
34743
|
-
|
|
34744
|
-
|
|
34752
|
+
if (!isRecord(value)) return false;
|
|
34753
|
+
if (!isVersion3(value.version)) return false;
|
|
34754
|
+
if (typeof value.generatedAt !== "string") return false;
|
|
34755
|
+
if (typeof value.workspace !== "string") return false;
|
|
34756
|
+
if (!isNumber(value.scannedFiles)) return false;
|
|
34757
|
+
if (!isNumber(value.scanDurationMs)) return false;
|
|
34758
|
+
if (!Array.isArray(value.patterns) || !value.patterns.every(isStructurePattern)) return false;
|
|
34759
|
+
if (!Array.isArray(value.components) || !value.components.every(isComponentFingerprint)) return false;
|
|
34760
|
+
return true;
|
|
34745
34761
|
}
|
|
34746
34762
|
function isConstitutionFile(value) {
|
|
34747
|
-
if (
|
|
34748
|
-
|
|
34749
|
-
|
|
34763
|
+
if (!isRecord(value)) return false;
|
|
34764
|
+
if (!isVersion3(value.version)) return false;
|
|
34765
|
+
if (typeof value.generatedAt !== "string") return false;
|
|
34766
|
+
if (typeof value.workspace !== "string") return false;
|
|
34767
|
+
if (!isRecord(value.declared)) return false;
|
|
34768
|
+
if (!isStringArray(value.forbidden)) return false;
|
|
34769
|
+
if (!isStringArray(value.forbiddenPrefixes)) return false;
|
|
34770
|
+
return true;
|
|
34750
34771
|
}
|
|
34751
34772
|
function isFileMtimeEntry(value) {
|
|
34752
|
-
if (
|
|
34753
|
-
|
|
34754
|
-
return typeof v.file === "string" && typeof v.mtimeMs === "number" && typeof v.hash === "string";
|
|
34773
|
+
if (!isRecord(value)) return false;
|
|
34774
|
+
return typeof value.file === "string" && isNumber(value.mtimeMs) && typeof value.hash === "string";
|
|
34755
34775
|
}
|
|
34756
34776
|
function isHealthFile(value) {
|
|
34757
|
-
if (
|
|
34758
|
-
|
|
34759
|
-
if (
|
|
34760
|
-
if (typeof
|
|
34761
|
-
if (
|
|
34762
|
-
if (
|
|
34763
|
-
if (
|
|
34764
|
-
if (
|
|
34765
|
-
if (
|
|
34766
|
-
|
|
34767
|
-
|
|
34768
|
-
if (
|
|
34769
|
-
if (
|
|
34770
|
-
if (
|
|
34771
|
-
if (
|
|
34772
|
-
|
|
34773
|
-
|
|
34774
|
-
|
|
34775
|
-
|
|
34776
|
-
|
|
34777
|
-
|
|
34778
|
-
if (
|
|
34779
|
-
|
|
34780
|
-
|
|
34781
|
-
|
|
34782
|
-
}
|
|
34783
|
-
if (v.scanDurationMs !== void 0 && typeof v.scanDurationMs !== "number") return false;
|
|
34777
|
+
if (!isRecord(value)) return false;
|
|
34778
|
+
if (!isVersion3(value.version)) return false;
|
|
34779
|
+
if (typeof value.generatedAt !== "string") return false;
|
|
34780
|
+
if (typeof value.workspace !== "string") return false;
|
|
34781
|
+
if (!isNumber(value.aiQuality)) return false;
|
|
34782
|
+
if (!isNumber(value.engineeringHygiene)) return false;
|
|
34783
|
+
if (!isNumber(value.security)) return false;
|
|
34784
|
+
if (!isNumber(value.repositoryHealth)) return false;
|
|
34785
|
+
if (!isRecord(value.issueCounts)) return false;
|
|
34786
|
+
const counts = value.issueCounts;
|
|
34787
|
+
if (!isNumber(counts.high)) return false;
|
|
34788
|
+
if (!isNumber(counts.medium)) return false;
|
|
34789
|
+
if (!isNumber(counts.low)) return false;
|
|
34790
|
+
if (value.slopIndex !== void 0 && !isNumber(value.slopIndex)) return false;
|
|
34791
|
+
if (value.categoryScores !== void 0) {
|
|
34792
|
+
if (!isRecord(value.categoryScores)) return false;
|
|
34793
|
+
for (const score of Object.values(value.categoryScores)) {
|
|
34794
|
+
if (!isNumber(score)) return false;
|
|
34795
|
+
}
|
|
34796
|
+
}
|
|
34797
|
+
if (value.constitutionDrift !== void 0 && !isNumber(value.constitutionDrift)) return false;
|
|
34798
|
+
if (value.topOffenseIds !== void 0) {
|
|
34799
|
+
if (!isStringArray(value.topOffenseIds)) return false;
|
|
34800
|
+
}
|
|
34801
|
+
if (value.scanDurationMs !== void 0 && !isNumber(value.scanDurationMs)) return false;
|
|
34784
34802
|
return true;
|
|
34785
34803
|
}
|
|
34786
34804
|
function inventoryPath(workspaceDir) {
|
|
@@ -36797,6 +36815,7 @@ var init_dist2 = __esm({
|
|
|
36797
36815
|
"../engine/dist/index.js"() {
|
|
36798
36816
|
"use strict";
|
|
36799
36817
|
init_dist();
|
|
36818
|
+
init_dist();
|
|
36800
36819
|
SMOOTHING = 0.5;
|
|
36801
36820
|
DEFAULT_PRIOR = { pAI: 0.5, pHuman: 0.5 };
|
|
36802
36821
|
TELEMETRY_FILE = join22(".slopbrick", "structure.json");
|
|
@@ -36805,7 +36824,11 @@ var init_dist2 = __esm({
|
|
|
36805
36824
|
RECALL_FLOOR = 1e-6;
|
|
36806
36825
|
LLR_CAP = 13.8;
|
|
36807
36826
|
DEFAULT_PRIOR_PREVALENCE = 0.3;
|
|
36808
|
-
ELIGIBLE_VERDICTS =
|
|
36827
|
+
ELIGIBLE_VERDICTS = new Set(
|
|
36828
|
+
["USEFUL", "OK", "NOISY", "INVERTED", "HYGIENE", "DORMANT"].filter(
|
|
36829
|
+
(v) => v !== "HYGIENE" && !isDefaultOff(v)
|
|
36830
|
+
)
|
|
36831
|
+
);
|
|
36809
36832
|
SUFFIXES_TO_STRIP = [
|
|
36810
36833
|
"RepositoryClient",
|
|
36811
36834
|
"ServiceFactory",
|
|
@@ -37645,7 +37668,7 @@ var init_math_gini_class_usage = __esm({
|
|
|
37645
37668
|
if (g < 0.5) return issues;
|
|
37646
37669
|
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
37647
37670
|
const topStr = sorted.map(([k, v]) => `${k}\xD7${v}`).join(", ");
|
|
37648
|
-
const anchor = facts.v2 ? flatClassNames(facts.v2)[0] : { line: 1, column: 1 };
|
|
37671
|
+
const anchor = facts.v2 ? flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 } : { line: 1, column: 1 };
|
|
37649
37672
|
issues.push({
|
|
37650
37673
|
ruleId: "logic/math-gini-class-usage",
|
|
37651
37674
|
category: "logic",
|
|
@@ -40948,7 +40971,7 @@ var init_math_font_entropy = __esm({
|
|
|
40948
40971
|
const { h, vocab, total } = shannonEntropy(counts);
|
|
40949
40972
|
if (total < 6) return issues;
|
|
40950
40973
|
if (h > 1.4) return issues;
|
|
40951
|
-
const anchor = flatClassNames(facts.v2)[0];
|
|
40974
|
+
const anchor = flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 };
|
|
40952
40975
|
issues.push({
|
|
40953
40976
|
ruleId: "visual/math-font-entropy",
|
|
40954
40977
|
category: "visual",
|
|
@@ -41088,7 +41111,7 @@ var init_math_rounded_entropy = __esm({
|
|
|
41088
41111
|
const { h, vocab, total } = shannonEntropy(counts);
|
|
41089
41112
|
if (total < 6) return issues;
|
|
41090
41113
|
if (h > 1.8) return issues;
|
|
41091
|
-
const anchor = flatClassNames(facts.v2)[0];
|
|
41114
|
+
const anchor = flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 };
|
|
41092
41115
|
issues.push({
|
|
41093
41116
|
ruleId: "visual/math-rounded-entropy",
|
|
41094
41117
|
category: "visual",
|
|
@@ -41136,7 +41159,7 @@ var init_math_spacing_entropy = __esm({
|
|
|
41136
41159
|
const { h, vocab, total } = shannonEntropy(counts);
|
|
41137
41160
|
if (total < 10) return issues;
|
|
41138
41161
|
if (h > 1.5) return issues;
|
|
41139
|
-
const anchor = flatClassNames(facts.v2)[0];
|
|
41162
|
+
const anchor = flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 };
|
|
41140
41163
|
issues.push({
|
|
41141
41164
|
ruleId: "visual/math-spacing-entropy",
|
|
41142
41165
|
category: "visual",
|
|
@@ -45349,7 +45372,7 @@ function formatPretty(report) {
|
|
|
45349
45372
|
}
|
|
45350
45373
|
function formatScoringExplainer(_report) {
|
|
45351
45374
|
return chalk.dim(
|
|
45352
|
-
"
|
|
45375
|
+
"Four orthogonal scores (all 0-100, higher = better): AI Quality (AI-slop signatures; the CI gate, AI Quality >= 70), Engineering Hygiene (issues per category across arch/logic/layout/component/test), Security (AI-flagged security risks, inverted from risk level), Repository Health (composite: 0.4*AI Quality + 0.3*Engineering Hygiene + 0.2*Security + 0.1*Test Quality). Only AI Quality gates CI; the others are informational. Default-off rules (INVERTED/NOISY/DORMANT) are suppressed from the scores automatically."
|
|
45353
45376
|
);
|
|
45354
45377
|
}
|
|
45355
45378
|
function formatWhyFailingReport(report) {
|
|
@@ -46008,11 +46031,11 @@ var init_pool = __esm({
|
|
|
46008
46031
|
for (let i = 0; i < this.threadCount; i++) {
|
|
46009
46032
|
spawnWorker();
|
|
46010
46033
|
}
|
|
46011
|
-
return new Promise((
|
|
46034
|
+
return new Promise((resolve20) => {
|
|
46012
46035
|
const check = setInterval(() => {
|
|
46013
46036
|
if (resolved) {
|
|
46014
46037
|
clearInterval(check);
|
|
46015
|
-
|
|
46038
|
+
resolve20(results);
|
|
46016
46039
|
}
|
|
46017
46040
|
}, 10);
|
|
46018
46041
|
});
|
|
@@ -49859,6 +49882,13 @@ function mergeComponentsByName(components) {
|
|
|
49859
49882
|
}
|
|
49860
49883
|
byName.set(c.name, {
|
|
49861
49884
|
...c,
|
|
49885
|
+
// All visitors (rust.ts, php.ts, go.ts, …) push Components with
|
|
49886
|
+
// `files: [filePath]` — non-empty by construction. The JSON
|
|
49887
|
+
// Schema (inventory.schema.json) requires `files` to be
|
|
49888
|
+
// `[string, ...string[]]` (at least 1); the cast is safe under
|
|
49889
|
+
// the visitor invariant. If a future visitor ever produces an
|
|
49890
|
+
// empty files array, the runtime validator
|
|
49891
|
+
// (`isInventoryFile`) will reject the artifact at write time.
|
|
49862
49892
|
files: c.files.slice(),
|
|
49863
49893
|
hooks: c.hooks.slice(),
|
|
49864
49894
|
props: c.props.slice()
|
|
@@ -49959,29 +49989,29 @@ function renderStructureMarkdown(inventory, constitution) {
|
|
|
49959
49989
|
return lines.join("\n");
|
|
49960
49990
|
}
|
|
49961
49991
|
async function writeStructureMarkdown(workspaceDir, md) {
|
|
49962
|
-
await new Promise((
|
|
49992
|
+
await new Promise((resolve20, reject) => {
|
|
49963
49993
|
try {
|
|
49964
49994
|
const path = join17(workspaceDir, STRUCTURE_MD_FILE);
|
|
49965
49995
|
mkdirSync6(dirname12(path), { recursive: true });
|
|
49966
49996
|
writeFileSync6(path, md, "utf-8");
|
|
49967
|
-
|
|
49997
|
+
resolve20();
|
|
49968
49998
|
} catch (err) {
|
|
49969
49999
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
49970
50000
|
}
|
|
49971
50001
|
});
|
|
49972
50002
|
}
|
|
49973
50003
|
async function readStructureMarkdown(workspaceDir) {
|
|
49974
|
-
return new Promise((
|
|
50004
|
+
return new Promise((resolve20) => {
|
|
49975
50005
|
try {
|
|
49976
50006
|
const path = join17(workspaceDir, STRUCTURE_MD_FILE);
|
|
49977
50007
|
if (!existsSync17(path)) {
|
|
49978
|
-
|
|
50008
|
+
resolve20(null);
|
|
49979
50009
|
return;
|
|
49980
50010
|
}
|
|
49981
50011
|
const content = readFileSync20(path, "utf-8");
|
|
49982
|
-
|
|
50012
|
+
resolve20(content);
|
|
49983
50013
|
} catch {
|
|
49984
|
-
|
|
50014
|
+
resolve20(null);
|
|
49985
50015
|
}
|
|
49986
50016
|
});
|
|
49987
50017
|
}
|
|
@@ -50247,11 +50277,12 @@ async function finalizeReport(input) {
|
|
|
50247
50277
|
if (options.noIncrease) {
|
|
50248
50278
|
const previous = (await readRuns(cwd, fsMemoryIO)).at(-1);
|
|
50249
50279
|
if (previous) {
|
|
50250
|
-
|
|
50280
|
+
const previousBaseline = previous.slopIndex;
|
|
50281
|
+
if ((report.aiQuality ?? 0) < previousBaseline) {
|
|
50251
50282
|
noIncreaseFailure = true;
|
|
50252
50283
|
if (!options.quiet) {
|
|
50253
50284
|
logger.error(
|
|
50254
|
-
`AI Quality went DOWN from ${
|
|
50285
|
+
`AI Quality went DOWN from ${previousBaseline.toFixed(1)} to ${(report.aiQuality ?? 0).toFixed(1)} \u2014 your code got sloppier. (Both values are 0-100, higher = better; the comparison is against the previous run's aiQuality, stored historically in the legacy slopIndex field.) See which files changed and fix the new issues.`
|
|
50255
50286
|
);
|
|
50256
50287
|
}
|
|
50257
50288
|
}
|
|
@@ -52710,7 +52741,7 @@ __export(tools_exports, {
|
|
|
52710
52741
|
handleToolCall: () => handleToolCall
|
|
52711
52742
|
});
|
|
52712
52743
|
import { readFileSync as readFileSync32 } from "fs";
|
|
52713
|
-
import { resolve as
|
|
52744
|
+
import { resolve as resolve18 } from "path";
|
|
52714
52745
|
function toolError(message) {
|
|
52715
52746
|
return {
|
|
52716
52747
|
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
@@ -52739,7 +52770,7 @@ async function runScanFile(args, ctx) {
|
|
|
52739
52770
|
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
52740
52771
|
};
|
|
52741
52772
|
}
|
|
52742
|
-
function
|
|
52773
|
+
function explainRule2(args, ctx) {
|
|
52743
52774
|
const ruleId = args.ruleId;
|
|
52744
52775
|
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
52745
52776
|
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
@@ -52850,7 +52881,7 @@ async function runGovernance(args, ctx) {
|
|
|
52850
52881
|
function runCheckConstitution(args, ctx) {
|
|
52851
52882
|
const path = args.path;
|
|
52852
52883
|
if (!path) return toolError("Missing required argument: path");
|
|
52853
|
-
const absPath =
|
|
52884
|
+
const absPath = resolve18(ctx.cwd, path);
|
|
52854
52885
|
let source;
|
|
52855
52886
|
try {
|
|
52856
52887
|
source = readFileSync32(absPath, "utf-8");
|
|
@@ -53013,7 +53044,7 @@ async function handleToolCall(toolName, args, ctx) {
|
|
|
53013
53044
|
case "slop_scan_file":
|
|
53014
53045
|
return runScanFile(args, ctx);
|
|
53015
53046
|
case "slop_explain_rule":
|
|
53016
|
-
return
|
|
53047
|
+
return explainRule2(args, ctx);
|
|
53017
53048
|
case "slop_list_rules":
|
|
53018
53049
|
return listRules(args, ctx);
|
|
53019
53050
|
case "slop_suggest":
|
|
@@ -53395,7 +53426,7 @@ init_dist2();
|
|
|
53395
53426
|
|
|
53396
53427
|
// src/cli/program.ts
|
|
53397
53428
|
import { existsSync as existsSync28, writeFileSync as writeFileSync19, readFileSync as readFileSync38, mkdirSync as mkdirSync13 } from "fs";
|
|
53398
|
-
import { resolve as
|
|
53429
|
+
import { resolve as resolve19, join as join28, dirname as dirname17, extname as extname9 } from "path";
|
|
53399
53430
|
import { performance } from "perf_hooks";
|
|
53400
53431
|
import { Command } from "commander";
|
|
53401
53432
|
|
|
@@ -54901,17 +54932,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
|
|
|
54901
54932
|
var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
|
|
54902
54933
|
var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
|
|
54903
54934
|
function promptText(rl, question, detected) {
|
|
54904
|
-
return new Promise((
|
|
54935
|
+
return new Promise((resolve20) => {
|
|
54905
54936
|
const lines = [
|
|
54906
54937
|
`? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
|
|
54907
54938
|
];
|
|
54908
54939
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
54909
54940
|
const trimmed = answer.trim();
|
|
54910
54941
|
if (trimmed === "") {
|
|
54911
|
-
|
|
54942
|
+
resolve20(void 0);
|
|
54912
54943
|
return;
|
|
54913
54944
|
}
|
|
54914
|
-
|
|
54945
|
+
resolve20(trimmed);
|
|
54915
54946
|
});
|
|
54916
54947
|
});
|
|
54917
54948
|
}
|
|
@@ -54919,7 +54950,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
54919
54950
|
const defaultIndex = options.indexOf(defaultValue);
|
|
54920
54951
|
const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
|
|
54921
54952
|
const safeDefaultValue = options[safeDefaultIndex];
|
|
54922
|
-
return new Promise((
|
|
54953
|
+
return new Promise((resolve20) => {
|
|
54923
54954
|
function ask() {
|
|
54924
54955
|
const lines = [
|
|
54925
54956
|
`? ${question} (detected: ${safeDefaultValue}):`,
|
|
@@ -54929,7 +54960,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
54929
54960
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
54930
54961
|
const trimmed = answer.trim();
|
|
54931
54962
|
if (trimmed === "") {
|
|
54932
|
-
|
|
54963
|
+
resolve20(safeDefaultValue);
|
|
54933
54964
|
return;
|
|
54934
54965
|
}
|
|
54935
54966
|
const num = parseInt(trimmed, 10);
|
|
@@ -54938,7 +54969,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
54938
54969
|
ask();
|
|
54939
54970
|
return;
|
|
54940
54971
|
}
|
|
54941
|
-
|
|
54972
|
+
resolve20(options[num - 1]);
|
|
54942
54973
|
});
|
|
54943
54974
|
}
|
|
54944
54975
|
ask();
|
|
@@ -54948,7 +54979,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
54948
54979
|
const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
|
|
54949
54980
|
const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
|
|
54950
54981
|
const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
|
|
54951
|
-
return new Promise((
|
|
54982
|
+
return new Promise((resolve20) => {
|
|
54952
54983
|
function ask() {
|
|
54953
54984
|
const lines = [
|
|
54954
54985
|
`? ${question} (detected: ${defaultDisplay}):`,
|
|
@@ -54959,7 +54990,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
54959
54990
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
54960
54991
|
const trimmed = answer.trim();
|
|
54961
54992
|
if (trimmed === "") {
|
|
54962
|
-
|
|
54993
|
+
resolve20(defaultValue.length > 0 ? defaultValue : []);
|
|
54963
54994
|
return;
|
|
54964
54995
|
}
|
|
54965
54996
|
const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
|
|
@@ -54969,11 +55000,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
54969
55000
|
return;
|
|
54970
55001
|
}
|
|
54971
55002
|
if (numbers.includes(1)) {
|
|
54972
|
-
|
|
55003
|
+
resolve20([]);
|
|
54973
55004
|
return;
|
|
54974
55005
|
}
|
|
54975
|
-
const selected = [...new Set(numbers.map((n) => options[n - 2]))];
|
|
54976
|
-
|
|
55006
|
+
const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
|
|
55007
|
+
resolve20(selected);
|
|
54977
55008
|
});
|
|
54978
55009
|
}
|
|
54979
55010
|
ask();
|
|
@@ -55153,6 +55184,234 @@ async function runDoctor(cwd) {
|
|
|
55153
55184
|
return exitCode;
|
|
55154
55185
|
}
|
|
55155
55186
|
|
|
55187
|
+
// src/cli/commands/badge.ts
|
|
55188
|
+
init_render();
|
|
55189
|
+
init_logger();
|
|
55190
|
+
init_scan();
|
|
55191
|
+
import { resolve as resolve15 } from "path";
|
|
55192
|
+
function registerBadge(program) {
|
|
55193
|
+
program.command("badge").description(
|
|
55194
|
+
"print a shields.io slop-index badge. Reads .slopbrick/health.json if present (no re-scan); falls back to a fresh scan."
|
|
55195
|
+
).action(async (_cmdOptions, command) => {
|
|
55196
|
+
const options = command.optsWithGlobals();
|
|
55197
|
+
const cwd = resolve15(options.workspace ?? process.cwd());
|
|
55198
|
+
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
55199
|
+
const health = loadHealth2(cwd);
|
|
55200
|
+
if (health) {
|
|
55201
|
+
const synthetic = {
|
|
55202
|
+
slopIndex: 100 - health.repositoryHealth
|
|
55203
|
+
};
|
|
55204
|
+
logger.info(formatBadge(synthetic));
|
|
55205
|
+
process.exit(0);
|
|
55206
|
+
}
|
|
55207
|
+
const { report } = await runScan(options);
|
|
55208
|
+
logger.info(formatBadge(report));
|
|
55209
|
+
process.exit(0);
|
|
55210
|
+
});
|
|
55211
|
+
}
|
|
55212
|
+
|
|
55213
|
+
// src/cli/commands/suggest.ts
|
|
55214
|
+
init_advice();
|
|
55215
|
+
init_unified_diff();
|
|
55216
|
+
init_logger();
|
|
55217
|
+
init_scan();
|
|
55218
|
+
import { resolve as resolve16 } from "path";
|
|
55219
|
+
function registerSuggest(program) {
|
|
55220
|
+
program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
|
|
55221
|
+
const options = command.optsWithGlobals();
|
|
55222
|
+
const { report } = await runScan(options);
|
|
55223
|
+
const cwd = resolve16(options.workspace ?? process.cwd());
|
|
55224
|
+
logger.info(formatAdvice(report));
|
|
55225
|
+
const diff = formatUnifiedDiff(report, cwd);
|
|
55226
|
+
if (diff) logger.info(diff);
|
|
55227
|
+
process.exit(0);
|
|
55228
|
+
});
|
|
55229
|
+
}
|
|
55230
|
+
|
|
55231
|
+
// src/cli/explain.ts
|
|
55232
|
+
var RULES_BASE_URL2 = "https://github.com/Dystx/slopbrick/blob/main/src/rules";
|
|
55233
|
+
function ruleIdToFilename2(ruleId) {
|
|
55234
|
+
const slash = ruleId.indexOf("/");
|
|
55235
|
+
return slash === -1 ? ruleId : ruleId.slice(slash + 1);
|
|
55236
|
+
}
|
|
55237
|
+
function explainRule(ruleId, rules, ruleHints) {
|
|
55238
|
+
const rule = rules.find((r) => r.id === ruleId);
|
|
55239
|
+
if (!rule) {
|
|
55240
|
+
return { error: "Unknown rule: " + ruleId + ". Run `slopbrick rules` to see all available rules." };
|
|
55241
|
+
}
|
|
55242
|
+
const filename = ruleIdToFilename2(rule.id);
|
|
55243
|
+
return {
|
|
55244
|
+
ruleId: rule.id,
|
|
55245
|
+
category: rule.category,
|
|
55246
|
+
severity: rule.severity,
|
|
55247
|
+
aiSpecific: rule.aiSpecific,
|
|
55248
|
+
pattern: ruleHints[rule.id] ?? "Patterns flagged by " + rule.id + ".",
|
|
55249
|
+
remediation: "See the rule source for the canonical before/after: src/rules/" + rule.category + "/" + filename + ".ts",
|
|
55250
|
+
sourcePath: "src/rules/" + rule.category + "/" + filename + ".ts",
|
|
55251
|
+
helpUri: `${RULES_BASE_URL2}/${rule.category}/${filename}.ts`,
|
|
55252
|
+
suppressionSnippet: 'rules: { "' + rule.id + '": "off" } // or set to a lower severity'
|
|
55253
|
+
};
|
|
55254
|
+
}
|
|
55255
|
+
function formatExplain(result) {
|
|
55256
|
+
if ("error" in result) return result.error;
|
|
55257
|
+
const lines = [];
|
|
55258
|
+
lines.push("Rule: " + result.ruleId);
|
|
55259
|
+
lines.push("Category: " + result.category);
|
|
55260
|
+
lines.push("Severity: " + result.severity);
|
|
55261
|
+
lines.push("AI-specific: " + (result.aiSpecific ? "yes (designed to fire on AI tells)" : "no (cross-cutting quality rule)"));
|
|
55262
|
+
lines.push("Source: " + result.sourcePath);
|
|
55263
|
+
lines.push("Help: " + result.helpUri);
|
|
55264
|
+
lines.push("");
|
|
55265
|
+
lines.push("Pattern:");
|
|
55266
|
+
lines.push(" " + result.pattern);
|
|
55267
|
+
lines.push("");
|
|
55268
|
+
lines.push("Remediation:");
|
|
55269
|
+
lines.push(" " + result.remediation);
|
|
55270
|
+
lines.push("");
|
|
55271
|
+
lines.push("Suppress / configure in slopbrick.config.mjs:");
|
|
55272
|
+
lines.push(" " + result.suppressionSnippet);
|
|
55273
|
+
lines.push("");
|
|
55274
|
+
return lines.join("\n");
|
|
55275
|
+
}
|
|
55276
|
+
|
|
55277
|
+
// src/cli/commands/explain.ts
|
|
55278
|
+
init_logger();
|
|
55279
|
+
init_builtins();
|
|
55280
|
+
|
|
55281
|
+
// src/snippet/data.ts
|
|
55282
|
+
var CATEGORY_DIRECTIVES = {
|
|
55283
|
+
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.',
|
|
55284
|
+
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.",
|
|
55285
|
+
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.',
|
|
55286
|
+
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".',
|
|
55287
|
+
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.",
|
|
55288
|
+
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
55289
|
+
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].",
|
|
55290
|
+
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
55291
|
+
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.",
|
|
55292
|
+
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'."
|
|
55293
|
+
};
|
|
55294
|
+
var RULE_HINTS = {
|
|
55295
|
+
// v0.16.0 hygiene: 35 out-of-scope orphan hints (keys with no matching
|
|
55296
|
+
// rule in src/rules/builtins.ts) were moved out of this map. The
|
|
55297
|
+
// verbatim source text is preserved in
|
|
55298
|
+
// docs/research/backlog-rule-hints.md
|
|
55299
|
+
// so future implementers can paste a hint back when the corresponding
|
|
55300
|
+
// rule ships. The 5 in-scope orphans (`security/eval`,
|
|
55301
|
+
// `security/localstorage-token`, `security/target-blank-no-noopener`,
|
|
55302
|
+
// `wcag/missing-alt`, `typo/placeholder-text`) are kept here for v0.16.0.
|
|
55303
|
+
"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.",
|
|
55304
|
+
"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.",
|
|
55305
|
+
"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.",
|
|
55306
|
+
"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.",
|
|
55307
|
+
"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.",
|
|
55308
|
+
"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.",
|
|
55309
|
+
"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.',
|
|
55310
|
+
"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.",
|
|
55311
|
+
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
55312
|
+
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
55313
|
+
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
55314
|
+
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
55315
|
+
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
55316
|
+
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
55317
|
+
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
55318
|
+
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
55319
|
+
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
55320
|
+
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
55321
|
+
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
55322
|
+
"arch/astro-island-leak": "For Astro: server-render everything by default. Only opt-in to client islands when interactivity is needed.",
|
|
55323
|
+
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
55324
|
+
"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.",
|
|
55325
|
+
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
55326
|
+
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
55327
|
+
"layout/forced-layout": "Vary structural patterns: some containers as grids, some as horizontal flex, some as blocks. Don't repeat `flex flex-col gap-4` everywhere.",
|
|
55328
|
+
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
55329
|
+
"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.",
|
|
55330
|
+
"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.",
|
|
55331
|
+
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
55332
|
+
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
55333
|
+
"logic/bayesian-conditional": "The Bayesian combiner aggregates multiple weak signals into a calibrated posterior P(AI|fires). Treat any fire above 0.7 as evidence of AI authorship; above 0.9 as strong evidence. (v0.12.0 \u2014 Bento et al. 2024 *Neurocomputing*.)",
|
|
55334
|
+
"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.)",
|
|
55335
|
+
"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.)",
|
|
55336
|
+
"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.)",
|
|
55337
|
+
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
55338
|
+
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
55339
|
+
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
55340
|
+
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
55341
|
+
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
55342
|
+
"logic/qwik-hook-leak": "Use Qwik primitives ($state, $effect, useSignal) instead of React hooks (useState, useEffect).",
|
|
55343
|
+
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
55344
|
+
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
55345
|
+
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
55346
|
+
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
55347
|
+
"perf/halstead-anomaly": "Introduce domain-specific identifiers and varied operations. Low vocabulary per line is a strong AI signature (Halstead 1977 \xA73).",
|
|
55348
|
+
"typo/calc-fontsize": "Use a design token (`var(--font-size-lg)`) or `clamp(min, fluid, max)` for responsive typography.",
|
|
55349
|
+
"typo/calc-raw-px": "Replace px values in calc() with rem or em units for scalable layout.",
|
|
55350
|
+
"typo/clamp-offscale": "Anchor clamp() values to standard sizes (12, 14, 16, 18, 20, 24, 30, 36, 48) so they remain on the design grid.",
|
|
55351
|
+
"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.',
|
|
55352
|
+
"typo/math-cta-vocabulary": 'Use domain-specific action verbs ("Reserve", "Confirm ride", "Activate card") instead of falling back on the AI-default CTA vocabulary.',
|
|
55353
|
+
"visual/clamp-soup": "Use design-system aliases (`--text-fluid-sm`, `--text-fluid-lg`) with bounded ranges (typically 2\xD7 max).",
|
|
55354
|
+
"visual/generic-centering": "Vary hero layouts: some as grids (`grid place-items-center`), some as blocks, some with different alignment.",
|
|
55355
|
+
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
55356
|
+
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
55357
|
+
"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.",
|
|
55358
|
+
"visual/math-gradient-hue-rotation": "Use wider hue spans across gradients (e.g. blue\u2192amber, emerald\u2192indigo) to break the violet-fuchsia monotony.",
|
|
55359
|
+
"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.",
|
|
55360
|
+
"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.",
|
|
55361
|
+
"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%.",
|
|
55362
|
+
"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.",
|
|
55363
|
+
"wcag/dragging-movements": "Provide an onClick, onKeyDown, or button role as an alternative to dragging (WCAG 2.1.1).",
|
|
55364
|
+
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
55365
|
+
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
55366
|
+
"wcag/target-size": "Add h-*, w-*, p-*, min-w-*, min-h-*, size-*, or an explicit width/height attribute to bring the target to \u2265 24\xD724 px.",
|
|
55367
|
+
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
55368
|
+
"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.",
|
|
55369
|
+
"test/missing-edge-case": "When generating tests, cover the alternate path: `else` branches, `catch` blocks, ternary alternates, and `??` fallbacks. Production branches without tests are a CI smell.",
|
|
55370
|
+
"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`).",
|
|
55371
|
+
"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.",
|
|
55372
|
+
"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.",
|
|
55373
|
+
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
55374
|
+
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
55375
|
+
"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).",
|
|
55376
|
+
"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).",
|
|
55377
|
+
"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).",
|
|
55378
|
+
"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).",
|
|
55379
|
+
"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).",
|
|
55380
|
+
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
55381
|
+
"ai/log-rank-histogram": "The token vocabulary is concentrated in the top-1000 most common tokens. Real codebases use more diverse identifiers (Gehrmann 2019 GLTR).",
|
|
55382
|
+
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
55383
|
+
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
55384
|
+
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
55385
|
+
// reclassified post-v7 calibration in v0.14.5d)
|
|
55386
|
+
"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.",
|
|
55387
|
+
"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.",
|
|
55388
|
+
"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.",
|
|
55389
|
+
"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.",
|
|
55390
|
+
"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.",
|
|
55391
|
+
"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.",
|
|
55392
|
+
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
55393
|
+
"db/missing-fk-index": "Add `CREATE INDEX ON <table> (<fk_column>);` for every foreign key column. Without it, parent deletes do a sequential scan on the child. Use `CREATE INDEX CONCURRENTLY` in production (Squawk `require-concurrent-index-creation`).",
|
|
55394
|
+
"db/duplicate-index": "Drop one of two indexes that cover the same column list \u2014 extra indexes slow writes without read benefit. Postgres does not warn about this; the duplicate will silently sit in production.",
|
|
55395
|
+
"db/missing-not-null": "Add `NOT NULL` (or `PRIMARY KEY`) on required-identifier columns (id, email, created_at, status, uuid, \u2026). Optional identifiers are a common AI-generated SQL smell that produces silent NULL inserts in production.",
|
|
55396
|
+
"db/enum-sprawl": "Enums with more than 12 values are brittle to extend and hard to localize. Move to a lookup table joined by foreign key.",
|
|
55397
|
+
"db/naming-inconsistency": "Standardize on snake_case (Postgres convention) or camelCase, but never mix both in the same schema. Mixed styles break ORM generators and confuse code-reviewers.",
|
|
55398
|
+
"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.',
|
|
55399
|
+
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
55400
|
+
"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.",
|
|
55401
|
+
"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.",
|
|
55402
|
+
"docs/expired-code-example": "Update the example to use a declared dependency, or add the package to package.json. A copy-pasteable example that does not install erodes trust in the whole docs site.",
|
|
55403
|
+
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy."
|
|
55404
|
+
};
|
|
55405
|
+
|
|
55406
|
+
// src/cli/commands/explain.ts
|
|
55407
|
+
function registerExplain(program) {
|
|
55408
|
+
program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
|
|
55409
|
+
const result = explainRule(ruleId, builtinRules, RULE_HINTS);
|
|
55410
|
+
logger.info(formatExplain(result));
|
|
55411
|
+
if ("error" in result) process.exit(2);
|
|
55412
|
+
});
|
|
55413
|
+
}
|
|
55414
|
+
|
|
55156
55415
|
// src/cli/program.ts
|
|
55157
55416
|
init_config();
|
|
55158
55417
|
init_git();
|
|
@@ -55503,7 +55762,7 @@ function slugify(value) {
|
|
|
55503
55762
|
// src/research/calibrator.ts
|
|
55504
55763
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
55505
55764
|
import { existsSync as existsSync24, readFileSync as readFileSync31, writeFileSync as writeFileSync14, mkdirSync as mkdirSync12 } from "fs";
|
|
55506
|
-
import { join as join24, resolve as
|
|
55765
|
+
import { join as join24, resolve as resolve17 } from "path";
|
|
55507
55766
|
var DEFAULT_POSITIVE = "/Users/cheng/ai-slop-baseline/extracted/positive";
|
|
55508
55767
|
var DEFAULT_NEGATIVE = "/Users/cheng/ai-slop-baseline/extracted/negative";
|
|
55509
55768
|
function buildFileList(dir, extensions) {
|
|
@@ -55836,7 +56095,7 @@ function errorResponse(id, code, message, data2) {
|
|
|
55836
56095
|
return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
|
|
55837
56096
|
}
|
|
55838
56097
|
async function runMcpServer(input, output, cwd) {
|
|
55839
|
-
return new Promise((
|
|
56098
|
+
return new Promise((resolve20) => {
|
|
55840
56099
|
let buffer = "";
|
|
55841
56100
|
input.setEncoding("utf-8");
|
|
55842
56101
|
input.on("data", (chunk) => {
|
|
@@ -55874,136 +56133,11 @@ async function runMcpServer(input, output, cwd) {
|
|
|
55874
56133
|
nlIdx = buffer.indexOf("\n");
|
|
55875
56134
|
}
|
|
55876
56135
|
});
|
|
55877
|
-
input.on("end", () =>
|
|
55878
|
-
input.on("close", () =>
|
|
56136
|
+
input.on("end", () => resolve20());
|
|
56137
|
+
input.on("close", () => resolve20());
|
|
55879
56138
|
});
|
|
55880
56139
|
}
|
|
55881
56140
|
|
|
55882
|
-
// src/snippet/data.ts
|
|
55883
|
-
var CATEGORY_DIRECTIVES = {
|
|
55884
|
-
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.',
|
|
55885
|
-
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.",
|
|
55886
|
-
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.',
|
|
55887
|
-
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".',
|
|
55888
|
-
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.",
|
|
55889
|
-
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
55890
|
-
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].",
|
|
55891
|
-
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
55892
|
-
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.",
|
|
55893
|
-
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'."
|
|
55894
|
-
};
|
|
55895
|
-
var RULE_HINTS = {
|
|
55896
|
-
// v0.16.0 hygiene: 35 out-of-scope orphan hints (keys with no matching
|
|
55897
|
-
// rule in src/rules/builtins.ts) were moved out of this map. The
|
|
55898
|
-
// verbatim source text is preserved in
|
|
55899
|
-
// docs/research/backlog-rule-hints.md
|
|
55900
|
-
// so future implementers can paste a hint back when the corresponding
|
|
55901
|
-
// rule ships. The 5 in-scope orphans (`security/eval`,
|
|
55902
|
-
// `security/localstorage-token`, `security/target-blank-no-noopener`,
|
|
55903
|
-
// `wcag/missing-alt`, `typo/placeholder-text`) are kept here for v0.16.0.
|
|
55904
|
-
"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.",
|
|
55905
|
-
"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.",
|
|
55906
|
-
"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.",
|
|
55907
|
-
"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.",
|
|
55908
|
-
"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.",
|
|
55909
|
-
"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.",
|
|
55910
|
-
"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.',
|
|
55911
|
-
"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.",
|
|
55912
|
-
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
55913
|
-
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
55914
|
-
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
55915
|
-
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
55916
|
-
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
55917
|
-
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
55918
|
-
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
55919
|
-
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
55920
|
-
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
55921
|
-
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
55922
|
-
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
55923
|
-
"arch/astro-island-leak": "For Astro: server-render everything by default. Only opt-in to client islands when interactivity is needed.",
|
|
55924
|
-
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
55925
|
-
"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.",
|
|
55926
|
-
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
55927
|
-
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
55928
|
-
"layout/forced-layout": "Vary structural patterns: some containers as grids, some as horizontal flex, some as blocks. Don't repeat `flex flex-col gap-4` everywhere.",
|
|
55929
|
-
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
55930
|
-
"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.",
|
|
55931
|
-
"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.",
|
|
55932
|
-
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
55933
|
-
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
55934
|
-
"logic/bayesian-conditional": "The Bayesian combiner aggregates multiple weak signals into a calibrated posterior P(AI|fires). Treat any fire above 0.7 as evidence of AI authorship; above 0.9 as strong evidence. (v0.12.0 \u2014 Bento et al. 2024 *Neurocomputing*.)",
|
|
55935
|
-
"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.)",
|
|
55936
|
-
"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.)",
|
|
55937
|
-
"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.)",
|
|
55938
|
-
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
55939
|
-
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
55940
|
-
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
55941
|
-
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
55942
|
-
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
55943
|
-
"logic/qwik-hook-leak": "Use Qwik primitives ($state, $effect, useSignal) instead of React hooks (useState, useEffect).",
|
|
55944
|
-
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
55945
|
-
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
55946
|
-
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
55947
|
-
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
55948
|
-
"perf/halstead-anomaly": "Introduce domain-specific identifiers and varied operations. Low vocabulary per line is a strong AI signature (Halstead 1977 \xA73).",
|
|
55949
|
-
"typo/calc-fontsize": "Use a design token (`var(--font-size-lg)`) or `clamp(min, fluid, max)` for responsive typography.",
|
|
55950
|
-
"typo/calc-raw-px": "Replace px values in calc() with rem or em units for scalable layout.",
|
|
55951
|
-
"typo/clamp-offscale": "Anchor clamp() values to standard sizes (12, 14, 16, 18, 20, 24, 30, 36, 48) so they remain on the design grid.",
|
|
55952
|
-
"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.',
|
|
55953
|
-
"typo/math-cta-vocabulary": 'Use domain-specific action verbs ("Reserve", "Confirm ride", "Activate card") instead of falling back on the AI-default CTA vocabulary.',
|
|
55954
|
-
"visual/clamp-soup": "Use design-system aliases (`--text-fluid-sm`, `--text-fluid-lg`) with bounded ranges (typically 2\xD7 max).",
|
|
55955
|
-
"visual/generic-centering": "Vary hero layouts: some as grids (`grid place-items-center`), some as blocks, some with different alignment.",
|
|
55956
|
-
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
55957
|
-
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
55958
|
-
"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.",
|
|
55959
|
-
"visual/math-gradient-hue-rotation": "Use wider hue spans across gradients (e.g. blue\u2192amber, emerald\u2192indigo) to break the violet-fuchsia monotony.",
|
|
55960
|
-
"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.",
|
|
55961
|
-
"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.",
|
|
55962
|
-
"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%.",
|
|
55963
|
-
"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.",
|
|
55964
|
-
"wcag/dragging-movements": "Provide an onClick, onKeyDown, or button role as an alternative to dragging (WCAG 2.1.1).",
|
|
55965
|
-
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
55966
|
-
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
55967
|
-
"wcag/target-size": "Add h-*, w-*, p-*, min-w-*, min-h-*, size-*, or an explicit width/height attribute to bring the target to \u2265 24\xD724 px.",
|
|
55968
|
-
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
55969
|
-
"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.",
|
|
55970
|
-
"test/missing-edge-case": "When generating tests, cover the alternate path: `else` branches, `catch` blocks, ternary alternates, and `??` fallbacks. Production branches without tests are a CI smell.",
|
|
55971
|
-
"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`).",
|
|
55972
|
-
"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.",
|
|
55973
|
-
"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.",
|
|
55974
|
-
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
55975
|
-
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
55976
|
-
"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).",
|
|
55977
|
-
"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).",
|
|
55978
|
-
"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).",
|
|
55979
|
-
"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).",
|
|
55980
|
-
"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).",
|
|
55981
|
-
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
55982
|
-
"ai/log-rank-histogram": "The token vocabulary is concentrated in the top-1000 most common tokens. Real codebases use more diverse identifiers (Gehrmann 2019 GLTR).",
|
|
55983
|
-
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
55984
|
-
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
55985
|
-
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
55986
|
-
// reclassified post-v7 calibration in v0.14.5d)
|
|
55987
|
-
"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.",
|
|
55988
|
-
"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.",
|
|
55989
|
-
"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.",
|
|
55990
|
-
"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.",
|
|
55991
|
-
"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.",
|
|
55992
|
-
"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.",
|
|
55993
|
-
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
55994
|
-
"db/missing-fk-index": "Add `CREATE INDEX ON <table> (<fk_column>);` for every foreign key column. Without it, parent deletes do a sequential scan on the child. Use `CREATE INDEX CONCURRENTLY` in production (Squawk `require-concurrent-index-creation`).",
|
|
55995
|
-
"db/duplicate-index": "Drop one of two indexes that cover the same column list \u2014 extra indexes slow writes without read benefit. Postgres does not warn about this; the duplicate will silently sit in production.",
|
|
55996
|
-
"db/missing-not-null": "Add `NOT NULL` (or `PRIMARY KEY`) on required-identifier columns (id, email, created_at, status, uuid, \u2026). Optional identifiers are a common AI-generated SQL smell that produces silent NULL inserts in production.",
|
|
55997
|
-
"db/enum-sprawl": "Enums with more than 12 values are brittle to extend and hard to localize. Move to a lookup table joined by foreign key.",
|
|
55998
|
-
"db/naming-inconsistency": "Standardize on snake_case (Postgres convention) or camelCase, but never mix both in the same schema. Mixed styles break ORM generators and confuse code-reviewers.",
|
|
55999
|
-
"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.',
|
|
56000
|
-
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
56001
|
-
"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.",
|
|
56002
|
-
"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.",
|
|
56003
|
-
"docs/expired-code-example": "Update the example to use a declared dependency, or add the package to package.json. A copy-pasteable example that does not install erodes trust in the whole docs site.",
|
|
56004
|
-
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy."
|
|
56005
|
-
};
|
|
56006
|
-
|
|
56007
56141
|
// src/snippet/targets.ts
|
|
56008
56142
|
import { join as join25 } from "path";
|
|
56009
56143
|
|
|
@@ -56250,52 +56384,6 @@ function renderMatrix() {
|
|
|
56250
56384
|
return lines.join("\n");
|
|
56251
56385
|
}
|
|
56252
56386
|
|
|
56253
|
-
// src/cli/explain.ts
|
|
56254
|
-
var RULES_BASE_URL2 = "https://github.com/Dystx/slopbrick/blob/main/src/rules";
|
|
56255
|
-
function ruleIdToFilename2(ruleId) {
|
|
56256
|
-
const slash = ruleId.indexOf("/");
|
|
56257
|
-
return slash === -1 ? ruleId : ruleId.slice(slash + 1);
|
|
56258
|
-
}
|
|
56259
|
-
function explainRule2(ruleId, rules, ruleHints) {
|
|
56260
|
-
const rule = rules.find((r) => r.id === ruleId);
|
|
56261
|
-
if (!rule) {
|
|
56262
|
-
return { error: "Unknown rule: " + ruleId + ". Run `slopbrick rules` to see all available rules." };
|
|
56263
|
-
}
|
|
56264
|
-
const filename = ruleIdToFilename2(rule.id);
|
|
56265
|
-
return {
|
|
56266
|
-
ruleId: rule.id,
|
|
56267
|
-
category: rule.category,
|
|
56268
|
-
severity: rule.severity,
|
|
56269
|
-
aiSpecific: rule.aiSpecific,
|
|
56270
|
-
pattern: ruleHints[rule.id] ?? "Patterns flagged by " + rule.id + ".",
|
|
56271
|
-
remediation: "See the rule source for the canonical before/after: src/rules/" + rule.category + "/" + filename + ".ts",
|
|
56272
|
-
sourcePath: "src/rules/" + rule.category + "/" + filename + ".ts",
|
|
56273
|
-
helpUri: `${RULES_BASE_URL2}/${rule.category}/${filename}.ts`,
|
|
56274
|
-
suppressionSnippet: 'rules: { "' + rule.id + '": "off" } // or set to a lower severity'
|
|
56275
|
-
};
|
|
56276
|
-
}
|
|
56277
|
-
function formatExplain(result) {
|
|
56278
|
-
if ("error" in result) return result.error;
|
|
56279
|
-
const lines = [];
|
|
56280
|
-
lines.push("Rule: " + result.ruleId);
|
|
56281
|
-
lines.push("Category: " + result.category);
|
|
56282
|
-
lines.push("Severity: " + result.severity);
|
|
56283
|
-
lines.push("AI-specific: " + (result.aiSpecific ? "yes (designed to fire on AI tells)" : "no (cross-cutting quality rule)"));
|
|
56284
|
-
lines.push("Source: " + result.sourcePath);
|
|
56285
|
-
lines.push("Help: " + result.helpUri);
|
|
56286
|
-
lines.push("");
|
|
56287
|
-
lines.push("Pattern:");
|
|
56288
|
-
lines.push(" " + result.pattern);
|
|
56289
|
-
lines.push("");
|
|
56290
|
-
lines.push("Remediation:");
|
|
56291
|
-
lines.push(" " + result.remediation);
|
|
56292
|
-
lines.push("");
|
|
56293
|
-
lines.push("Suppress / configure in slopbrick.config.mjs:");
|
|
56294
|
-
lines.push(" " + result.suppressionSnippet);
|
|
56295
|
-
lines.push("");
|
|
56296
|
-
return lines.join("\n");
|
|
56297
|
-
}
|
|
56298
|
-
|
|
56299
56387
|
// src/cli/program.ts
|
|
56300
56388
|
init_validation();
|
|
56301
56389
|
init_signal_strength2();
|
|
@@ -56453,7 +56541,6 @@ function formatMarkdown3(report) {
|
|
|
56453
56541
|
}
|
|
56454
56542
|
|
|
56455
56543
|
// src/cli/program.ts
|
|
56456
|
-
init_advice();
|
|
56457
56544
|
init_unified_diff();
|
|
56458
56545
|
init_heatmap();
|
|
56459
56546
|
|
|
@@ -56989,7 +57076,7 @@ async function runCli({ start }) {
|
|
|
56989
57076
|
logger.info(renderMatrix());
|
|
56990
57077
|
process.exit(0);
|
|
56991
57078
|
}
|
|
56992
|
-
const cwd =
|
|
57079
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
56993
57080
|
const configPath = join28(cwd, "slopbrick.config.mjs");
|
|
56994
57081
|
const detected = detectStack(cwd);
|
|
56995
57082
|
const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
|
|
@@ -57085,7 +57172,7 @@ async function runCli({ start }) {
|
|
|
57085
57172
|
});
|
|
57086
57173
|
program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
57087
57174
|
const options = command.optsWithGlobals();
|
|
57088
|
-
const cwd =
|
|
57175
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57089
57176
|
const root = getGitRoot(cwd);
|
|
57090
57177
|
if (!root) {
|
|
57091
57178
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -57099,7 +57186,7 @@ async function runCli({ start }) {
|
|
|
57099
57186
|
});
|
|
57100
57187
|
program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
57101
57188
|
const options = command.optsWithGlobals();
|
|
57102
|
-
const cwd =
|
|
57189
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57103
57190
|
const root = getGitRoot(cwd);
|
|
57104
57191
|
if (!root) {
|
|
57105
57192
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -57111,34 +57198,12 @@ async function runCli({ start }) {
|
|
|
57111
57198
|
}
|
|
57112
57199
|
process.exit(result.exitCode);
|
|
57113
57200
|
});
|
|
57114
|
-
program
|
|
57115
|
-
|
|
57116
|
-
|
|
57117
|
-
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
57118
|
-
const health = loadHealth2(cwd);
|
|
57119
|
-
if (health) {
|
|
57120
|
-
const synthetic = {
|
|
57121
|
-
slopIndex: 100 - health.repositoryHealth
|
|
57122
|
-
};
|
|
57123
|
-
logger.info(formatBadge(synthetic));
|
|
57124
|
-
process.exit(0);
|
|
57125
|
-
}
|
|
57126
|
-
const { report } = await runScan(options);
|
|
57127
|
-
logger.info(formatBadge(report));
|
|
57128
|
-
process.exit(0);
|
|
57129
|
-
});
|
|
57130
|
-
program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
|
|
57131
|
-
const options = command.optsWithGlobals();
|
|
57132
|
-
const { report } = await runScan(options);
|
|
57133
|
-
const cwd = resolve17(options.workspace ?? process.cwd());
|
|
57134
|
-
logger.info(formatAdvice(report));
|
|
57135
|
-
const diff = formatUnifiedDiff(report, cwd);
|
|
57136
|
-
if (diff) logger.info(diff);
|
|
57137
|
-
process.exit(0);
|
|
57138
|
-
});
|
|
57201
|
+
registerBadge(program);
|
|
57202
|
+
registerSuggest(program);
|
|
57203
|
+
registerExplain(program);
|
|
57139
57204
|
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) => {
|
|
57140
57205
|
const options = command.optsWithGlobals();
|
|
57141
|
-
const cwd =
|
|
57206
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57142
57207
|
const payloads = readTelemetry(cwd);
|
|
57143
57208
|
if (payloads.length === 0) {
|
|
57144
57209
|
logger.info("No flywheel telemetry found. Run a scan first.");
|
|
@@ -57146,7 +57211,7 @@ async function runCli({ start }) {
|
|
|
57146
57211
|
}
|
|
57147
57212
|
const summary = summarizeTelemetry(payloads);
|
|
57148
57213
|
if (cmdOptions.export) {
|
|
57149
|
-
const exportPath =
|
|
57214
|
+
const exportPath = resolve19(cmdOptions.export);
|
|
57150
57215
|
mkdirSync13(dirname17(exportPath), { recursive: true });
|
|
57151
57216
|
writeFileSync19(exportPath, JSON.stringify(summary, null, 2), "utf-8");
|
|
57152
57217
|
logger.info(`Wrote flywheel summary to ${exportPath}`);
|
|
@@ -57171,7 +57236,7 @@ async function runCli({ start }) {
|
|
|
57171
57236
|
logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
|
|
57172
57237
|
process.exit(2);
|
|
57173
57238
|
}
|
|
57174
|
-
const cwd =
|
|
57239
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57175
57240
|
if (options.trend !== void 0) {
|
|
57176
57241
|
const runs = await readRuns(cwd, fsMemoryIO);
|
|
57177
57242
|
if (runs.length === 0) {
|
|
@@ -57204,7 +57269,7 @@ async function runCli({ start }) {
|
|
|
57204
57269
|
const scanElapsed = Math.round(performance.now() - scanStart);
|
|
57205
57270
|
const totalElapsed = Math.round(performance.now() - start);
|
|
57206
57271
|
if (options.baseline) {
|
|
57207
|
-
const cwd2 =
|
|
57272
|
+
const cwd2 = resolve19(options.workspace ?? process.cwd());
|
|
57208
57273
|
const configHash = hashConfig(config);
|
|
57209
57274
|
const gitHead = await getGitHead(cwd2) ?? "unknown";
|
|
57210
57275
|
const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
|
|
@@ -57298,14 +57363,14 @@ async function runCli({ start }) {
|
|
|
57298
57363
|
framework: cmdOptions.framework,
|
|
57299
57364
|
componentType: cmdOptions.componentType,
|
|
57300
57365
|
provider,
|
|
57301
|
-
outputDir:
|
|
57366
|
+
outputDir: resolve19(cmdOptions.outputDir),
|
|
57302
57367
|
temperature: cmdOptions.temperature
|
|
57303
57368
|
});
|
|
57304
57369
|
logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
|
|
57305
57370
|
});
|
|
57306
57371
|
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) => {
|
|
57307
57372
|
try {
|
|
57308
|
-
const metadataPath =
|
|
57373
|
+
const metadataPath = resolve19(cmdOptions.inputDir, "metadata.json");
|
|
57309
57374
|
if (!existsSync28(metadataPath)) {
|
|
57310
57375
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
57311
57376
|
process.exit(2);
|
|
@@ -57313,7 +57378,7 @@ async function runCli({ start }) {
|
|
|
57313
57378
|
const samples = JSON.parse(readFileSync38(metadataPath, "utf8"));
|
|
57314
57379
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
57315
57380
|
const analysis = await analyzeSamples(samples, config);
|
|
57316
|
-
const outputPath =
|
|
57381
|
+
const outputPath = resolve19(cmdOptions.output);
|
|
57317
57382
|
mkdirSync13(dirname17(outputPath), { recursive: true });
|
|
57318
57383
|
writeFileSync19(outputPath, JSON.stringify(analysis, null, 2), "utf8");
|
|
57319
57384
|
logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
|
|
@@ -57325,7 +57390,7 @@ async function runCli({ start }) {
|
|
|
57325
57390
|
});
|
|
57326
57391
|
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) => {
|
|
57327
57392
|
try {
|
|
57328
|
-
const metadataPath =
|
|
57393
|
+
const metadataPath = resolve19(cmdOptions.inputDir, "metadata.json");
|
|
57329
57394
|
if (!existsSync28(metadataPath)) {
|
|
57330
57395
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
57331
57396
|
process.exit(2);
|
|
@@ -57340,7 +57405,7 @@ async function runCli({ start }) {
|
|
|
57340
57405
|
const candidates = clustersToCandidates(extraction.clusters, {
|
|
57341
57406
|
minFrequency: cmdOptions.minFrequency
|
|
57342
57407
|
});
|
|
57343
|
-
const outputPath =
|
|
57408
|
+
const outputPath = resolve19(cmdOptions.output);
|
|
57344
57409
|
mkdirSync13(dirname17(outputPath), { recursive: true });
|
|
57345
57410
|
const payload = {
|
|
57346
57411
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -57366,7 +57431,7 @@ async function runCli({ start }) {
|
|
|
57366
57431
|
positiveLimit: cmdOptions.positiveLimit,
|
|
57367
57432
|
negativeLimit: cmdOptions.negativeLimit
|
|
57368
57433
|
});
|
|
57369
|
-
const outputPath = cmdOptions.output ?
|
|
57434
|
+
const outputPath = cmdOptions.output ? resolve19(cwd, cmdOptions.output) : resolve19(cwd, "corpus", "calibration-empirical.md");
|
|
57370
57435
|
mkdirSync13(dirname17(outputPath), { recursive: true });
|
|
57371
57436
|
writeFileSync19(outputPath, reportToMarkdown(report), "utf8");
|
|
57372
57437
|
logger.info(
|
|
@@ -57406,7 +57471,7 @@ async function runCli({ start }) {
|
|
|
57406
57471
|
const options = command.optsWithGlobals();
|
|
57407
57472
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
57408
57473
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
57409
|
-
const cwd =
|
|
57474
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57410
57475
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57411
57476
|
const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
|
|
57412
57477
|
logger.info(formatDrift(result, { json: format === "json" }));
|
|
@@ -57423,7 +57488,7 @@ async function runCli({ start }) {
|
|
|
57423
57488
|
async (cmdOptions, command) => {
|
|
57424
57489
|
try {
|
|
57425
57490
|
const options = command.optsWithGlobals();
|
|
57426
|
-
const cwd =
|
|
57491
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57427
57492
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57428
57493
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57429
57494
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
@@ -57450,7 +57515,7 @@ async function runCli({ start }) {
|
|
|
57450
57515
|
const options = command.optsWithGlobals();
|
|
57451
57516
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
57452
57517
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
57453
|
-
const cwd =
|
|
57518
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57454
57519
|
const { report } = await runScan({ ...options, workspace: cwd });
|
|
57455
57520
|
const securityIssues = report.issues.filter((i) => i.category === "security");
|
|
57456
57521
|
const { risk, findings } = computeAiSecurityRisk(securityIssues);
|
|
@@ -57501,7 +57566,7 @@ async function runCli({ start }) {
|
|
|
57501
57566
|
const options = command.optsWithGlobals();
|
|
57502
57567
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
57503
57568
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
57504
|
-
const cwd =
|
|
57569
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57505
57570
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57506
57571
|
const { result } = await runTestScan(cwd, config, { strict: options.strict });
|
|
57507
57572
|
logger.info(formatTestReport(result, { json: format === "json" }));
|
|
@@ -57520,7 +57585,7 @@ async function runCli({ start }) {
|
|
|
57520
57585
|
const options = command.optsWithGlobals();
|
|
57521
57586
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
57522
57587
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
57523
|
-
const cwd =
|
|
57588
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57524
57589
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57525
57590
|
const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
|
|
57526
57591
|
const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
|
|
@@ -57540,7 +57605,7 @@ async function runCli({ start }) {
|
|
|
57540
57605
|
const options = command.optsWithGlobals();
|
|
57541
57606
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57542
57607
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57543
|
-
const cwd =
|
|
57608
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57544
57609
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57545
57610
|
const result = await runBusinessLogicScan(cwd, config, {
|
|
57546
57611
|
maxFiles: cmdOptions.maxFiles
|
|
@@ -57562,7 +57627,7 @@ async function runCli({ start }) {
|
|
|
57562
57627
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57563
57628
|
const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
|
|
57564
57629
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
57565
|
-
const cwd =
|
|
57630
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57566
57631
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57567
57632
|
const result = await runMaintenanceCostScan(cwd, config, {
|
|
57568
57633
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -57585,7 +57650,7 @@ async function runCli({ start }) {
|
|
|
57585
57650
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57586
57651
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57587
57652
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
57588
|
-
const cwd =
|
|
57653
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57589
57654
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57590
57655
|
const result = await runDocsScan(cwd, config, {
|
|
57591
57656
|
maxDocFiles: cmdOptions.maxFiles,
|
|
@@ -57613,7 +57678,7 @@ async function runCli({ start }) {
|
|
|
57613
57678
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57614
57679
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57615
57680
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
57616
|
-
const cwd =
|
|
57681
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57617
57682
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57618
57683
|
const result = await runDbScan(cwd, config, {
|
|
57619
57684
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -57640,7 +57705,7 @@ async function runCli({ start }) {
|
|
|
57640
57705
|
const options = command.optsWithGlobals();
|
|
57641
57706
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57642
57707
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57643
|
-
const cwd =
|
|
57708
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57644
57709
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57645
57710
|
const result = await runPatternsScan(cwd, config, {
|
|
57646
57711
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -57671,14 +57736,14 @@ async function runCli({ start }) {
|
|
|
57671
57736
|
...rawGlobals,
|
|
57672
57737
|
noIncrease: rawGlobals.increase === false
|
|
57673
57738
|
};
|
|
57674
|
-
const cwd =
|
|
57739
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57675
57740
|
const { watchProject: watchProject2 } = await Promise.resolve().then(() => (init_scan(), scan_exports));
|
|
57676
57741
|
await scanAction([], options, command);
|
|
57677
57742
|
await watchProject2(options, cwd, []);
|
|
57678
57743
|
});
|
|
57679
57744
|
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(
|
|
57680
57745
|
(cmdOptions) => {
|
|
57681
|
-
const cwd =
|
|
57746
|
+
const cwd = resolve19(cmdOptions.workspace ?? process.cwd());
|
|
57682
57747
|
const { installHook: installHook2, uninstallHook: uninstallHook2 } = (init_installer(), __toCommonJS(installer_exports));
|
|
57683
57748
|
if (cmdOptions.uninstall) {
|
|
57684
57749
|
const result2 = uninstallHook2(cwd);
|
|
@@ -57708,7 +57773,7 @@ async function runCli({ start }) {
|
|
|
57708
57773
|
// scan only changed files
|
|
57709
57774
|
format: cmdOptions.format ?? "json"
|
|
57710
57775
|
};
|
|
57711
|
-
const cwd =
|
|
57776
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
57712
57777
|
await scanAction([], options, command);
|
|
57713
57778
|
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
57714
57779
|
const health = loadHealth2(cwd);
|
|
@@ -57736,7 +57801,7 @@ async function runCli({ start }) {
|
|
|
57736
57801
|
);
|
|
57737
57802
|
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(
|
|
57738
57803
|
async (cmdOptions) => {
|
|
57739
|
-
const cwd =
|
|
57804
|
+
const cwd = resolve19(cmdOptions.workspace ?? process.cwd());
|
|
57740
57805
|
const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
|
|
57741
57806
|
const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
57742
57807
|
if (cmdOptions.regenerate) {
|
|
@@ -57769,7 +57834,7 @@ async function runCli({ start }) {
|
|
|
57769
57834
|
(cmdOptions, command) => {
|
|
57770
57835
|
const globals = command.optsWithGlobals();
|
|
57771
57836
|
const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
|
|
57772
|
-
const cwd =
|
|
57837
|
+
const cwd = resolve19(cmdOptions.workspace ?? process.cwd());
|
|
57773
57838
|
const { runMigrate: runMigrate2, formatMigrate: formatMigrate2 } = (init_migrate(), __toCommonJS(migrate_exports));
|
|
57774
57839
|
const result = runMigrate2({
|
|
57775
57840
|
workspace: cwd,
|
|
@@ -57864,13 +57929,8 @@ async function runCli({ start }) {
|
|
|
57864
57929
|
}
|
|
57865
57930
|
logger.info(lines.join("\n"));
|
|
57866
57931
|
});
|
|
57867
|
-
program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
|
|
57868
|
-
const result = explainRule2(ruleId, builtinRules, RULE_HINTS);
|
|
57869
|
-
logger.info(formatExplain(result));
|
|
57870
|
-
if ("error" in result) process.exit(2);
|
|
57871
|
-
});
|
|
57872
57932
|
program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
|
|
57873
|
-
const path = configPath ?
|
|
57933
|
+
const path = configPath ? resolve19(configPath) : resolve19(process.cwd(), "slopbrick.config.mjs");
|
|
57874
57934
|
if (!existsSync28(path)) {
|
|
57875
57935
|
logger.error(`Error: config file not found: ${path}`);
|
|
57876
57936
|
process.exit(2);
|