slopbrick 0.40.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 +621 -420
- package/dist/index.js +619 -418
- 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) {
|
|
@@ -54238,6 +54247,21 @@ function safeRelative(cwd, filePath) {
|
|
|
54238
54247
|
return filePath;
|
|
54239
54248
|
}
|
|
54240
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
|
+
}
|
|
54241
54265
|
function aggregateViolations(report) {
|
|
54242
54266
|
const counts = /* @__PURE__ */ new Map();
|
|
54243
54267
|
for (const issue of report.issues) {
|
|
@@ -54317,7 +54341,7 @@ function readTelemetry(cwd) {
|
|
|
54317
54341
|
}
|
|
54318
54342
|
return payloads;
|
|
54319
54343
|
}
|
|
54320
|
-
function recordTelemetry(cwd, report, results, config) {
|
|
54344
|
+
function recordTelemetry(cwd, report, results, config, inventory) {
|
|
54321
54345
|
if (config.telemetry === false) {
|
|
54322
54346
|
return void 0;
|
|
54323
54347
|
}
|
|
@@ -54338,7 +54362,14 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54338
54362
|
framework: config.framework
|
|
54339
54363
|
},
|
|
54340
54364
|
violations: aggregateViolations(report),
|
|
54341
|
-
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) } : {}
|
|
54342
54373
|
};
|
|
54343
54374
|
const path = telemetryPath2(cwd);
|
|
54344
54375
|
const dir = (0, import_node_path22.dirname)(path);
|
|
@@ -54349,7 +54380,7 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54349
54380
|
(0, import_node_fs20.appendFileSync)(path, JSON.stringify(payload) + "\n", "utf-8");
|
|
54350
54381
|
return payload;
|
|
54351
54382
|
}
|
|
54352
|
-
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;
|
|
54353
54384
|
var init_telemetry = __esm({
|
|
54354
54385
|
"src/engine/telemetry.ts"() {
|
|
54355
54386
|
"use strict";
|
|
@@ -54360,6 +54391,7 @@ var init_telemetry = __esm({
|
|
|
54360
54391
|
TELEMETRY_FILE2 = "scans.jsonl";
|
|
54361
54392
|
MAX_TELEMETRY_BYTES = 10 * 1024 * 1024;
|
|
54362
54393
|
MAX_ROTATED_FILES = 5;
|
|
54394
|
+
TELEMETRY_INVENTORY_NAME_CAP = 50;
|
|
54363
54395
|
}
|
|
54364
54396
|
});
|
|
54365
54397
|
|
|
@@ -54673,11 +54705,11 @@ async function persistRun(input) {
|
|
|
54673
54705
|
}
|
|
54674
54706
|
report.issues.push(...flywheelOutput.hotspotIssues);
|
|
54675
54707
|
}
|
|
54676
|
-
|
|
54708
|
+
let patternInventory;
|
|
54677
54709
|
if (config.projectMemory !== false) {
|
|
54678
54710
|
try {
|
|
54679
54711
|
const durationMs = Date.now() - startTime;
|
|
54680
|
-
|
|
54712
|
+
patternInventory = await buildPatternInventory(cwd, config);
|
|
54681
54713
|
const inventory = buildInventoryFromScan(
|
|
54682
54714
|
{ cwd, results },
|
|
54683
54715
|
patternInventory,
|
|
@@ -54711,6 +54743,7 @@ async function persistRun(input) {
|
|
|
54711
54743
|
}
|
|
54712
54744
|
}
|
|
54713
54745
|
}
|
|
54746
|
+
recordTelemetry(cwd, report, results, config, patternInventory);
|
|
54714
54747
|
}
|
|
54715
54748
|
var import_node_fs22, import_node_path24;
|
|
54716
54749
|
var init_persistRun = __esm({
|
|
@@ -55092,6 +55125,7 @@ function formatSarif(report, options) {
|
|
|
55092
55125
|
const results = report.issues.map(
|
|
55093
55126
|
(issue) => buildResultFromIssue(issue, options?.cwd, fileContentCache)
|
|
55094
55127
|
);
|
|
55128
|
+
const driverProperties = report.compositeScore ? { compositeScore: report.compositeScore } : void 0;
|
|
55095
55129
|
const log = {
|
|
55096
55130
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
55097
55131
|
version: "2.1.0",
|
|
@@ -55102,7 +55136,8 @@ function formatSarif(report, options) {
|
|
|
55102
55136
|
name: "slopbrick",
|
|
55103
55137
|
version: report.version,
|
|
55104
55138
|
informationUri: REPO_INFORMATION_URI,
|
|
55105
|
-
rules
|
|
55139
|
+
rules,
|
|
55140
|
+
...driverProperties ? { properties: driverProperties } : {}
|
|
55106
55141
|
}
|
|
55107
55142
|
},
|
|
55108
55143
|
results
|
|
@@ -57096,402 +57131,6 @@ var init_scan2 = __esm({
|
|
|
57096
57131
|
}
|
|
57097
57132
|
});
|
|
57098
57133
|
|
|
57099
|
-
// src/mcp/slop-suggest-structure.ts
|
|
57100
|
-
async function runSuggestWithStructure(args, ctx) {
|
|
57101
|
-
const cached = await readStructureMarkdown(ctx.cwd);
|
|
57102
|
-
if (cached !== null) {
|
|
57103
|
-
return {
|
|
57104
|
-
content: [{ type: "text", text: cached }]
|
|
57105
|
-
};
|
|
57106
|
-
}
|
|
57107
|
-
const { handleToolCall: handleToolCall2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
|
|
57108
|
-
const result = await handleToolCall2("slop_suggest", args, ctx);
|
|
57109
|
-
if (result.isError) return result;
|
|
57110
|
-
try {
|
|
57111
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
57112
|
-
if (parsed !== null && typeof parsed === "object") {
|
|
57113
|
-
parsed.structureHint = STRUCTURE_NOT_FOUND_HINT;
|
|
57114
|
-
return {
|
|
57115
|
-
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }]
|
|
57116
|
-
};
|
|
57117
|
-
}
|
|
57118
|
-
} catch {
|
|
57119
|
-
}
|
|
57120
|
-
return result;
|
|
57121
|
-
}
|
|
57122
|
-
var STRUCTURE_NOT_FOUND_HINT;
|
|
57123
|
-
var init_slop_suggest_structure = __esm({
|
|
57124
|
-
"src/mcp/slop-suggest-structure.ts"() {
|
|
57125
|
-
"use strict";
|
|
57126
|
-
init_structure_md();
|
|
57127
|
-
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.";
|
|
57128
|
-
}
|
|
57129
|
-
});
|
|
57130
|
-
|
|
57131
|
-
// src/mcp/tools.ts
|
|
57132
|
-
var tools_exports = {};
|
|
57133
|
-
__export(tools_exports, {
|
|
57134
|
-
TOOL_DEFINITIONS: () => TOOL_DEFINITIONS,
|
|
57135
|
-
canonicalToolNames: () => canonicalToolNames,
|
|
57136
|
-
getDeprecation: () => getDeprecation,
|
|
57137
|
-
handleToolCall: () => handleToolCall
|
|
57138
|
-
});
|
|
57139
|
-
function toolError(message) {
|
|
57140
|
-
return {
|
|
57141
|
-
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
57142
|
-
isError: true
|
|
57143
|
-
};
|
|
57144
|
-
}
|
|
57145
|
-
async function runScanFile(args, ctx) {
|
|
57146
|
-
const path = args.path;
|
|
57147
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57148
|
-
const result = await scanFile(path, ctx.config);
|
|
57149
|
-
const simplified = {
|
|
57150
|
-
filePath: result.filePath,
|
|
57151
|
-
componentCount: result.componentCount,
|
|
57152
|
-
parseError: result.parseError,
|
|
57153
|
-
issues: result.issues.map((i) => ({
|
|
57154
|
-
ruleId: i.ruleId,
|
|
57155
|
-
category: i.category,
|
|
57156
|
-
severity: i.severity,
|
|
57157
|
-
line: i.line,
|
|
57158
|
-
column: i.column,
|
|
57159
|
-
message: i.message,
|
|
57160
|
-
advice: i.advice
|
|
57161
|
-
}))
|
|
57162
|
-
};
|
|
57163
|
-
return {
|
|
57164
|
-
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
57165
|
-
};
|
|
57166
|
-
}
|
|
57167
|
-
function explainRule2(args, ctx) {
|
|
57168
|
-
const ruleId = args.ruleId;
|
|
57169
|
-
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
57170
|
-
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
57171
|
-
if (!rule) return toolError("Unknown rule: " + ruleId);
|
|
57172
|
-
const explanation = {
|
|
57173
|
-
ruleId: rule.id,
|
|
57174
|
-
category: rule.category,
|
|
57175
|
-
severity: rule.severity,
|
|
57176
|
-
aiSpecific: rule.aiSpecific,
|
|
57177
|
-
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 + ".",
|
|
57178
|
-
whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
|
|
57179
|
-
};
|
|
57180
|
-
return {
|
|
57181
|
-
content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
|
|
57182
|
-
};
|
|
57183
|
-
}
|
|
57184
|
-
function listRules(args, ctx) {
|
|
57185
|
-
const category = args.category;
|
|
57186
|
-
const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
|
|
57187
|
-
const rules = filtered.map((r) => ({
|
|
57188
|
-
id: r.id,
|
|
57189
|
-
category: r.category,
|
|
57190
|
-
severity: r.severity,
|
|
57191
|
-
aiSpecific: r.aiSpecific
|
|
57192
|
-
}));
|
|
57193
|
-
return {
|
|
57194
|
-
content: [
|
|
57195
|
-
{
|
|
57196
|
-
type: "text",
|
|
57197
|
-
text: JSON.stringify({ count: rules.length, rules }, null, 2)
|
|
57198
|
-
}
|
|
57199
|
-
]
|
|
57200
|
-
};
|
|
57201
|
-
}
|
|
57202
|
-
async function runSuggest(args, ctx) {
|
|
57203
|
-
const maxFilesRaw = args.maxFiles;
|
|
57204
|
-
const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
|
|
57205
|
-
try {
|
|
57206
|
-
const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
|
|
57207
|
-
const doNotCreate = [
|
|
57208
|
-
...ctx.config.constitution?.forbidden ?? []
|
|
57209
|
-
];
|
|
57210
|
-
const declared = /* @__PURE__ */ new Set();
|
|
57211
|
-
for (const list of [
|
|
57212
|
-
ctx.config.constitution?.stateManagement ?? [],
|
|
57213
|
-
ctx.config.constitution?.dataFetching ?? [],
|
|
57214
|
-
ctx.config.constitution?.uiLibrary ?? [],
|
|
57215
|
-
ctx.config.constitution?.forms ?? [],
|
|
57216
|
-
ctx.config.constitution?.styling ?? [],
|
|
57217
|
-
ctx.config.constitution?.routing ?? []
|
|
57218
|
-
]) {
|
|
57219
|
-
for (const lib of list) declared.add(lib);
|
|
57220
|
-
}
|
|
57221
|
-
const doNotCreateCapped = doNotCreate.slice(0, 10);
|
|
57222
|
-
return {
|
|
57223
|
-
content: [
|
|
57224
|
-
{
|
|
57225
|
-
type: "text",
|
|
57226
|
-
text: JSON.stringify(
|
|
57227
|
-
{
|
|
57228
|
-
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.",
|
|
57229
|
-
doNotCreate: doNotCreateCapped,
|
|
57230
|
-
declaredStack: Array.from(declared),
|
|
57231
|
-
existingPatterns: inventory
|
|
57232
|
-
},
|
|
57233
|
-
null,
|
|
57234
|
-
2
|
|
57235
|
-
)
|
|
57236
|
-
}
|
|
57237
|
-
]
|
|
57238
|
-
};
|
|
57239
|
-
} catch (err) {
|
|
57240
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57241
|
-
}
|
|
57242
|
-
}
|
|
57243
|
-
function runCheckConstitution(args, ctx) {
|
|
57244
|
-
const path = args.path;
|
|
57245
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57246
|
-
const absPath = (0, import_node_path40.resolve)(ctx.cwd, path);
|
|
57247
|
-
let source;
|
|
57248
|
-
try {
|
|
57249
|
-
source = (0, import_node_fs32.readFileSync)(absPath, "utf-8");
|
|
57250
|
-
} catch (err) {
|
|
57251
|
-
return toolError(
|
|
57252
|
-
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
57253
|
-
);
|
|
57254
|
-
}
|
|
57255
|
-
const result = checkFileConstitution(source, ctx.config.constitution);
|
|
57256
|
-
return {
|
|
57257
|
-
content: [
|
|
57258
|
-
{
|
|
57259
|
-
type: "text",
|
|
57260
|
-
text: JSON.stringify(
|
|
57261
|
-
{
|
|
57262
|
-
file: absPath,
|
|
57263
|
-
importCount: result.imports.length,
|
|
57264
|
-
violationCount: result.violations.length,
|
|
57265
|
-
imports: result.imports,
|
|
57266
|
-
violations: result.violations,
|
|
57267
|
-
// Field name kept stable for backward compatibility with
|
|
57268
|
-
// older consumers; the value reflects whether the merged
|
|
57269
|
-
// `config.constitution` was declared, detected, or absent.
|
|
57270
|
-
conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
|
|
57271
|
-
},
|
|
57272
|
-
null,
|
|
57273
|
-
2
|
|
57274
|
-
)
|
|
57275
|
-
}
|
|
57276
|
-
]
|
|
57277
|
-
};
|
|
57278
|
-
}
|
|
57279
|
-
async function runFindSimilar(args, ctx) {
|
|
57280
|
-
const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
57281
|
-
const hooks = Array.isArray(args.hooks) ? args.hooks : [];
|
|
57282
|
-
const props = Array.isArray(args.props) ? args.props : [];
|
|
57283
|
-
const limitRaw = args.limit;
|
|
57284
|
-
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
|
|
57285
|
-
try {
|
|
57286
|
-
const matches = await findSimilarFunctions2(
|
|
57287
|
-
{
|
|
57288
|
-
name: typeof args.name === "string" ? args.name : void 0,
|
|
57289
|
-
hooks,
|
|
57290
|
-
props,
|
|
57291
|
-
limit,
|
|
57292
|
-
workspaceDir: ctx.cwd
|
|
57293
|
-
},
|
|
57294
|
-
{ cwd: ctx.cwd }
|
|
57295
|
-
);
|
|
57296
|
-
return {
|
|
57297
|
-
content: [
|
|
57298
|
-
{
|
|
57299
|
-
type: "text",
|
|
57300
|
-
text: JSON.stringify(
|
|
57301
|
-
{
|
|
57302
|
-
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.",
|
|
57303
|
-
count: matches.length,
|
|
57304
|
-
matches: matches.map((m) => ({
|
|
57305
|
-
name: m.signature.name,
|
|
57306
|
-
file: m.signature.fileRel,
|
|
57307
|
-
line: m.signature.line,
|
|
57308
|
-
similarity: Number(m.similarity.toFixed(3)),
|
|
57309
|
-
fingerprint: m.fingerprint,
|
|
57310
|
-
hooks: m.signature.hooks,
|
|
57311
|
-
props: m.signature.props,
|
|
57312
|
-
params: m.signature.params
|
|
57313
|
-
}))
|
|
57314
|
-
},
|
|
57315
|
-
null,
|
|
57316
|
-
2
|
|
57317
|
-
)
|
|
57318
|
-
}
|
|
57319
|
-
]
|
|
57320
|
-
};
|
|
57321
|
-
} catch (err) {
|
|
57322
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57323
|
-
}
|
|
57324
|
-
}
|
|
57325
|
-
async function handleToolCall(toolName, args, ctx) {
|
|
57326
|
-
const deprecation = getDeprecation(toolName);
|
|
57327
|
-
const deprecationNotice = deprecation ? {
|
|
57328
|
-
tool: toolName,
|
|
57329
|
-
replacedBy: deprecation.replacedBy,
|
|
57330
|
-
removedIn: deprecation.removedIn ?? "next major",
|
|
57331
|
-
reason: deprecation.reason
|
|
57332
|
-
} : void 0;
|
|
57333
|
-
switch (toolName) {
|
|
57334
|
-
case "slop_scan_file":
|
|
57335
|
-
return runScanFile(args, ctx);
|
|
57336
|
-
case "slop_explain_rule":
|
|
57337
|
-
return explainRule2(args, ctx);
|
|
57338
|
-
case "slop_list_rules":
|
|
57339
|
-
return listRules(args, ctx);
|
|
57340
|
-
case "slop_suggest":
|
|
57341
|
-
return runSuggest(args, ctx);
|
|
57342
|
-
case "slop_suggest_with_structure":
|
|
57343
|
-
return runSuggestWithStructure(args, ctx);
|
|
57344
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57345
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57346
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57347
|
-
// Their runner functions (runGovernance, runArchitectureScore,
|
|
57348
|
-
// runBusinessLogicScore) are kept in the file for now
|
|
57349
|
-
// (marked @deprecated) to keep the diff small; they can be
|
|
57350
|
-
// deleted in a follow-up. New clients will never see these
|
|
57351
|
-
// tools listed in the MCP tools/list response.
|
|
57352
|
-
case "slop_check_constitution":
|
|
57353
|
-
return runCheckConstitution(args, ctx);
|
|
57354
|
-
case "slop_find_similar":
|
|
57355
|
-
return runFindSimilar(args, ctx);
|
|
57356
|
-
default:
|
|
57357
|
-
return toolError("Unknown tool: " + toolName);
|
|
57358
|
-
}
|
|
57359
|
-
}
|
|
57360
|
-
function canonicalToolNames() {
|
|
57361
|
-
return TOOL_DEFINITIONS.filter((t) => !t.deprecated).map((t) => t.name);
|
|
57362
|
-
}
|
|
57363
|
-
function getDeprecation(toolName) {
|
|
57364
|
-
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
57365
|
-
}
|
|
57366
|
-
var import_node_fs32, import_node_path40, TOOL_DEFINITIONS;
|
|
57367
|
-
var init_tools = __esm({
|
|
57368
|
-
"src/mcp/tools.ts"() {
|
|
57369
|
-
"use strict";
|
|
57370
|
-
import_node_fs32 = require("fs");
|
|
57371
|
-
import_node_path40 = require("path");
|
|
57372
|
-
init_worker();
|
|
57373
|
-
init_patterns();
|
|
57374
|
-
init_architecture_score();
|
|
57375
|
-
init_business_logic();
|
|
57376
|
-
init_slop_suggest_structure();
|
|
57377
|
-
TOOL_DEFINITIONS = [
|
|
57378
|
-
{
|
|
57379
|
-
name: "slop_scan_file",
|
|
57380
|
-
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.",
|
|
57381
|
-
inputSchema: {
|
|
57382
|
-
type: "object",
|
|
57383
|
-
properties: {
|
|
57384
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
|
|
57385
|
-
framework: {
|
|
57386
|
-
type: "string",
|
|
57387
|
-
enum: ["react", "vue", "svelte", "astro", "html"],
|
|
57388
|
-
description: "Framework multiplier to apply. Defaults to the configured framework."
|
|
57389
|
-
}
|
|
57390
|
-
},
|
|
57391
|
-
required: ["path"]
|
|
57392
|
-
}
|
|
57393
|
-
},
|
|
57394
|
-
{
|
|
57395
|
-
name: "slop_explain_rule",
|
|
57396
|
-
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.",
|
|
57397
|
-
inputSchema: {
|
|
57398
|
-
type: "object",
|
|
57399
|
-
properties: {
|
|
57400
|
-
ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
|
|
57401
|
-
},
|
|
57402
|
-
required: ["ruleId"]
|
|
57403
|
-
}
|
|
57404
|
-
},
|
|
57405
|
-
{
|
|
57406
|
-
name: "slop_list_rules",
|
|
57407
|
-
description: "List all registered rules with their category, severity, and aiSpecific flag. Optional category filter (visual | logic | wcag | security | perf | typo | layout | component | arch).",
|
|
57408
|
-
inputSchema: {
|
|
57409
|
-
type: "object",
|
|
57410
|
-
properties: {
|
|
57411
|
-
category: { type: "string", description: "Optional category filter." }
|
|
57412
|
-
}
|
|
57413
|
-
}
|
|
57414
|
-
},
|
|
57415
|
-
{
|
|
57416
|
-
name: "slop_suggest",
|
|
57417
|
-
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.",
|
|
57418
|
-
inputSchema: {
|
|
57419
|
-
type: "object",
|
|
57420
|
-
properties: {
|
|
57421
|
-
maxFiles: {
|
|
57422
|
-
type: "number",
|
|
57423
|
-
description: "Cap on files scanned to keep the inventory cheap. Defaults to 200."
|
|
57424
|
-
}
|
|
57425
|
-
}
|
|
57426
|
-
}
|
|
57427
|
-
},
|
|
57428
|
-
{
|
|
57429
|
-
name: "slop_suggest_with_structure",
|
|
57430
|
-
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.",
|
|
57431
|
-
inputSchema: {
|
|
57432
|
-
type: "object",
|
|
57433
|
-
properties: {
|
|
57434
|
-
maxFiles: {
|
|
57435
|
-
type: "number",
|
|
57436
|
-
description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
|
|
57437
|
-
}
|
|
57438
|
-
}
|
|
57439
|
-
}
|
|
57440
|
-
},
|
|
57441
|
-
{
|
|
57442
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57443
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57444
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57445
|
-
// They were strict subsets of slop_suggest; users should
|
|
57446
|
-
// call slop_suggest and read repositoryHealth /
|
|
57447
|
-
// architectureConsistency / businessLogicCoherence.
|
|
57448
|
-
name: "slop_check_constitution",
|
|
57449
|
-
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.",
|
|
57450
|
-
inputSchema: {
|
|
57451
|
-
type: "object",
|
|
57452
|
-
properties: {
|
|
57453
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
|
|
57454
|
-
},
|
|
57455
|
-
required: ["path"]
|
|
57456
|
-
}
|
|
57457
|
-
},
|
|
57458
|
-
{
|
|
57459
|
-
// v0.10.1: find_similar_function. The GIR (Give-Implementation-
|
|
57460
|
-
// Reference) primitive for slop_suggest. Given a function signature
|
|
57461
|
-
// (name + hooks + props), find the most similar existing
|
|
57462
|
-
// implementations across the codebase. Uses AST fingerprints
|
|
57463
|
-
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
57464
|
-
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
57465
|
-
name: "slop_find_similar",
|
|
57466
|
-
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].",
|
|
57467
|
-
inputSchema: {
|
|
57468
|
-
type: "object",
|
|
57469
|
-
properties: {
|
|
57470
|
-
name: {
|
|
57471
|
-
type: "string",
|
|
57472
|
-
description: "Function/component name to match. Omit to match by hooks+props only."
|
|
57473
|
-
},
|
|
57474
|
-
hooks: {
|
|
57475
|
-
type: "array",
|
|
57476
|
-
items: { type: "string" },
|
|
57477
|
-
description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
|
|
57478
|
-
},
|
|
57479
|
-
props: {
|
|
57480
|
-
type: "array",
|
|
57481
|
-
items: { type: "string" },
|
|
57482
|
-
description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
|
|
57483
|
-
},
|
|
57484
|
-
limit: {
|
|
57485
|
-
type: "number",
|
|
57486
|
-
description: "Top-k results to return. Default 10. Capped at 50."
|
|
57487
|
-
}
|
|
57488
|
-
}
|
|
57489
|
-
}
|
|
57490
|
-
}
|
|
57491
|
-
];
|
|
57492
|
-
}
|
|
57493
|
-
});
|
|
57494
|
-
|
|
57495
57134
|
// src/index.ts
|
|
57496
57135
|
var src_exports = {};
|
|
57497
57136
|
__export(src_exports, {
|
|
@@ -57579,7 +57218,7 @@ init_dist2();
|
|
|
57579
57218
|
|
|
57580
57219
|
// src/cli/program.ts
|
|
57581
57220
|
var import_node_path72 = require("path");
|
|
57582
|
-
var
|
|
57221
|
+
var import_commander3 = require("commander");
|
|
57583
57222
|
|
|
57584
57223
|
// src/cli/options.ts
|
|
57585
57224
|
var import_commander = require("commander");
|
|
@@ -62236,7 +61875,374 @@ init_logger();
|
|
|
62236
61875
|
init_builtins();
|
|
62237
61876
|
init_config2();
|
|
62238
61877
|
init_header();
|
|
62239
|
-
|
|
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
|
|
62240
62246
|
var SERVER_INFO = {
|
|
62241
62247
|
name: "slopbrick",
|
|
62242
62248
|
// v0.39.0: use the VERSION constant (same source as the CLI)
|
|
@@ -63676,6 +63682,7 @@ var import_node_fs38 = require("fs");
|
|
|
63676
63682
|
var import_node_path52 = require("path");
|
|
63677
63683
|
init_discover();
|
|
63678
63684
|
init_patterns();
|
|
63685
|
+
init_telemetry();
|
|
63679
63686
|
async function runDrift(cwd, config, options = {}) {
|
|
63680
63687
|
const maxFiles = options.maxFiles ?? 1e3;
|
|
63681
63688
|
const allFiles = await discoverFiles(cwd, config);
|
|
@@ -63783,26 +63790,217 @@ function formatDrift(result, opts = {}) {
|
|
|
63783
63790
|
function driftExitCode(result) {
|
|
63784
63791
|
return result.totalViolations > 0 ? 1 : 0;
|
|
63785
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
|
+
}
|
|
63786
63970
|
|
|
63787
63971
|
// src/cli/commands/drift.ts
|
|
63788
63972
|
function registerDrift(program) {
|
|
63789
63973
|
program.command("drift").description(
|
|
63790
63974
|
"detect imports that violate declared constitution (state, data-fetching, UI, forms, styling, routing) or import forbidden packages"
|
|
63791
|
-
).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(
|
|
63792
63979
|
async (cmdOptions, command) => {
|
|
63793
|
-
|
|
63794
|
-
|
|
63795
|
-
|
|
63796
|
-
|
|
63797
|
-
|
|
63798
|
-
const
|
|
63799
|
-
|
|
63800
|
-
|
|
63801
|
-
|
|
63802
|
-
|
|
63803
|
-
|
|
63804
|
-
|
|
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;
|
|
63805
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`);
|
|
63806
64004
|
}
|
|
63807
64005
|
);
|
|
63808
64006
|
}
|
|
@@ -66461,8 +66659,9 @@ process.on("uncaughtException", (err) => {
|
|
|
66461
66659
|
});
|
|
66462
66660
|
async function runCli({ start }) {
|
|
66463
66661
|
try {
|
|
66464
|
-
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)");
|
|
66465
66663
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
66664
|
+
setExitOverride(program);
|
|
66466
66665
|
registerInit(program);
|
|
66467
66666
|
registerInstall(program);
|
|
66468
66667
|
registerUninstall(program);
|
|
@@ -66647,7 +66846,9 @@ async function runCli({ start }) {
|
|
|
66647
66846
|
program.outputHelp();
|
|
66648
66847
|
process.exit(0);
|
|
66649
66848
|
}
|
|
66650
|
-
await program
|
|
66849
|
+
await dispatch(program, async () => {
|
|
66850
|
+
await program.parseAsync(process.argv);
|
|
66851
|
+
});
|
|
66651
66852
|
} catch (err) {
|
|
66652
66853
|
if (err instanceof ConfigValidationError) {
|
|
66653
66854
|
logger.error(err.message);
|