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.cjs
CHANGED
|
@@ -30242,7 +30242,7 @@ var init_math_element_uniformity = __esm({
|
|
|
30242
30242
|
if (!anchor) anchor = { line: el.line, column: el.column };
|
|
30243
30243
|
}
|
|
30244
30244
|
}
|
|
30245
|
-
const values = [counts.button, counts.input, counts.select].filter((v) => v > 0);
|
|
30245
|
+
const values = [counts.button, counts.input, counts.select].filter((v) => typeof v === "number" && v > 0);
|
|
30246
30246
|
if (values.length < 2) return issues;
|
|
30247
30247
|
const max = Math.max(...values);
|
|
30248
30248
|
const min = Math.min(...values);
|
|
@@ -30300,7 +30300,7 @@ var init_math_grid_uniformity = __esm({
|
|
|
30300
30300
|
const { h, vocab, total } = shannonEntropy(counts);
|
|
30301
30301
|
if (total < 4) return issues;
|
|
30302
30302
|
if (h > 1) return issues;
|
|
30303
|
-
const anchor = facts.v2 ? flatClassNames(facts.v2)[0] : { line: 1, column: 1 };
|
|
30303
|
+
const anchor = facts.v2 ? flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 } : { line: 1, column: 1 };
|
|
30304
30304
|
issues.push({
|
|
30305
30305
|
ruleId: "layout/math-grid-uniformity",
|
|
30306
30306
|
category: "layout",
|
|
@@ -34733,59 +34733,77 @@ __export(dist_exports, {
|
|
|
34733
34733
|
writeCacheFromInventory: () => writeCacheFromInventory,
|
|
34734
34734
|
writeJsonAtomic: () => writeJsonAtomic
|
|
34735
34735
|
});
|
|
34736
|
+
function isRecord(v) {
|
|
34737
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34738
|
+
}
|
|
34739
|
+
function isStringArray(v) {
|
|
34740
|
+
return Array.isArray(v) && v.every((i) => typeof i === "string");
|
|
34741
|
+
}
|
|
34742
|
+
function isNumber(v) {
|
|
34743
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
34744
|
+
}
|
|
34736
34745
|
function isStructurePattern(value) {
|
|
34737
|
-
if (
|
|
34738
|
-
|
|
34739
|
-
return typeof v.category === "string" && typeof v.name === "string" && Array.isArray(v.imports) && v.imports.every((i) => typeof i === "string") && typeof v.fileCount === "number";
|
|
34746
|
+
if (!isRecord(value)) return false;
|
|
34747
|
+
return typeof value.category === "string" && typeof value.name === "string" && isStringArray(value.imports) && isNumber(value.fileCount);
|
|
34740
34748
|
}
|
|
34741
34749
|
function isComponentFingerprint(value) {
|
|
34742
|
-
if (
|
|
34743
|
-
|
|
34744
|
-
|
|
34750
|
+
if (!isRecord(value)) return false;
|
|
34751
|
+
return typeof value.name === "string" && isStringArray(value.files) && typeof value.fingerprint === "string" && isStringArray(value.hooks) && isStringArray(value.props) && isNumber(value.line) && isNumber(value.endLine);
|
|
34752
|
+
}
|
|
34753
|
+
function isVersion3(value) {
|
|
34754
|
+
return value === STRUCTURE_SCHEMA_VERSION;
|
|
34745
34755
|
}
|
|
34746
34756
|
function isInventoryFile(value) {
|
|
34747
|
-
if (
|
|
34748
|
-
|
|
34749
|
-
|
|
34757
|
+
if (!isRecord(value)) return false;
|
|
34758
|
+
if (!isVersion3(value.version)) return false;
|
|
34759
|
+
if (typeof value.generatedAt !== "string") return false;
|
|
34760
|
+
if (typeof value.workspace !== "string") return false;
|
|
34761
|
+
if (!isNumber(value.scannedFiles)) return false;
|
|
34762
|
+
if (!isNumber(value.scanDurationMs)) return false;
|
|
34763
|
+
if (!Array.isArray(value.patterns) || !value.patterns.every(isStructurePattern)) return false;
|
|
34764
|
+
if (!Array.isArray(value.components) || !value.components.every(isComponentFingerprint)) return false;
|
|
34765
|
+
return true;
|
|
34750
34766
|
}
|
|
34751
34767
|
function isConstitutionFile(value) {
|
|
34752
|
-
if (
|
|
34753
|
-
|
|
34754
|
-
|
|
34768
|
+
if (!isRecord(value)) return false;
|
|
34769
|
+
if (!isVersion3(value.version)) return false;
|
|
34770
|
+
if (typeof value.generatedAt !== "string") return false;
|
|
34771
|
+
if (typeof value.workspace !== "string") return false;
|
|
34772
|
+
if (!isRecord(value.declared)) return false;
|
|
34773
|
+
if (!isStringArray(value.forbidden)) return false;
|
|
34774
|
+
if (!isStringArray(value.forbiddenPrefixes)) return false;
|
|
34775
|
+
return true;
|
|
34755
34776
|
}
|
|
34756
34777
|
function isFileMtimeEntry(value) {
|
|
34757
|
-
if (
|
|
34758
|
-
|
|
34759
|
-
return typeof v.file === "string" && typeof v.mtimeMs === "number" && typeof v.hash === "string";
|
|
34778
|
+
if (!isRecord(value)) return false;
|
|
34779
|
+
return typeof value.file === "string" && isNumber(value.mtimeMs) && typeof value.hash === "string";
|
|
34760
34780
|
}
|
|
34761
34781
|
function isHealthFile(value) {
|
|
34762
|
-
if (
|
|
34763
|
-
|
|
34764
|
-
if (
|
|
34765
|
-
if (typeof
|
|
34766
|
-
if (
|
|
34767
|
-
if (
|
|
34768
|
-
if (
|
|
34769
|
-
if (
|
|
34770
|
-
if (
|
|
34771
|
-
|
|
34772
|
-
|
|
34773
|
-
if (
|
|
34774
|
-
if (
|
|
34775
|
-
if (
|
|
34776
|
-
if (
|
|
34777
|
-
|
|
34778
|
-
|
|
34779
|
-
|
|
34780
|
-
|
|
34781
|
-
|
|
34782
|
-
|
|
34783
|
-
if (
|
|
34784
|
-
|
|
34785
|
-
|
|
34786
|
-
|
|
34787
|
-
}
|
|
34788
|
-
if (v.scanDurationMs !== void 0 && typeof v.scanDurationMs !== "number") return false;
|
|
34782
|
+
if (!isRecord(value)) return false;
|
|
34783
|
+
if (!isVersion3(value.version)) return false;
|
|
34784
|
+
if (typeof value.generatedAt !== "string") return false;
|
|
34785
|
+
if (typeof value.workspace !== "string") return false;
|
|
34786
|
+
if (!isNumber(value.aiQuality)) return false;
|
|
34787
|
+
if (!isNumber(value.engineeringHygiene)) return false;
|
|
34788
|
+
if (!isNumber(value.security)) return false;
|
|
34789
|
+
if (!isNumber(value.repositoryHealth)) return false;
|
|
34790
|
+
if (!isRecord(value.issueCounts)) return false;
|
|
34791
|
+
const counts = value.issueCounts;
|
|
34792
|
+
if (!isNumber(counts.high)) return false;
|
|
34793
|
+
if (!isNumber(counts.medium)) return false;
|
|
34794
|
+
if (!isNumber(counts.low)) return false;
|
|
34795
|
+
if (value.slopIndex !== void 0 && !isNumber(value.slopIndex)) return false;
|
|
34796
|
+
if (value.categoryScores !== void 0) {
|
|
34797
|
+
if (!isRecord(value.categoryScores)) return false;
|
|
34798
|
+
for (const score of Object.values(value.categoryScores)) {
|
|
34799
|
+
if (!isNumber(score)) return false;
|
|
34800
|
+
}
|
|
34801
|
+
}
|
|
34802
|
+
if (value.constitutionDrift !== void 0 && !isNumber(value.constitutionDrift)) return false;
|
|
34803
|
+
if (value.topOffenseIds !== void 0) {
|
|
34804
|
+
if (!isStringArray(value.topOffenseIds)) return false;
|
|
34805
|
+
}
|
|
34806
|
+
if (value.scanDurationMs !== void 0 && !isNumber(value.scanDurationMs)) return false;
|
|
34789
34807
|
return true;
|
|
34790
34808
|
}
|
|
34791
34809
|
function inventoryPath(workspaceDir) {
|
|
@@ -36801,6 +36819,7 @@ var init_dist2 = __esm({
|
|
|
36801
36819
|
import_path3 = require("path");
|
|
36802
36820
|
import_crypto2 = require("crypto");
|
|
36803
36821
|
init_dist();
|
|
36822
|
+
init_dist();
|
|
36804
36823
|
import_crypto3 = require("crypto");
|
|
36805
36824
|
import_promises2 = require("fs/promises");
|
|
36806
36825
|
import_path4 = require("path");
|
|
@@ -36812,7 +36831,11 @@ var init_dist2 = __esm({
|
|
|
36812
36831
|
RECALL_FLOOR = 1e-6;
|
|
36813
36832
|
LLR_CAP = 13.8;
|
|
36814
36833
|
DEFAULT_PRIOR_PREVALENCE = 0.3;
|
|
36815
|
-
ELIGIBLE_VERDICTS =
|
|
36834
|
+
ELIGIBLE_VERDICTS = new Set(
|
|
36835
|
+
["USEFUL", "OK", "NOISY", "INVERTED", "HYGIENE", "DORMANT"].filter(
|
|
36836
|
+
(v) => v !== "HYGIENE" && !isDefaultOff(v)
|
|
36837
|
+
)
|
|
36838
|
+
);
|
|
36816
36839
|
SUFFIXES_TO_STRIP = [
|
|
36817
36840
|
"RepositoryClient",
|
|
36818
36841
|
"ServiceFactory",
|
|
@@ -37652,7 +37675,7 @@ var init_math_gini_class_usage = __esm({
|
|
|
37652
37675
|
if (g < 0.5) return issues;
|
|
37653
37676
|
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
37654
37677
|
const topStr = sorted.map(([k, v]) => `${k}\xD7${v}`).join(", ");
|
|
37655
|
-
const anchor = facts.v2 ? flatClassNames(facts.v2)[0] : { line: 1, column: 1 };
|
|
37678
|
+
const anchor = facts.v2 ? flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 } : { line: 1, column: 1 };
|
|
37656
37679
|
issues.push({
|
|
37657
37680
|
ruleId: "logic/math-gini-class-usage",
|
|
37658
37681
|
category: "logic",
|
|
@@ -39912,11 +39935,11 @@ function coveredFunctionNames(names, testFileSources, cwd) {
|
|
|
39912
39935
|
}
|
|
39913
39936
|
return covered;
|
|
39914
39937
|
}
|
|
39915
|
-
var
|
|
39938
|
+
var import_core4, import_node_fs10, MAX_PER_FILE, missingEdgeCaseRule;
|
|
39916
39939
|
var init_missing_edge_case = __esm({
|
|
39917
39940
|
"src/rules/test/missing-edge-case.ts"() {
|
|
39918
39941
|
"use strict";
|
|
39919
|
-
|
|
39942
|
+
import_core4 = require("@swc/core");
|
|
39920
39943
|
import_node_fs10 = require("fs");
|
|
39921
39944
|
init_rule();
|
|
39922
39945
|
MAX_PER_FILE = 20;
|
|
@@ -39961,7 +39984,7 @@ var init_missing_edge_case = __esm({
|
|
|
39961
39984
|
if (!source) return issues;
|
|
39962
39985
|
let ast;
|
|
39963
39986
|
try {
|
|
39964
|
-
ast = (0,
|
|
39987
|
+
ast = (0, import_core4.parseSync)(source, {
|
|
39965
39988
|
syntax: "typescript",
|
|
39966
39989
|
tsx: filePath.endsWith("tsx") || filePath.endsWith("jsx"),
|
|
39967
39990
|
target: "es2022"
|
|
@@ -40955,7 +40978,7 @@ var init_math_font_entropy = __esm({
|
|
|
40955
40978
|
const { h, vocab, total } = shannonEntropy(counts);
|
|
40956
40979
|
if (total < 6) return issues;
|
|
40957
40980
|
if (h > 1.4) return issues;
|
|
40958
|
-
const anchor = flatClassNames(facts.v2)[0];
|
|
40981
|
+
const anchor = flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 };
|
|
40959
40982
|
issues.push({
|
|
40960
40983
|
ruleId: "visual/math-font-entropy",
|
|
40961
40984
|
category: "visual",
|
|
@@ -41095,7 +41118,7 @@ var init_math_rounded_entropy = __esm({
|
|
|
41095
41118
|
const { h, vocab, total } = shannonEntropy(counts);
|
|
41096
41119
|
if (total < 6) return issues;
|
|
41097
41120
|
if (h > 1.8) return issues;
|
|
41098
|
-
const anchor = flatClassNames(facts.v2)[0];
|
|
41121
|
+
const anchor = flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 };
|
|
41099
41122
|
issues.push({
|
|
41100
41123
|
ruleId: "visual/math-rounded-entropy",
|
|
41101
41124
|
category: "visual",
|
|
@@ -41143,7 +41166,7 @@ var init_math_spacing_entropy = __esm({
|
|
|
41143
41166
|
const { h, vocab, total } = shannonEntropy(counts);
|
|
41144
41167
|
if (total < 10) return issues;
|
|
41145
41168
|
if (h > 1.5) return issues;
|
|
41146
|
-
const anchor = flatClassNames(facts.v2)[0];
|
|
41169
|
+
const anchor = flatClassNames(facts.v2)[0] ?? { line: 1, column: 1 };
|
|
41147
41170
|
issues.push({
|
|
41148
41171
|
ruleId: "visual/math-spacing-entropy",
|
|
41149
41172
|
category: "visual",
|
|
@@ -45356,7 +45379,7 @@ function formatPretty(report) {
|
|
|
45356
45379
|
}
|
|
45357
45380
|
function formatScoringExplainer(_report) {
|
|
45358
45381
|
return import_chalk.default.dim(
|
|
45359
|
-
"
|
|
45382
|
+
"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."
|
|
45360
45383
|
);
|
|
45361
45384
|
}
|
|
45362
45385
|
function formatWhyFailingReport(report) {
|
|
@@ -46018,11 +46041,11 @@ var init_pool = __esm({
|
|
|
46018
46041
|
for (let i = 0; i < this.threadCount; i++) {
|
|
46019
46042
|
spawnWorker();
|
|
46020
46043
|
}
|
|
46021
|
-
return new Promise((
|
|
46044
|
+
return new Promise((resolve20) => {
|
|
46022
46045
|
const check = setInterval(() => {
|
|
46023
46046
|
if (resolved) {
|
|
46024
46047
|
clearInterval(check);
|
|
46025
|
-
|
|
46048
|
+
resolve20(results);
|
|
46026
46049
|
}
|
|
46027
46050
|
}, 10);
|
|
46028
46051
|
});
|
|
@@ -48206,7 +48229,7 @@ function confirmMagicRateLiteral(line) {
|
|
|
48206
48229
|
const probe = `let __probe = (${line});`;
|
|
48207
48230
|
let ast;
|
|
48208
48231
|
try {
|
|
48209
|
-
ast = (0,
|
|
48232
|
+
ast = (0, import_core6.parseSync)(probe, { syntax: "ecmascript", target: "es2022" });
|
|
48210
48233
|
} catch {
|
|
48211
48234
|
return true;
|
|
48212
48235
|
}
|
|
@@ -48485,11 +48508,11 @@ function extractIdentifier(line) {
|
|
|
48485
48508
|
const m = line.match(/Math\.round\s*\(\s*([A-Za-z_$][\w$.]*)/);
|
|
48486
48509
|
return m ? m[1] ?? "" : "<expr>";
|
|
48487
48510
|
}
|
|
48488
|
-
var
|
|
48511
|
+
var import_core6, BUSINESS_LOGIC_WEIGHTS, CURRENCY_IDENTIFIERS, COMMON_RATE_LITERALS, MATH_ROUND_CENTS, MAGIC_RATE_DECIMAL, HARDCODED_CURRENCY_SYMBOL, SOURCE_EXTENSION, ZOD_STRING, ZOD_STRING_CALL, REQUIRED_ERROR, INVALID_TYPE_ERROR, HARDCODED_ISO_DATE, LOCALE_STRING_NO_OPTIONS, RAW_CURRENCY_IN_TEMPLATE;
|
|
48489
48512
|
var init_business_logic = __esm({
|
|
48490
48513
|
"src/engine/business-logic.ts"() {
|
|
48491
48514
|
"use strict";
|
|
48492
|
-
|
|
48515
|
+
import_core6 = require("@swc/core");
|
|
48493
48516
|
BUSINESS_LOGIC_WEIGHTS = {
|
|
48494
48517
|
pricing: 3,
|
|
48495
48518
|
validation: 2,
|
|
@@ -49860,6 +49883,13 @@ function mergeComponentsByName(components) {
|
|
|
49860
49883
|
}
|
|
49861
49884
|
byName.set(c.name, {
|
|
49862
49885
|
...c,
|
|
49886
|
+
// All visitors (rust.ts, php.ts, go.ts, …) push Components with
|
|
49887
|
+
// `files: [filePath]` — non-empty by construction. The JSON
|
|
49888
|
+
// Schema (inventory.schema.json) requires `files` to be
|
|
49889
|
+
// `[string, ...string[]]` (at least 1); the cast is safe under
|
|
49890
|
+
// the visitor invariant. If a future visitor ever produces an
|
|
49891
|
+
// empty files array, the runtime validator
|
|
49892
|
+
// (`isInventoryFile`) will reject the artifact at write time.
|
|
49863
49893
|
files: c.files.slice(),
|
|
49864
49894
|
hooks: c.hooks.slice(),
|
|
49865
49895
|
props: c.props.slice()
|
|
@@ -49960,29 +49990,29 @@ function renderStructureMarkdown(inventory, constitution) {
|
|
|
49960
49990
|
return lines.join("\n");
|
|
49961
49991
|
}
|
|
49962
49992
|
async function writeStructureMarkdown(workspaceDir, md) {
|
|
49963
|
-
await new Promise((
|
|
49993
|
+
await new Promise((resolve20, reject) => {
|
|
49964
49994
|
try {
|
|
49965
49995
|
const path = (0, import_node_path21.join)(workspaceDir, STRUCTURE_MD_FILE);
|
|
49966
49996
|
(0, import_node_fs21.mkdirSync)((0, import_node_path21.dirname)(path), { recursive: true });
|
|
49967
49997
|
(0, import_node_fs21.writeFileSync)(path, md, "utf-8");
|
|
49968
|
-
|
|
49998
|
+
resolve20();
|
|
49969
49999
|
} catch (err) {
|
|
49970
50000
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
49971
50001
|
}
|
|
49972
50002
|
});
|
|
49973
50003
|
}
|
|
49974
50004
|
async function readStructureMarkdown(workspaceDir) {
|
|
49975
|
-
return new Promise((
|
|
50005
|
+
return new Promise((resolve20) => {
|
|
49976
50006
|
try {
|
|
49977
50007
|
const path = (0, import_node_path21.join)(workspaceDir, STRUCTURE_MD_FILE);
|
|
49978
50008
|
if (!(0, import_node_fs21.existsSync)(path)) {
|
|
49979
|
-
|
|
50009
|
+
resolve20(null);
|
|
49980
50010
|
return;
|
|
49981
50011
|
}
|
|
49982
50012
|
const content = (0, import_node_fs21.readFileSync)(path, "utf-8");
|
|
49983
|
-
|
|
50013
|
+
resolve20(content);
|
|
49984
50014
|
} catch {
|
|
49985
|
-
|
|
50015
|
+
resolve20(null);
|
|
49986
50016
|
}
|
|
49987
50017
|
});
|
|
49988
50018
|
}
|
|
@@ -50251,11 +50281,12 @@ async function finalizeReport(input) {
|
|
|
50251
50281
|
if (options.noIncrease) {
|
|
50252
50282
|
const previous = (await readRuns(cwd, fsMemoryIO)).at(-1);
|
|
50253
50283
|
if (previous) {
|
|
50254
|
-
|
|
50284
|
+
const previousBaseline = previous.slopIndex;
|
|
50285
|
+
if ((report.aiQuality ?? 0) < previousBaseline) {
|
|
50255
50286
|
noIncreaseFailure = true;
|
|
50256
50287
|
if (!options.quiet) {
|
|
50257
50288
|
logger.error(
|
|
50258
|
-
`AI Quality went DOWN from ${
|
|
50289
|
+
`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.`
|
|
50259
50290
|
);
|
|
50260
50291
|
}
|
|
50261
50292
|
}
|
|
@@ -52530,11 +52561,11 @@ __export(installer_exports, {
|
|
|
52530
52561
|
uninstallHook: () => uninstallHook
|
|
52531
52562
|
});
|
|
52532
52563
|
function hookPath(gitRoot) {
|
|
52533
|
-
const huskyDir = (0,
|
|
52564
|
+
const huskyDir = (0, import_node_path39.join)(gitRoot, ".husky");
|
|
52534
52565
|
if ((0, import_node_fs35.existsSync)(huskyDir)) {
|
|
52535
|
-
return (0,
|
|
52566
|
+
return (0, import_node_path39.join)(huskyDir, "pre-commit");
|
|
52536
52567
|
}
|
|
52537
|
-
return (0,
|
|
52568
|
+
return (0, import_node_path39.join)(gitRoot, ".git", "hooks", "pre-commit");
|
|
52538
52569
|
}
|
|
52539
52570
|
function readHookContent(path) {
|
|
52540
52571
|
return (0, import_node_fs35.readFileSync)(path, "utf8");
|
|
@@ -52599,7 +52630,7 @@ function installHook(gitRoot) {
|
|
|
52599
52630
|
exitCode: 0
|
|
52600
52631
|
};
|
|
52601
52632
|
}
|
|
52602
|
-
(0, import_node_fs35.mkdirSync)((0,
|
|
52633
|
+
(0, import_node_fs35.mkdirSync)((0, import_node_path39.dirname)(path), { recursive: true });
|
|
52603
52634
|
(0, import_node_fs35.writeFileSync)(path, SENTINEL_BLOCK, { mode: 493 });
|
|
52604
52635
|
(0, import_node_fs35.chmodSync)(path, 493);
|
|
52605
52636
|
return {
|
|
@@ -52654,12 +52685,12 @@ function uninstallHook(gitRoot) {
|
|
|
52654
52685
|
exitCode: 0
|
|
52655
52686
|
};
|
|
52656
52687
|
}
|
|
52657
|
-
var import_node_fs35,
|
|
52688
|
+
var import_node_fs35, import_node_path39, BEGIN_SENTINEL, END_SENTINEL, SENTINEL_BLOCK;
|
|
52658
52689
|
var init_installer = __esm({
|
|
52659
52690
|
"src/cli/installer.ts"() {
|
|
52660
52691
|
"use strict";
|
|
52661
52692
|
import_node_fs35 = require("fs");
|
|
52662
|
-
|
|
52693
|
+
import_node_path39 = require("path");
|
|
52663
52694
|
BEGIN_SENTINEL = "# slopbrick-hook-begin";
|
|
52664
52695
|
END_SENTINEL = "# slopbrick-hook-end";
|
|
52665
52696
|
SENTINEL_BLOCK = `${BEGIN_SENTINEL}
|
|
@@ -52737,7 +52768,7 @@ async function runScanFile(args, ctx) {
|
|
|
52737
52768
|
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
52738
52769
|
};
|
|
52739
52770
|
}
|
|
52740
|
-
function
|
|
52771
|
+
function explainRule2(args, ctx) {
|
|
52741
52772
|
const ruleId = args.ruleId;
|
|
52742
52773
|
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
52743
52774
|
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
@@ -52848,7 +52879,7 @@ async function runGovernance(args, ctx) {
|
|
|
52848
52879
|
function runCheckConstitution(args, ctx) {
|
|
52849
52880
|
const path = args.path;
|
|
52850
52881
|
if (!path) return toolError("Missing required argument: path");
|
|
52851
|
-
const absPath = (0,
|
|
52882
|
+
const absPath = (0, import_node_path42.resolve)(ctx.cwd, path);
|
|
52852
52883
|
let source;
|
|
52853
52884
|
try {
|
|
52854
52885
|
source = (0, import_node_fs38.readFileSync)(absPath, "utf-8");
|
|
@@ -53011,7 +53042,7 @@ async function handleToolCall(toolName, args, ctx) {
|
|
|
53011
53042
|
case "slop_scan_file":
|
|
53012
53043
|
return runScanFile(args, ctx);
|
|
53013
53044
|
case "slop_explain_rule":
|
|
53014
|
-
return
|
|
53045
|
+
return explainRule2(args, ctx);
|
|
53015
53046
|
case "slop_list_rules":
|
|
53016
53047
|
return listRules(args, ctx);
|
|
53017
53048
|
case "slop_suggest":
|
|
@@ -53038,12 +53069,12 @@ function canonicalToolNames() {
|
|
|
53038
53069
|
function getDeprecation(toolName) {
|
|
53039
53070
|
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
53040
53071
|
}
|
|
53041
|
-
var import_node_fs38,
|
|
53072
|
+
var import_node_fs38, import_node_path42, TOOL_DEFINITIONS;
|
|
53042
53073
|
var init_tools = __esm({
|
|
53043
53074
|
"src/mcp/tools.ts"() {
|
|
53044
53075
|
"use strict";
|
|
53045
53076
|
import_node_fs38 = require("fs");
|
|
53046
|
-
|
|
53077
|
+
import_node_path42 = require("path");
|
|
53047
53078
|
init_worker();
|
|
53048
53079
|
init_patterns();
|
|
53049
53080
|
init_architecture_score();
|
|
@@ -53229,36 +53260,36 @@ function planMigration(workspaceDir) {
|
|
|
53229
53260
|
const moves = [];
|
|
53230
53261
|
const rewrites = [];
|
|
53231
53262
|
const gitignoreEdits = [];
|
|
53232
|
-
const oldDir = (0,
|
|
53233
|
-
const newDir = (0,
|
|
53263
|
+
const oldDir = (0, import_node_path45.join)(workspaceDir, ".slop-audit");
|
|
53264
|
+
const newDir = (0, import_node_path45.join)(workspaceDir, ".slopbrick");
|
|
53234
53265
|
if ((0, import_node_fs44.existsSync)(oldDir)) {
|
|
53235
53266
|
moves.push({ from: oldDir, to: newDir, kind: "dir" });
|
|
53236
53267
|
rewrites.push({
|
|
53237
|
-
path: (0,
|
|
53268
|
+
path: (0, import_node_path45.join)(newDir, "inventory.json"),
|
|
53238
53269
|
field: "version",
|
|
53239
53270
|
from: '"1"',
|
|
53240
53271
|
to: '"2"'
|
|
53241
53272
|
});
|
|
53242
53273
|
rewrites.push({
|
|
53243
|
-
path: (0,
|
|
53274
|
+
path: (0, import_node_path45.join)(newDir, "constitution.json"),
|
|
53244
53275
|
field: "version",
|
|
53245
53276
|
from: '"1"',
|
|
53246
53277
|
to: '"2"'
|
|
53247
53278
|
});
|
|
53248
53279
|
}
|
|
53249
|
-
const oldCache = (0,
|
|
53250
|
-
const newCache = (0,
|
|
53280
|
+
const oldCache = (0, import_node_path45.join)(workspaceDir, ".slop-audit-cache.json");
|
|
53281
|
+
const newCache = (0, import_node_path45.join)(workspaceDir, ".slopbrick-cache.json");
|
|
53251
53282
|
if ((0, import_node_fs44.existsSync)(oldCache)) {
|
|
53252
53283
|
moves.push({ from: oldCache, to: newCache, kind: "file" });
|
|
53253
53284
|
}
|
|
53254
53285
|
for (const ext of ["mjs", "cjs", "js"]) {
|
|
53255
|
-
const oldCfg = (0,
|
|
53256
|
-
const newCfg = (0,
|
|
53286
|
+
const oldCfg = (0, import_node_path45.join)(workspaceDir, `slop-audit.config.${ext}`);
|
|
53287
|
+
const newCfg = (0, import_node_path45.join)(workspaceDir, `slopbrick.config.${ext}`);
|
|
53257
53288
|
if ((0, import_node_fs44.existsSync)(oldCfg)) {
|
|
53258
53289
|
moves.push({ from: oldCfg, to: newCfg, kind: "config" });
|
|
53259
53290
|
}
|
|
53260
53291
|
}
|
|
53261
|
-
const gi = (0,
|
|
53292
|
+
const gi = (0, import_node_path45.join)(workspaceDir, ".gitignore");
|
|
53262
53293
|
if ((0, import_node_fs44.existsSync)(gi)) {
|
|
53263
53294
|
const content = (0, import_node_fs44.readFileSync)(gi, "utf-8");
|
|
53264
53295
|
if (content.includes(".slop-audit/")) {
|
|
@@ -53279,7 +53310,7 @@ function planMigration(workspaceDir) {
|
|
|
53279
53310
|
return { moves, rewrites, gitignoreEdits };
|
|
53280
53311
|
}
|
|
53281
53312
|
function isAlreadyMigrated(workspaceDir) {
|
|
53282
|
-
return (0, import_node_fs44.existsSync)((0,
|
|
53313
|
+
return (0, import_node_fs44.existsSync)((0, import_node_path45.join)(workspaceDir, ".slopbrick")) && !(0, import_node_fs44.existsSync)((0, import_node_path45.join)(workspaceDir, ".slop-audit"));
|
|
53283
53314
|
}
|
|
53284
53315
|
function applyMigration(plan, options = {}) {
|
|
53285
53316
|
if (options.dryRun) return;
|
|
@@ -53310,8 +53341,8 @@ function runMigrate(options) {
|
|
|
53310
53341
|
};
|
|
53311
53342
|
}
|
|
53312
53343
|
const alreadyMigrated = isAlreadyMigrated(workspace);
|
|
53313
|
-
const newDir = (0,
|
|
53314
|
-
const oldDir = (0,
|
|
53344
|
+
const newDir = (0, import_node_path45.join)(workspace, ".slopbrick");
|
|
53345
|
+
const oldDir = (0, import_node_path45.join)(workspace, ".slop-audit");
|
|
53315
53346
|
if ((0, import_node_fs44.existsSync)(newDir) && (0, import_node_fs44.existsSync)(oldDir) && !force) {
|
|
53316
53347
|
return {
|
|
53317
53348
|
ok: false,
|
|
@@ -53379,12 +53410,12 @@ function formatMigrate(result) {
|
|
|
53379
53410
|
}
|
|
53380
53411
|
return lines.join("\n");
|
|
53381
53412
|
}
|
|
53382
|
-
var import_node_fs44,
|
|
53413
|
+
var import_node_fs44, import_node_path45;
|
|
53383
53414
|
var init_migrate = __esm({
|
|
53384
53415
|
"src/cli/migrate.ts"() {
|
|
53385
53416
|
"use strict";
|
|
53386
53417
|
import_node_fs44 = require("fs");
|
|
53387
|
-
|
|
53418
|
+
import_node_path45 = require("path");
|
|
53388
53419
|
init_logger();
|
|
53389
53420
|
}
|
|
53390
53421
|
});
|
|
@@ -53476,7 +53507,7 @@ init_dist2();
|
|
|
53476
53507
|
|
|
53477
53508
|
// src/cli/program.ts
|
|
53478
53509
|
var import_node_fs45 = require("fs");
|
|
53479
|
-
var
|
|
53510
|
+
var import_node_path46 = require("path");
|
|
53480
53511
|
var import_node_perf_hooks = require("perf_hooks");
|
|
53481
53512
|
var import_commander2 = require("commander");
|
|
53482
53513
|
|
|
@@ -54982,17 +55013,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
|
|
|
54982
55013
|
var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
|
|
54983
55014
|
var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
|
|
54984
55015
|
function promptText(rl, question, detected) {
|
|
54985
|
-
return new Promise((
|
|
55016
|
+
return new Promise((resolve20) => {
|
|
54986
55017
|
const lines = [
|
|
54987
55018
|
`? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
|
|
54988
55019
|
];
|
|
54989
55020
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
54990
55021
|
const trimmed = answer.trim();
|
|
54991
55022
|
if (trimmed === "") {
|
|
54992
|
-
|
|
55023
|
+
resolve20(void 0);
|
|
54993
55024
|
return;
|
|
54994
55025
|
}
|
|
54995
|
-
|
|
55026
|
+
resolve20(trimmed);
|
|
54996
55027
|
});
|
|
54997
55028
|
});
|
|
54998
55029
|
}
|
|
@@ -55000,7 +55031,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
55000
55031
|
const defaultIndex = options.indexOf(defaultValue);
|
|
55001
55032
|
const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
|
|
55002
55033
|
const safeDefaultValue = options[safeDefaultIndex];
|
|
55003
|
-
return new Promise((
|
|
55034
|
+
return new Promise((resolve20) => {
|
|
55004
55035
|
function ask() {
|
|
55005
55036
|
const lines = [
|
|
55006
55037
|
`? ${question} (detected: ${safeDefaultValue}):`,
|
|
@@ -55010,7 +55041,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
55010
55041
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
55011
55042
|
const trimmed = answer.trim();
|
|
55012
55043
|
if (trimmed === "") {
|
|
55013
|
-
|
|
55044
|
+
resolve20(safeDefaultValue);
|
|
55014
55045
|
return;
|
|
55015
55046
|
}
|
|
55016
55047
|
const num = parseInt(trimmed, 10);
|
|
@@ -55019,7 +55050,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
55019
55050
|
ask();
|
|
55020
55051
|
return;
|
|
55021
55052
|
}
|
|
55022
|
-
|
|
55053
|
+
resolve20(options[num - 1]);
|
|
55023
55054
|
});
|
|
55024
55055
|
}
|
|
55025
55056
|
ask();
|
|
@@ -55029,7 +55060,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
55029
55060
|
const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
|
|
55030
55061
|
const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
|
|
55031
55062
|
const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
|
|
55032
|
-
return new Promise((
|
|
55063
|
+
return new Promise((resolve20) => {
|
|
55033
55064
|
function ask() {
|
|
55034
55065
|
const lines = [
|
|
55035
55066
|
`? ${question} (detected: ${defaultDisplay}):`,
|
|
@@ -55040,7 +55071,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
55040
55071
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
55041
55072
|
const trimmed = answer.trim();
|
|
55042
55073
|
if (trimmed === "") {
|
|
55043
|
-
|
|
55074
|
+
resolve20(defaultValue.length > 0 ? defaultValue : []);
|
|
55044
55075
|
return;
|
|
55045
55076
|
}
|
|
55046
55077
|
const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
|
|
@@ -55050,11 +55081,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
55050
55081
|
return;
|
|
55051
55082
|
}
|
|
55052
55083
|
if (numbers.includes(1)) {
|
|
55053
|
-
|
|
55084
|
+
resolve20([]);
|
|
55054
55085
|
return;
|
|
55055
55086
|
}
|
|
55056
|
-
const selected = [...new Set(numbers.map((n) => options[n - 2]))];
|
|
55057
|
-
|
|
55087
|
+
const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
|
|
55088
|
+
resolve20(selected);
|
|
55058
55089
|
});
|
|
55059
55090
|
}
|
|
55060
55091
|
ask();
|
|
@@ -55234,6 +55265,234 @@ async function runDoctor(cwd) {
|
|
|
55234
55265
|
return exitCode;
|
|
55235
55266
|
}
|
|
55236
55267
|
|
|
55268
|
+
// src/cli/commands/badge.ts
|
|
55269
|
+
var import_node_path37 = require("path");
|
|
55270
|
+
init_render();
|
|
55271
|
+
init_logger();
|
|
55272
|
+
init_scan();
|
|
55273
|
+
function registerBadge(program) {
|
|
55274
|
+
program.command("badge").description(
|
|
55275
|
+
"print a shields.io slop-index badge. Reads .slopbrick/health.json if present (no re-scan); falls back to a fresh scan."
|
|
55276
|
+
).action(async (_cmdOptions, command) => {
|
|
55277
|
+
const options = command.optsWithGlobals();
|
|
55278
|
+
const cwd = (0, import_node_path37.resolve)(options.workspace ?? process.cwd());
|
|
55279
|
+
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
55280
|
+
const health = loadHealth2(cwd);
|
|
55281
|
+
if (health) {
|
|
55282
|
+
const synthetic = {
|
|
55283
|
+
slopIndex: 100 - health.repositoryHealth
|
|
55284
|
+
};
|
|
55285
|
+
logger.info(formatBadge(synthetic));
|
|
55286
|
+
process.exit(0);
|
|
55287
|
+
}
|
|
55288
|
+
const { report } = await runScan(options);
|
|
55289
|
+
logger.info(formatBadge(report));
|
|
55290
|
+
process.exit(0);
|
|
55291
|
+
});
|
|
55292
|
+
}
|
|
55293
|
+
|
|
55294
|
+
// src/cli/commands/suggest.ts
|
|
55295
|
+
var import_node_path38 = require("path");
|
|
55296
|
+
init_advice();
|
|
55297
|
+
init_unified_diff();
|
|
55298
|
+
init_logger();
|
|
55299
|
+
init_scan();
|
|
55300
|
+
function registerSuggest(program) {
|
|
55301
|
+
program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
|
|
55302
|
+
const options = command.optsWithGlobals();
|
|
55303
|
+
const { report } = await runScan(options);
|
|
55304
|
+
const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
|
|
55305
|
+
logger.info(formatAdvice(report));
|
|
55306
|
+
const diff = formatUnifiedDiff(report, cwd);
|
|
55307
|
+
if (diff) logger.info(diff);
|
|
55308
|
+
process.exit(0);
|
|
55309
|
+
});
|
|
55310
|
+
}
|
|
55311
|
+
|
|
55312
|
+
// src/cli/explain.ts
|
|
55313
|
+
var RULES_BASE_URL2 = "https://github.com/Dystx/slopbrick/blob/main/src/rules";
|
|
55314
|
+
function ruleIdToFilename2(ruleId) {
|
|
55315
|
+
const slash = ruleId.indexOf("/");
|
|
55316
|
+
return slash === -1 ? ruleId : ruleId.slice(slash + 1);
|
|
55317
|
+
}
|
|
55318
|
+
function explainRule(ruleId, rules, ruleHints) {
|
|
55319
|
+
const rule = rules.find((r) => r.id === ruleId);
|
|
55320
|
+
if (!rule) {
|
|
55321
|
+
return { error: "Unknown rule: " + ruleId + ". Run `slopbrick rules` to see all available rules." };
|
|
55322
|
+
}
|
|
55323
|
+
const filename = ruleIdToFilename2(rule.id);
|
|
55324
|
+
return {
|
|
55325
|
+
ruleId: rule.id,
|
|
55326
|
+
category: rule.category,
|
|
55327
|
+
severity: rule.severity,
|
|
55328
|
+
aiSpecific: rule.aiSpecific,
|
|
55329
|
+
pattern: ruleHints[rule.id] ?? "Patterns flagged by " + rule.id + ".",
|
|
55330
|
+
remediation: "See the rule source for the canonical before/after: src/rules/" + rule.category + "/" + filename + ".ts",
|
|
55331
|
+
sourcePath: "src/rules/" + rule.category + "/" + filename + ".ts",
|
|
55332
|
+
helpUri: `${RULES_BASE_URL2}/${rule.category}/${filename}.ts`,
|
|
55333
|
+
suppressionSnippet: 'rules: { "' + rule.id + '": "off" } // or set to a lower severity'
|
|
55334
|
+
};
|
|
55335
|
+
}
|
|
55336
|
+
function formatExplain(result) {
|
|
55337
|
+
if ("error" in result) return result.error;
|
|
55338
|
+
const lines = [];
|
|
55339
|
+
lines.push("Rule: " + result.ruleId);
|
|
55340
|
+
lines.push("Category: " + result.category);
|
|
55341
|
+
lines.push("Severity: " + result.severity);
|
|
55342
|
+
lines.push("AI-specific: " + (result.aiSpecific ? "yes (designed to fire on AI tells)" : "no (cross-cutting quality rule)"));
|
|
55343
|
+
lines.push("Source: " + result.sourcePath);
|
|
55344
|
+
lines.push("Help: " + result.helpUri);
|
|
55345
|
+
lines.push("");
|
|
55346
|
+
lines.push("Pattern:");
|
|
55347
|
+
lines.push(" " + result.pattern);
|
|
55348
|
+
lines.push("");
|
|
55349
|
+
lines.push("Remediation:");
|
|
55350
|
+
lines.push(" " + result.remediation);
|
|
55351
|
+
lines.push("");
|
|
55352
|
+
lines.push("Suppress / configure in slopbrick.config.mjs:");
|
|
55353
|
+
lines.push(" " + result.suppressionSnippet);
|
|
55354
|
+
lines.push("");
|
|
55355
|
+
return lines.join("\n");
|
|
55356
|
+
}
|
|
55357
|
+
|
|
55358
|
+
// src/cli/commands/explain.ts
|
|
55359
|
+
init_logger();
|
|
55360
|
+
init_builtins();
|
|
55361
|
+
|
|
55362
|
+
// src/snippet/data.ts
|
|
55363
|
+
var CATEGORY_DIRECTIVES = {
|
|
55364
|
+
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.',
|
|
55365
|
+
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.",
|
|
55366
|
+
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.',
|
|
55367
|
+
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".',
|
|
55368
|
+
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.",
|
|
55369
|
+
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
55370
|
+
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].",
|
|
55371
|
+
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
55372
|
+
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.",
|
|
55373
|
+
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'."
|
|
55374
|
+
};
|
|
55375
|
+
var RULE_HINTS = {
|
|
55376
|
+
// v0.16.0 hygiene: 35 out-of-scope orphan hints (keys with no matching
|
|
55377
|
+
// rule in src/rules/builtins.ts) were moved out of this map. The
|
|
55378
|
+
// verbatim source text is preserved in
|
|
55379
|
+
// docs/research/backlog-rule-hints.md
|
|
55380
|
+
// so future implementers can paste a hint back when the corresponding
|
|
55381
|
+
// rule ships. The 5 in-scope orphans (`security/eval`,
|
|
55382
|
+
// `security/localstorage-token`, `security/target-blank-no-noopener`,
|
|
55383
|
+
// `wcag/missing-alt`, `typo/placeholder-text`) are kept here for v0.16.0.
|
|
55384
|
+
"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.",
|
|
55385
|
+
"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.",
|
|
55386
|
+
"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.",
|
|
55387
|
+
"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.",
|
|
55388
|
+
"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.",
|
|
55389
|
+
"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.",
|
|
55390
|
+
"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.',
|
|
55391
|
+
"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.",
|
|
55392
|
+
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
55393
|
+
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
55394
|
+
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
55395
|
+
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
55396
|
+
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
55397
|
+
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
55398
|
+
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
55399
|
+
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
55400
|
+
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
55401
|
+
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
55402
|
+
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
55403
|
+
"arch/astro-island-leak": "For Astro: server-render everything by default. Only opt-in to client islands when interactivity is needed.",
|
|
55404
|
+
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
55405
|
+
"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.",
|
|
55406
|
+
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
55407
|
+
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
55408
|
+
"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.",
|
|
55409
|
+
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
55410
|
+
"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.",
|
|
55411
|
+
"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.",
|
|
55412
|
+
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
55413
|
+
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
55414
|
+
"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*.)",
|
|
55415
|
+
"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.)",
|
|
55416
|
+
"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.)",
|
|
55417
|
+
"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.)",
|
|
55418
|
+
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
55419
|
+
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
55420
|
+
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
55421
|
+
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
55422
|
+
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
55423
|
+
"logic/qwik-hook-leak": "Use Qwik primitives ($state, $effect, useSignal) instead of React hooks (useState, useEffect).",
|
|
55424
|
+
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
55425
|
+
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
55426
|
+
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
55427
|
+
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
55428
|
+
"perf/halstead-anomaly": "Introduce domain-specific identifiers and varied operations. Low vocabulary per line is a strong AI signature (Halstead 1977 \xA73).",
|
|
55429
|
+
"typo/calc-fontsize": "Use a design token (`var(--font-size-lg)`) or `clamp(min, fluid, max)` for responsive typography.",
|
|
55430
|
+
"typo/calc-raw-px": "Replace px values in calc() with rem or em units for scalable layout.",
|
|
55431
|
+
"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.",
|
|
55432
|
+
"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.',
|
|
55433
|
+
"typo/math-cta-vocabulary": 'Use domain-specific action verbs ("Reserve", "Confirm ride", "Activate card") instead of falling back on the AI-default CTA vocabulary.',
|
|
55434
|
+
"visual/clamp-soup": "Use design-system aliases (`--text-fluid-sm`, `--text-fluid-lg`) with bounded ranges (typically 2\xD7 max).",
|
|
55435
|
+
"visual/generic-centering": "Vary hero layouts: some as grids (`grid place-items-center`), some as blocks, some with different alignment.",
|
|
55436
|
+
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
55437
|
+
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
55438
|
+
"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.",
|
|
55439
|
+
"visual/math-gradient-hue-rotation": "Use wider hue spans across gradients (e.g. blue\u2192amber, emerald\u2192indigo) to break the violet-fuchsia monotony.",
|
|
55440
|
+
"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.",
|
|
55441
|
+
"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.",
|
|
55442
|
+
"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%.",
|
|
55443
|
+
"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.",
|
|
55444
|
+
"wcag/dragging-movements": "Provide an onClick, onKeyDown, or button role as an alternative to dragging (WCAG 2.1.1).",
|
|
55445
|
+
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
55446
|
+
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
55447
|
+
"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.",
|
|
55448
|
+
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
55449
|
+
"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.",
|
|
55450
|
+
"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.",
|
|
55451
|
+
"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`).",
|
|
55452
|
+
"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.",
|
|
55453
|
+
"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.",
|
|
55454
|
+
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
55455
|
+
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
55456
|
+
"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).",
|
|
55457
|
+
"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).",
|
|
55458
|
+
"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).",
|
|
55459
|
+
"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).",
|
|
55460
|
+
"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).",
|
|
55461
|
+
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
55462
|
+
"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).",
|
|
55463
|
+
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
55464
|
+
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
55465
|
+
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
55466
|
+
// reclassified post-v7 calibration in v0.14.5d)
|
|
55467
|
+
"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.",
|
|
55468
|
+
"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.",
|
|
55469
|
+
"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.",
|
|
55470
|
+
"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.",
|
|
55471
|
+
"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.",
|
|
55472
|
+
"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.",
|
|
55473
|
+
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
55474
|
+
"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`).",
|
|
55475
|
+
"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.",
|
|
55476
|
+
"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.",
|
|
55477
|
+
"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.",
|
|
55478
|
+
"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.",
|
|
55479
|
+
"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.',
|
|
55480
|
+
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
55481
|
+
"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.",
|
|
55482
|
+
"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.",
|
|
55483
|
+
"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.",
|
|
55484
|
+
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy."
|
|
55485
|
+
};
|
|
55486
|
+
|
|
55487
|
+
// src/cli/commands/explain.ts
|
|
55488
|
+
function registerExplain(program) {
|
|
55489
|
+
program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
|
|
55490
|
+
const result = explainRule(ruleId, builtinRules, RULE_HINTS);
|
|
55491
|
+
logger.info(formatExplain(result));
|
|
55492
|
+
if ("error" in result) process.exit(2);
|
|
55493
|
+
});
|
|
55494
|
+
}
|
|
55495
|
+
|
|
55237
55496
|
// src/cli/program.ts
|
|
55238
55497
|
init_config();
|
|
55239
55498
|
init_git();
|
|
@@ -55295,7 +55554,7 @@ function createProvider(config) {
|
|
|
55295
55554
|
|
|
55296
55555
|
// src/research/generator.ts
|
|
55297
55556
|
var import_node_fs36 = require("fs");
|
|
55298
|
-
var
|
|
55557
|
+
var import_node_path40 = require("path");
|
|
55299
55558
|
|
|
55300
55559
|
// src/research/prompts.ts
|
|
55301
55560
|
var DEFAULT_PROMPT_TEMPLATES = [
|
|
@@ -55358,13 +55617,13 @@ async function generateSamples(options) {
|
|
|
55358
55617
|
}
|
|
55359
55618
|
const samples = [];
|
|
55360
55619
|
const ext = extForFramework(framework);
|
|
55361
|
-
const dir = (0,
|
|
55620
|
+
const dir = (0, import_node_path40.join)(outputDir, framework, componentType);
|
|
55362
55621
|
(0, import_node_fs36.mkdirSync)(dir, { recursive: true });
|
|
55363
55622
|
for (let i = 1; i <= count; i += 1) {
|
|
55364
55623
|
const raw = await provider.generateSample(renderPrompt(template), { temperature });
|
|
55365
55624
|
const code = extractCodeFromMarkdown(raw);
|
|
55366
55625
|
const fileName = `sample-${i}${ext}`;
|
|
55367
|
-
const filePath = (0,
|
|
55626
|
+
const filePath = (0, import_node_path40.join)(dir, fileName);
|
|
55368
55627
|
(0, import_node_fs36.writeFileSync)(filePath, code, "utf8");
|
|
55369
55628
|
const sample = {
|
|
55370
55629
|
filePath,
|
|
@@ -55376,7 +55635,7 @@ async function generateSamples(options) {
|
|
|
55376
55635
|
};
|
|
55377
55636
|
samples.push(sample);
|
|
55378
55637
|
}
|
|
55379
|
-
const metadataPath = (0,
|
|
55638
|
+
const metadataPath = (0, import_node_path40.join)(dir, "metadata.json");
|
|
55380
55639
|
(0, import_node_fs36.writeFileSync)(metadataPath, JSON.stringify(samples, null, 2), "utf8");
|
|
55381
55640
|
return samples;
|
|
55382
55641
|
}
|
|
@@ -55584,11 +55843,11 @@ function slugify(value) {
|
|
|
55584
55843
|
// src/research/calibrator.ts
|
|
55585
55844
|
var import_node_child_process3 = require("child_process");
|
|
55586
55845
|
var import_node_fs37 = require("fs");
|
|
55587
|
-
var
|
|
55846
|
+
var import_node_path41 = require("path");
|
|
55588
55847
|
var DEFAULT_POSITIVE = "/Users/cheng/ai-slop-baseline/extracted/positive";
|
|
55589
55848
|
var DEFAULT_NEGATIVE = "/Users/cheng/ai-slop-baseline/extracted/negative";
|
|
55590
55849
|
function buildFileList(dir, extensions) {
|
|
55591
|
-
const tmpList = (0,
|
|
55850
|
+
const tmpList = (0, import_node_path41.join)("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
|
|
55592
55851
|
const expr = extensions.map((e) => `-name '*.${e}'`).join(" -o ");
|
|
55593
55852
|
(0, import_node_child_process3.execFileSync)("bash", ["-c", `find ${dir} -maxdepth 8 -type f \\( ${expr} \\) -print0 | xargs -0 realpath > ${tmpList}`]);
|
|
55594
55853
|
const out = (0, import_node_fs37.readFileSync)(tmpList, "utf8");
|
|
@@ -55601,13 +55860,13 @@ function runScan2(fileListPath) {
|
|
|
55601
55860
|
const ruleFires = /* @__PURE__ */ new Map();
|
|
55602
55861
|
const uniqueFilesPerRule = /* @__PURE__ */ new Map();
|
|
55603
55862
|
let fileCount = 0;
|
|
55604
|
-
const tmpOut = (0,
|
|
55863
|
+
const tmpOut = (0, import_node_path41.join)("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
55605
55864
|
for (let i = 0; i < files.length; i += CHUNK) {
|
|
55606
55865
|
const chunk = files.slice(i, i + CHUNK);
|
|
55607
55866
|
try {
|
|
55608
55867
|
(0, import_node_child_process3.execFileSync)(
|
|
55609
55868
|
"node",
|
|
55610
|
-
[(0,
|
|
55869
|
+
[(0, import_node_path41.join)(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
|
|
55611
55870
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
55612
55871
|
);
|
|
55613
55872
|
} catch {
|
|
@@ -55647,8 +55906,8 @@ async function calibrate(cwd, options = {}) {
|
|
|
55647
55906
|
const negativeFiles = buildFileList(negativeDir, ["tsx", "ts"]);
|
|
55648
55907
|
const posSample = options.positiveLimit ? positiveFiles.slice(0, options.positiveLimit) : positiveFiles;
|
|
55649
55908
|
const negSample = options.negativeLimit ? negativeFiles.slice(0, options.negativeLimit) : negativeFiles;
|
|
55650
|
-
const posListPath = (0,
|
|
55651
|
-
const negListPath = (0,
|
|
55909
|
+
const posListPath = (0, import_node_path41.join)("/tmp", `cal-pos-${Date.now()}.txt`);
|
|
55910
|
+
const negListPath = (0, import_node_path41.join)("/tmp", `cal-neg-${Date.now()}.txt`);
|
|
55652
55911
|
(0, import_node_fs37.writeFileSync)(posListPath, posSample.join("\n"));
|
|
55653
55912
|
(0, import_node_fs37.writeFileSync)(negListPath, negSample.join("\n"));
|
|
55654
55913
|
const builtins = await Promise.resolve().then(() => (init_builtins(), builtins_exports));
|
|
@@ -55917,7 +56176,7 @@ function errorResponse(id, code, message, data2) {
|
|
|
55917
56176
|
return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
|
|
55918
56177
|
}
|
|
55919
56178
|
async function runMcpServer(input, output, cwd) {
|
|
55920
|
-
return new Promise((
|
|
56179
|
+
return new Promise((resolve20) => {
|
|
55921
56180
|
let buffer = "";
|
|
55922
56181
|
input.setEncoding("utf-8");
|
|
55923
56182
|
input.on("data", (chunk) => {
|
|
@@ -55955,138 +56214,13 @@ async function runMcpServer(input, output, cwd) {
|
|
|
55955
56214
|
nlIdx = buffer.indexOf("\n");
|
|
55956
56215
|
}
|
|
55957
56216
|
});
|
|
55958
|
-
input.on("end", () =>
|
|
55959
|
-
input.on("close", () =>
|
|
56217
|
+
input.on("end", () => resolve20());
|
|
56218
|
+
input.on("close", () => resolve20());
|
|
55960
56219
|
});
|
|
55961
56220
|
}
|
|
55962
56221
|
|
|
55963
|
-
// src/snippet/data.ts
|
|
55964
|
-
var CATEGORY_DIRECTIVES = {
|
|
55965
|
-
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.',
|
|
55966
|
-
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.",
|
|
55967
|
-
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.',
|
|
55968
|
-
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".',
|
|
55969
|
-
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.",
|
|
55970
|
-
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
55971
|
-
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].",
|
|
55972
|
-
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
55973
|
-
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.",
|
|
55974
|
-
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'."
|
|
55975
|
-
};
|
|
55976
|
-
var RULE_HINTS = {
|
|
55977
|
-
// v0.16.0 hygiene: 35 out-of-scope orphan hints (keys with no matching
|
|
55978
|
-
// rule in src/rules/builtins.ts) were moved out of this map. The
|
|
55979
|
-
// verbatim source text is preserved in
|
|
55980
|
-
// docs/research/backlog-rule-hints.md
|
|
55981
|
-
// so future implementers can paste a hint back when the corresponding
|
|
55982
|
-
// rule ships. The 5 in-scope orphans (`security/eval`,
|
|
55983
|
-
// `security/localstorage-token`, `security/target-blank-no-noopener`,
|
|
55984
|
-
// `wcag/missing-alt`, `typo/placeholder-text`) are kept here for v0.16.0.
|
|
55985
|
-
"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.",
|
|
55986
|
-
"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.",
|
|
55987
|
-
"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.",
|
|
55988
|
-
"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.",
|
|
55989
|
-
"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.",
|
|
55990
|
-
"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.",
|
|
55991
|
-
"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.',
|
|
55992
|
-
"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.",
|
|
55993
|
-
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
55994
|
-
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
55995
|
-
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
55996
|
-
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
55997
|
-
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
55998
|
-
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
55999
|
-
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
56000
|
-
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
56001
|
-
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
56002
|
-
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
56003
|
-
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
56004
|
-
"arch/astro-island-leak": "For Astro: server-render everything by default. Only opt-in to client islands when interactivity is needed.",
|
|
56005
|
-
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
56006
|
-
"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.",
|
|
56007
|
-
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
56008
|
-
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
56009
|
-
"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.",
|
|
56010
|
-
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
56011
|
-
"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.",
|
|
56012
|
-
"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.",
|
|
56013
|
-
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
56014
|
-
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
56015
|
-
"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*.)",
|
|
56016
|
-
"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.)",
|
|
56017
|
-
"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.)",
|
|
56018
|
-
"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.)",
|
|
56019
|
-
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
56020
|
-
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
56021
|
-
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
56022
|
-
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
56023
|
-
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
56024
|
-
"logic/qwik-hook-leak": "Use Qwik primitives ($state, $effect, useSignal) instead of React hooks (useState, useEffect).",
|
|
56025
|
-
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
56026
|
-
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
56027
|
-
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
56028
|
-
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
56029
|
-
"perf/halstead-anomaly": "Introduce domain-specific identifiers and varied operations. Low vocabulary per line is a strong AI signature (Halstead 1977 \xA73).",
|
|
56030
|
-
"typo/calc-fontsize": "Use a design token (`var(--font-size-lg)`) or `clamp(min, fluid, max)` for responsive typography.",
|
|
56031
|
-
"typo/calc-raw-px": "Replace px values in calc() with rem or em units for scalable layout.",
|
|
56032
|
-
"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.",
|
|
56033
|
-
"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.',
|
|
56034
|
-
"typo/math-cta-vocabulary": 'Use domain-specific action verbs ("Reserve", "Confirm ride", "Activate card") instead of falling back on the AI-default CTA vocabulary.',
|
|
56035
|
-
"visual/clamp-soup": "Use design-system aliases (`--text-fluid-sm`, `--text-fluid-lg`) with bounded ranges (typically 2\xD7 max).",
|
|
56036
|
-
"visual/generic-centering": "Vary hero layouts: some as grids (`grid place-items-center`), some as blocks, some with different alignment.",
|
|
56037
|
-
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
56038
|
-
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
56039
|
-
"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.",
|
|
56040
|
-
"visual/math-gradient-hue-rotation": "Use wider hue spans across gradients (e.g. blue\u2192amber, emerald\u2192indigo) to break the violet-fuchsia monotony.",
|
|
56041
|
-
"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.",
|
|
56042
|
-
"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.",
|
|
56043
|
-
"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%.",
|
|
56044
|
-
"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.",
|
|
56045
|
-
"wcag/dragging-movements": "Provide an onClick, onKeyDown, or button role as an alternative to dragging (WCAG 2.1.1).",
|
|
56046
|
-
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
56047
|
-
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
56048
|
-
"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.",
|
|
56049
|
-
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
56050
|
-
"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.",
|
|
56051
|
-
"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.",
|
|
56052
|
-
"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`).",
|
|
56053
|
-
"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.",
|
|
56054
|
-
"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.",
|
|
56055
|
-
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
56056
|
-
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
56057
|
-
"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).",
|
|
56058
|
-
"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).",
|
|
56059
|
-
"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).",
|
|
56060
|
-
"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).",
|
|
56061
|
-
"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).",
|
|
56062
|
-
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
56063
|
-
"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).",
|
|
56064
|
-
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
56065
|
-
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
56066
|
-
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
56067
|
-
// reclassified post-v7 calibration in v0.14.5d)
|
|
56068
|
-
"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.",
|
|
56069
|
-
"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.",
|
|
56070
|
-
"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.",
|
|
56071
|
-
"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.",
|
|
56072
|
-
"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.",
|
|
56073
|
-
"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.",
|
|
56074
|
-
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
56075
|
-
"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`).",
|
|
56076
|
-
"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.",
|
|
56077
|
-
"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.",
|
|
56078
|
-
"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.",
|
|
56079
|
-
"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.",
|
|
56080
|
-
"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.',
|
|
56081
|
-
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
56082
|
-
"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.",
|
|
56083
|
-
"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.",
|
|
56084
|
-
"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.",
|
|
56085
|
-
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy."
|
|
56086
|
-
};
|
|
56087
|
-
|
|
56088
56222
|
// src/snippet/targets.ts
|
|
56089
|
-
var
|
|
56223
|
+
var import_node_path43 = require("path");
|
|
56090
56224
|
|
|
56091
56225
|
// src/snippet/render.ts
|
|
56092
56226
|
function aiSpecificRules(rules) {
|
|
@@ -56317,7 +56451,7 @@ var SNIPPET_TARGETS = [
|
|
|
56317
56451
|
}
|
|
56318
56452
|
];
|
|
56319
56453
|
function resolveTargetPath(target) {
|
|
56320
|
-
return target.isFolder ? (0,
|
|
56454
|
+
return target.isFolder ? (0, import_node_path43.join)(target.path, target.filename) : target.path;
|
|
56321
56455
|
}
|
|
56322
56456
|
function renderMatrix() {
|
|
56323
56457
|
const lines = [];
|
|
@@ -56331,52 +56465,6 @@ function renderMatrix() {
|
|
|
56331
56465
|
return lines.join("\n");
|
|
56332
56466
|
}
|
|
56333
56467
|
|
|
56334
|
-
// src/cli/explain.ts
|
|
56335
|
-
var RULES_BASE_URL2 = "https://github.com/Dystx/slopbrick/blob/main/src/rules";
|
|
56336
|
-
function ruleIdToFilename2(ruleId) {
|
|
56337
|
-
const slash = ruleId.indexOf("/");
|
|
56338
|
-
return slash === -1 ? ruleId : ruleId.slice(slash + 1);
|
|
56339
|
-
}
|
|
56340
|
-
function explainRule2(ruleId, rules, ruleHints) {
|
|
56341
|
-
const rule = rules.find((r) => r.id === ruleId);
|
|
56342
|
-
if (!rule) {
|
|
56343
|
-
return { error: "Unknown rule: " + ruleId + ". Run `slopbrick rules` to see all available rules." };
|
|
56344
|
-
}
|
|
56345
|
-
const filename = ruleIdToFilename2(rule.id);
|
|
56346
|
-
return {
|
|
56347
|
-
ruleId: rule.id,
|
|
56348
|
-
category: rule.category,
|
|
56349
|
-
severity: rule.severity,
|
|
56350
|
-
aiSpecific: rule.aiSpecific,
|
|
56351
|
-
pattern: ruleHints[rule.id] ?? "Patterns flagged by " + rule.id + ".",
|
|
56352
|
-
remediation: "See the rule source for the canonical before/after: src/rules/" + rule.category + "/" + filename + ".ts",
|
|
56353
|
-
sourcePath: "src/rules/" + rule.category + "/" + filename + ".ts",
|
|
56354
|
-
helpUri: `${RULES_BASE_URL2}/${rule.category}/${filename}.ts`,
|
|
56355
|
-
suppressionSnippet: 'rules: { "' + rule.id + '": "off" } // or set to a lower severity'
|
|
56356
|
-
};
|
|
56357
|
-
}
|
|
56358
|
-
function formatExplain(result) {
|
|
56359
|
-
if ("error" in result) return result.error;
|
|
56360
|
-
const lines = [];
|
|
56361
|
-
lines.push("Rule: " + result.ruleId);
|
|
56362
|
-
lines.push("Category: " + result.category);
|
|
56363
|
-
lines.push("Severity: " + result.severity);
|
|
56364
|
-
lines.push("AI-specific: " + (result.aiSpecific ? "yes (designed to fire on AI tells)" : "no (cross-cutting quality rule)"));
|
|
56365
|
-
lines.push("Source: " + result.sourcePath);
|
|
56366
|
-
lines.push("Help: " + result.helpUri);
|
|
56367
|
-
lines.push("");
|
|
56368
|
-
lines.push("Pattern:");
|
|
56369
|
-
lines.push(" " + result.pattern);
|
|
56370
|
-
lines.push("");
|
|
56371
|
-
lines.push("Remediation:");
|
|
56372
|
-
lines.push(" " + result.remediation);
|
|
56373
|
-
lines.push("");
|
|
56374
|
-
lines.push("Suppress / configure in slopbrick.config.mjs:");
|
|
56375
|
-
lines.push(" " + result.suppressionSnippet);
|
|
56376
|
-
lines.push("");
|
|
56377
|
-
return lines.join("\n");
|
|
56378
|
-
}
|
|
56379
|
-
|
|
56380
56468
|
// src/cli/program.ts
|
|
56381
56469
|
init_validation();
|
|
56382
56470
|
init_signal_strength2();
|
|
@@ -56534,13 +56622,12 @@ function formatMarkdown3(report) {
|
|
|
56534
56622
|
}
|
|
56535
56623
|
|
|
56536
56624
|
// src/cli/program.ts
|
|
56537
|
-
init_advice();
|
|
56538
56625
|
init_unified_diff();
|
|
56539
56626
|
init_heatmap();
|
|
56540
56627
|
|
|
56541
56628
|
// src/report/flywheel.ts
|
|
56542
56629
|
var import_node_fs39 = require("fs");
|
|
56543
|
-
var
|
|
56630
|
+
var import_node_path44 = require("path");
|
|
56544
56631
|
function average(values) {
|
|
56545
56632
|
if (values.length === 0) return 0;
|
|
56546
56633
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
@@ -57070,8 +57157,8 @@ async function runCli({ start }) {
|
|
|
57070
57157
|
logger.info(renderMatrix());
|
|
57071
57158
|
process.exit(0);
|
|
57072
57159
|
}
|
|
57073
|
-
const cwd = (0,
|
|
57074
|
-
const configPath = (0,
|
|
57160
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57161
|
+
const configPath = (0, import_node_path46.join)(cwd, "slopbrick.config.mjs");
|
|
57075
57162
|
const detected = detectStack(cwd);
|
|
57076
57163
|
const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
|
|
57077
57164
|
const proposed = serializeConfig(fallbackConfig);
|
|
@@ -57127,8 +57214,8 @@ async function runCli({ start }) {
|
|
|
57127
57214
|
return Boolean(opts[t.flag]);
|
|
57128
57215
|
});
|
|
57129
57216
|
for (const target of targetsToWrite) {
|
|
57130
|
-
const snippetPath = (0,
|
|
57131
|
-
(0, import_node_fs45.mkdirSync)((0,
|
|
57217
|
+
const snippetPath = (0, import_node_path46.join)(cwd, resolveTargetPath(target));
|
|
57218
|
+
(0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(snippetPath), { recursive: true });
|
|
57132
57219
|
const generated = target.generator(builtinRules);
|
|
57133
57220
|
if (!target.isFolder && (0, import_node_fs45.existsSync)(snippetPath)) {
|
|
57134
57221
|
const existing = (0, import_node_fs45.readFileSync)(snippetPath, "utf8");
|
|
@@ -57166,7 +57253,7 @@ async function runCli({ start }) {
|
|
|
57166
57253
|
});
|
|
57167
57254
|
program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
57168
57255
|
const options = command.optsWithGlobals();
|
|
57169
|
-
const cwd = (0,
|
|
57256
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57170
57257
|
const root = getGitRoot(cwd);
|
|
57171
57258
|
if (!root) {
|
|
57172
57259
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -57180,7 +57267,7 @@ async function runCli({ start }) {
|
|
|
57180
57267
|
});
|
|
57181
57268
|
program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
57182
57269
|
const options = command.optsWithGlobals();
|
|
57183
|
-
const cwd = (0,
|
|
57270
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57184
57271
|
const root = getGitRoot(cwd);
|
|
57185
57272
|
if (!root) {
|
|
57186
57273
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -57192,34 +57279,12 @@ async function runCli({ start }) {
|
|
|
57192
57279
|
}
|
|
57193
57280
|
process.exit(result.exitCode);
|
|
57194
57281
|
});
|
|
57195
|
-
program
|
|
57196
|
-
|
|
57197
|
-
|
|
57198
|
-
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
57199
|
-
const health = loadHealth2(cwd);
|
|
57200
|
-
if (health) {
|
|
57201
|
-
const synthetic = {
|
|
57202
|
-
slopIndex: 100 - health.repositoryHealth
|
|
57203
|
-
};
|
|
57204
|
-
logger.info(formatBadge(synthetic));
|
|
57205
|
-
process.exit(0);
|
|
57206
|
-
}
|
|
57207
|
-
const { report } = await runScan(options);
|
|
57208
|
-
logger.info(formatBadge(report));
|
|
57209
|
-
process.exit(0);
|
|
57210
|
-
});
|
|
57211
|
-
program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
|
|
57212
|
-
const options = command.optsWithGlobals();
|
|
57213
|
-
const { report } = await runScan(options);
|
|
57214
|
-
const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
|
|
57215
|
-
logger.info(formatAdvice(report));
|
|
57216
|
-
const diff = formatUnifiedDiff(report, cwd);
|
|
57217
|
-
if (diff) logger.info(diff);
|
|
57218
|
-
process.exit(0);
|
|
57219
|
-
});
|
|
57282
|
+
registerBadge(program);
|
|
57283
|
+
registerSuggest(program);
|
|
57284
|
+
registerExplain(program);
|
|
57220
57285
|
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) => {
|
|
57221
57286
|
const options = command.optsWithGlobals();
|
|
57222
|
-
const cwd = (0,
|
|
57287
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57223
57288
|
const payloads = readTelemetry(cwd);
|
|
57224
57289
|
if (payloads.length === 0) {
|
|
57225
57290
|
logger.info("No flywheel telemetry found. Run a scan first.");
|
|
@@ -57227,8 +57292,8 @@ async function runCli({ start }) {
|
|
|
57227
57292
|
}
|
|
57228
57293
|
const summary = summarizeTelemetry(payloads);
|
|
57229
57294
|
if (cmdOptions.export) {
|
|
57230
|
-
const exportPath = (0,
|
|
57231
|
-
(0, import_node_fs45.mkdirSync)((0,
|
|
57295
|
+
const exportPath = (0, import_node_path46.resolve)(cmdOptions.export);
|
|
57296
|
+
(0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(exportPath), { recursive: true });
|
|
57232
57297
|
(0, import_node_fs45.writeFileSync)(exportPath, JSON.stringify(summary, null, 2), "utf-8");
|
|
57233
57298
|
logger.info(`Wrote flywheel summary to ${exportPath}`);
|
|
57234
57299
|
process.exit(0);
|
|
@@ -57252,7 +57317,7 @@ async function runCli({ start }) {
|
|
|
57252
57317
|
logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
|
|
57253
57318
|
process.exit(2);
|
|
57254
57319
|
}
|
|
57255
|
-
const cwd = (0,
|
|
57320
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57256
57321
|
if (options.trend !== void 0) {
|
|
57257
57322
|
const runs = await readRuns(cwd, fsMemoryIO);
|
|
57258
57323
|
if (runs.length === 0) {
|
|
@@ -57285,7 +57350,7 @@ async function runCli({ start }) {
|
|
|
57285
57350
|
const scanElapsed = Math.round(import_node_perf_hooks.performance.now() - scanStart);
|
|
57286
57351
|
const totalElapsed = Math.round(import_node_perf_hooks.performance.now() - start);
|
|
57287
57352
|
if (options.baseline) {
|
|
57288
|
-
const cwd2 = (0,
|
|
57353
|
+
const cwd2 = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57289
57354
|
const configHash = hashConfig(config);
|
|
57290
57355
|
const gitHead = await getGitHead(cwd2) ?? "unknown";
|
|
57291
57356
|
const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
|
|
@@ -57379,14 +57444,14 @@ async function runCli({ start }) {
|
|
|
57379
57444
|
framework: cmdOptions.framework,
|
|
57380
57445
|
componentType: cmdOptions.componentType,
|
|
57381
57446
|
provider,
|
|
57382
|
-
outputDir: (0,
|
|
57447
|
+
outputDir: (0, import_node_path46.resolve)(cmdOptions.outputDir),
|
|
57383
57448
|
temperature: cmdOptions.temperature
|
|
57384
57449
|
});
|
|
57385
57450
|
logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
|
|
57386
57451
|
});
|
|
57387
57452
|
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) => {
|
|
57388
57453
|
try {
|
|
57389
|
-
const metadataPath = (0,
|
|
57454
|
+
const metadataPath = (0, import_node_path46.resolve)(cmdOptions.inputDir, "metadata.json");
|
|
57390
57455
|
if (!(0, import_node_fs45.existsSync)(metadataPath)) {
|
|
57391
57456
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
57392
57457
|
process.exit(2);
|
|
@@ -57394,8 +57459,8 @@ async function runCli({ start }) {
|
|
|
57394
57459
|
const samples = JSON.parse((0, import_node_fs45.readFileSync)(metadataPath, "utf8"));
|
|
57395
57460
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
57396
57461
|
const analysis = await analyzeSamples(samples, config);
|
|
57397
|
-
const outputPath = (0,
|
|
57398
|
-
(0, import_node_fs45.mkdirSync)((0,
|
|
57462
|
+
const outputPath = (0, import_node_path46.resolve)(cmdOptions.output);
|
|
57463
|
+
(0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(outputPath), { recursive: true });
|
|
57399
57464
|
(0, import_node_fs45.writeFileSync)(outputPath, JSON.stringify(analysis, null, 2), "utf8");
|
|
57400
57465
|
logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
|
|
57401
57466
|
logger.info(`Wrote analysis to ${outputPath}`);
|
|
@@ -57406,7 +57471,7 @@ async function runCli({ start }) {
|
|
|
57406
57471
|
});
|
|
57407
57472
|
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) => {
|
|
57408
57473
|
try {
|
|
57409
|
-
const metadataPath = (0,
|
|
57474
|
+
const metadataPath = (0, import_node_path46.resolve)(cmdOptions.inputDir, "metadata.json");
|
|
57410
57475
|
if (!(0, import_node_fs45.existsSync)(metadataPath)) {
|
|
57411
57476
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
57412
57477
|
process.exit(2);
|
|
@@ -57421,8 +57486,8 @@ async function runCli({ start }) {
|
|
|
57421
57486
|
const candidates = clustersToCandidates(extraction.clusters, {
|
|
57422
57487
|
minFrequency: cmdOptions.minFrequency
|
|
57423
57488
|
});
|
|
57424
|
-
const outputPath = (0,
|
|
57425
|
-
(0, import_node_fs45.mkdirSync)((0,
|
|
57489
|
+
const outputPath = (0, import_node_path46.resolve)(cmdOptions.output);
|
|
57490
|
+
(0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(outputPath), { recursive: true });
|
|
57426
57491
|
const payload = {
|
|
57427
57492
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
57428
57493
|
sampleCount: analysis.summary.total,
|
|
@@ -57447,8 +57512,8 @@ async function runCli({ start }) {
|
|
|
57447
57512
|
positiveLimit: cmdOptions.positiveLimit,
|
|
57448
57513
|
negativeLimit: cmdOptions.negativeLimit
|
|
57449
57514
|
});
|
|
57450
|
-
const outputPath = cmdOptions.output ? (0,
|
|
57451
|
-
(0, import_node_fs45.mkdirSync)((0,
|
|
57515
|
+
const outputPath = cmdOptions.output ? (0, import_node_path46.resolve)(cwd, cmdOptions.output) : (0, import_node_path46.resolve)(cwd, "corpus", "calibration-empirical.md");
|
|
57516
|
+
(0, import_node_fs45.mkdirSync)((0, import_node_path46.dirname)(outputPath), { recursive: true });
|
|
57452
57517
|
(0, import_node_fs45.writeFileSync)(outputPath, reportToMarkdown(report), "utf8");
|
|
57453
57518
|
logger.info(
|
|
57454
57519
|
"Calibrated " + report.rules.length + " rules across " + report.positiveFileCount + " positive + " + report.negativeFileCount + " negative files."
|
|
@@ -57487,7 +57552,7 @@ async function runCli({ start }) {
|
|
|
57487
57552
|
const options = command.optsWithGlobals();
|
|
57488
57553
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
57489
57554
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
57490
|
-
const cwd = (0,
|
|
57555
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57491
57556
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57492
57557
|
const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
|
|
57493
57558
|
logger.info(formatDrift(result, { json: format === "json" }));
|
|
@@ -57504,7 +57569,7 @@ async function runCli({ start }) {
|
|
|
57504
57569
|
async (cmdOptions, command) => {
|
|
57505
57570
|
try {
|
|
57506
57571
|
const options = command.optsWithGlobals();
|
|
57507
|
-
const cwd = (0,
|
|
57572
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57508
57573
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57509
57574
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57510
57575
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
@@ -57531,7 +57596,7 @@ async function runCli({ start }) {
|
|
|
57531
57596
|
const options = command.optsWithGlobals();
|
|
57532
57597
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
57533
57598
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
57534
|
-
const cwd = (0,
|
|
57599
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57535
57600
|
const { report } = await runScan({ ...options, workspace: cwd });
|
|
57536
57601
|
const securityIssues = report.issues.filter((i) => i.category === "security");
|
|
57537
57602
|
const { risk, findings } = computeAiSecurityRisk(securityIssues);
|
|
@@ -57582,7 +57647,7 @@ async function runCli({ start }) {
|
|
|
57582
57647
|
const options = command.optsWithGlobals();
|
|
57583
57648
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
57584
57649
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
57585
|
-
const cwd = (0,
|
|
57650
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57586
57651
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57587
57652
|
const { result } = await runTestScan(cwd, config, { strict: options.strict });
|
|
57588
57653
|
logger.info(formatTestReport(result, { json: format === "json" }));
|
|
@@ -57601,7 +57666,7 @@ async function runCli({ start }) {
|
|
|
57601
57666
|
const options = command.optsWithGlobals();
|
|
57602
57667
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
57603
57668
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
57604
|
-
const cwd = (0,
|
|
57669
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57605
57670
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57606
57671
|
const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
|
|
57607
57672
|
const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
|
|
@@ -57621,7 +57686,7 @@ async function runCli({ start }) {
|
|
|
57621
57686
|
const options = command.optsWithGlobals();
|
|
57622
57687
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57623
57688
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57624
|
-
const cwd = (0,
|
|
57689
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57625
57690
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57626
57691
|
const result = await runBusinessLogicScan(cwd, config, {
|
|
57627
57692
|
maxFiles: cmdOptions.maxFiles
|
|
@@ -57643,7 +57708,7 @@ async function runCli({ start }) {
|
|
|
57643
57708
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57644
57709
|
const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
|
|
57645
57710
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
57646
|
-
const cwd = (0,
|
|
57711
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57647
57712
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57648
57713
|
const result = await runMaintenanceCostScan(cwd, config, {
|
|
57649
57714
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -57666,7 +57731,7 @@ async function runCli({ start }) {
|
|
|
57666
57731
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57667
57732
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57668
57733
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
57669
|
-
const cwd = (0,
|
|
57734
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57670
57735
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57671
57736
|
const result = await runDocsScan(cwd, config, {
|
|
57672
57737
|
maxDocFiles: cmdOptions.maxFiles,
|
|
@@ -57694,7 +57759,7 @@ async function runCli({ start }) {
|
|
|
57694
57759
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57695
57760
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57696
57761
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
57697
|
-
const cwd = (0,
|
|
57762
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57698
57763
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57699
57764
|
const result = await runDbScan(cwd, config, {
|
|
57700
57765
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -57721,7 +57786,7 @@ async function runCli({ start }) {
|
|
|
57721
57786
|
const options = command.optsWithGlobals();
|
|
57722
57787
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
57723
57788
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
57724
|
-
const cwd = (0,
|
|
57789
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57725
57790
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
57726
57791
|
const result = await runPatternsScan(cwd, config, {
|
|
57727
57792
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -57752,14 +57817,14 @@ async function runCli({ start }) {
|
|
|
57752
57817
|
...rawGlobals,
|
|
57753
57818
|
noIncrease: rawGlobals.increase === false
|
|
57754
57819
|
};
|
|
57755
|
-
const cwd = (0,
|
|
57820
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57756
57821
|
const { watchProject: watchProject2 } = await Promise.resolve().then(() => (init_scan(), scan_exports));
|
|
57757
57822
|
await scanAction([], options, command);
|
|
57758
57823
|
await watchProject2(options, cwd, []);
|
|
57759
57824
|
});
|
|
57760
57825
|
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(
|
|
57761
57826
|
(cmdOptions) => {
|
|
57762
|
-
const cwd = (0,
|
|
57827
|
+
const cwd = (0, import_node_path46.resolve)(cmdOptions.workspace ?? process.cwd());
|
|
57763
57828
|
const { installHook: installHook2, uninstallHook: uninstallHook2 } = (init_installer(), __toCommonJS(installer_exports));
|
|
57764
57829
|
if (cmdOptions.uninstall) {
|
|
57765
57830
|
const result2 = uninstallHook2(cwd);
|
|
@@ -57789,7 +57854,7 @@ async function runCli({ start }) {
|
|
|
57789
57854
|
// scan only changed files
|
|
57790
57855
|
format: cmdOptions.format ?? "json"
|
|
57791
57856
|
};
|
|
57792
|
-
const cwd = (0,
|
|
57857
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
57793
57858
|
await scanAction([], options, command);
|
|
57794
57859
|
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
57795
57860
|
const health = loadHealth2(cwd);
|
|
@@ -57817,7 +57882,7 @@ async function runCli({ start }) {
|
|
|
57817
57882
|
);
|
|
57818
57883
|
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(
|
|
57819
57884
|
async (cmdOptions) => {
|
|
57820
|
-
const cwd = (0,
|
|
57885
|
+
const cwd = (0, import_node_path46.resolve)(cmdOptions.workspace ?? process.cwd());
|
|
57821
57886
|
const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
|
|
57822
57887
|
const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
57823
57888
|
if (cmdOptions.regenerate) {
|
|
@@ -57850,7 +57915,7 @@ async function runCli({ start }) {
|
|
|
57850
57915
|
(cmdOptions, command) => {
|
|
57851
57916
|
const globals = command.optsWithGlobals();
|
|
57852
57917
|
const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
|
|
57853
|
-
const cwd = (0,
|
|
57918
|
+
const cwd = (0, import_node_path46.resolve)(cmdOptions.workspace ?? process.cwd());
|
|
57854
57919
|
const { runMigrate: runMigrate2, formatMigrate: formatMigrate2 } = (init_migrate(), __toCommonJS(migrate_exports));
|
|
57855
57920
|
const result = runMigrate2({
|
|
57856
57921
|
workspace: cwd,
|
|
@@ -57945,19 +58010,14 @@ async function runCli({ start }) {
|
|
|
57945
58010
|
}
|
|
57946
58011
|
logger.info(lines.join("\n"));
|
|
57947
58012
|
});
|
|
57948
|
-
program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
|
|
57949
|
-
const result = explainRule2(ruleId, builtinRules, RULE_HINTS);
|
|
57950
|
-
logger.info(formatExplain(result));
|
|
57951
|
-
if ("error" in result) process.exit(2);
|
|
57952
|
-
});
|
|
57953
58013
|
program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
|
|
57954
|
-
const path = configPath ? (0,
|
|
58014
|
+
const path = configPath ? (0, import_node_path46.resolve)(configPath) : (0, import_node_path46.resolve)(process.cwd(), "slopbrick.config.mjs");
|
|
57955
58015
|
if (!(0, import_node_fs45.existsSync)(path)) {
|
|
57956
58016
|
logger.error(`Error: config file not found: ${path}`);
|
|
57957
58017
|
process.exit(2);
|
|
57958
58018
|
}
|
|
57959
58019
|
try {
|
|
57960
|
-
const mod = (0,
|
|
58020
|
+
const mod = (0, import_node_path46.extname)(path) === ".cjs" ? require(path) : await import(path);
|
|
57961
58021
|
const userConfig = mod.default ?? mod;
|
|
57962
58022
|
const result = validateConfig(userConfig);
|
|
57963
58023
|
if (result.errors.length === 0) {
|