slopbrick 0.39.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +665 -423
- package/dist/index.d.cts +63 -1
- package/dist/index.d.ts +63 -1
- package/dist/index.js +663 -421
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -36,7 +36,7 @@ var VERSION;
|
|
|
36
36
|
var init_header = __esm({
|
|
37
37
|
"src/types/_header.ts"() {
|
|
38
38
|
"use strict";
|
|
39
|
-
VERSION = "0.
|
|
39
|
+
VERSION = "0.41.0";
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -45823,6 +45823,15 @@ function formatCompositeScore(report) {
|
|
|
45823
45823
|
)
|
|
45824
45824
|
);
|
|
45825
45825
|
}
|
|
45826
|
+
const composite = report.compositeScore;
|
|
45827
|
+
if (composite !== void 0) {
|
|
45828
|
+
lines.push("");
|
|
45829
|
+
lines.push(
|
|
45830
|
+
import_chalk.default.dim(
|
|
45831
|
+
`composite=${composite.tier}@${composite.mean.toFixed(2)} \u2014 project-level Bayesian aggregate across ${composite.fileCount} file${composite.fileCount === 1 ? "" : "s"} (max ${composite.max.toFixed(2)}); informational, does not gate CI.`
|
|
45832
|
+
)
|
|
45833
|
+
);
|
|
45834
|
+
}
|
|
45826
45835
|
return lines.join("\n");
|
|
45827
45836
|
}
|
|
45828
45837
|
function formatCoherenceScores(report) {
|
|
@@ -50764,6 +50773,12 @@ function severityBump(severity) {
|
|
|
50764
50773
|
const idx = order.indexOf(severity);
|
|
50765
50774
|
return order[Math.min(idx + 1, order.length - 1)];
|
|
50766
50775
|
}
|
|
50776
|
+
function severityRelax(severity) {
|
|
50777
|
+
const order = ["off", "low", "medium", "high"];
|
|
50778
|
+
const idx = order.indexOf(severity);
|
|
50779
|
+
if (idx <= 0) return "off";
|
|
50780
|
+
return order[idx - 1];
|
|
50781
|
+
}
|
|
50767
50782
|
function resolveEffectiveSeverity(override, defaultSeverity) {
|
|
50768
50783
|
if (override === void 0 || override === "off" || override === "auto") {
|
|
50769
50784
|
return defaultSeverity;
|
|
@@ -50794,6 +50809,7 @@ function countConsecutiveTopFileAppearances(currentHash, recentTopHashes) {
|
|
|
50794
50809
|
}
|
|
50795
50810
|
function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatchedStringLiterals, config, rules) {
|
|
50796
50811
|
const autoTuned = [];
|
|
50812
|
+
const autoRelaxed = [];
|
|
50797
50813
|
const hotspotIssues = [];
|
|
50798
50814
|
const suggestions = [];
|
|
50799
50815
|
const ruleById = new Map(rules.map((r) => [r.id, r]));
|
|
@@ -50814,6 +50830,29 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
|
|
|
50814
50830
|
}
|
|
50815
50831
|
}
|
|
50816
50832
|
}
|
|
50833
|
+
if (runs.length >= IGNORE_THRESHOLD) {
|
|
50834
|
+
const window = runs.slice(-IGNORE_THRESHOLD);
|
|
50835
|
+
const candidateRuleIds = new Set(window.flatMap((r) => r.topOffenseIds));
|
|
50836
|
+
for (const ruleId of candidateRuleIds) {
|
|
50837
|
+
const rule = ruleById.get(ruleId);
|
|
50838
|
+
const defaultSeverity = rule?.severity ?? "medium";
|
|
50839
|
+
const currentSeverity = resolveEffectiveSeverity(
|
|
50840
|
+
config.rules[ruleId] ?? "auto",
|
|
50841
|
+
defaultSeverity
|
|
50842
|
+
);
|
|
50843
|
+
const streak = countConsecutiveTopOffenses(runs, ruleId);
|
|
50844
|
+
if (streak >= IGNORE_THRESHOLD) {
|
|
50845
|
+
autoRelaxed.push({
|
|
50846
|
+
ruleId,
|
|
50847
|
+
severity: severityRelax(currentSeverity),
|
|
50848
|
+
previousSeverity: currentSeverity,
|
|
50849
|
+
reason: `In top-3 offenses for ${streak} consecutive scans; corpus prior ${DEFAULT_RELAXATION_PRIOR.toFixed(2)} suggests the rule is high-FP for this corpus slice.`,
|
|
50850
|
+
defaultPrior: DEFAULT_RELAXATION_PRIOR,
|
|
50851
|
+
relaxedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
50852
|
+
});
|
|
50853
|
+
}
|
|
50854
|
+
}
|
|
50855
|
+
}
|
|
50817
50856
|
if (currentTopFiles.length > 0 && runs.length >= CONSECUTIVE_THRESHOLD) {
|
|
50818
50857
|
for (const file of currentTopFiles) {
|
|
50819
50858
|
const consecutive = countConsecutiveTopFileAppearances(file.hash, recentTopHashes);
|
|
@@ -50848,13 +50887,14 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
|
|
|
50848
50887
|
});
|
|
50849
50888
|
}
|
|
50850
50889
|
}
|
|
50851
|
-
return { autoTuned, hotspotIssues, suggestions };
|
|
50890
|
+
return { autoTuned, autoRelaxed, hotspotIssues, suggestions };
|
|
50852
50891
|
}
|
|
50853
50892
|
function migrateFlywheelState(state) {
|
|
50854
50893
|
return {
|
|
50855
50894
|
version: FLYWHEEL_VERSION,
|
|
50856
50895
|
updatedAt: state.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
50857
50896
|
autoTuned: state.autoTuned ?? [],
|
|
50897
|
+
autoRelaxed: state.autoRelaxed ?? [],
|
|
50858
50898
|
research: state.research
|
|
50859
50899
|
};
|
|
50860
50900
|
}
|
|
@@ -50910,7 +50950,7 @@ function saveFlywheelState(cwd, state) {
|
|
|
50910
50950
|
function hashFile(filePath) {
|
|
50911
50951
|
return (0, import_node_crypto6.createHash)("sha256").update(filePath).digest("hex").slice(0, 16);
|
|
50912
50952
|
}
|
|
50913
|
-
var import_node_crypto6, import_node_fs14, import_node_path15, FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, FLYWHEEL_VERSION;
|
|
50953
|
+
var import_node_crypto6, import_node_fs14, import_node_path15, FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, IGNORE_THRESHOLD, DEFAULT_RELAXATION_PRIOR, FLYWHEEL_VERSION;
|
|
50914
50954
|
var init_flywheel = __esm({
|
|
50915
50955
|
"src/engine/flywheel.ts"() {
|
|
50916
50956
|
"use strict";
|
|
@@ -50920,7 +50960,9 @@ var init_flywheel = __esm({
|
|
|
50920
50960
|
FLYWHEEL_DIR = ".slopbrick/flywheel";
|
|
50921
50961
|
STATE_FILE = "auto-tuned.json";
|
|
50922
50962
|
CONSECUTIVE_THRESHOLD = 3;
|
|
50923
|
-
|
|
50963
|
+
IGNORE_THRESHOLD = 5;
|
|
50964
|
+
DEFAULT_RELAXATION_PRIOR = 0.3;
|
|
50965
|
+
FLYWHEEL_VERSION = "3";
|
|
50924
50966
|
}
|
|
50925
50967
|
});
|
|
50926
50968
|
|
|
@@ -54205,6 +54247,21 @@ function safeRelative(cwd, filePath) {
|
|
|
54205
54247
|
return filePath;
|
|
54206
54248
|
}
|
|
54207
54249
|
}
|
|
54250
|
+
function buildInventorySummary(inventory) {
|
|
54251
|
+
const patternCounts = {};
|
|
54252
|
+
const patternNames = {};
|
|
54253
|
+
for (const [category, matches] of Object.entries(inventory.patterns)) {
|
|
54254
|
+
if (matches.length === 0) continue;
|
|
54255
|
+
const names = Array.from(new Set(matches.map((m) => m.name))).sort();
|
|
54256
|
+
patternCounts[category] = names.length;
|
|
54257
|
+
patternNames[category] = names.slice(0, TELEMETRY_INVENTORY_NAME_CAP);
|
|
54258
|
+
}
|
|
54259
|
+
return {
|
|
54260
|
+
scannedFiles: inventory.scannedFiles,
|
|
54261
|
+
patternCounts,
|
|
54262
|
+
patternNames
|
|
54263
|
+
};
|
|
54264
|
+
}
|
|
54208
54265
|
function aggregateViolations(report) {
|
|
54209
54266
|
const counts = /* @__PURE__ */ new Map();
|
|
54210
54267
|
for (const issue of report.issues) {
|
|
@@ -54284,7 +54341,7 @@ function readTelemetry(cwd) {
|
|
|
54284
54341
|
}
|
|
54285
54342
|
return payloads;
|
|
54286
54343
|
}
|
|
54287
|
-
function recordTelemetry(cwd, report, results, config) {
|
|
54344
|
+
function recordTelemetry(cwd, report, results, config, inventory) {
|
|
54288
54345
|
if (config.telemetry === false) {
|
|
54289
54346
|
return void 0;
|
|
54290
54347
|
}
|
|
@@ -54305,7 +54362,14 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54305
54362
|
framework: config.framework
|
|
54306
54363
|
},
|
|
54307
54364
|
violations: aggregateViolations(report),
|
|
54308
|
-
files: buildFileRecords(cwd, report, results)
|
|
54365
|
+
files: buildFileRecords(cwd, report, results),
|
|
54366
|
+
// v0.41.0 (Sprint 2, task 2a.1): the inventory summary is
|
|
54367
|
+
// additive — omitting it (legacy callers) keeps the JSONL line
|
|
54368
|
+
// shape identical to v0.40.x payloads, so old readers stay
|
|
54369
|
+
// green. New readers (`slopbrick drift --since <date>` in
|
|
54370
|
+
// Sprint 2a.2) treat the field as optional and fall back to
|
|
54371
|
+
// a re-scan when it's missing.
|
|
54372
|
+
...inventory ? { inventory: buildInventorySummary(inventory) } : {}
|
|
54309
54373
|
};
|
|
54310
54374
|
const path = telemetryPath2(cwd);
|
|
54311
54375
|
const dir = (0, import_node_path22.dirname)(path);
|
|
@@ -54316,7 +54380,7 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54316
54380
|
(0, import_node_fs20.appendFileSync)(path, JSON.stringify(payload) + "\n", "utf-8");
|
|
54317
54381
|
return payload;
|
|
54318
54382
|
}
|
|
54319
|
-
var import_node_fs20, import_node_crypto8, import_node_path22, TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES;
|
|
54383
|
+
var import_node_fs20, import_node_crypto8, import_node_path22, TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES, TELEMETRY_INVENTORY_NAME_CAP;
|
|
54320
54384
|
var init_telemetry = __esm({
|
|
54321
54385
|
"src/engine/telemetry.ts"() {
|
|
54322
54386
|
"use strict";
|
|
@@ -54327,6 +54391,7 @@ var init_telemetry = __esm({
|
|
|
54327
54391
|
TELEMETRY_FILE2 = "scans.jsonl";
|
|
54328
54392
|
MAX_TELEMETRY_BYTES = 10 * 1024 * 1024;
|
|
54329
54393
|
MAX_ROTATED_FILES = 5;
|
|
54394
|
+
TELEMETRY_INVENTORY_NAME_CAP = 50;
|
|
54330
54395
|
}
|
|
54331
54396
|
});
|
|
54332
54397
|
|
|
@@ -54623,6 +54688,7 @@ async function persistRun(input) {
|
|
|
54623
54688
|
);
|
|
54624
54689
|
const state = loadFlywheelState(cwd);
|
|
54625
54690
|
state.autoTuned = flywheelOutput.autoTuned;
|
|
54691
|
+
state.autoRelaxed = flywheelOutput.autoRelaxed;
|
|
54626
54692
|
state.research = loadResearchMetricsFromDisk(cwd);
|
|
54627
54693
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
54628
54694
|
saveFlywheelState(cwd, state);
|
|
@@ -54639,11 +54705,11 @@ async function persistRun(input) {
|
|
|
54639
54705
|
}
|
|
54640
54706
|
report.issues.push(...flywheelOutput.hotspotIssues);
|
|
54641
54707
|
}
|
|
54642
|
-
|
|
54708
|
+
let patternInventory;
|
|
54643
54709
|
if (config.projectMemory !== false) {
|
|
54644
54710
|
try {
|
|
54645
54711
|
const durationMs = Date.now() - startTime;
|
|
54646
|
-
|
|
54712
|
+
patternInventory = await buildPatternInventory(cwd, config);
|
|
54647
54713
|
const inventory = buildInventoryFromScan(
|
|
54648
54714
|
{ cwd, results },
|
|
54649
54715
|
patternInventory,
|
|
@@ -54677,6 +54743,7 @@ async function persistRun(input) {
|
|
|
54677
54743
|
}
|
|
54678
54744
|
}
|
|
54679
54745
|
}
|
|
54746
|
+
recordTelemetry(cwd, report, results, config, patternInventory);
|
|
54680
54747
|
}
|
|
54681
54748
|
var import_node_fs22, import_node_path24;
|
|
54682
54749
|
var init_persistRun = __esm({
|
|
@@ -55058,6 +55125,7 @@ function formatSarif(report, options) {
|
|
|
55058
55125
|
const results = report.issues.map(
|
|
55059
55126
|
(issue) => buildResultFromIssue(issue, options?.cwd, fileContentCache)
|
|
55060
55127
|
);
|
|
55128
|
+
const driverProperties = report.compositeScore ? { compositeScore: report.compositeScore } : void 0;
|
|
55061
55129
|
const log = {
|
|
55062
55130
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
55063
55131
|
version: "2.1.0",
|
|
@@ -55068,7 +55136,8 @@ function formatSarif(report, options) {
|
|
|
55068
55136
|
name: "slopbrick",
|
|
55069
55137
|
version: report.version,
|
|
55070
55138
|
informationUri: REPO_INFORMATION_URI,
|
|
55071
|
-
rules
|
|
55139
|
+
rules,
|
|
55140
|
+
...driverProperties ? { properties: driverProperties } : {}
|
|
55072
55141
|
}
|
|
55073
55142
|
},
|
|
55074
55143
|
results
|
|
@@ -56903,6 +56972,13 @@ async function runScan(options, explicitPaths) {
|
|
|
56903
56972
|
if (defaultOffRules.has(tuned.ruleId)) continue;
|
|
56904
56973
|
config.rules[tuned.ruleId] = tuned.severity;
|
|
56905
56974
|
}
|
|
56975
|
+
for (const relaxed of flywheelState.autoRelaxed) {
|
|
56976
|
+
if (config.rules[relaxed.ruleId] === "off") {
|
|
56977
|
+
continue;
|
|
56978
|
+
}
|
|
56979
|
+
if (defaultOffRules.has(relaxed.ruleId)) continue;
|
|
56980
|
+
config.rules[relaxed.ruleId] = relaxed.severity;
|
|
56981
|
+
}
|
|
56906
56982
|
}
|
|
56907
56983
|
const pool = new WorkerPool({
|
|
56908
56984
|
config,
|
|
@@ -57055,402 +57131,6 @@ var init_scan2 = __esm({
|
|
|
57055
57131
|
}
|
|
57056
57132
|
});
|
|
57057
57133
|
|
|
57058
|
-
// src/mcp/slop-suggest-structure.ts
|
|
57059
|
-
async function runSuggestWithStructure(args, ctx) {
|
|
57060
|
-
const cached = await readStructureMarkdown(ctx.cwd);
|
|
57061
|
-
if (cached !== null) {
|
|
57062
|
-
return {
|
|
57063
|
-
content: [{ type: "text", text: cached }]
|
|
57064
|
-
};
|
|
57065
|
-
}
|
|
57066
|
-
const { handleToolCall: handleToolCall2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
|
|
57067
|
-
const result = await handleToolCall2("slop_suggest", args, ctx);
|
|
57068
|
-
if (result.isError) return result;
|
|
57069
|
-
try {
|
|
57070
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
57071
|
-
if (parsed !== null && typeof parsed === "object") {
|
|
57072
|
-
parsed.structureHint = STRUCTURE_NOT_FOUND_HINT;
|
|
57073
|
-
return {
|
|
57074
|
-
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }]
|
|
57075
|
-
};
|
|
57076
|
-
}
|
|
57077
|
-
} catch {
|
|
57078
|
-
}
|
|
57079
|
-
return result;
|
|
57080
|
-
}
|
|
57081
|
-
var STRUCTURE_NOT_FOUND_HINT;
|
|
57082
|
-
var init_slop_suggest_structure = __esm({
|
|
57083
|
-
"src/mcp/slop-suggest-structure.ts"() {
|
|
57084
|
-
"use strict";
|
|
57085
|
-
init_structure_md();
|
|
57086
|
-
STRUCTURE_NOT_FOUND_HINT = "No .slopbrick/structure.md found. Run `slopbrick scan` to persist the pattern inventory, then call this tool again for the O(read file) fast path.";
|
|
57087
|
-
}
|
|
57088
|
-
});
|
|
57089
|
-
|
|
57090
|
-
// src/mcp/tools.ts
|
|
57091
|
-
var tools_exports = {};
|
|
57092
|
-
__export(tools_exports, {
|
|
57093
|
-
TOOL_DEFINITIONS: () => TOOL_DEFINITIONS,
|
|
57094
|
-
canonicalToolNames: () => canonicalToolNames,
|
|
57095
|
-
getDeprecation: () => getDeprecation,
|
|
57096
|
-
handleToolCall: () => handleToolCall
|
|
57097
|
-
});
|
|
57098
|
-
function toolError(message) {
|
|
57099
|
-
return {
|
|
57100
|
-
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
57101
|
-
isError: true
|
|
57102
|
-
};
|
|
57103
|
-
}
|
|
57104
|
-
async function runScanFile(args, ctx) {
|
|
57105
|
-
const path = args.path;
|
|
57106
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57107
|
-
const result = await scanFile(path, ctx.config);
|
|
57108
|
-
const simplified = {
|
|
57109
|
-
filePath: result.filePath,
|
|
57110
|
-
componentCount: result.componentCount,
|
|
57111
|
-
parseError: result.parseError,
|
|
57112
|
-
issues: result.issues.map((i) => ({
|
|
57113
|
-
ruleId: i.ruleId,
|
|
57114
|
-
category: i.category,
|
|
57115
|
-
severity: i.severity,
|
|
57116
|
-
line: i.line,
|
|
57117
|
-
column: i.column,
|
|
57118
|
-
message: i.message,
|
|
57119
|
-
advice: i.advice
|
|
57120
|
-
}))
|
|
57121
|
-
};
|
|
57122
|
-
return {
|
|
57123
|
-
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
57124
|
-
};
|
|
57125
|
-
}
|
|
57126
|
-
function explainRule2(args, ctx) {
|
|
57127
|
-
const ruleId = args.ruleId;
|
|
57128
|
-
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
57129
|
-
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
57130
|
-
if (!rule) return toolError("Unknown rule: " + ruleId);
|
|
57131
|
-
const explanation = {
|
|
57132
|
-
ruleId: rule.id,
|
|
57133
|
-
category: rule.category,
|
|
57134
|
-
severity: rule.severity,
|
|
57135
|
-
aiSpecific: rule.aiSpecific,
|
|
57136
|
-
rationale: "This rule flags " + rule.category + " patterns associated with AI-generated code. It is marked as " + (rule.aiSpecific ? "AI-specific" : "cross-cutting") + ". Severity: " + rule.severity + ".",
|
|
57137
|
-
whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
|
|
57138
|
-
};
|
|
57139
|
-
return {
|
|
57140
|
-
content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
|
|
57141
|
-
};
|
|
57142
|
-
}
|
|
57143
|
-
function listRules(args, ctx) {
|
|
57144
|
-
const category = args.category;
|
|
57145
|
-
const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
|
|
57146
|
-
const rules = filtered.map((r) => ({
|
|
57147
|
-
id: r.id,
|
|
57148
|
-
category: r.category,
|
|
57149
|
-
severity: r.severity,
|
|
57150
|
-
aiSpecific: r.aiSpecific
|
|
57151
|
-
}));
|
|
57152
|
-
return {
|
|
57153
|
-
content: [
|
|
57154
|
-
{
|
|
57155
|
-
type: "text",
|
|
57156
|
-
text: JSON.stringify({ count: rules.length, rules }, null, 2)
|
|
57157
|
-
}
|
|
57158
|
-
]
|
|
57159
|
-
};
|
|
57160
|
-
}
|
|
57161
|
-
async function runSuggest(args, ctx) {
|
|
57162
|
-
const maxFilesRaw = args.maxFiles;
|
|
57163
|
-
const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
|
|
57164
|
-
try {
|
|
57165
|
-
const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
|
|
57166
|
-
const doNotCreate = [
|
|
57167
|
-
...ctx.config.constitution?.forbidden ?? []
|
|
57168
|
-
];
|
|
57169
|
-
const declared = /* @__PURE__ */ new Set();
|
|
57170
|
-
for (const list of [
|
|
57171
|
-
ctx.config.constitution?.stateManagement ?? [],
|
|
57172
|
-
ctx.config.constitution?.dataFetching ?? [],
|
|
57173
|
-
ctx.config.constitution?.uiLibrary ?? [],
|
|
57174
|
-
ctx.config.constitution?.forms ?? [],
|
|
57175
|
-
ctx.config.constitution?.styling ?? [],
|
|
57176
|
-
ctx.config.constitution?.routing ?? []
|
|
57177
|
-
]) {
|
|
57178
|
-
for (const lib of list) declared.add(lib);
|
|
57179
|
-
}
|
|
57180
|
-
const doNotCreateCapped = doNotCreate.slice(0, 10);
|
|
57181
|
-
return {
|
|
57182
|
-
content: [
|
|
57183
|
-
{
|
|
57184
|
-
type: "text",
|
|
57185
|
-
text: JSON.stringify(
|
|
57186
|
-
{
|
|
57187
|
-
hint: "Use these patterns instead of creating new ones. Pick the closest existing entry and import it. The `doNotCreate` list is the deny-list \u2014 never import any of these.",
|
|
57188
|
-
doNotCreate: doNotCreateCapped,
|
|
57189
|
-
declaredStack: Array.from(declared),
|
|
57190
|
-
existingPatterns: inventory
|
|
57191
|
-
},
|
|
57192
|
-
null,
|
|
57193
|
-
2
|
|
57194
|
-
)
|
|
57195
|
-
}
|
|
57196
|
-
]
|
|
57197
|
-
};
|
|
57198
|
-
} catch (err) {
|
|
57199
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57200
|
-
}
|
|
57201
|
-
}
|
|
57202
|
-
function runCheckConstitution(args, ctx) {
|
|
57203
|
-
const path = args.path;
|
|
57204
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57205
|
-
const absPath = (0, import_node_path40.resolve)(ctx.cwd, path);
|
|
57206
|
-
let source;
|
|
57207
|
-
try {
|
|
57208
|
-
source = (0, import_node_fs32.readFileSync)(absPath, "utf-8");
|
|
57209
|
-
} catch (err) {
|
|
57210
|
-
return toolError(
|
|
57211
|
-
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
57212
|
-
);
|
|
57213
|
-
}
|
|
57214
|
-
const result = checkFileConstitution(source, ctx.config.constitution);
|
|
57215
|
-
return {
|
|
57216
|
-
content: [
|
|
57217
|
-
{
|
|
57218
|
-
type: "text",
|
|
57219
|
-
text: JSON.stringify(
|
|
57220
|
-
{
|
|
57221
|
-
file: absPath,
|
|
57222
|
-
importCount: result.imports.length,
|
|
57223
|
-
violationCount: result.violations.length,
|
|
57224
|
-
imports: result.imports,
|
|
57225
|
-
violations: result.violations,
|
|
57226
|
-
// Field name kept stable for backward compatibility with
|
|
57227
|
-
// older consumers; the value reflects whether the merged
|
|
57228
|
-
// `config.constitution` was declared, detected, or absent.
|
|
57229
|
-
conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
|
|
57230
|
-
},
|
|
57231
|
-
null,
|
|
57232
|
-
2
|
|
57233
|
-
)
|
|
57234
|
-
}
|
|
57235
|
-
]
|
|
57236
|
-
};
|
|
57237
|
-
}
|
|
57238
|
-
async function runFindSimilar(args, ctx) {
|
|
57239
|
-
const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
57240
|
-
const hooks = Array.isArray(args.hooks) ? args.hooks : [];
|
|
57241
|
-
const props = Array.isArray(args.props) ? args.props : [];
|
|
57242
|
-
const limitRaw = args.limit;
|
|
57243
|
-
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
|
|
57244
|
-
try {
|
|
57245
|
-
const matches = await findSimilarFunctions2(
|
|
57246
|
-
{
|
|
57247
|
-
name: typeof args.name === "string" ? args.name : void 0,
|
|
57248
|
-
hooks,
|
|
57249
|
-
props,
|
|
57250
|
-
limit,
|
|
57251
|
-
workspaceDir: ctx.cwd
|
|
57252
|
-
},
|
|
57253
|
-
{ cwd: ctx.cwd }
|
|
57254
|
-
);
|
|
57255
|
-
return {
|
|
57256
|
-
content: [
|
|
57257
|
-
{
|
|
57258
|
-
type: "text",
|
|
57259
|
-
text: JSON.stringify(
|
|
57260
|
-
{
|
|
57261
|
-
hint: "Each match is ranked by Jaccard similarity over (hooks \u222A props \u222A params). similarity=1 means the matched signature has an identical feature set. Agents should prefer the top match instead of writing a new implementation.",
|
|
57262
|
-
count: matches.length,
|
|
57263
|
-
matches: matches.map((m) => ({
|
|
57264
|
-
name: m.signature.name,
|
|
57265
|
-
file: m.signature.fileRel,
|
|
57266
|
-
line: m.signature.line,
|
|
57267
|
-
similarity: Number(m.similarity.toFixed(3)),
|
|
57268
|
-
fingerprint: m.fingerprint,
|
|
57269
|
-
hooks: m.signature.hooks,
|
|
57270
|
-
props: m.signature.props,
|
|
57271
|
-
params: m.signature.params
|
|
57272
|
-
}))
|
|
57273
|
-
},
|
|
57274
|
-
null,
|
|
57275
|
-
2
|
|
57276
|
-
)
|
|
57277
|
-
}
|
|
57278
|
-
]
|
|
57279
|
-
};
|
|
57280
|
-
} catch (err) {
|
|
57281
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57282
|
-
}
|
|
57283
|
-
}
|
|
57284
|
-
async function handleToolCall(toolName, args, ctx) {
|
|
57285
|
-
const deprecation = getDeprecation(toolName);
|
|
57286
|
-
const deprecationNotice = deprecation ? {
|
|
57287
|
-
tool: toolName,
|
|
57288
|
-
replacedBy: deprecation.replacedBy,
|
|
57289
|
-
removedIn: deprecation.removedIn ?? "next major",
|
|
57290
|
-
reason: deprecation.reason
|
|
57291
|
-
} : void 0;
|
|
57292
|
-
switch (toolName) {
|
|
57293
|
-
case "slop_scan_file":
|
|
57294
|
-
return runScanFile(args, ctx);
|
|
57295
|
-
case "slop_explain_rule":
|
|
57296
|
-
return explainRule2(args, ctx);
|
|
57297
|
-
case "slop_list_rules":
|
|
57298
|
-
return listRules(args, ctx);
|
|
57299
|
-
case "slop_suggest":
|
|
57300
|
-
return runSuggest(args, ctx);
|
|
57301
|
-
case "slop_suggest_with_structure":
|
|
57302
|
-
return runSuggestWithStructure(args, ctx);
|
|
57303
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57304
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57305
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57306
|
-
// Their runner functions (runGovernance, runArchitectureScore,
|
|
57307
|
-
// runBusinessLogicScore) are kept in the file for now
|
|
57308
|
-
// (marked @deprecated) to keep the diff small; they can be
|
|
57309
|
-
// deleted in a follow-up. New clients will never see these
|
|
57310
|
-
// tools listed in the MCP tools/list response.
|
|
57311
|
-
case "slop_check_constitution":
|
|
57312
|
-
return runCheckConstitution(args, ctx);
|
|
57313
|
-
case "slop_find_similar":
|
|
57314
|
-
return runFindSimilar(args, ctx);
|
|
57315
|
-
default:
|
|
57316
|
-
return toolError("Unknown tool: " + toolName);
|
|
57317
|
-
}
|
|
57318
|
-
}
|
|
57319
|
-
function canonicalToolNames() {
|
|
57320
|
-
return TOOL_DEFINITIONS.filter((t) => !t.deprecated).map((t) => t.name);
|
|
57321
|
-
}
|
|
57322
|
-
function getDeprecation(toolName) {
|
|
57323
|
-
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
57324
|
-
}
|
|
57325
|
-
var import_node_fs32, import_node_path40, TOOL_DEFINITIONS;
|
|
57326
|
-
var init_tools = __esm({
|
|
57327
|
-
"src/mcp/tools.ts"() {
|
|
57328
|
-
"use strict";
|
|
57329
|
-
import_node_fs32 = require("fs");
|
|
57330
|
-
import_node_path40 = require("path");
|
|
57331
|
-
init_worker();
|
|
57332
|
-
init_patterns();
|
|
57333
|
-
init_architecture_score();
|
|
57334
|
-
init_business_logic();
|
|
57335
|
-
init_slop_suggest_structure();
|
|
57336
|
-
TOOL_DEFINITIONS = [
|
|
57337
|
-
{
|
|
57338
|
-
name: "slop_scan_file",
|
|
57339
|
-
description: "Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, severity, line, column, message, advice) and the file-level Slop Index.",
|
|
57340
|
-
inputSchema: {
|
|
57341
|
-
type: "object",
|
|
57342
|
-
properties: {
|
|
57343
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
|
|
57344
|
-
framework: {
|
|
57345
|
-
type: "string",
|
|
57346
|
-
enum: ["react", "vue", "svelte", "astro", "html"],
|
|
57347
|
-
description: "Framework multiplier to apply. Defaults to the configured framework."
|
|
57348
|
-
}
|
|
57349
|
-
},
|
|
57350
|
-
required: ["path"]
|
|
57351
|
-
}
|
|
57352
|
-
},
|
|
57353
|
-
{
|
|
57354
|
-
name: "slop_explain_rule",
|
|
57355
|
-
description: "Return metadata for a single rule (id, category, severity, aiSpecific) plus a rationale and the recommended fix. Use this before auto-applying --fix to understand what the rule catches.",
|
|
57356
|
-
inputSchema: {
|
|
57357
|
-
type: "object",
|
|
57358
|
-
properties: {
|
|
57359
|
-
ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
|
|
57360
|
-
},
|
|
57361
|
-
required: ["ruleId"]
|
|
57362
|
-
}
|
|
57363
|
-
},
|
|
57364
|
-
{
|
|
57365
|
-
name: "slop_list_rules",
|
|
57366
|
-
description: "List all registered rules with their category, severity, and aiSpecific flag. Optional category filter (visual | logic | wcag | security | perf | typo | layout | component | arch).",
|
|
57367
|
-
inputSchema: {
|
|
57368
|
-
type: "object",
|
|
57369
|
-
properties: {
|
|
57370
|
-
category: { type: "string", description: "Optional category filter." }
|
|
57371
|
-
}
|
|
57372
|
-
}
|
|
57373
|
-
},
|
|
57374
|
-
{
|
|
57375
|
-
name: "slop_suggest",
|
|
57376
|
-
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), top issues by rule, hot files by issue count, and the composite Repository Health score. Call this BEFORE writing new code so the agent reuses existing patterns instead of duplicating them.",
|
|
57377
|
-
inputSchema: {
|
|
57378
|
-
type: "object",
|
|
57379
|
-
properties: {
|
|
57380
|
-
maxFiles: {
|
|
57381
|
-
type: "number",
|
|
57382
|
-
description: "Cap on files scanned to keep the inventory cheap. Defaults to 200."
|
|
57383
|
-
}
|
|
57384
|
-
}
|
|
57385
|
-
}
|
|
57386
|
-
},
|
|
57387
|
-
{
|
|
57388
|
-
name: "slop_suggest_with_structure",
|
|
57389
|
-
description: "Fast-path variant of `slop_suggest` that reads `.slopbrick/structure.md` from disk instead of re-scanning the codebase. Requires a prior `slopbrick scan` to have persisted the inventory (100\u20131000\xD7 latency win on the agent integration). If `structure.md` is missing, falls back to `slop_suggest` and annotates the response with `structureHint` so the caller knows to run `slopbrick scan` first.",
|
|
57390
|
-
inputSchema: {
|
|
57391
|
-
type: "object",
|
|
57392
|
-
properties: {
|
|
57393
|
-
maxFiles: {
|
|
57394
|
-
type: "number",
|
|
57395
|
-
description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
|
|
57396
|
-
}
|
|
57397
|
-
}
|
|
57398
|
-
}
|
|
57399
|
-
},
|
|
57400
|
-
{
|
|
57401
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57402
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57403
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57404
|
-
// They were strict subsets of slop_suggest; users should
|
|
57405
|
-
// call slop_suggest and read repositoryHealth /
|
|
57406
|
-
// architectureConsistency / businessLogicCoherence.
|
|
57407
|
-
name: "slop_check_constitution",
|
|
57408
|
-
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns a list of imports that violate declared values or hit the deny-list. Use this on a newly-written or modified file before suggesting a PR.",
|
|
57409
|
-
inputSchema: {
|
|
57410
|
-
type: "object",
|
|
57411
|
-
properties: {
|
|
57412
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
|
|
57413
|
-
},
|
|
57414
|
-
required: ["path"]
|
|
57415
|
-
}
|
|
57416
|
-
},
|
|
57417
|
-
{
|
|
57418
|
-
// v0.10.1: find_similar_function. The GIR (Give-Implementation-
|
|
57419
|
-
// Reference) primitive for slop_suggest. Given a function signature
|
|
57420
|
-
// (name + hooks + props), find the most similar existing
|
|
57421
|
-
// implementations across the codebase. Uses AST fingerprints
|
|
57422
|
-
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
57423
|
-
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
57424
|
-
name: "slop_find_similar",
|
|
57425
|
-
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns top-k matches with name, file, line, fingerprint, and similarity score in [0, 1].",
|
|
57426
|
-
inputSchema: {
|
|
57427
|
-
type: "object",
|
|
57428
|
-
properties: {
|
|
57429
|
-
name: {
|
|
57430
|
-
type: "string",
|
|
57431
|
-
description: "Function/component name to match. Omit to match by hooks+props only."
|
|
57432
|
-
},
|
|
57433
|
-
hooks: {
|
|
57434
|
-
type: "array",
|
|
57435
|
-
items: { type: "string" },
|
|
57436
|
-
description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
|
|
57437
|
-
},
|
|
57438
|
-
props: {
|
|
57439
|
-
type: "array",
|
|
57440
|
-
items: { type: "string" },
|
|
57441
|
-
description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
|
|
57442
|
-
},
|
|
57443
|
-
limit: {
|
|
57444
|
-
type: "number",
|
|
57445
|
-
description: "Top-k results to return. Default 10. Capped at 50."
|
|
57446
|
-
}
|
|
57447
|
-
}
|
|
57448
|
-
}
|
|
57449
|
-
}
|
|
57450
|
-
];
|
|
57451
|
-
}
|
|
57452
|
-
});
|
|
57453
|
-
|
|
57454
57134
|
// src/index.ts
|
|
57455
57135
|
var src_exports = {};
|
|
57456
57136
|
__export(src_exports, {
|
|
@@ -57538,7 +57218,7 @@ init_dist2();
|
|
|
57538
57218
|
|
|
57539
57219
|
// src/cli/program.ts
|
|
57540
57220
|
var import_node_path72 = require("path");
|
|
57541
|
-
var
|
|
57221
|
+
var import_commander3 = require("commander");
|
|
57542
57222
|
|
|
57543
57223
|
// src/cli/options.ts
|
|
57544
57224
|
var import_commander = require("commander");
|
|
@@ -62195,7 +61875,374 @@ init_logger();
|
|
|
62195
61875
|
init_builtins();
|
|
62196
61876
|
init_config2();
|
|
62197
61877
|
init_header();
|
|
62198
|
-
|
|
61878
|
+
|
|
61879
|
+
// src/mcp/tools.ts
|
|
61880
|
+
var import_node_fs32 = require("fs");
|
|
61881
|
+
var import_node_path40 = require("path");
|
|
61882
|
+
init_worker();
|
|
61883
|
+
init_patterns();
|
|
61884
|
+
init_architecture_score();
|
|
61885
|
+
init_business_logic();
|
|
61886
|
+
init_structure_md();
|
|
61887
|
+
var TOOL_DEFINITIONS = [
|
|
61888
|
+
{
|
|
61889
|
+
name: "slop_scan_file",
|
|
61890
|
+
description: "Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, severity, line, column, message, advice) and the file-level Slop Index.",
|
|
61891
|
+
inputSchema: {
|
|
61892
|
+
type: "object",
|
|
61893
|
+
properties: {
|
|
61894
|
+
path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
|
|
61895
|
+
framework: {
|
|
61896
|
+
type: "string",
|
|
61897
|
+
enum: ["react", "vue", "svelte", "astro", "html"],
|
|
61898
|
+
description: "Framework multiplier to apply. Defaults to the configured framework."
|
|
61899
|
+
}
|
|
61900
|
+
},
|
|
61901
|
+
required: ["path"]
|
|
61902
|
+
}
|
|
61903
|
+
},
|
|
61904
|
+
{
|
|
61905
|
+
name: "slop_explain_rule",
|
|
61906
|
+
description: "Return metadata for a single rule (id, category, severity, aiSpecific) plus a rationale and the recommended fix. Use this before auto-applying --fix to understand what the rule catches.",
|
|
61907
|
+
inputSchema: {
|
|
61908
|
+
type: "object",
|
|
61909
|
+
properties: {
|
|
61910
|
+
ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
|
|
61911
|
+
},
|
|
61912
|
+
required: ["ruleId"]
|
|
61913
|
+
}
|
|
61914
|
+
},
|
|
61915
|
+
{
|
|
61916
|
+
name: "slop_list_rules",
|
|
61917
|
+
description: "List all registered rules with their category, severity, and aiSpecific flag. Optional category filter (visual | logic | wcag | security | perf | typo | layout | component | arch).",
|
|
61918
|
+
inputSchema: {
|
|
61919
|
+
type: "object",
|
|
61920
|
+
properties: {
|
|
61921
|
+
category: { type: "string", description: "Optional category filter." }
|
|
61922
|
+
}
|
|
61923
|
+
}
|
|
61924
|
+
},
|
|
61925
|
+
{
|
|
61926
|
+
name: "slop_suggest",
|
|
61927
|
+
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), top issues by rule, hot files by issue count, and the composite Repository Health score. Call this BEFORE writing new code so the agent reuses existing patterns instead of duplicating them.",
|
|
61928
|
+
inputSchema: {
|
|
61929
|
+
type: "object",
|
|
61930
|
+
properties: {
|
|
61931
|
+
maxFiles: {
|
|
61932
|
+
type: "number",
|
|
61933
|
+
description: "Cap on files scanned to keep the inventory cheap. Defaults to 200."
|
|
61934
|
+
}
|
|
61935
|
+
}
|
|
61936
|
+
}
|
|
61937
|
+
},
|
|
61938
|
+
{
|
|
61939
|
+
name: "slop_suggest_with_structure",
|
|
61940
|
+
description: "Fast-path variant of `slop_suggest` that reads `.slopbrick/structure.md` from disk instead of re-scanning the codebase. Requires a prior `slopbrick scan` to have persisted the inventory (100\u20131000\xD7 latency win on the agent integration). If `structure.md` is missing, falls back to `slop_suggest` and annotates the response with `structureHint` so the caller knows to run `slopbrick scan` first.",
|
|
61941
|
+
inputSchema: {
|
|
61942
|
+
type: "object",
|
|
61943
|
+
properties: {
|
|
61944
|
+
maxFiles: {
|
|
61945
|
+
type: "number",
|
|
61946
|
+
description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
|
|
61947
|
+
}
|
|
61948
|
+
}
|
|
61949
|
+
}
|
|
61950
|
+
},
|
|
61951
|
+
{
|
|
61952
|
+
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
61953
|
+
// slop_architecture_score, slop_business_logic_score) that
|
|
61954
|
+
// were marked for removal in v0.13.0 but never removed.
|
|
61955
|
+
// They were strict subsets of slop_suggest; users should
|
|
61956
|
+
// call slop_suggest and read repositoryHealth /
|
|
61957
|
+
// architectureConsistency / businessLogicCoherence.
|
|
61958
|
+
name: "slop_check_constitution",
|
|
61959
|
+
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns a list of imports that violate declared values or hit the deny-list. Use this on a newly-written or modified file before suggesting a PR.",
|
|
61960
|
+
inputSchema: {
|
|
61961
|
+
type: "object",
|
|
61962
|
+
properties: {
|
|
61963
|
+
path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
|
|
61964
|
+
},
|
|
61965
|
+
required: ["path"]
|
|
61966
|
+
}
|
|
61967
|
+
},
|
|
61968
|
+
{
|
|
61969
|
+
// v0.10.1: find_similar_function. The GIR (Give-Implementation-
|
|
61970
|
+
// Reference) primitive for slop_suggest. Given a function signature
|
|
61971
|
+
// (name + hooks + props), find the most similar existing
|
|
61972
|
+
// implementations across the codebase. Uses AST fingerprints
|
|
61973
|
+
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
61974
|
+
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
61975
|
+
name: "slop_find_similar",
|
|
61976
|
+
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns top-k matches with name, file, line, fingerprint, and similarity score in [0, 1].",
|
|
61977
|
+
inputSchema: {
|
|
61978
|
+
type: "object",
|
|
61979
|
+
properties: {
|
|
61980
|
+
name: {
|
|
61981
|
+
type: "string",
|
|
61982
|
+
description: "Function/component name to match. Omit to match by hooks+props only."
|
|
61983
|
+
},
|
|
61984
|
+
hooks: {
|
|
61985
|
+
type: "array",
|
|
61986
|
+
items: { type: "string" },
|
|
61987
|
+
description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
|
|
61988
|
+
},
|
|
61989
|
+
props: {
|
|
61990
|
+
type: "array",
|
|
61991
|
+
items: { type: "string" },
|
|
61992
|
+
description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
|
|
61993
|
+
},
|
|
61994
|
+
limit: {
|
|
61995
|
+
type: "number",
|
|
61996
|
+
description: "Top-k results to return. Default 10. Capped at 50."
|
|
61997
|
+
}
|
|
61998
|
+
}
|
|
61999
|
+
}
|
|
62000
|
+
}
|
|
62001
|
+
];
|
|
62002
|
+
function toolError(message) {
|
|
62003
|
+
return {
|
|
62004
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
62005
|
+
isError: true
|
|
62006
|
+
};
|
|
62007
|
+
}
|
|
62008
|
+
async function runScanFile(args, ctx) {
|
|
62009
|
+
const path = args.path;
|
|
62010
|
+
if (!path) return toolError("Missing required argument: path");
|
|
62011
|
+
const result = await scanFile(path, ctx.config);
|
|
62012
|
+
const simplified = {
|
|
62013
|
+
filePath: result.filePath,
|
|
62014
|
+
componentCount: result.componentCount,
|
|
62015
|
+
parseError: result.parseError,
|
|
62016
|
+
issues: result.issues.map((i) => ({
|
|
62017
|
+
ruleId: i.ruleId,
|
|
62018
|
+
category: i.category,
|
|
62019
|
+
severity: i.severity,
|
|
62020
|
+
line: i.line,
|
|
62021
|
+
column: i.column,
|
|
62022
|
+
message: i.message,
|
|
62023
|
+
advice: i.advice
|
|
62024
|
+
}))
|
|
62025
|
+
};
|
|
62026
|
+
return {
|
|
62027
|
+
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
62028
|
+
};
|
|
62029
|
+
}
|
|
62030
|
+
function explainRule2(args, ctx) {
|
|
62031
|
+
const ruleId = args.ruleId;
|
|
62032
|
+
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
62033
|
+
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
62034
|
+
if (!rule) return toolError("Unknown rule: " + ruleId);
|
|
62035
|
+
const explanation = {
|
|
62036
|
+
ruleId: rule.id,
|
|
62037
|
+
category: rule.category,
|
|
62038
|
+
severity: rule.severity,
|
|
62039
|
+
aiSpecific: rule.aiSpecific,
|
|
62040
|
+
rationale: "This rule flags " + rule.category + " patterns associated with AI-generated code. It is marked as " + (rule.aiSpecific ? "AI-specific" : "cross-cutting") + ". Severity: " + rule.severity + ".",
|
|
62041
|
+
whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
|
|
62042
|
+
};
|
|
62043
|
+
return {
|
|
62044
|
+
content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
|
|
62045
|
+
};
|
|
62046
|
+
}
|
|
62047
|
+
function listRules(args, ctx) {
|
|
62048
|
+
const category = args.category;
|
|
62049
|
+
const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
|
|
62050
|
+
const rules = filtered.map((r) => ({
|
|
62051
|
+
id: r.id,
|
|
62052
|
+
category: r.category,
|
|
62053
|
+
severity: r.severity,
|
|
62054
|
+
aiSpecific: r.aiSpecific
|
|
62055
|
+
}));
|
|
62056
|
+
return {
|
|
62057
|
+
content: [
|
|
62058
|
+
{
|
|
62059
|
+
type: "text",
|
|
62060
|
+
text: JSON.stringify({ count: rules.length, rules }, null, 2)
|
|
62061
|
+
}
|
|
62062
|
+
]
|
|
62063
|
+
};
|
|
62064
|
+
}
|
|
62065
|
+
var STRUCTURE_NOT_FOUND_HINT = "No .slopbrick/structure.md found. Run `slopbrick scan` to persist the pattern inventory, then call this tool again for the O(read file) fast path.";
|
|
62066
|
+
async function runSuggest(args, ctx, options = {}) {
|
|
62067
|
+
const { includeStructure = false } = options;
|
|
62068
|
+
if (includeStructure) {
|
|
62069
|
+
const cached = await readStructureMarkdown(ctx.cwd);
|
|
62070
|
+
if (cached !== null) {
|
|
62071
|
+
return {
|
|
62072
|
+
content: [{ type: "text", text: cached }]
|
|
62073
|
+
};
|
|
62074
|
+
}
|
|
62075
|
+
}
|
|
62076
|
+
const maxFilesRaw = args.maxFiles;
|
|
62077
|
+
const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
|
|
62078
|
+
try {
|
|
62079
|
+
const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
|
|
62080
|
+
const doNotCreate = [
|
|
62081
|
+
...ctx.config.constitution?.forbidden ?? []
|
|
62082
|
+
];
|
|
62083
|
+
const declared = /* @__PURE__ */ new Set();
|
|
62084
|
+
for (const list of [
|
|
62085
|
+
ctx.config.constitution?.stateManagement ?? [],
|
|
62086
|
+
ctx.config.constitution?.dataFetching ?? [],
|
|
62087
|
+
ctx.config.constitution?.uiLibrary ?? [],
|
|
62088
|
+
ctx.config.constitution?.forms ?? [],
|
|
62089
|
+
ctx.config.constitution?.styling ?? [],
|
|
62090
|
+
ctx.config.constitution?.routing ?? []
|
|
62091
|
+
]) {
|
|
62092
|
+
for (const lib of list) declared.add(lib);
|
|
62093
|
+
}
|
|
62094
|
+
const doNotCreateCapped = doNotCreate.slice(0, 10);
|
|
62095
|
+
const payload = {
|
|
62096
|
+
hint: "Use these patterns instead of creating new ones. Pick the closest existing entry and import it. The `doNotCreate` list is the deny-list \u2014 never import any of these.",
|
|
62097
|
+
doNotCreate: doNotCreateCapped,
|
|
62098
|
+
declaredStack: Array.from(declared),
|
|
62099
|
+
existingPatterns: inventory
|
|
62100
|
+
};
|
|
62101
|
+
if (includeStructure) {
|
|
62102
|
+
payload.structureHint = STRUCTURE_NOT_FOUND_HINT;
|
|
62103
|
+
}
|
|
62104
|
+
try {
|
|
62105
|
+
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
62106
|
+
const health = loadHealth2(ctx.cwd);
|
|
62107
|
+
if (health?.compositeScore) {
|
|
62108
|
+
payload.compositeScore = health.compositeScore;
|
|
62109
|
+
}
|
|
62110
|
+
} catch {
|
|
62111
|
+
}
|
|
62112
|
+
return {
|
|
62113
|
+
content: [
|
|
62114
|
+
{
|
|
62115
|
+
type: "text",
|
|
62116
|
+
text: JSON.stringify(payload, null, 2)
|
|
62117
|
+
}
|
|
62118
|
+
]
|
|
62119
|
+
};
|
|
62120
|
+
} catch (err) {
|
|
62121
|
+
return toolError(err instanceof Error ? err.message : String(err));
|
|
62122
|
+
}
|
|
62123
|
+
}
|
|
62124
|
+
function runCheckConstitution(args, ctx) {
|
|
62125
|
+
const path = args.path;
|
|
62126
|
+
if (!path) return toolError("Missing required argument: path");
|
|
62127
|
+
const absPath = (0, import_node_path40.resolve)(ctx.cwd, path);
|
|
62128
|
+
let source;
|
|
62129
|
+
try {
|
|
62130
|
+
source = (0, import_node_fs32.readFileSync)(absPath, "utf-8");
|
|
62131
|
+
} catch (err) {
|
|
62132
|
+
return toolError(
|
|
62133
|
+
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
62134
|
+
);
|
|
62135
|
+
}
|
|
62136
|
+
const result = checkFileConstitution(source, ctx.config.constitution);
|
|
62137
|
+
return {
|
|
62138
|
+
content: [
|
|
62139
|
+
{
|
|
62140
|
+
type: "text",
|
|
62141
|
+
text: JSON.stringify(
|
|
62142
|
+
{
|
|
62143
|
+
file: absPath,
|
|
62144
|
+
importCount: result.imports.length,
|
|
62145
|
+
violationCount: result.violations.length,
|
|
62146
|
+
imports: result.imports,
|
|
62147
|
+
violations: result.violations,
|
|
62148
|
+
// Field name kept stable for backward compatibility with
|
|
62149
|
+
// older consumers; the value reflects whether the merged
|
|
62150
|
+
// `config.constitution` was declared, detected, or absent.
|
|
62151
|
+
conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
|
|
62152
|
+
},
|
|
62153
|
+
null,
|
|
62154
|
+
2
|
|
62155
|
+
)
|
|
62156
|
+
}
|
|
62157
|
+
]
|
|
62158
|
+
};
|
|
62159
|
+
}
|
|
62160
|
+
async function runFindSimilar(args, ctx) {
|
|
62161
|
+
const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
62162
|
+
const hooks = Array.isArray(args.hooks) ? args.hooks : [];
|
|
62163
|
+
const props = Array.isArray(args.props) ? args.props : [];
|
|
62164
|
+
const limitRaw = args.limit;
|
|
62165
|
+
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
|
|
62166
|
+
try {
|
|
62167
|
+
const matches = await findSimilarFunctions2(
|
|
62168
|
+
{
|
|
62169
|
+
name: typeof args.name === "string" ? args.name : void 0,
|
|
62170
|
+
hooks,
|
|
62171
|
+
props,
|
|
62172
|
+
limit,
|
|
62173
|
+
workspaceDir: ctx.cwd
|
|
62174
|
+
},
|
|
62175
|
+
{ cwd: ctx.cwd }
|
|
62176
|
+
);
|
|
62177
|
+
return {
|
|
62178
|
+
content: [
|
|
62179
|
+
{
|
|
62180
|
+
type: "text",
|
|
62181
|
+
text: JSON.stringify(
|
|
62182
|
+
{
|
|
62183
|
+
hint: "Each match is ranked by Jaccard similarity over (hooks \u222A props \u222A params). similarity=1 means the matched signature has an identical feature set. Agents should prefer the top match instead of writing a new implementation.",
|
|
62184
|
+
count: matches.length,
|
|
62185
|
+
matches: matches.map((m) => ({
|
|
62186
|
+
name: m.signature.name,
|
|
62187
|
+
file: m.signature.fileRel,
|
|
62188
|
+
line: m.signature.line,
|
|
62189
|
+
similarity: Number(m.similarity.toFixed(3)),
|
|
62190
|
+
fingerprint: m.fingerprint,
|
|
62191
|
+
hooks: m.signature.hooks,
|
|
62192
|
+
props: m.signature.props,
|
|
62193
|
+
params: m.signature.params
|
|
62194
|
+
}))
|
|
62195
|
+
},
|
|
62196
|
+
null,
|
|
62197
|
+
2
|
|
62198
|
+
)
|
|
62199
|
+
}
|
|
62200
|
+
]
|
|
62201
|
+
};
|
|
62202
|
+
} catch (err) {
|
|
62203
|
+
return toolError(err instanceof Error ? err.message : String(err));
|
|
62204
|
+
}
|
|
62205
|
+
}
|
|
62206
|
+
async function handleToolCall(toolName, args, ctx) {
|
|
62207
|
+
const deprecation = getDeprecation(toolName);
|
|
62208
|
+
const deprecationNotice = deprecation ? {
|
|
62209
|
+
tool: toolName,
|
|
62210
|
+
replacedBy: deprecation.replacedBy,
|
|
62211
|
+
removedIn: deprecation.removedIn ?? "next major",
|
|
62212
|
+
reason: deprecation.reason
|
|
62213
|
+
} : void 0;
|
|
62214
|
+
switch (toolName) {
|
|
62215
|
+
case "slop_scan_file":
|
|
62216
|
+
return runScanFile(args, ctx);
|
|
62217
|
+
case "slop_explain_rule":
|
|
62218
|
+
return explainRule2(args, ctx);
|
|
62219
|
+
case "slop_list_rules":
|
|
62220
|
+
return listRules(args, ctx);
|
|
62221
|
+
case "slop_suggest":
|
|
62222
|
+
return runSuggest(args, ctx);
|
|
62223
|
+
case "slop_suggest_with_structure":
|
|
62224
|
+
return runSuggest(args, ctx, { includeStructure: true });
|
|
62225
|
+
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
62226
|
+
// slop_architecture_score, slop_business_logic_score) that
|
|
62227
|
+
// were marked for removal in v0.13.0 but never removed.
|
|
62228
|
+
// Their runner functions (runGovernance, runArchitectureScore,
|
|
62229
|
+
// runBusinessLogicScore) are kept in the file for now
|
|
62230
|
+
// (marked @deprecated) to keep the diff small; they can be
|
|
62231
|
+
// deleted in a follow-up. New clients will never see these
|
|
62232
|
+
// tools listed in the MCP tools/list response.
|
|
62233
|
+
case "slop_check_constitution":
|
|
62234
|
+
return runCheckConstitution(args, ctx);
|
|
62235
|
+
case "slop_find_similar":
|
|
62236
|
+
return runFindSimilar(args, ctx);
|
|
62237
|
+
default:
|
|
62238
|
+
return toolError("Unknown tool: " + toolName);
|
|
62239
|
+
}
|
|
62240
|
+
}
|
|
62241
|
+
function getDeprecation(toolName) {
|
|
62242
|
+
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
62243
|
+
}
|
|
62244
|
+
|
|
62245
|
+
// src/mcp/server.ts
|
|
62199
62246
|
var SERVER_INFO = {
|
|
62200
62247
|
name: "slopbrick",
|
|
62201
62248
|
// v0.39.0: use the VERSION constant (same source as the CLI)
|
|
@@ -63635,6 +63682,7 @@ var import_node_fs38 = require("fs");
|
|
|
63635
63682
|
var import_node_path52 = require("path");
|
|
63636
63683
|
init_discover();
|
|
63637
63684
|
init_patterns();
|
|
63685
|
+
init_telemetry();
|
|
63638
63686
|
async function runDrift(cwd, config, options = {}) {
|
|
63639
63687
|
const maxFiles = options.maxFiles ?? 1e3;
|
|
63640
63688
|
const allFiles = await discoverFiles(cwd, config);
|
|
@@ -63742,26 +63790,217 @@ function formatDrift(result, opts = {}) {
|
|
|
63742
63790
|
function driftExitCode(result) {
|
|
63743
63791
|
return result.totalViolations > 0 ? 1 : 0;
|
|
63744
63792
|
}
|
|
63793
|
+
async function runDriftOverTime(cwd, config, options) {
|
|
63794
|
+
const payloads = readTelemetry(cwd);
|
|
63795
|
+
const sortedAsc = [...payloads].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
63796
|
+
const withInventory = sortedAsc.filter((p) => p.inventory !== void 0);
|
|
63797
|
+
const baselineSource = options.since === "baseline" ? "baseline" : "since";
|
|
63798
|
+
let baseline;
|
|
63799
|
+
if (baselineSource === "baseline") {
|
|
63800
|
+
baseline = withInventory[0];
|
|
63801
|
+
} else {
|
|
63802
|
+
baseline = withInventory.find((p) => p.timestamp >= options.since);
|
|
63803
|
+
}
|
|
63804
|
+
const current = withInventory.at(-1);
|
|
63805
|
+
if (!baseline || !current) {
|
|
63806
|
+
return {
|
|
63807
|
+
scannedFiles: 0,
|
|
63808
|
+
filesWithViolations: 0,
|
|
63809
|
+
totalViolations: 0,
|
|
63810
|
+
byCategory: {},
|
|
63811
|
+
byFile: [],
|
|
63812
|
+
conventionSource: deriveSource(config.constitution),
|
|
63813
|
+
constitution: config.constitution,
|
|
63814
|
+
introduced: [],
|
|
63815
|
+
removed: [],
|
|
63816
|
+
introducedUndeclared: [],
|
|
63817
|
+
snapshotsConsidered: withInventory.length,
|
|
63818
|
+
driftScore: 0,
|
|
63819
|
+
baselineAt: baseline?.timestamp ?? "",
|
|
63820
|
+
currentAt: current?.timestamp ?? "",
|
|
63821
|
+
baselineSource
|
|
63822
|
+
};
|
|
63823
|
+
}
|
|
63824
|
+
const baselineNames = flattenPatternNames(baseline.inventory.patternNames);
|
|
63825
|
+
const currentNames = flattenPatternNames(current.inventory.patternNames);
|
|
63826
|
+
const baselineSet = new Set(baselineNames.map((p) => `${p.category}\0${p.name}`));
|
|
63827
|
+
const currentSet = new Set(currentNames.map((p) => `${p.category}\0${p.name}`));
|
|
63828
|
+
const introduced = [];
|
|
63829
|
+
const removed = [];
|
|
63830
|
+
for (const p of currentNames) {
|
|
63831
|
+
const key = `${p.category}\0${p.name}`;
|
|
63832
|
+
if (!baselineSet.has(key)) introduced.push(p);
|
|
63833
|
+
}
|
|
63834
|
+
for (const p of baselineNames) {
|
|
63835
|
+
const key = `${p.category}\0${p.name}`;
|
|
63836
|
+
if (!currentSet.has(key)) removed.push(p);
|
|
63837
|
+
}
|
|
63838
|
+
introduced.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
63839
|
+
removed.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
63840
|
+
const declared = collectDeclaredNames(config.constitution);
|
|
63841
|
+
const introducedUndeclared = introduced.filter((p) => !declared.has(p.name.toLowerCase()));
|
|
63842
|
+
const baselineTotal = Math.max(baselineSet.size, 1);
|
|
63843
|
+
const driftScore = Math.min(
|
|
63844
|
+
100,
|
|
63845
|
+
Math.round((introduced.length + removed.length) / baselineTotal * 100)
|
|
63846
|
+
);
|
|
63847
|
+
return {
|
|
63848
|
+
scannedFiles: current.inventory.scannedFiles,
|
|
63849
|
+
filesWithViolations: 0,
|
|
63850
|
+
totalViolations: 0,
|
|
63851
|
+
byCategory: {},
|
|
63852
|
+
byFile: [],
|
|
63853
|
+
conventionSource: deriveSource(config.constitution),
|
|
63854
|
+
constitution: config.constitution,
|
|
63855
|
+
introduced,
|
|
63856
|
+
removed,
|
|
63857
|
+
introducedUndeclared,
|
|
63858
|
+
snapshotsConsidered: withInventory.length,
|
|
63859
|
+
driftScore,
|
|
63860
|
+
baselineAt: baseline.timestamp,
|
|
63861
|
+
currentAt: current.timestamp,
|
|
63862
|
+
baselineSource
|
|
63863
|
+
};
|
|
63864
|
+
}
|
|
63865
|
+
function flattenPatternNames(patternNames) {
|
|
63866
|
+
const out = [];
|
|
63867
|
+
for (const [category, names] of Object.entries(patternNames)) {
|
|
63868
|
+
for (const name of names) {
|
|
63869
|
+
out.push({ category, name });
|
|
63870
|
+
}
|
|
63871
|
+
}
|
|
63872
|
+
out.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
63873
|
+
return out;
|
|
63874
|
+
}
|
|
63875
|
+
function collectDeclaredNames(constitution) {
|
|
63876
|
+
const out = /* @__PURE__ */ new Set();
|
|
63877
|
+
if (!constitution) return out;
|
|
63878
|
+
const fields = ["stateManagement", "dataFetching", "uiLibrary", "forms", "styling", "routing"];
|
|
63879
|
+
for (const f of fields) {
|
|
63880
|
+
for (const v of constitution[f] ?? []) {
|
|
63881
|
+
out.add(v.toLowerCase());
|
|
63882
|
+
}
|
|
63883
|
+
}
|
|
63884
|
+
for (const list of Object.values(constitution.custom ?? {})) {
|
|
63885
|
+
for (const v of list) {
|
|
63886
|
+
out.add(v.toLowerCase());
|
|
63887
|
+
}
|
|
63888
|
+
}
|
|
63889
|
+
for (const v of constitution.forbidden ?? []) {
|
|
63890
|
+
out.add(v.toLowerCase());
|
|
63891
|
+
}
|
|
63892
|
+
return out;
|
|
63893
|
+
}
|
|
63894
|
+
function formatDriftOverTime(result) {
|
|
63895
|
+
const lines = [];
|
|
63896
|
+
lines.push("Temporal drift report");
|
|
63897
|
+
lines.push("");
|
|
63898
|
+
if (result.snapshotsConsidered === 0 || !result.baselineAt || !result.currentAt) {
|
|
63899
|
+
lines.push(" No historical telemetry found at .slopbrick/flywheel/scans.jsonl.");
|
|
63900
|
+
lines.push(" Run a few scans with `slopbrick scan` first; the temporal drift");
|
|
63901
|
+
lines.push(" detector needs \u2265 2 scan payloads to compute a baseline window.");
|
|
63902
|
+
return lines.join("\n");
|
|
63903
|
+
}
|
|
63904
|
+
const sinceLabel = result.baselineSource === "baseline" ? `baseline (oldest scan)` : `since ${result.baselineAt}`;
|
|
63905
|
+
lines.push(` Window: ${sinceLabel} \u2192 ${result.currentAt}`);
|
|
63906
|
+
lines.push(` Snapshots considered: ${result.snapshotsConsidered}`);
|
|
63907
|
+
lines.push(` Drift score: ${result.driftScore} / 100 (informational)`);
|
|
63908
|
+
lines.push("");
|
|
63909
|
+
lines.push(` Patterns introduced: ${result.introduced.length}`);
|
|
63910
|
+
for (const p of result.introduced) {
|
|
63911
|
+
lines.push(` + ${p.category}/${p.name}`);
|
|
63912
|
+
}
|
|
63913
|
+
if (result.introducedUndeclared.length > 0) {
|
|
63914
|
+
lines.push("");
|
|
63915
|
+
lines.push(
|
|
63916
|
+
` Patterns introduced but not in declared constitution: ${result.introducedUndeclared.length}`
|
|
63917
|
+
);
|
|
63918
|
+
for (const p of result.introducedUndeclared) {
|
|
63919
|
+
lines.push(` ! ${p.category}/${p.name}`);
|
|
63920
|
+
}
|
|
63921
|
+
}
|
|
63922
|
+
lines.push("");
|
|
63923
|
+
lines.push(` Patterns removed: ${result.removed.length}`);
|
|
63924
|
+
for (const p of result.removed) {
|
|
63925
|
+
lines.push(` - ${p.category}/${p.name}`);
|
|
63926
|
+
}
|
|
63927
|
+
lines.push("");
|
|
63928
|
+
if (result.introducedUndeclared.length > 0) {
|
|
63929
|
+
lines.push(
|
|
63930
|
+
" Tip: add undeclared patterns to slopbrick.config.mjs#constitution to"
|
|
63931
|
+
);
|
|
63932
|
+
lines.push(" promote them into the declared set on the next scan.");
|
|
63933
|
+
} else if (result.introduced.length === 0 && result.removed.length === 0) {
|
|
63934
|
+
lines.push(" \u2713 No pattern churn since the baseline window.");
|
|
63935
|
+
}
|
|
63936
|
+
return lines.join("\n");
|
|
63937
|
+
}
|
|
63938
|
+
function driftOverTimeExitCode(result) {
|
|
63939
|
+
return result.introducedUndeclared.length > 0 ? 1 : 0;
|
|
63940
|
+
}
|
|
63941
|
+
|
|
63942
|
+
// src/cli/commands/drift.ts
|
|
63943
|
+
init_load();
|
|
63944
|
+
|
|
63945
|
+
// src/cli/commands/_shared.ts
|
|
63946
|
+
var import_commander2 = require("commander");
|
|
63947
|
+
init_logger();
|
|
63948
|
+
function setExitOverride(program) {
|
|
63949
|
+
program.exitOverride();
|
|
63950
|
+
}
|
|
63951
|
+
async function dispatch(program, runFn) {
|
|
63952
|
+
try {
|
|
63953
|
+
await runFn();
|
|
63954
|
+
} catch (err) {
|
|
63955
|
+
if (err instanceof import_commander2.CommanderError) {
|
|
63956
|
+
if (err.code !== "commander.helpDisplayed" && err.code !== "commander.help") {
|
|
63957
|
+
logger.error(err.message);
|
|
63958
|
+
}
|
|
63959
|
+
process.exit(err.exitCode);
|
|
63960
|
+
return;
|
|
63961
|
+
}
|
|
63962
|
+
throw err;
|
|
63963
|
+
}
|
|
63964
|
+
}
|
|
63965
|
+
function withExitCode(result, compute, message) {
|
|
63966
|
+
const code = compute(result);
|
|
63967
|
+
if (code === 0) return;
|
|
63968
|
+
throw new import_commander2.CommanderError(code, "slopbrick.exit", message);
|
|
63969
|
+
}
|
|
63745
63970
|
|
|
63746
63971
|
// src/cli/commands/drift.ts
|
|
63747
63972
|
function registerDrift(program) {
|
|
63748
63973
|
program.command("drift").description(
|
|
63749
63974
|
"detect imports that violate declared constitution (state, data-fetching, UI, forms, styling, routing) or import forbidden packages"
|
|
63750
|
-
).option("--format <pretty|json>", "output format", "pretty").option("--max-files <n>", "cap on files scanned", parseCount, 1e3).
|
|
63975
|
+
).option("--format <pretty|json>", "output format", "pretty").option("--max-files <n>", "cap on files scanned", parseCount, 1e3).option(
|
|
63976
|
+
"--temporal-since <date>",
|
|
63977
|
+
`temporal drift since the given date (ISO-8601, or the literal string \`baseline\` to use the oldest scan in scans.jsonl)`
|
|
63978
|
+
).action(
|
|
63751
63979
|
async (cmdOptions, command) => {
|
|
63752
|
-
|
|
63753
|
-
|
|
63754
|
-
|
|
63755
|
-
|
|
63756
|
-
|
|
63757
|
-
const
|
|
63758
|
-
|
|
63759
|
-
|
|
63760
|
-
|
|
63761
|
-
|
|
63762
|
-
|
|
63763
|
-
|
|
63980
|
+
const options = command.optsWithGlobals();
|
|
63981
|
+
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
63982
|
+
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
63983
|
+
const cwd = (0, import_node_path53.resolve)(options.workspace ?? process.cwd());
|
|
63984
|
+
if (cmdOptions.temporalSince !== void 0) {
|
|
63985
|
+
const temporalSince = cmdOptions.temporalSince.trim();
|
|
63986
|
+
if (temporalSince.length === 0) {
|
|
63987
|
+
throw new Error("--temporal-since expects a non-empty ISO date or `baseline`");
|
|
63988
|
+
}
|
|
63989
|
+
const sinceArg = temporalSince === "baseline" ? "baseline" : temporalSince;
|
|
63990
|
+
const config2 = await loadConfig(cwd);
|
|
63991
|
+
const result2 = await runDriftOverTime(cwd, config2, { since: sinceArg });
|
|
63992
|
+
logger.info(formatDriftOverTime(result2));
|
|
63993
|
+
withExitCode(
|
|
63994
|
+
result2,
|
|
63995
|
+
driftOverTimeExitCode,
|
|
63996
|
+
`drift --temporal-since: ${result2.introducedUndeclared.length} undeclared patterns introduced`
|
|
63997
|
+
);
|
|
63998
|
+
return;
|
|
63764
63999
|
}
|
|
64000
|
+
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64001
|
+
const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
|
|
64002
|
+
logger.info(formatDrift(result, { json: format === "json" }));
|
|
64003
|
+
withExitCode(result, driftExitCode, `drift: ${result.totalViolations} violations`);
|
|
63765
64004
|
}
|
|
63766
64005
|
);
|
|
63767
64006
|
}
|
|
@@ -66420,8 +66659,9 @@ process.on("uncaughtException", (err) => {
|
|
|
66420
66659
|
});
|
|
66421
66660
|
async function runCli({ start }) {
|
|
66422
66661
|
try {
|
|
66423
|
-
const program = new
|
|
66662
|
+
const program = new import_commander3.Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if slop index increased since last run").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
|
|
66424
66663
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
66664
|
+
setExitOverride(program);
|
|
66425
66665
|
registerInit(program);
|
|
66426
66666
|
registerInstall(program);
|
|
66427
66667
|
registerUninstall(program);
|
|
@@ -66606,7 +66846,9 @@ async function runCli({ start }) {
|
|
|
66606
66846
|
program.outputHelp();
|
|
66607
66847
|
process.exit(0);
|
|
66608
66848
|
}
|
|
66609
|
-
await program
|
|
66849
|
+
await dispatch(program, async () => {
|
|
66850
|
+
await program.parseAsync(process.argv);
|
|
66851
|
+
});
|
|
66610
66852
|
} catch (err) {
|
|
66611
66853
|
if (err instanceof ConfigValidationError) {
|
|
66612
66854
|
logger.error(err.message);
|